1. 程式人生 > >Python 影象處理 OpenCV (15):影象輪廓

Python 影象處理 OpenCV (15):影象輪廓

![](https://cdn.geekdigging.com/opencv/opencv_header.png) 前文傳送門: [「Python 影象處理 OpenCV (1):入門」](https://www.geekdigging.com/2020/05/17/5513454552/) [「Python 影象處理 OpenCV (2):畫素處理與 Numpy 操作以及 Matplotlib 顯示影象」](https://www.geekdigging.com/2020/05/18/4936041986/) [「Python 影象處理 OpenCV (3):影象屬性、影象感興趣 ROI 區域及通道處理」](https://www.geekdigging.com/2020/05/19/1227329671/) [「Python 影象處理 OpenCV (4):影象算數運算以及修改顏色空間」](https://www.geekdigging.com/2020/05/21/1757913240/) [「Python 影象處理 OpenCV (5):影象的幾何變換」](https://www.geekdigging.com/2020/05/23/4331122737/) [「Python 影象處理 OpenCV (6):影象的閾值處理」](https://www.geekdigging.com/2020/06/03/6651375581/) [「Python 影象處理 OpenCV (7):影象平滑(濾波)處理」](https://www.geekdigging.com/2020/06/06/8676263283/) [「Python 影象處理 OpenCV (8):影象腐蝕與影象膨脹」](https://www.geekdigging.com/2020/06/08/5731186312/) [「Python 影象處理 OpenCV (9):影象處理形態學開運算、閉運算以及梯度運算」](https://www.geekdigging.com/2020/06/11/5023174082/) [「Python 影象處理 OpenCV (10):影象處理形態學之頂帽運算與黑帽運算」](https://www.geekdigging.com/2020/06/18/9182078666/) [「Python 影象處理 OpenCV (11):Canny 運算元邊緣檢測技術」](https://www.geekdigging.com/2020/06/25/4009152544/) [「Python 影象處理 OpenCV (12): Roberts 運算元、 Prewitt 運算元、 Sobel 運算元和 Laplacian 運算元邊緣檢測技術」](https://www.geekdigging.com/2020/06/26/7999051794/) [「Python 影象處理 OpenCV (13): Scharr 運算元和 LOG 運算元邊緣檢測技術」](https://www.geekdigging.com/2020/07/09/4977894293/) [「Python 影象處理 OpenCV (14):影象金字塔」](https://www.geekdigging.com/2020/07/12/7005924297/) ## 引言 其實蠻不好意思的,剛才翻了翻自己的部落格,上次寫 OpenCV 的文章已經接近半個月以前了,我用 3 秒鐘的時間回想了下最近兩星期時間都花在哪了。 每次思考這種問題總會下意識甩鍋給工作,最近工作忙的一批,emmmmmmmmmmmm。。。。。。。。。 這麼騙自己是不對的! 實際上是美劇真香,最近把「反擊」從第一季到第六季看了一遍,還不錯,喜歡看動作類的同學可以嘗試下。 本篇文章是關於影象處理輪廓方面的,下面開始正文,希望能幫到各位。 Q:什麼是輪廓? A:輪廓是一系列相連的點組成的曲線,代表了物體的基本外形,相對於邊緣,輪廓是連續的,邊緣並不全部連續。 ## 尋找輪廓 尋找輪廓 OpenCV 為我們提供了一個現成的函式 `findContours()` 。 在 OpenCV 中,輪廓提取函式 `findContours()` 實現的是 1985 年由一名叫做 `Satoshi Suzuki` 的人發表的一篇論文中的演算法,如下: > Satoshi Suzuki and others. Topological structural analysis of digitized binary images by border following. Computer Vision, Graphics, and Image Processing, 30(1):32–46, 1985. 對原理感興趣的同學可以去搜搜看,不是很難理解。 先看一個示例程式碼: ```python import cv2 as cv img = cv.imread("black.png") gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 降噪 ret, thresh = cv.threshold(gray_img, 127, 255, 0) # 尋找輪廓 contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_NONE) print(len(contours[0])) ``` 這段程式碼先用 `threshold()` 對影象進行降噪處理,它的原型函式如下: ```python retval, dst = cv.threshold(src, thresh, maxval, type[, dst] ) ``` * dst:結果影象。 * src:原影象。 * thresh:當前閾值。 * maxVal:最大閾值,一般為255。 * type:閾值型別,可選值如下: ```python enum ThresholdTypes { THRESH_BINARY = 0, # 大於閾值的部分被置為 255 ,小於部分被置為 0 THRESH_BINARY_INV = 1, # 大於閾值部分被置為 0 ,小於部分被置為 255 THRESH_TRUNC = 2, # 大於閾值部分被置為 threshold ,小於部分保持原樣 THRESH_TOZERO = 3, # 小於閾值部分被置為 0 ,大於部分保持不變 THRESH_TOZERO_INV = 4, # 大於閾值部分被置為 0 ,小於部分保持不變 THRESH_OTSU = 8, # 自動處理,影象自適應二值化,常用區間 [0,255] }; ``` 查詢輪廓使用的函式為 `findContours()` ,它的原型函式如下: ```python cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]])   ``` * image:源影象。 * mode:表示輪廓檢索模式。 ```shell cv2.RETR_EXTERNAL 表示只檢測外輪廓。 cv2.RETR_LIST 檢測的輪廓不建立等級關係。 cv2.RETR_CCOMP 建立兩個等級的輪廓,上面的一層為外邊界,裡面的一層為內孔的邊界資訊。如果內孔內還有一個連通物體,這個物體的邊界也在頂層。 cv2.RETR_TREE 建立一個等級樹結構的輪廓。 ``` * method:表示輪廓近似方法。 ```shell cv2.CHAIN_APPROX_NONE 儲存所有的輪廓點。 cv2.CHAIN_APPROX_SIMPLE 壓縮水平方向,垂直方向,對角線方向的元素,只保留該方向的終點座標,例如一個矩形輪廓只需4個點來儲存輪廓資訊。 ``` 這裡可以使用 `print(len(contours[0]))` 函式將包含的點的數量打印出來,比如在上面的示例中,使用引數 `cv2.CHAIN_APPROX_NONE` 輪廓點有 1382 個,而使用引數 `cv2.CHAIN_APPROX_SIMPLE` 則輪廓點只有 4 個。 ![](https://cdn.geekdigging.com/opencv/15/none.jpg) ## 繪製輪廓 繪製輪廓使用到的 OpenCV 為我們提供的 `drawContours()` 這個函式,下面是它的三個簡單的例子: ```python # To draw all the contours in an image: cv2.drawContours(img, contours, -1, (0,255,0), 3) # To draw an individual contour, say 4th contour: cv2.drawContours(img, contours, 3, (0,255,0), 3) # But most of the time, below method will be useful: cnt = contours[4] cv2.drawContours(img, [cnt], 0, (0,255,0), 3) ``` `drawContours()` 函式中有五個引數: * 第一個引數是源影象。 * 第二個引數是應該包含輪廓的列表。 * 第三個引數是列表索引,用來選擇要繪製的輪廓,為-1時表示繪製所有輪廓。 * 第四個引數是輪廓顏色。 * 第五個引數是輪廓線的寬度,為 -1 時表示填充。 我們接著前面的示例把使用 `findContours()` 找出來的輪廓繪製出來: ```python import cv2 as cv img = cv.imread("black.png") gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) cv.imshow("img", img) # 降噪 ret, thresh = cv.threshold(gray_img, 127, 255, 0) # 尋找輪廓 contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE) print(len(contours[0])) # 繪製綠色輪廓 cv.drawContours(img, contours, -1, (0,255,0), 3) cv.imshow("draw", img) cv.waitKey(0) cv.destroyAllWindows() ``` ![](https://cdn.geekdigging.com/opencv/15/contour_result.png) ## 特徵矩 特徵矩可以幫助我們計算一些影象的特徵,例如物體的質心,物體的面積等,使用的函式為 `moments()` 。 ![](https://cdn.geekdigging.com/opencv/15/moments_result1.png) `moments()` 函式會將計算得到的矩以字典形式返回。 ```python import cv2 as cv img = cv.imread("number.png") gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 降噪 ret, thresh = cv.threshold(gray_img, 127, 255, 0) # 尋找輪廓 contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE) cnt = contours[0] # 獲取影象矩 M = cv.moments(cnt) print(M) # 質心 cx = int(M['m10'] / M['m00']) cy = int(M['m01'] / M['m00']) print(f'質心為:[{cx}, {cy}]') ``` 這時,我們取得了這個影象的矩,矩 M 中包含了很多輪廓的特徵資訊,除了示例中展示的質心的計算,還有如 M['m00'] 表示輪廓面積。 ## 輪廓面積 ```python area = cv.contourArea(cnt) print(f'輪廓面積為:{area}') ``` 這裡取到的輪廓面積和上面 M['m00'] 保持一致。 ## 輪廓周長 ```python perimeter = cv.arcLength(cnt, True) print(f'輪廓周長為:{perimeter}') ``` 引數 `True` 表示輪廓是否封閉,我們這裡的輪廓是封閉的,所以這裡寫 `True` 。 ## 輪廓外接矩形 輪廓外接矩形分為正矩形和最小矩形。使用 `cv2.boundingRect(cnt)` 來獲取輪廓的外接正矩形,它不考慮物體的旋轉,所以該矩形的面積一般不會最小;使用 `cv.minAreaRect(cnt)` 可以獲取輪廓的外接最小矩形。 ![](https://cdn.geekdigging.com/opencv/15/rect_result.png) 兩者的區別如上圖,綠線代表的是外接正矩形,紅線代表的是外接最小矩形,程式碼如下: ```python import cv2 as cv import numpy as np img = cv.imread("number.png") gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 降噪 ret, thresh = cv.threshold(gray_img, 127, 255, 0) # 尋找輪廓 contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE) cnt = contours[0] # 外接正矩形 x, y, w, h = cv.boundingRect(cnt) cv.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) # 外接最小矩形 min_rect = cv.minAreaRect(cnt) print(min_rect) box = cv.boxPoints(min_rect) box = np.int0(box) cv.drawContours(img, [box], 0, (0, 0, 255), 2) cv.imshow("draw", img) cv.waitKey(0) cv.destroyAllWindows() ``` `boundingRect()` 函式的返回值包含四個值,矩形框左上角的座標 (x, y) 、寬度 w 和高度 h 。 `minAreaRect()` 函式的返回值中還包含旋轉資訊,返回值資訊為包括中心點座標 (x,y) ,寬高 (w, h) 和旋轉角度。 ## 輪廓近似 根據我們指定的精度,它可以將輪廓形狀近似為頂點數量較少的其他形狀。它是由 Douglas-Peucker 演算法實現的。 OpenCV 提供的函式是 `approxPolyDP(cnt, epsilon, True)` ,第二個引數 epsilon 用於輪廓近似的精度,表示原始輪廓與其近似輪廓的最大距離,值越小,近似輪廓越擬合原輪廓。第三個引數指定近似輪廓是否是閉合的。具體用法如下: ```python import cv2 as cv img = cv.imread("number.png") gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 降噪 ret, thresh = cv.threshold(gray_img, 127, 255, 0) # 尋找輪廓 contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE) cnt = contours[0] # 計算 epsilon ,按照周長百分比進行計算,分別取周長 1% 和 10% epsilon_1 = 0.1 * cv.arcLength(cnt, True) epsilon_2 = 0.01 * cv.arcLength(cnt, True) # 進行多邊形逼近 approx_1 = cv.approxPolyDP(cnt, epsilon_1, True) approx_2 = cv.approxPolyDP(cnt, epsilon_2, True) # 畫出多邊形 image_1 = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR) image_2 = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR) cv.polylines(image_1, [approx_1], True, (0, 0, 255), 2) cv.polylines(image_2, [approx_2], True, (0, 0, 255), 2) cv.imshow("image_1", image_1) cv.imshow("image_2", image_2) cv.waitKey(0) cv.destroyAllWindows() ``` ![](https://cdn.geekdigging.com/opencv/15/approx_result.png) 第一張圖是 epsilon 為原始輪廓周長的 10% 時的近似輪廓,第二張圖中綠線就是 epsilon 為原始輪廓周長的 1% 時的近似輪廓。 ## 輪廓凸包 凸包外觀看起來與輪廓逼近相似,只不過它是物體最外層的「凸」多邊形。 如下圖,紅色的部分為手掌的凸包,雙箭頭部分表示凸缺陷(Convexity Defects),凸缺陷常用來進行手勢識別等。 ![](https://cdn.geekdigging.com/opencv/15/convexHull_image.jpg) ```python import cv2 as cv img = cv.imread("number.png") gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 降噪 ret, thresh = cv.threshold(gray_img, 127, 255, 0) # 尋找輪廓 contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE) cnt = contours[0] # 繪製輪廓 image = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR) cv.drawContours(image, contours, -1, (0, 0 , 255), 2) # 尋找凸包,得到凸包的角點 hull = cv.convexHull(cnt) # 繪製凸包 cv.polylines(image, [hull], True, (0, 255, 0), 2) cv.imshow("image", image) cv.waitKey(0) cv.destroyAllWindows() ``` ![](https://cdn.geekdigging.com/opencv/15/convex_result.png) 還有一個函式,是可以用來判斷圖形是否凸形的: ```python print(cv.isContourConvex(hull)) # True ``` 它的返回值是 True 或者 False 。 ## 最小閉合圈 接下來,使用函式 `cv.minEnclosingCircle()` 查詢物件的圓周。它是一個以最小面積完全覆蓋物體的圓。 ```python import cv2 as cv img = cv.imread("number.png") gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 降噪 ret, thresh = cv.threshold(gray_img, 127, 255, 0) # 尋找輪廓 contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE) cnt = contours[0] # 繪製最小外接圓 (x, y), radius = cv.minEnclosingCircle(cnt) center = (int(x), int(y)) radius = int(radius) cv.circle(img, center, radius, (0, 255, 0), 2) cv.imshow("img", img) cv.waitKey(0) cv.destroyAllWindows() ``` ![](https://cdn.geekdigging.com/opencv/15/min_circle.png) 下一個是把一個橢圓擬合到一個物體上。它返回內接橢圓的旋轉矩形。 ```python ellipse = cv.fitEllipse(cnt) cv.ellipse(img, ellipse, (0, 255, 0), 2) ``` ![](https://cdn.geekdigging.com/opencv/15/ellipse_circle.png) ## 參考 https://zhuanlan.zhihu.com/p/61328775 https://zhuanlan.zhihu.com/p/