前文傳送門:

「Python 圖像處理 OpenCV (1):入門」

「Python 圖像處理 OpenCV (2):像素處理與 Numpy 操作以及 Matplotlib 顯示圖像」

「Python 圖像處理 OpenCV (3):圖像屬性、圖像感興趣 ROI 區域及通道處理」

「Python 圖像處理 OpenCV (4):圖像算數運算以及修改顏色空間」

「Python 圖像處理 OpenCV (5):圖像的幾何變換」

「Python 圖像處理 OpenCV (6):圖像的閾值處理」

「Python 圖像處理 OpenCV (7):圖像平滑(濾波)處理」

「Python 圖像處理 OpenCV (8):圖像腐蝕與圖像膨脹」

「Python 圖像處理 OpenCV (9):圖像處理形態學開運算、閉運算以及梯度運算」

「Python 圖像處理 OpenCV (10):圖像處理形態學之頂帽運算與黑帽運算」

「Python 圖像處理 OpenCV (11):Canny 算子邊緣檢測技術」

「Python 圖像處理 OpenCV (12): Roberts 算子、 Prewitt 算子、 Sobel 算子和 Laplacian 算子邊緣檢測技術」

「Python 圖像處理 OpenCV (13): Scharr 算子和 LOG 算子邊緣檢測技術」

「Python 圖像處理 OpenCV (14):圖像金字塔」

引言

其實蠻不好意思的,剛纔翻了翻自己的博客,上次寫 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.

對原理感興趣的同學可以去搜搜看,不是很難理解。

先看一個示例代碼:

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() 對圖像進行降噪處理,它的原型函數如下:

retval, dst = cv.threshold(src, thresh, maxval, type[, dst] )
  • dst:結果圖像。
  • src:原圖像。
  • thresh:當前閾值。
  • maxVal:最大閾值,一般爲255。
  • type:閾值類型,可選值如下:
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() ,它的原型函數如下:

cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]])  
  • image:源圖像。
  • mode:表示輪廓檢索模式。
cv2.RETR_EXTERNAL 表示只檢測外輪廓。
cv2.RETR_LIST 檢測的輪廓不建立等級關係。
cv2.RETR_CCOMP 建立兩個等級的輪廓,上面的一層爲外邊界,裏面的一層爲內孔的邊界信息。如果內孔內還有一個連通物體,這個物體的邊界也在頂層。
cv2.RETR_TREE 建立一個等級樹結構的輪廓。
  • method:表示輪廓近似方法。
cv2.CHAIN_APPROX_NONE 存儲所有的輪廓點。
cv2.CHAIN_APPROX_SIMPLE 壓縮水平方向,垂直方向,對角線方向的元素,只保留該方向的終點座標,例如一個矩形輪廓只需4個點來保存輪廓信息。

這裏可以使用 print(len(contours[0])) 函數將包含的點的數量打印出來,比如在上面的示例中,使用參數 cv2.CHAIN_APPROX_NONE 輪廓點有 1382 個,而使用參數 cv2.CHAIN_APPROX_SIMPLE 則輪廓點只有 4 個。

繪製輪廓

繪製輪廓使用到的 OpenCV 爲我們提供的 drawContours() 這個函數,下面是它的三個簡單的例子:

# 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() 找出來的輪廓繪製出來:

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()

特徵矩

特徵矩可以幫助我們計算一些圖像的特徵,例如物體的質心,物體的面積等,使用的函數爲 moments()

moments() 函數會將計算得到的矩以字典形式返回。

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'] 表示輪廓面積。

輪廓面積

area = cv.contourArea(cnt)
print(f'輪廓面積爲:{area}')

這裏取到的輪廓面積和上面 M['m00'] 保持一致。

輪廓周長

perimeter = cv.arcLength(cnt, True)
print(f'輪廓周長爲:{perimeter}')

參數 True 表示輪廓是否封閉,我們這裏的輪廓是封閉的,所以這裏寫 True

輪廓外接矩形

輪廓外接矩形分爲正矩形和最小矩形。使用 cv2.boundingRect(cnt) 來獲取輪廓的外接正矩形,它不考慮物體的旋轉,所以該矩形的面積一般不會最小;使用 cv.minAreaRect(cnt) 可以獲取輪廓的外接最小矩形。

兩者的區別如上圖,綠線代表的是外接正矩形,紅線代表的是外接最小矩形,代碼如下:

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 用於輪廓近似的精度,表示原始輪廓與其近似輪廓的最大距離,值越小,近似輪廓越擬合原輪廓。第三個參數指定近似輪廓是否是閉合的。具體用法如下:

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()

第一張圖是 epsilon 爲原始輪廓周長的 10% 時的近似輪廓,第二張圖中綠線就是 epsilon 爲原始輪廓周長的 1% 時的近似輪廓。

輪廓凸包

凸包外觀看起來與輪廓逼近相似,只不過它是物體最外層的「凸」多邊形。

如下圖,紅色的部分爲手掌的凸包,雙箭頭部分表示凸缺陷(Convexity Defects),凸缺陷常用來進行手勢識別等。

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()

還有一個函數,是可以用來判斷圖形是否凸形的:

print(cv.isContourConvex(hull)) # True

它的返回值是 True 或者 False 。

最小閉合圈

接下來,使用函數 cv.minEnclosingCircle() 查找對象的圓周。它是一個以最小面積完全覆蓋物體的圓。

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()

下一個是把一個橢圓擬合到一個物體上。它返回內接橢圓的旋轉矩形。

ellipse = cv.fitEllipse(cnt)
cv.ellipse(img, ellipse, (0, 255, 0), 2)

參考

https://zhuanlan.zhihu.com/p/61328775

https://zhuanlan.zhihu.com/p/77783347

相關文章