本節重點在圖片處理算法的實現,本例的算法的巧妙之處是在灰度變換中的閥值的選取,它並沒有用到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<height;i++)
{
unsigned char*p=(unsigned char*)src->imageData+src->widthStep*i;
for( j=0;j<width;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<dConArea)//這裡用於提取最大的圖像面積和第二大圖像面積,一般情況下參考物是一枚硬幣
//作為第二大面積來出現
{
SecMaxArea=MaxArea;//第二大面積
MaxArea=dConArea;
}
//if((SecMaxArea<dConArea)&&(dConArea<MaxArea))
//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);
至此,整個程序部分結束,程序的參照物是一個一角的硬幣,提供固定的參照面積。當然,程序還有很多不足,其中,提取輪廓後的現實圖形並沒有進行縮放,當打開比較大的圖片時顯示的不是很全,這也許會在以後改進。