前言
在自動計算圖像中有幾枚硬幣的任務中,分離出前景和背景后是否就可以馬上實現自動計件,如果可以,如何實現?如果不可以,為什么?
答案是否定的。二值化之后我們的得到的只是前景總像素的多少,并不知道哪些像素屬于同一枚硬幣。想要實現自動計件功能還需要用到連通域標記的知識。
連通域標記的方法這里我們使用種子填充法:
算法步驟:
1、遍歷一幅圖像。
2、如果遇到前景且該點未被標記,說明在該點附近可能存在與該點相連通的像素點,即可能存在連通域,停止遍歷。否則繼續遍歷。
3、以該點為seed點,遍歷seed點4鄰域或者8鄰域。如果同為前景,將坐標存到一個棧中,并將這點貼上label,表示已經訪問過該像素,避免重復訪問。
4、將棧中的坐標取出,以該點為seed點,重復2操作。
5、直到棧中的所有元素都取出,說明已經遍歷完了該label的所有元素。
6、label++;從一開始停止遍歷的點繼續遍歷。
7、重復2-6直到遍歷到最后一個像素
代碼實現:
*--------------------------【練習】連通域標記-------------------------------------*//*參數說明:
src_img:輸入圖像
flag_img:作為標記的空間(在函數內部設置為單通道)
draw_img:作為輸出的圖像,不同的連通域的顏色不同
iFlag:作為判斷屬于連通域的像素目標值,一般來說我們是對二值圖進行連通域分析,所以這個值為0或者255,物體是0/1,則iFlag是0/1
type: type==4 :用4鄰域 type==8 :用8鄰域
nums: 設定的label像素個數截斷值,被標記的連通域像素個數必須大于nums才算是正確的連通域。用來防止二值化后的效果并不好的情況。
*/
void seed_Connected_Component_labeling(Mat& src_img,Mat& flag_img,Mat& draw_img, int iFlag,int type, int nums)
{int img_row = src_img.rows;int img_col = src_img.cols;flag_img = cv::Mat::zeros(cv::Size(img_col, img_row), CV_8UC1);//標志矩陣,為0則當前像素點未訪問過draw_img = cv::Mat::zeros(cv::Size(img_col, img_row), CV_8UC3);//繪圖矩陣Point cdd[111000]; //棧的大小可根據實際圖像大小來設置long int cddi = 0;int next_label = 1; //連通域標簽int tflag = iFlag;long int nums_of_everylabel[100] = { 0 }; //存放每個區域的像素個數//Mat(縱坐標,橫坐標)//Point(橫坐標,縱坐標)for (int j = 0; j < img_row; j++) //height{for (int i = 0; i < img_col; i++) //width{//一行一行來if ((src_img).at<uchar>(j, i) == tflag && (flag_img).at<uchar>(j, i) == 0) //滿足條件且未被訪問過{//將該像素坐標壓入棧中cdd[cddi] = Point(i, j);cddi++;//將該像素標記(flag_img).at<uchar>(j, i) = next_label;//將棧中元素取出處理while (cddi != 0){Point tmp = cdd[cddi - 1];cddi--;//對4鄰域進行標記if (type == 4){Point p[4];//鄰域像素點,這里用的四鄰域p[0] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y); //左p[1] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右p[2] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上p[3] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下//順時針//p[0] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上//p[1] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右//p[2] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下//p[3] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y); //左//逆時針//p[3] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上//p[2] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右//p[1] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下//p[0] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y); //左for (int m = 0; m < 4; m++){if ((src_img).at<uchar>(p[m].y, p[m].x) == tflag && (flag_img).at<uchar>(p[m].y, p[m].x) == 0) //滿足條件且未被訪問過{//將該像素坐標壓入棧中cdd[cddi] = p[m];cddi++;//將該像素標記(flag_img).at<uchar>(p[m].y, p[m].x) = next_label;}}}//對8鄰域進行標記else if (type == 8){Point p[8];//鄰域像素點,這里用的四鄰域p[0] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y - 1 > 0 ? tmp.y - 1 : 0); //左上p[1] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上p[2] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1,tmp.y - 1 > 0 ? tmp.y - 1 : 0); //右上p[3] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y); //左p[4] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右p[5] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//左下p[6] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下p[7] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//右下for (int m = 0; m < 7; m++){if ((src_img).at<uchar>(p[m].y, p[m].x) == tflag && (flag_img).at<uchar>(p[m].y, p[m].x) == 0) //滿足條件且未被訪問過{//將該像素坐標壓入棧中cdd[cddi] = p[m];cddi++;//將該像素標記(flag_img).at<uchar>(p[m].y, p[m].x) = next_label;}}}}next_label++;}}}next_label = next_label - 1;int all_labels = next_label;std::cout << "labels : " << next_label <<std::endl;//給不同連通域的涂色并且記錄下每個連通域的像素個數for (int j = 0;j < img_row;j++) //行循環{for (int i = 0;i < img_col;i++) //列循環{int now_label = (flag_img).at<uchar>(j, i); //當前像素的labelnums_of_everylabel[now_label]++; float scale = now_label * 1.0f / all_labels;//-------【開始處理每個像素】---------------draw_img.at<Vec3b>(j, i)[0] = 255 - 255 * scale; //B通道draw_img.at<Vec3b>(j, i)[1] = 128 - 128 * scale; //G通道draw_img.at<Vec3b>(j, i)[2] = 255 * scale; //R通道//-------【處理結束】---------------}}std::cout << "初步結論 : " << std::endl;for (int i = 1;i <= next_label;i++){std::cout << "labels : " << i<<"像素個數 " << nums_of_everylabel[i] <<std::endl;}std::cout << "最后結論 : " << std::endl;std::cout << "截斷像素數目 : " << nums << std::endl;for (int i = 1;i <= next_label;i++){if (nums_of_everylabel[i] <= nums){all_labels--;}}std::cout << "labels : " << all_labels << std::endl;}int main()
{Mat flag_img;Mat draw_img;Mat srcImage = imread("D:\\opencv_picture_test\\閾值處理\\硬幣.png", 0); //讀入的時候轉化為灰度圖//Mat srcImage = imread("D:\\opencv_picture_test\\閾值處理\\黑白.jpg", 0); //讀入的時候轉化為灰度圖namedWindow("原始圖", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口imshow("原始圖", srcImage);cout << "srcImage.rows : " << srcImage.rows << endl; //308cout << "srcImage.cols : " << srcImage.cols << endl; //372Mat dstImage;dstImage.create(srcImage.rows, srcImage.cols, CV_8UC1);//閾值處理+二值化My_artificial(&srcImage, &dstImage, 84);// flag_img = cv::Mat::zeros(src.size(), src.type());//cvtColor(src, src, COLOR_RGB2GRAY); //這一句很重要,必須保證輸入的是單通道的圖,否則所讀取的數據是錯誤的double time0 = static_cast<double>(getTickCount()); //記錄起始時間seed_Connected_Component_labeling(dstImage,flag_img,draw_img,255,4,500); //白色部分被標記time0 = ((double)getTickCount() - time0) / getTickFrequency();cout << "此方法運行時間為:" << time0 << "秒" << endl; //輸出運行時間imshow("dstImage", dstImage);namedWindow("draw_img", WINDOW_NORMAL);//WINDOW_NORMAL允許用戶自由伸縮窗口imshow("draw_img", draw_img);waitKey(0);return 0;
}
實現效果:
原圖:
二值圖(可以看到有幾個噪點,而且圖像的右邊和上邊是白色的,這是因為原圖我是截圖的,邊界并沒有剪裁好,這點在下面的連通域標記會有影響)
我給屬于不同連通域的物體涂上不同的顏色。
下面是打印出來的信息:初步得到的label是19個,其中label1就是我所說的截圖邊界問題。其他的幾個像素個數小的就是噪點。
通過設定門限,像素個數小于500的標簽物體我們將它視為噪聲。最后得到的label數目正好是10,也就是硬幣的數目。
發現的問題
連通域標記函數代碼部分,可以看到我還嘗試了其他兩種遍歷seed周圍元素的方式,分別是順時針和逆時針。但是運算速度沒有第一種快,至于原因我沒有深究。希望有心人能給我講解一波。此外,試了一下8鄰域,運算速度也得到了下降。
這就是我說的剪裁錯誤,嘿嘿。
此外,二值化的方法我是用的人工調整,原圖受到非均勻光線的照射,全局大津閾值得到的效果并不是很好,反而由于直方圖雙峰性比較明顯,迭代法看起來還不錯。不過為了連通域標記的時候能夠準確一點,我就用滑條調整閾值了。
滑動條調整閾值的代碼在這兒:https://blog.csdn.net/qq_42604176/article/details/104764731
迭代法、大津的代碼在這兒:https://blog.csdn.net/qq_42604176/article/details/104341126
3.15更新,加入形態學腐蝕操作
首先回顧之前遇到的問題:受到噪聲影響,十個硬幣竟然貼了19個labels,盡管利用限制像素個數的方法來限制,但這種方法有許多弊端。
這幾天學習了一些簡單的形態學操作,其中腐蝕操作有個作用:去除黏連像素以及噪聲。
這不就正好能解決之前遇到的問題嘛!
操作也很簡單,加上兩行代碼就行。
、
結果運行如下(把自己簡陋的限制像素函數去掉了)
效果很好啊!
關于腐蝕的詳細講解請看這邊:
https://blog.csdn.net/qq_42604176/article/details/104815801