Learn OpenCV之Convex Hull
這篇文章講的是如何尋找給出的點集的凸包(Convex Hull),先簡單介紹演算法原理,之後利用OpenCV實現一個找凸包的程式。
什麼是凸包(Convex Hull)?
這個問題可以分成兩個概念理解,Convex 和 Hull
凸形狀(Convex object)就是沒有大於180°的內角的形狀。不是凸形狀的稱為非凸(Non-Convex)或者凹的(Concave)。下圖就是凸形狀影象和凹形狀影象的例子。
包(Hull)可以理解為一個物體的外形。
因此,凸包就是圍繞點或形狀的緊密擬合的凸邊界。
上圖中兩個影象的凸包如下圖所示。一個凸形狀的凸包就是其邊界,而一個凹邊形的凸包就是一個能緊密包圍該凹邊形的凸邊界。
Gift Wrapping Algorithms
給定一個點集,如何找出該點集的凸包?找凸包的演算法稱為Gift Wrapping Algorithms。有個YOUTUbe視訊(打不開的話進原文觀看)通過動畫形式講述了幾個尋找凸包的演算法。
表面看起來簡單的演算法,如果考慮上一些約束的話,事情就會變得不那麼簡單了。比如考慮演算法的時間複雜度。
拿Jarvis March演算法來說,時間複雜度為O(nh),其中n是輸入的點集總數,h是凸包上的點的個數。另外一個演算法Chan’s algorithm複雜度為O(nlogh)。
是否有複雜度為O(n)的演算法?答案是有的。但是在這些
第一個O(n)演算法是Sklansky在1972年發表。之後被證明是錯的。在1972-1989幾年中,有16個O(n)演算法發表,但是有7個被證明是錯的。更令人尷尬的是OpenCV實現的尋找凸包演算法(Sklansky (1982))。也被證明是錯的。
但是不要方。OpenCV實現的這個演算法仍是一個很流行的演算法而且在絕大多數情況下輸出是正確的。下面來看看如何使用OpenCV來尋找凸包。
利用OpenCV尋找凸包
通過上面描述我們知道Gift Wrapping algorithms的目的是尋找點集的凸包。
那麼我們如何將這個演算法在影象中使用呢?
方法如下:先將一張圖片二值化,然後找到影象中的輪廓,最後找出各個輪廓的凸包。下面一步一步通過程式碼來講解:
Step 1:讀入圖片
Python
src = cv2.imread("sample.jpg", 1) # read input image
C++
Mat src;
src = imread("sample.jpg", 1); // read input image
Step 2: 二值化影象
二值化影象有下面三步:
- 講圖片轉化為灰度影象
- 通過blur函式去除一些噪點
- 將灰度影象二值化
程式碼如下:
Python
gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) # convert to grayscale
blur = cv2.blur(gray, (3, 3)) # blur the image
ret, thresh = cv2.threshold(blur, 50, 255, cv2.THRESH_BINARY)
C++
Mat gray, blur_image, threshold_output;
cvtColor(src, gray, COLOR_BGR2GRAY); //convert to grayscale
blur(gray, blur_image, Size(3, 3)); // apply blur to grayscaled image
threshold(blur_image, threshold_output, 50, 255, THRESH_BINARY); //apply binary thresholding
三步的輸出分別如下圖所示
Step 3: 使用findContour尋找輪廓
下面我們使用OpenCV的findContour函式找到二值影象中的所有輪廓。你可能會問為什麼不適用邊緣檢測,邊緣檢測出的只是每個邊緣的位置,而findContour函式返回的是每個輪廓為集合的一個列表,這是我們需要的。
Python
# Finding contours for the threshold image
im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
C++
vector<vector<Point> > contours; // list of contours points
vector<Vec4i> hierarchy;
// find contours
findContours(threshold_output, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0))
結果如下
Step 4:使用convexHull函式找輪廓的凸包
找凸包的函式使用方法如下
Python
# create hull array for convex hull points
hull = []
#calculate points for each contour
for i in range(len(contours)):
#creating convex hull object for each contour
hull.append(cv2.convexHull(contours[i], False))
C++
// create hull array for convex hull points
vector<vector<Point> > hull(contours.size());
for(int i = 0; i < contours.size(); i++)
convexHull(Mat(contours[i]), hull[i], False);
Step 5:顯示凸包
因為凸包也是一種輪廓,所以可以使用OpenCV中的drawContours函式畫出來。
Python
# create an empty black image
drawing = np.zeros((thresh.shape[0], thresh.shape[1], 3), np.uint8)
# draw contours and hull points
for i in range(len(contours)):
color_contours = (0, 255, 0) # green - color for contours
color = (255, 0, 0) # blue - color for convex hull
# draw ith contour
cv2.drawContours(drawing, contours, i, color_contours, 1, 8, hierarchy)
# draw ith convex hull object
cv2.drawContours(drawing, hull, i, color, 1, 8)
C++
// create a blank image (black image)
Mat drawing = Mat::zeros(threhold_output.size(), CV_8UC3)
for(int i = 0; i < contours.size(); i++){
Scalar color_contours = Scalar(0, 255, 0); // green - color for contours
Scalar color = Scalar(255, 0, 0); // blue - color for convex hull
// draw ith contour
drawContours(drawing, contours, i, colot_contours, 1, 8, vector<Vec4i>(), 0, Point());
// draw ith contour
drawContours(drawing, hull, i, color, 1, 8, vector<Vec4i>(), 0, Point());
}
最終輸入如下圖所示
關於凸包的應用
下面舉幾個凸包應用的例子
給一個點集找邊界
比如之前的換臉的應用。通過Dlib庫檢測出人臉特徵點後需要提取出人臉,這時候可以用尋找凸包的方法,如下圖所示
這方面還有其他應用,比如Kinect,灰度深度影象就是一個點集,我們需要利用凸包來定位物體在場景中的位置等等。
防止發生碰撞(Collision Avoidance)
想象一輛汽車由一個點集構成的,如果想防止這輛汽車與其他障礙物發生碰撞,求任意輪廓之間的交比求兩個凸多邊形之間的衝突在計算量上要大多,這時候更應該使用凸包來代替輪廓。
參考文獻
1.Applications and Algorithms of Convex Hull : Brilliant
2.Series of Convex Hull Algorithms and their Implementations: GeeksForGeeks
3.ConvexHull Documentation: OpenCV Docs
4.Sklansky’s Algorithm (1982)