细胞识别课程设计.docx
《细胞识别课程设计.docx》由会员分享,可在线阅读,更多相关《细胞识别课程设计.docx(12页珍藏版)》请在冰点文库上搜索。
![细胞识别课程设计.docx](https://file1.bingdoc.com/fileroot1/2023-5/6/ac3bf330-a6f9-4fff-9875-28c7fa689268/ac3bf330-a6f9-4fff-9875-28c7fa6892681.gif)
细胞识别课程设计
数字图像处理课程设计报告
细胞识别
实验课题:
细胞识别
实验目的:
对血液细胞切片图片进行各种处理,最终得出细胞的数目、面积等信息
一、实验内容
基于VC++6.0软件下的细胞识别,通过细胞的标记、二值化、填洞、收缩、找中心点、计数等过程完成实验目的。
1、图像信息获取
(1)在onDraw函数中添加代码实现打开一幅图像的功能。
(2)通过:
查看——建立类向导——添加OnInitialUpdate()函数,实现对自动打开固定图片。
获取RGB、HSI信息
(1)通过:
查看——建立类向导——添加OnMouseMove()函数,添加代码实现获取所要信息。
(2)通过函数RgbtoHsi实现RGB向HSI的转化。
2、标记Mark点
实现是Mark点,,边界(edge)的标记,MayBeMarkToMark,将细胞、可能是细胞的区域、不可能是细胞的区域、细胞边界分别用红色、暗红、蓝色和绿色标记出来。
3、二值化
将原有彩色图像变换为二值图像,背景灰度值为128,细胞灰度值为240,边缘为255。
4、填洞
将细胞中灰度值为128的部分的灰度值设置为240。
5、收缩
扫描图像,对图像进行预先的3次腐蚀,判断所生成边界点,然后根据原理判定是否标注该点,存放所标志的中心点,便于统计细胞个数及计算细胞半径。
6、获取中心点
根据前面所作工作统计获得的中心点个数,去掉一系列不符合要求的点得出最终的细胞个数、细胞的平均半径和平均面积,用对话框输出统计结果。
二、学习心得
这次的课程设计,我受益颇多!
让我们明白了VC6.0这款软件的强大,这几天的学习,让我知道了MFC(MicrosoftFoundationClasses)这个非常好用的类库,也学会了它的使用方法。
通过这几天的探究,我明白要想把一个东西做好,必须下足功夫,必须专心致志,必须去亲自动手。
总之,经过这几天的努力,我们终于完成了这份课程设计任务。
明白了以前学的C++原来是很重要的,还让我也对以往的知识有了个回顾和进一步的加深。
也让我对别的知识有了了解。
此次课程设计给我们提供了一个既能学习又能锻炼的机会,使我们养成了查找资料的习惯,将理论与实际相结合起来,锻炼了分析问题和实际解决问题的能力。
提高了适应能力,为今后的学习和实践打下了基础。
三、算法分析
1.打开图像
建立当文档工程
添加CDIB类
添加CImgcell203View的公共成员函数m_Cdib
添加Serialize()
添加显示代码
2.RgbtoHsi(&rgb,&Hsi)
RGB向HSI模型的转换是由一个基于笛卡尔直角坐标系的单位立方体向基于圆柱极坐标的双锥体的转换。
基本要求是将RGB中的亮度因素分离,将色度分解为色调和饱和度,并用角向量表示色调。
如果直接对R、G、B处理,其处理过程中很可能会引起三个量不同程度的变化,这样就会产生色差问题,甚至带来颜色上的失真。
HSI模型的出现,使得在保持色彩无失真的情况下实现图像处理成为可能。
HSI可以更好地区分细胞与非细胞。
3.OnMouseMove(UINTnFlags,CPointpoint)
当鼠标移动时调用此函数。
nFlags指示各种虚拟按键是否按下。
point:
鼠标的X,Y坐标:
该坐标为鼠标距离截获该消息的窗口左上角的位置是一个相对位置而不是在屏幕像素上的绝对位置。
在OnMouseMove函数里调用RgbtoHsi(&rgb,&Hsi)函数,可以在屏幕上显示鼠标所指点的坐标以及RGB、HSI和灰度值,通过HSI的可以选取合适的阈值来找到细胞以及边界。
4.OnMark()
Mark点指的是我们要寻找的细胞内的点。
通过计算色调的平均值设置一个门限值,将色调与平均值差距在门限范围内的点设置为Mark点,同理通过计算色调的平均值设置一个MaybeMark的门限,将色调与平均值差距在门限范围内的点设置为MaybeMark点。
if(dis*lpSrc=0;*(lpSrc+1)=0;*(lpSrc+2)=255;//Red
}
elseif(dis*lpSrc=255;*(lpSrc+1)=0;*(lpSrc+2)=0;//Blue
}
else{//notMark/maybeMark
if(*lpSrc==0)*lpSrc=1;//Mark
elseif(*lpSrc==255)*lpSrc=254;//maybemark
if(*(lpSrc+1)==255)*(lpSrc+1)=254;//edge
5.OnmayMarktoMark()
这一步是把可能的Mark点变成Mark点,因为在拍摄图片时由于光线等原因是本来的Mark点变暗,在进行Mark处理时被标记为MaybeMark,所以需要把可能的Mark点还原Mark点。
for(i=0;i{
for(j=0;j{
lpSrc=pDoc->m_pDib->m_lpImage+
lLineBytes*(lHeight-1-i)+j*3;
if(*lpSrc==255){//maybeMark
boolbProc=false;
if(j>0)if(*(lpSrc-3)==0)bProc=true;
if(jif(i>0)if(*(lpSrc+lLineBytes)==0)bProc=true;
if(i//maybeMarkhaveMarkPointtoMark
if(bProc){
*lpSrc=0;
MarkChg=true;
*(lpSrc+2)=128;
}
}
}
}
6、OnEdge()
OnEdge()是把边缘标记出来,方便后面的图像处理。
提取边缘的过程是先开辟一块内存用于存放数据,通过设置的门限值来找到边缘点,在提取边缘时用到重要的sobel算子,sobel算子主要用作边缘检测。
在技术上,它是一离散性差分算子,用来运算图像亮度函数的梯度之近似值。
在图像的任何一点使用此算子,将会产生对应的梯度矢量或是其法矢量。
该算子包含两组3x3的矩阵,分别为横向及纵向,将之与图像作平面卷积,即可分别得出横向及纵向的亮度差分近似值。
doublepixel[9];
lpDst=pDoc->m_pDib->m_lpImage+lLineBytes*(lHeight-1-i)+j*3;
if(*(lpDst)==0||*(lpDst)==255){//Mark/MaybeMark
doublepixel[9];
lpSrc=lpNewDIBBits+lLineBytes*(lHeight-1-i)+j*3;
for(intm=-1;m<2;m++)
for(intn=-1;n<2;n++){
unsignedchar*lpSrc1=lpSrc-lLineBytes*m+3*n;
pixel[(m+1)*3+n+1]=((int)*lpSrc1+*(lpSrc1+1)+*(lpSrc1+2))/3;
}
//Sobel
doubletmp1=
pixel[0]+2*pixel[1]+pixel[2]-pixel[6]-2*pixel[7]-pixel[8];
doubletmp2=
pixel[0]+2*pixel[3]+pixel[6]-pixel[2]-2*pixel[5]-pixel[8];
doubleedge=sqrt(tmp1*tmp1+tmp2*tmp2);
if(edge>edgeDoor)
*(lpDst+1)=255;//edge
}
constintM=5;
boolbdelete;
//filter
for(inti=StartPoint.y+M;ifor(intj=StartPoint.x+M;jpDst=pDoc->m_pDib->m_lpImage+lLineBytes*(lHeight-1-i)+j*3;
if(*(lpDst+1)==255)//edge
{bdelete=true;
for(intm=-M;m<=M;m++)
for(intn=-M;n<=M;n++)
{if(m==-M||m==M||n==-M||n==M){在边缘上
if(*(lpDst+lLineBytes*m+n*3)||(*(lpDst+lLineBytes*m+n*3+1)==255))//noMark&&noEdge
{bdelete=false;
m=M+1;n=M+1;//out跳出循环
}}}
if(bdelete)
*(lpDst+1)=0;//deleteedge
}}}
7.OnTwoVaule()
一幅图像包括目标物体、背景还有噪声,要想从多值的数字图像中直接提取出目标物体,最常用的方法就是设定一个阈值T,用T将图像的数据分成两部分:
大于T的像素群和小于T的像素群。
这是研究灰度变换的最特殊的方法,称为图像的二值化。
二值化后更加有利于做图像处理判别。
二值化时还要进行均值滤波,因为获得的图像有很多噪音。
这主要由于平时的工作和环境引起的,图像增强是减弱噪音,增强对比度。
想得到比较干净清晰的图像并不是容易的事情。
为这个目标而为处理图像所涉及的操作是设计一个适合、匹配的滤波器和恰当的阈值。
均值滤波的基本原理是用均值代替原图像中的各个像素值,即对待处理的当前像素点(x,y),选择一个模板,该模板由其近邻的若干像素组成,求模板中所有像素的均值,再把该均值赋予当前像素点(x,y),作为处理后图像在该点上的灰度个g(x,y),即个g(x,y)=1/m∑f(x,y)m为该模板中包含当前像素在内的像素总个数。
8.OnFillHoles()
孔洞填充的原理如下:
在阈值处理时,如果像素在阈值范围内,则像素将被标志。
孔洞填充将先统计所有连通的非标志区域面积,总会有一个或者几个面积特别大的区域,其它的都是面积相对较小的区域。
较小或者很小的往往就是系统所要填充的孔洞了。
从一个为访问过的非MARK点开始(该点可以看做一粒种子),从该点开始向四周扩散找未被访问过的非MARK点。
将找到的符合条件的点同时压如堆栈和队列中。
之所以要同时在堆栈和队列中同时保存相同的点是因为堆栈中的点并没有保存,每次循环都会弹出一个点作为新的种子进行扩散,当堆栈中的数据全部弹出之后,即表示搜索到了洞内的所有点,而队列中则保存了洞内的所有点。
因此通过判断队列的大小可以知道洞的大小从而判断是否进行填充。
填充就是经洞内的非MARK点变换为MARK点。
while(s.size())
{
//Addnewmemberstostack
//Abovecurrentpixel
lpSrc=(unsignedchar*)pDoc->m_pDib->m_lpImage+lLineBytes*(lHeight-1-yt)+xt;
if(yt>StartPoint.y){//循环结束条件是队列中的内容为空
//ifno-marked&no-visited
if(!
(*(lpSrc+lLineBytes)&MARK_VISITED))
{
s.push(CPoint(xt,yt-1));
v.push_back(CPoint(xt,yt-1));
*(lpSrc+lLineBytes)|=VISITED;//将访问过的点加上VISITED标记
}
}elsebBorder=true;
……
//上下左右四个方向寻找未访问过的非MARK点。
并将找到的点压如堆栈和队列
xt=s.top().x;
yt=s.top().y;
s.pop();//将堆栈顶端的数据弹出,作为新的种子进行扩散
9.shrink()
先去掉边缘点,然后将剩下的Mark点生成边缘点,再去掉在生成,直到去掉三次边缘,就这样收缩,如果收缩过程中有些Mark点收缩到很小,可以认为那不是细胞,那就可以直接去掉。
GenEdge();
for(intk=0;k{
for(inti=0;i{
for(intj=0;j{
lpSrc=(unsignedchar*)pDoc->m_pDib->m_lpImage+lLineBytes*(lHeight-1-i)+j;
if(*lpSrc==0xf0)//||*lpSrc==0xc0if(*lpSrc&EDGEPOINT)
*lpSrc=0;
//(*lpSrc)&=NO_MARK;//marked=0;
}
}
if(k%2==0)
GenEdge4();
else
GenEdge();
MessageBox("times");
Invalidate(true);}
Invalidate(true);
10.findcenter()
此处我将收缩算法和中心点的获取放在一起写,是因为我觉得收缩算法是获取中心点的一部分,中心点的获取就是通过不断地进行收缩直到最后一次收缩后会导致中心点消失时停止。
收缩算法的思想就是在识别出细胞和其边界的基础上,首先进行一次遍历将所有已标记为边界的点变为非MARK点,然后再通过genEdge()、genEdge4()从八方向和四方向进行交替生成边界,至于为什么要八方向和四方向交替使用可能是因为这样效果比较好的缘故。
生成边界的思想很简单,就是通过判断MARK点上下左右四个方向或者八个方向是否有非Mark点,如果有的话即认为是边界,将该点加上边界标志。
而中心点的获取就是在每一次的收缩之前先进性一次判断,如果该点是孤立点或者是全边界点则将该点保存起来。
因为对于孤立点和全边界点如果不进行保存的话在进行一次收缩之后该点就会消失,因此为了保存该点,就要在其消失前将其保存。
最后每一个细胞收缩到最后可能剩下一个点到四个点。
因此第一次获取的中心点个数会是真正中心点个数的三倍左右。
细胞半径大小的获取是通过判断该中心点是通过多少次收缩之后获得的从而近似获得细胞的半径。
if(*lpSrc&EDGEPOINT)
(*lpSrc)&=NO_MARK;//marked=0;//收缩即将所有的边缘点变为
//非MARK点
if(k%2==0)//交替生成新的边界
GenEdge4();//四方向生成边界只要该MARK点相邻有非MARN点
//则认为是边缘点
else
GenEdge();//同样只要该点八个方向有非MARK点则认为是边缘点
m_bFullEdge=true;
if(*lpSrc&EDGEPOINT&&!
(*lpSrc&VISITED))//首先判断是否为未访问过的//边界点
{
if(!
(*(lpSrc-1)&MARKED)&&
!
(*(lpSrc+1)&MARKED)&&
!
(*(lpSrc+lLineBytes)&MARKED)&&
!
(*(lpSrc-lLineBytes)&MARKED))//判断是否为孤立点
{
if(k==0)continue;//如果进行第一次收缩即消失的点
//则认为该点是噪点,不进行保存直接进行下一次收缩
*lpSrc|=CENTERED;//对孤立点加上中心点标志
pt.x=i;pt.y=j;
pt.radius=k+pre_shrink_count+4;//circleadjust
points_temp.push_back(pt);//保存CENTER_POINT信息
continue;
}
elseMarkIt(i,j);//判断是否需要保存,保存成立的条件是该点是//全边缘点,即与该点相连的只有非MARK点和边缘点,如果条件满足的话则保存于该点相//邻的边缘点
//没有访问过标志了并且是边缘邻域
//需要保存!
if(m_bFullEdge)SaveIt(i,j,k+pre_shrink_count+3);//保存全边界
}
四、建议
刚开始老师上课讲程序时,可以说大概听懂了。
过了几天以后再去实验室实际操作,经过了半天的学习才学会了MFC的用法,但是同时发现原理忘的差不多了,建议先讲软件MFC用法,然后再讲原理,或者实验做到一定程度在上一次课讲讲程序,那样不至于忘得太快,效果也会好很多。
五、错误举例
1.指针飞掉,在没有定义指针边界时,指针会飞掉。
2.图像为读取时会出现很多连带错误'pDoc':
undeclaredidentifier
errorleftof'->m_pDib'mustpointtoclass/struct/union
errorleftof'->m_lpBMIH'mustpointtoclass/struct/union
errorleftof'->biWidth'mustpointtoclass/struct/union等等。
3,将memcpy(lpNewDIBBits,lpSrc,lLineBytes*lHeight);放在MaybeMark前会出现MaybeMark和Mark点之间也会生成边界,收缩之后会出现不少错误,不少细胞收缩不见了。