本节重点在图片处理算法的实现,本例的算法的巧妙之处是在灰度变换中的阀值的选取,它并没有用到OpenCV库中的Canny和sobel等算子,这些算子很经典,但是都有其局限性,要求图片不能有阴影,不然变换后进行轮廓提取时误差会非常的大,甚至超出许可范围,本例算法是根据图片亮度求出平均的亮度,将其作为阀值,在实践中取得了很好的效果,误差集中在1%左右。
废话不多,直接看代码,首先在dlg类下新建一个int类型的Otsu(IplImage *src)函数,返回值即为可用的阀值,函数参数为刚加载的原图:
int i,j;
int height=src->height;
int width=src->width;
float histogram[256]={0};
for( i=0;i { unsigned char*p=(unsigned char*)src->p_w_picpathData+src->widthStep*i; for( j=0;j { histogram[*p++]++; } } int size=height*width; for( i=0;i<256;i++) { histogram[i]=histogram[i]/size; } float avgValue=0; for(i=0;i<256;i++) { avgValue+=i*histogram[i];//整幅图像的平均灰度 } int threshold; float maxVariance=0; float w=0,u=0; for( i=0;i<256;i++) { w+=histogram[i]; //假设当前灰度i为阀值,0-i灰度的像素所占整幅图的比例 u+=i*histogram[i]; float t=avgValue*w-u; float variance=t*t/(w*(1-w)); if(variance>maxVariance) { maxVariance=variance; threshold=i; } } return threshold; 以上代码即为本例中的重点,可以提高精度,下面就是就是一些枝枝节节的问题,利用OpenCV提供的cvThreshold函数提取轮廓,但是这样会出来很多轮廓,很多事不需要的,因此下面就要实现对轮廓的过滤盒填充,使用的函数是voidl类型的FillInternalCintours,其有两个参数,一个是图片指针,一个是过滤阀值,这个阀值是过滤轮廓的关键,当小于这个轮廓是进行填充,填充白色,将其过滤掉,这样剩下的就是比较大的黑色区域的轮廓,然后利用OpenCV提供的计算轮廓面积的函数计算面积,code如下: double dConArea,MaxArea=0,SecMaxArea=0; CvSeq *pContour=0; CvSeq *pConlnner=0; CvMemStorage *pStorage=0; if(pBinary) { //查找所有轮廓 pStorage=cvCreateMemStorage(0); cvFindContours(pBinary,pStorage,&pContour,sizeof(CvContour),CV_RETR_CCOMP,CV_CHAIN_APPROX_SIMPLE); //填充所有轮廓 cvDrawContours(pBinary,pContour,CV_RGB(255,255,255),CV_RGB(255,255,255),2,CV_FILLED,8); //外轮廓循环 int wai=0,nei=0; for(;pContour!=NULL;pContour=pContour->h_next) { wai++; //内轮廓循环 for(pConlnner=pContour->v_next;pConlnner!=NULL;pConlnner=pConlnner->h_next) { nei++;//内轮廓的个数,后期可以用于扫描图像中不相连个体的个数,当然这个内是包括外图像的 dConArea=fabs(cvContourArea(pConlnner,CV_WHOLE_SEQ));//计算轮廓面积 //printf("%f\n",dConArea); if(dConArea<=dAreaThre)//小于过滤阀值就填充 { cvDrawContours(pBinary,pConlnner,CV_RGB(255,255,255),CV_RGB(255,255,255),0,CV_FILLED,8); } SecMaxArea=dConArea; if(MaxArea //作为第二大面积来出现 { SecMaxArea=MaxArea;//第二大面积 MaxArea=dConArea; } //if((SecMaxArea //SecMaxArea=dConArea; } } char ch1[20],ch2[20]; itoa(MaxArea,ch1,10);//10代表的是转换后的进制数 itoa(SecMaxArea,ch2,10); GetDlgItem(IDC_EDIT1)->SetWindowText(ch1); GetDlgItem(IDC_EDIT2)->SetWindowText(ch2); cvReleaseMemStorage(&pStorage); //ShowImage(pBinary,IDC_ShowImg);//调用显示图片函数,这里本来是想实现单框多视图,但是现在能力不足,又不想 //调用显示函数把原图像覆盖掉,只能另建一个窗口来显示过滤填充以后的图像,这样易于比较,以后还是可以改进 //cvReleaseMemStorage(&pStorage); pStorage=NULL; } 最关键的代码已经实现,下面将剩余的功能补全,添加Combo Box控件,用于选择过滤阀值,本例添加的是1000-6000,一般选择5000最适当,当然还是要根据图片的大小。这部分选择过滤阀值的代码比较简单,将其添加到OnButton1ReadImg函数下,且在加载图片的代码之前,code如下: int thresh; CString str; int m; m_down.GetWindowText(str); if(str=="") { AfxMessageBox("请选择过滤阀值!"); return; } m=atoi(str); 再者就是图片处理部分函数的调用和多个设备描述符和设备句柄的关闭。将它们添加到加载图片部分的后面,实现程序的收尾工作。code如下: IplImage *bin=cvCreateImage(cvGetSize(pic),8,1); thresh=Otsu(pic); cvThreshold(pic,bin,thresh,255,CV_THRESH_BINARY); FillInternalCintours(bin,m); cvNamedWindow("Result",CV_WINDOW_AUTOSIZE); cvShowImage("Result",bin); //cvWaitKey(-1); //ResizeImage(pic);//对读入的图片进行缩放使其长宽最大值刚好为256,再复制到TheImage中 //ShowImage(TheImage,IDC_ShowImg);//调用显示图片函数 //读取图片缓存到局部变量ipl中 cvWaitKey(-1); cvReleaseImage(&ipl); cvReleaseImage(&bin); cvReleaseImage(&pic); 至此,整个程序部分结束,程序的参照物是一个一角的硬币,提供固定的参照面积。当然,程序还有很多不足,其中,提取轮廓后的现实图形并没有进行缩放,当打开比较大的图片时显示的不是很全,这也许会在以后改进。