在前面的報告中我們實現了用SURF算法計算目標在移動攝像機拍攝到的視頻中的位置。由於攝像機本身像素的限制,加之算法處理時間會隨着圖像質量的提高而提高,實際實驗發現在背景復雜的情況下,結果偏差可能會很大。
本次改進是預備在原先檢測到的特征點上加上某種限制條件,以提高准確率。
問題:如何判定檢測到的特征點是否是我們需要的點(也就是目標區域上的點)?
可行方案:用形態學找出目標的大致區域,然后對特征點判定。
特征點(SURF算法或者其他的算法)已有,我們來一步步實現找到目標大致區域。
下圖假設為視頻中的某一幀
我們要在這一幀中找出“停”字的大致區域(“停”的顏色和背景顏色可以酌情設置,並且和少量代碼相關,可以修改)。
目標被設置成了兩種顏色(后面的操作也是基於兩個通道R和B),原因是一種顏色太簡單以致不好分離通道然后計算,三種顏色往上也沒有必要,因為我們只計算兩通道,多了會增加計算時間。(其他情況留作進一步的討論)
1、分離通道
Mat img_scene = imread("image1.jpg"); //讀取圖像 resize(img_scene,img_scene,cvSize(0,0),0.2,0.2); //把圖像調整到合適的大小 vector<Mat> channels; split(img_scene, channels); // 分離色彩通道, 把一個3通道圖像轉換成3個單通道圖像 Mat img_scene_BlueChannel = channels.at(0); // 紅通道 Mat img_scene_GreenChannel = channels.at(1); // 綠通道 Mat img_scene_RedChannel = channels.at(2); // 藍通道
結果如下圖:
紅色通道
藍色通道
我們看到原圖的紅色部分在red channel中位深色,藍色部分在blue channel中為深色,這一步我們分離了目標的兩個部分,兩幅圖得背景看起來差別不大。
2、對圖像進行二值化
首先定義閾值
#define threshold_value_red 150 // 紅色通道閾值 #define threshold_value_blue 160 // 藍色通道閾值
二值化處理
Mat Seg_img_red; // 紅色通道閾值分割結果圖 Mat Seg_img_blue; // 藍色通道閾值分割結果圖 threshold(img_scene_RedChannel, Seg_img_red, threshold_value_red, 255, THRESH_BINARY); // 紅色通道進行閾值分割(大於閾值時候取255) threshold(img_scene_BlueChannel, Seg_img_blue, threshold_value_blue, 255, THRESH_BINARY); // 藍色通道進行閾值分割(小於閾值時候取255) imshow("Seg_img_red",Seg_img_red); imshow("Seg_img_blue",Seg_img_blue);
處理結果
二值化后圖像簡單化了,去除了冗余,值得高興的是目標看起來更突出了,目標區域似乎能夠檢測出來,但是背景也有大片干擾,下面就是用兩個通道的好處了。
3、合並圖像
這里我們注意到red channel中“停”字為黑色,背景是白色,而blue channel中剛好相反,於是想到試着對兩幅圖進行異或運算
Mat Object_img; bitwise_xor(Seg_img_red, Seg_img_blue, Object_img); // 求兩個圖像的交集獲取目標的潛在顏色區域 imshow("Object_img",Object_img);
結果
好的,看起來更好了,雖然背景還是有干擾(這也是無法避免的),目前我們已經使用上了兩個通道的信息,只能繼續往下尋找其他方法。
4、腐蝕圖像
圖像中有不少像胡椒粉一樣的噪聲,先用腐蝕去掉
erode(Object_img,Object_img,cv::Mat()); imshow("Object_img_erode",Object_img);
結果
清爽不少
5、計算連通域
vector<vector<Point> > contours; findContours(Object_img, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); Mat result(Object_img.size(), CV_8U, Scalar(0)); drawContours(result, contours, //畫出輪廓 -1, // 畫出所有的輪廓 Scalar(255), // 用白線畫出 2); // 輪廓線的粗細為2 namedWindow("Contours"); imshow("Contours", result); // 顯示圖像中所有的連通域輪廓
處理結果
有很多小的連通域,可以放心的先去掉
6、連通域去噪
方法一:
// 去除圖像中的連通域噪聲 int cmin = 50; // 最小的輪廓長度 int cmax = 1500; // 最大的輪廓長度 vector<vector<Point>>::const_iterator itc = contours.begin(); while (itc != contours.end()) { if (itc->size() < cmin || itc->size() > cmax) itc = contours.erase(itc); // 刪除當前連通域輪廓 else ++itc; } // 畫出去掉連通域噪聲后的連通域 Mat original(Object_img.size(), CV_8U, Scalar(0)); Mat result_hull(Object_img.size(), CV_8U, Scalar(0)); Mat threshold_output(Object_img.size(), CV_8U, Scalar(0)); drawContours(original, contours, -1, // 畫出所有的輪廓 Scalar(255), // 用白線畫出 1); // 輪廓線的粗度為2 namedWindow("Contours noise reduced"); imshow("Contours noise reduce", original); // 畫出去掉連通域噪聲后的連通域
處理結果(輪廓長度參數表示我們要選取適當的連通域,將范圍縮小可以在單張圖片中取得更好的結果,例如將cmin=400直接就有
但是為了在視頻中處理要將范圍適當放寬),結果如下:
看起來離目標不遠了。
方法二:
方法一使用了人為設置的參數,但在視頻處理中要盡量避免這種做法,方法二將得到的連通域按邊界像素數量進行排序,然后選取邊界像素數量最大的contours.size()*1/n數量的連通域(n按實際幀大小和攝像機距離目標距離等因素選取適當的值)。
int *ca = new int[contours.size()]; //定義連通域邊界像素排序數組 itContours = contours.begin(); for (int i = 0;i < contours.size(), itContours != contours.end();++i, ++itContours) { ca[i] = itContours->size(); } sort(ca, ca + contours.size()); //按連通域邊界像素的多少排序 int s = contours.size()/10; int threshhold_con = ca[contours.size() - s]; vector<vector<Point>>::const_iterator itc = contours.begin(); while (itc != contours.end()) { if (itc->size() < threshhold_con) itc = contours.erase(itc); // 刪除當前連通域輪廓 else ++itc; }
方法二結果
看起來似乎沒什么差別,算法復雜了一點,但為了最后在視頻中處理,我們選用了第二種方法。
7、計算連通域的凸包絡、填充包絡
Mat result_hull(Object_img.size(), CV_8U, Scalar(0)); Mat threshold_output(Object_img.size(), CV_8U, Scalar(0)); /// 對每個輪廓計算其凸包 vector<vector<Point> >hull( contours.size() ); for( int i = 0; i < contours.size(); i++ ) { convexHull( Mat(contours[i]), hull[i], false ); } /// 繪出輪廓及其凸包 Mat drawing = Mat::zeros( threshold_output.size(), CV_8UC3 ); for( int i = 0; i< contours.size(); i++ ) { Scalar color = Scalar( rng.uniform(0, 255), rng.uniform(0,255), rng.uniform(0,255) ); drawContours( drawing, contours, i, color, 1, 8, vector<Vec4i>(), 0, Point() ); drawContours( drawing, hull, i, color, 1, 8, vector<Vec4i>(), 0, Point() ); fillConvexPoly(result_hull,&hull[i][0],hull[i].size(),Scalar(255,0,0)); }
處理結果
實際上到這里,如果我們的把落入上圖白色區域的特征點認為是正確的點已經能夠減少一部分誤差了,但本着精益求精的精神,還是應該再好一點。
8、計算連通域的外接矩形
這一步為可選項,可以看到前面一張圖中的目標是近似正方形的,實際我們所用的目標就是正方形的,那么在攝像機離目標中心法線偏離角度不是太離譜的情況,得到的目標圖像的外接矩形的長寬比的變化可以表示為一個固定的范圍,這里思路是,計算所有連通域外接矩形長寬比(用短的比長的),得到一組0~1的浮點數,排序后和第七步一樣選取適當的百分比,篩選長寬比靠近1的外接矩形和對應的連通域,代碼如下:
Rect *r = new Rect[contours.size()];//定義外接矩形數組 double *ra = new double[contours.size()]; //定義外接矩形長寬比數組 double *rb = new double[contours.size()]; Mat obj_rec = Mat::zeros( threshold_output.size(), CV_8UC3 ); for( int i = 0; i < contours.size(); i++ ) { convexHull( Mat(contours[i]), hull[i], false ); r[i] = boundingRect(Mat(contours[i]));//boundingRect獲取這個外接矩形; rb[i] = ra[i] = rate(r[i].width,r[i].height); //計算長寬比 rectangle(obj_rec, r[i], Scalar(255), 2); } sort(ra,ra+contours.size()); //將外接矩形長寬比排序 int k = contours.size()/3; double threshhold_rate = ra[contours.size() - k]; //定義外接矩形長寬比閾值 //繪制通過長寬比閾值限制后的外接矩形 Mat obj_rec_thr = Mat::zeros( threshold_output.size(), CV_8UC3 ); itContours = contours.begin(); for( int i = 0; i < contours.size(), itContours != contours.end(); ++i) { if(rb[i]>threshhold_rate) { rectangle(obj_rec_thr, r[i], Scalar(255,0,0), 2); ++itContours; } else itContours = contours.erase(itContours); }
篩選前外接矩形:
篩選后連通域外接矩形:
相應的連通域圖:
只剩下一點噪聲了。
9、去掉最后的噪聲
第三步中合並圖像我們使用的是異或(xor),也就是同一個位置像素在兩個通道中值相同和不同這兩種結果在目標圖像中以黑和白區分開來了,我們這個例子中目標圖像白色區域是為兩個通道相同像素點值不同的結果。也就是說,前一張圖中的兩塊連通域中的像素在第二步閾值處理后的兩張圖中相應位置的像素值剛好不同。於是我們將得到的兩個連通域的外接矩形設置為興趣區,我們得到兩個興趣區,以第一個興趣區的位置和大小在閾值處理后的圖像上分別截取對應的區域(做這步我們要將
threshold(img_scene_RedChannel, Seg_img_red, threshold_value_red, 255, THRESH_BINARY); // 紅色通道進行閾值分割(大於閾值時候取255) threshold(img_scene_BlueChannel, Seg_img_blue, threshold_value_blue, 255, THRESH_BINARY); // 藍色通道進行閾值分割(小於閾值時候取255)
中的255改為1,方便累加處理),又得到兩塊區域,分別求像素和sum_red,sum_blue,求比值(小的比大的),我們得目標區域是既有藍色的又有紅色的(比值可通過自己設計目標圖形和顏色修改),噪聲區域是不一定的,我們只保留比值大於0.2的,太小的就不要了。
實現代碼
for( int i = 0; i < contours.size(), itContours != contours.end(); ++i) { if(rb[i]>threshhold_rate) { rectangle(obj_rec_thr, r[i], Scalar(255,0,0), 2); // ++itContours; Mat imageROI_red = Seg_img_red(cv::Rect(r[i].x, r[i].y, r[i].width, r[i].height)); Mat imageROI_blue = Seg_img_blue(cv::Rect(r[i].x, r[i].y, r[i].width, r[i].height)); long long int sum_red = 0, sum_blue = 0; int nr=imageROI_red.rows; int nc=imageROI_red.cols; // outImage.create(image.size(),image.type()); if(imageROI_red.isContinuous()) { nr=1; nc=nc*imageROI_red.rows*imageROI_red.channels(); } for(int i=0;i<nr;i++) { const uchar* Data_red=imageROI_red.ptr<uchar>(i); const uchar* Data_blue=imageROI_blue.ptr<uchar>(i); // uchar* outData=outImage.ptr<uchar>(i); for(int j=0;j<nc;j++) { sum_red += *Data_red; sum_blue += *Data_blue; // *outData++=*inData++/div*div+div/2; } } double pixel_sum_rate = rate((double)sum_red, (double)sum_blue); cout << sum_red << "," << sum_blue << endl; cout << pixel_sum_rate << endl; if(pixel_sum_rate < 0.2) itContours = contours.erase(itContours); else ++itContours; imshow("imageROI_red", imageROI_red); imshow("imageROI_blue", imageROI_blue); } else itContours = contours.erase(itContours); }
代碼是直接在第八步中插入的,結果如下:
通過輸出中間值可看到
第一個外接矩形區域sum_red=0,sum_blue=420,比值為0;
第二個外接矩形區域sum_red=12317,sum_blue=9153,比值為0.743119;
10、結論
我們已經成功找到目標所在區域,實現了預期的效果,用這個區域去限制SURF特征點預計可以得到更加精確的結果。