目標分割和檢測筆記(OpenCV例項精解)
版本說明:Opencv 3.2.0版本
一.預處理
1.去噪聲
根據噪聲的種類選擇合適的濾波器進行去除。
2.去除光亮
需從場景中的其他影象提取位於完全相同位置,沒有任何物件,並且具有相同光照條件的影象。然後用一種簡單的數學運算,刪除光這個模式:
1)差分
2)除法
影象差分是最簡單的方法。如果有光紋矩陣L和影象矩陣I,去除R的結果是他們之間的差值:
R=L-I
除法去除R的結果是
R=255×(1-I/L)
下面給出差分程式碼 img為需要刪除光的影象,pattern位光紋遮擋
Mat removeLight(Mat img,Mat pattern)
{
Mat result;
result= pattern-img;//R=L-I
return result;
}
下面是除法
Mat removeLight(Mat img,Mat pattern)
{
Mat result,img32,pattern32;
img.covertTo(img32,CV_32F);
pattern.convertTo(pattern32,CV_32F);
//影象除以模式
result=1-(img32/pattern32);
result=result*255;//縮放以轉換為8位模式
result.convertTo(result,CV_8U);//轉換為8位模式
return result;
}
注意 :除法需要用32位浮點型才能分離影象。
可以用blur原圖來估計背景。估計背景的方法如下:
在輸入影象上使用大尺寸核矩陣模糊化,這是一種在OCR中常用的技術。雖然與上述方法存在差異,但足以消除背景類。函式如下:
Mat calculateLightPattern(Mat img)
{
Mat pattern;
blur(img,pattern,Size(img.cols/3,img.rows/3));
return pattern;
}
下面給出自己根據上述函式做的效果圖。這是用差分法後的去光效果:
下面是用除法的
可以發現兩種方式都能有效的把背景光去掉。注意,我可是開的手電筒哦。。
3. 閥值操作
刪除背景後,還需要能在為來進行影象分割的二值影象。
threshold 全域性二值化
一幅影象包括目標物體、背景還有噪聲,要想從多值的數字影象中直接提取出目標物體,最常用的方法就是設定一個全域性的閾值T,用T將影象的資料分成兩部分:大於T的畫素群和小於T的畫素群。將大於T的畫素群的畫素值設定為白色(或者黑色),小於T的畫素群的畫素值設定為黑色(或者白色)。
這裡可以使用兩個不同閥值的threshold函式:從圖片中可以看出非興趣區域是黑色或者很低值,刪除光/背景時,可以使用一個低值.
下面是閥值化後的效果
可以看出大部分感興趣區域已經亮起來啦,這裡的閥值需要自己根據實際慢慢調整。。當然我這個興趣區域太大啦而且是在晚上室內燈下做的,可能效果不好。
double threshold( InputArray src, OutputArray dst, double thresh, double maxval, int type )
引數說明:
src:輸入影象。
dst:輸出影象。
thresh:閥值T。
maxval:向上最大值
type:型別
詳細解釋大家可以看看這位大佬總結的http://blog.csdn.net/guduruyu/article/details/68059450
二.分割
這裡介紹了用於分割閥值影象的兩種技術:
1)連通區域
2)findContours函式
1.連通區域演算法
連通區域是分割和識別二進位制影象部分的常用演算法。
連通域是使用8或者4連結畫素標記影象的迭代演算法。
4連通:只有橫向和豎向連通。
8連通:在4連通的基礎上加上了對角連通。
OpenCV3中提供有下面兩個不同功能的連通區域演算法:
1)connectedComponents(image,labels,connectivity=8,type=CV_32S)
2)connectedComponentsWithStats(image,labels,stats,centroids,connectivity=8,ltype=CV_32S)
這兩個函式返回一個表示檢測到幾個標籤的整數,標籤0表示背景,1表示第一個物件,然後依次類推。
connectedComponents的引數說明:
Image :待標記的輸入影象
Label :這是一個Mat,是輸出,其輸出一個與輸入大小相同的影象,每個畫素都有各自標籤值,0代表背景,1代表連通區域的第一個物件,2代表第二個物件,以此類推。
Connectivity:有兩個可能值,4或者8。分別表示4連通方式還是8連通方式。
Type:這是想要使用的標籤影象型別:只允許兩種型別,CV_32S或CV_16U.
connectedComponentsWithStats的引數說明:
Image :待標記的輸入影象
Label :這是一個Mat,是輸出,其輸出一個與輸入大小相同的影象,每個畫素都有各自標籤值,0代表背景,1代表連通區域的第一個物件,2代表第二個物件,以此類推。
Connectivity:有兩個可能值,4或者8。分別表示4連通方式還是8連通方式。
Type:這是想要使用的標籤影象型別:只允許兩種型別,CV_32S或CV_16U.
Stats :包括背景標籤在內的所有標籤的輸出引數。以下統計值可以通過統計資料(標籤。列)訪問,列也同樣定義,具體如下:
××××××CC_STAT_LEFT:這是連通區域物件的左邊x座標
××××××CC_STAT_TOP:這是連通區域物件的頂層y座標
××××××CC_STAT_WIDTH:這定義連通區域物件邊框的寬度
××××××CC_STAT_HEIGHT:這定義連通區域物件邊框的高度
××××××CC_STAT_AREA:這是連通區域物件的畫素(區)數量
Centroids:每個標籤(包含背景)的浮點型質心點。
下面給出函式,就不給圖了(總不能把我自己分割類吧),書上是針對一些零件分割的,自行想象上面已出的圖,效果應該不錯。
void ConnectedComponents(Mat img)
{
Mat lebels;
//檢測到連通區域的數目給lebels
int num_objects=connectedComponents(img,labels);
if(num_objects<2)//1的時候應該是隻有背景(個人理解)
{
cout<<"No object detected"<<endl;
return;
}
else
{
cout<<"Number of objects detected:"<<num_objects-1<<endl;
}
//建立彩色目標輸出影象
Mat output=Mat::zeros(img.rows,img.cols,CV_8UC3);//3通道8位uchar 0矩陣
RNG rng(0xFFFFFFFF);//RNG為隨機數生成器類
for(int i=1;i<num_objects;i++)
{
Mat mask=(labels==i);//A
output.setTo(randomColor(rng),mask);//B
}
imshow("result",output);
}
A:此處括號是我自己加的 便於理解,個人認為labels是mat類,裡面存以不同標籤號的圖
B:randomColor其實就是將隨機數/255的作用。 setTo(x,mask)自己理解為只處理mask,即矩陣不為0的地方,並將x得到的三通到顏色給予mask
下面用connectedComponentsWithStats()來顯示更多資訊的輸出結果影象。
下面是其函式:
void ConnectedComponentsStats(Mat img)
{
//連通區域統計資訊的三個量
Mat labels,stats,centroids;
int num_objects=connectedComponentsWithStats(img,labels,stats,centroids);
//檢查檢測到連通域的數目
if(num_objects<2)//1的時候應該是隻有背景(個人理解)
{
cout<<"No object detected"<<endl;
return;
}
else
{
cout<<"Number of objects detected:"<<num_objects-1<<endl;
}
Mat output=Mat::zeros(img.rows,img.cols,CV_8UC3);
RNG rng(0xFFFFFFFF);//RNG為隨機數生成器類
for(int i=1;i<num_objects;i++)
{
cout<<"Object"<<i<<"with pos:"<<centroids.at<Point2d>(i)<<"with area"<<stats.at<int>(i,CC_STAT_AREA)<<endl;//A
Mat mask=(labels==i);
output.setTo(randomColor(rng),mask);
//使用區域繪製文字
stringstream ss;//新增統計區域資訊
ss<<"area:"<<stats.at<int>(i,CC_STAT_AREA);
putText(output,ss.str(),centroids.at<Point2d>(i),FONT_HERSHEY_SIMPLEX,0.4,Scalar(255,255,255));
//在output上描述資訊(這裡利用了putText函式)
}
imshow("result",output);
}
A: centroids.at < Point2d > (i)表示第i個連通域的質心 stats.at < int>(i,CC_STAT_AREA)為連通域畫素的數量。
2.查詢輪廓(findContours演算法)
其函式宣告如下:
void findContours(InputOutputArray image, OutputArrayofArrays contours, OutputArray hierarchy, int mode,int method,Point offset=Point())
每個引數的含義如下:
image:二進位制輸入影象。
contours:輸出輪廓,每個檢測出來的輸出輪廓是點向量,通常用vector < vector< Point> >型別來儲存。
hierarchy:儲存輪廓層次結構的可選輸出向量,可以得到每個輪廓之間的關係的影象拓撲。具體可以在learning Opencv 裡面看。
mode:檢測輪廓的方法:
××××××RETR_EXTERNAL:檢測外部輪廓
××××××RETR_LIST:檢索沒有建立層次結構的輪廓
××××××RETR_CCOMP:檢索有兩個級別的層次結構的所有輪廓:外部和孔,如果另一個物件在一個洞裡,那麼將其放在層次結構的頂層。
method:檢測輪廓形狀的近似方法:
××××××CV_CHAIN_APPROX_NONE:這並不適用於近似任何輪廓和儲存所有的輪廓點。
××××××CV_CHAIN_APPROX_SIMPLE:這壓縮儲存水平,垂直和對角線段的起始點和結束點。例如,矩形就只會有4個角點。
××××××CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS:這適用於Teh-Chin chain近似演算法。
offset:用於轉移所有輪廓的可選點。當ROI工作中,這是非常有用的,而且需要檢索全域性位置。
這裡再給出與之成對使用的函式DrawContours:
void drawContours(InputOutputArray image, InputArrayOfArrays contours, int contourIdx, const Scalar& color, int thickness=1, int lineType=8, InputArray hierarchy=noArray(), int maxLevel=INT_MAX, Point offset=Point() )
image:用來繪製輪廓的輸出影象
contours:輪廓向量一般與findContours裡面一致
contourIdx:指示輪廓繪製的數字。即繪製contours裡面的第幾個輪廓,若為負數,則全部繪出
Scalar& color:繪製輪廓的顏色
thickness:輪廓線的粗細。若為負數則填充整個輪廓內部。
lineType:線型
hierarchy:繪製輪廓層次結構的可選輸出向量,可以得到每個輪廓之間的關係的影象拓撲。具體可以在learning Opencv 裡面看。
maxLevel:可選引數,當層次結構引數可用時,可對他進行設定。若設定為0,只繪製指定輪廓;若設定為1,這個函式繪製當前輪廓及巢狀;若為2,演算法繪製所有指定輪廓層次。
offset:可選引數,改變輪廓。
下面給出程式碼及個人理解:
void FindContoursBasic(Mat img)
{
vector<vector<Point> > contours;//點集陣列.那個空格必須有,不然會被讀成>>。
findContours(img,contours,RETR_EXTERNAL,CHAIN_APPROX_SIMPLE);
Mat output=Mat::zeros(img.rows,img.cols,CV_8UC3);//定義繪製板
if(contours.size()==0)
{
cout<<"No objects founded"<<endl;
return -1;
}
else
{
cout<<"objects number:"<<contours.size()<<endl;
}
RNG rng(0xFFFFFFFF);//隨機數
for(int i=0;i<contours.size(),i++)
{
drawContours(output,contours,i,randomColor(rng));//依次將輪廓繪製到output上
}
imshow("Output",output);
}