黑夜給我了黑色眼睛:OpenCV識別黑色矩形
在計算機圖形領域,識別特定形狀是一項基本功能,但從初學者的角度來說,這一切並不容易,接下來我將為大家詳細介紹如何利用OpenCV實現簡單的顏色+形狀識別。
一、目標介紹。
實現一個這樣的程序,能夠將識別攝像頭的實時視頻流中黑色矩形,並將其標記出來
二、思路分解以及代碼實現。
具體實現思路是這樣的:
1、抓取電腦攝像頭的實時視頻流,將其一幀用Mat類存儲起來,利用while循環就可以將視屏流一幀幀地放入Mat,然後針對每一幀的圖片進行處理。這樣我們就將處理視頻的任務轉化為處理圖片。
while(1){Mat video; capture >> video;}
2、將圖片(Mat Video)中黑色的區域分離出來,用一個二值圖(值只有0和255,即黑和白)來儲存黑色區域的信息,其中值為255(顯示為白色)的點代表黑色。具體的執行細節如下:
a、將圖片由RGB色域轉化為HSV色域。為什麼?要知道,光照情況會影響顏色,比如白色的物體在光線較暗時,會被電腦識別為黑色。而使用HSV色域可以極大地減少光照對顏色識別的影響。OpenCV提供了cvtColor()這個函數處理此情況。
關於HSV:https://zh.wikipedia.org/wiki/HSL和HSV色彩空間
Mat hsv;cvtColor(frame,hsv,COLOR_BGR2HSV);
b、利用inRange()函數分離出圖片的黑色部分
Mat blkobj;inRange(hsv,Scalar(0,0,0),Scalar(180,255,46),blkobj);
圖片的黑色部分就已經儲存在了Mat blkobj 里。
3、圖片的黑色區域已經得到了,那麼我們該如何在其中找出矩形呢?
這裡提供兩個思路
思路一:化繁為簡,不妨先在圖片中找出直線,如果存在直線能夠拼成一個矩形,那麼我們就判定圖片中存在矩形,並把它標記出來。
思路二:具體分三個步驟
I、找出圖片中最大的黑色物體(當然,你也可以選擇把每個黑色物體都找出來)
II、找出其最小的外接矩形,然後求出最大黑色物體的面積與最小外接矩形面積的比值
III、如果比值足夠接近1,說明這個黑色物體是矩形
思路一網上已經有人實現過了,但似乎只適用於識別簡單的圖片,用於視頻識別的效果不佳。小編也自己實現了一遍,發現該思路存在著許多問題:
問題一:使用霍夫變換HoughLIne()尋找直線,效果不佳,對畫面質量敏感。
問題二:演算法實現較為複雜,小編實現出來後,未經優化演算法的複雜度為O(n^4),如果線的數量n一多,視頻就會卡死。
問題三:找出來的直線即使拼成矩形,也不一定是黑色矩形所在的位置。
思路一問題太多,果斷放棄。
思路二也存在一些問題待解決,但實現的可能還是較高的。
問題一:如何找出圖片中最大的黑色物體?(或者說,如何將圖片的黑色物體一個個分離出來?)
小編利用的方法是:尋找連通域
什麼意思呢?
黑色物體在識別出來的二值圖上是這樣的:
也就是說,一個黑色物體二值圖上表現為:互鄰,且值為255的點。
具體的代碼實現如下,作用是:一旦找到一個值為255的點,那麼就從上下左右進行搜尋,搜尋一個標記一個(這樣就可以不走回頭路了)直到無法搜尋為止,期間將所有值為255的點都放在一個vector<Point> Chunk(元素為二維點的一維數組) 中。這樣一來,每個Chunk(數組)就代表著一個黑色物體。
void MazeTrack(int r,int c)//search for the Biggest connected area{ Maze.at<uchar>(r,c)=0; Point *p=new Point(c,r);//Attention! int Point class, x means cols, y means rows; Chunk.push_back(*p); delete p; //** if adjacent point is not black or unmarked, end the function. if(Maze.at<uchar>(r-1,c)==0&&Maze.at<uchar>(r+1,c)==0&&Maze.at<uchar>(r,c-1)==0&&Maze.at<uchar>(r,c+1)==0) return; //** if any of adjacent point is black and unmarked, recurse. if((r-1)>=0 && Maze.at<uchar>(r-1,c)==255)//up(x-1,y) { MazeTrack(r-1,c); } if((r+1)<Maze.cols && Maze.at<uchar>(r+1,c)==255)//down(x+1,y) { MazeTrack(r+1,c); } if((c-1)>=0 && Maze.at<uchar>(r,c-1)==255)//left(x,y-1) { MazeTrack(r,c-1); } if((c+1)<Maze.cols && Maze.at<uchar>(r,c+1)==255)//right(x,y+1) { MazeTrack(r,c+1); }}
問題二:給定一些點,如何尋找其最小外接矩形?
這真是一個不簡單的問題,不過好在OpenCV就有函數能幫我們解決:
//** Use RotatedRect find the minAreaRect of ChunkMax Point2f vertex[4]; RotatedRect box=minAreaRect(ChunkMax); box.points(vertex);//the vertex of rectangle is stored in vertex[4]
vertex[4]里放著最小外接矩形的4個頂點。
好啦,剩下的問題就簡單多了
問題三:求面積之比。
對於最大的黑色物體,其面積就是像素的個數,即Chunk.size()。對於最小外接矩形,其面積......我不知道怎麼求,不過好在不需要我教你對吧。
問題四:畫出矩形。
利用line()函數具體實現看代碼。
好啦,思路理清了,咱們看代碼吧:
三、完整代碼
PS:由於Mazetrack()這個函數是針對像素點的遞歸函數,在解析度過高的時候,遞歸次數驚人,程序會異常退出。故小編在數據處理時將解析度都除以了6,最後標記矩形的時候又將這個6乘了回來。所以,這個演算法是有待進一步優化的。
PS:注釋應該是夠詳盡的,更多建議請留言。
#include <iostream>#include <opencv2/opencv.hpp>#include <opencv2/highgui.hpp>#include <vector>#include <cmath>using namespace std;using namespace cv;Mat Maze;vector<Point>Chunk;void MazeTrack(int r,int c);int main(int argc, const char * argv[]) { VideoCapture capture(0); while(1) {//My Code Chunk#if 1 //** capture the video Mat video; capture >> video; Mat frame; resize(video, frame, Size(video.cols/6,video.rows/6)); imshow("Original", frame); //** convert to HSV Mat hsv; cvtColor(frame,hsv,COLOR_BGR2HSV); //** Find the BLACK Area Mat blkobj; Mat element = getStructuringElement(MORPH_RECT, Size(3,3 )); //morphologyEx(hsv, hsv, MORPH_OPEN,element); //morphologyEx(hsv, hsv, MORPH_CLOSE,element); inRange(hsv,Scalar(0,0,0),Scalar(180,255,46),blkobj); imshow("BlackArea", blkobj); //** Find the BIGGEST black area Maze=blkobj.clone(); vector<vector<Point> >Chunks; vector<Point>ChunkMax; int rows = Maze.rows; int cols = Maze.cols; for(int i = 0; i < rows; i++) { for(int j = 0; j < cols; j++) { if(Maze.at<uchar>(i,j)==255)//at<>(rows,cols); { MazeTrack(i,j); Chunks.push_back(Chunk); Chunk.clear(); vector <Point>().swap(Chunk); } } } long max=0; for(int n=0;n<Chunks.size();n++) { if(max<Chunks[n].size()) { max=Chunks[n].size(); ChunkMax.swap(Chunks[n]); } } Chunks.clear(); vector <vector<Point> >().swap(Chunks); //** Use RotatedRect find the minAreaRect of ChunkMax Point2f vertex[4]; RotatedRect box=minAreaRect(ChunkMax); box.points(vertex);//the vertex of rectangle is stored in vertex[4] //** use the coordinate of vertex to get the high and the length of minAreaRectangle double rectl=sqrt(pow(vertex[0].x-vertex[1].x,2)+pow(vertex[0].y-vertex[1].y,2)); double recth=sqrt(pow(vertex[1].x-vertex[2].x,2)+pow(vertex[1].y-vertex[2].y,2)); double area=recth*rectl; //** if the ratio of ChunkMax.size() : Area of rectangle is high enough, judge it to be rectangle double ratio=double(ChunkMax.size())/area; cout<<ratio<<endl; if(ratio>=0.75) { for(int i=0;i<4;i++) { line(video,6*vertex[i],6*vertex[(i+1)%4],Scalar(255,0,0),2,8); } } resize(frame, frame, Size(frame.cols*2,frame.rows*2)); imshow("Rect", video); waitKey(50);#endif }}void MazeTrack(int r,int c)//search for the Biggest connected area{ Maze.at<uchar>(r,c)=0; Point *p=new Point(c,r);//Attention! int Point class, x means cols, y means rows; Chunk.push_back(*p); delete p; //** if adjacent point is not black or unmarked, end the function. if(Maze.at<uchar>(r-1,c)==0&&Maze.at<uchar>(r+1,c)==0&&Maze.at<uchar>(r,c-1)==0&&Maze.at<uchar>(r,c+1)==0) return; //** if any of adjacent point is black and unmarked, recurse. if((r-1)>=0 && Maze.at<uchar>(r-1,c)==255)//up(x-1,y) { MazeTrack(r-1,c); } if((r+1)<Maze.cols && Maze.at<uchar>(r+1,c)==255)//down(x+1,y) { MazeTrack(r+1,c); } if((c-1)>=0 && Maze.at<uchar>(r,c-1)==255)//left(x,y-1) { MazeTrack(r,c-1); } if((c+1)<Maze.cols && Maze.at<uchar>(r,c+1)==255)//right(x,y+1) { MazeTrack(r,c+1); }}
四、效果展示
https://www.zhihu.com/video/920698725767188480
五、更大的世界
你們喜歡看書嘛~愛書人士快快關注小編的讀書公眾號:林木蔚然讀書會
推薦閱讀:
※關於MFC是否out了的問題?
※每天一練P6-Python和OpenCV做圖像處理(Canny)
※opencv和pcl的區別?
※[171031] Python OpenCV 圖像處理專欄開通啦
※1.26【OpenCV圖像處理】模板匹配