前言
在自動計算圖像中有幾枚硬幣的任務中,分離出前景和背景後是否就可以馬上實現自動計件,如果可以,如何實現?如果不可以,爲什麼?
答案是否定的。二值化之後我們的得到的只是前景總像素的多少,並不知道哪些像素屬於同一枚硬幣。想要實現自動計件功能還需要用到連通域標記的知識。
連通域標記的方法這裏我們使用種子填充法:
算法步驟:
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;
}
實現效果:
原圖:
二值圖(可以看到有幾個噪點,而且圖像的右邊和上邊是白色的,這是因爲原圖我是截圖的,邊界並沒有剪裁好,這點在下面的連通域標記會有影響)
我給屬於不同連通域的物體塗上不同的顏色。
下面是打印出來的信息:初步得到的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