連通域標記——實現硬幣自動計件

前言

在自動計算圖像中有幾枚硬幣的任務中,分離出前景和背景後是否就可以馬上實現自動計件,如果可以,如何實現?如果不可以,爲什麼?
答案是否定的。二值化之後我們的得到的只是前景總像素的多少,並不知道哪些像素屬於同一枚硬幣。想要實現自動計件功能還需要用到連通域標記的知識。
連通域標記的方法這裏我們使用種子填充法:
方法

算法步驟:

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);		//當前像素的label
			nums_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;		//308
	cout << "srcImage.cols : " << srcImage.cols << endl;		//372
	Mat 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;
}

實現效果:

原圖:
1
二值圖(可以看到有幾個噪點,而且圖像的右邊和上邊是白色的,這是因爲原圖我是截圖的,邊界並沒有剪裁好,這點在下面的連通域標記會有影響)
2
我給屬於不同連通域的物體塗上不同的顏色。
3
下面是打印出來的信息:初步得到的label是19個,其中label1就是我所說的截圖邊界問題。其他的幾個像素個數小的就是噪點。
通過設定門限,像素個數小於500的標籤物體我們將它視爲噪聲。最後得到的label數目正好是10,也就是硬幣的數目。
4

發現的問題

連通域標記函數代碼部分,可以看到我還嘗試了其他兩種遍歷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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章