OpenCV 中的圖像處理 009_OpenCV 中的輪廓

本文主要內容來自于 OpenCV-Python 教程OpenCV 中的圖像處理 部分,這部分的全部主要內容如下:

輪廓:入門

目標

輪廓是什么?

輪廓可以簡單地解釋為連接所有連續點(沿邊界)的曲線,具有相同的顏色或強度。輪廓對于形狀分析,目標探測和識別是一個很有用的工具。

  • 為了更高的精度,使用二值圖像。因此,在尋找輪廓之前,應用閾值或 canny 邊緣探測
  • 自 OpenCV 3.2 開始,findContours() 不再修改原始圖像
  • 在 OpenCV 中,尋找輪廓就像從黑色背景中找出白色物體一樣。因此要記得,要尋找的物體應該為白色的,背景應該為黑色的。

讓我們看一下如何尋找二值圖像的輪廓:

import numpy as np
import cv2 as cv


def simple_find_contours():
    cv.samples.addSamplesDataSearchPath("/home/hanpfei/data/multimedia/opencv/samples/data")
    img = cv.imread(cv.samples.findFile('butterfly.jpg'))
    imgray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(imgray, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    print("hierarchy shape ", hierarchy.shape)


if __name__ == '__main__':
    simple_find_contours()

可以看到,cv.findContours() 函數中有三個參數,第一個是源圖像,第二個是輪廓檢索模式,第三個是輪廓逼近法。它輸出輪廓和層次結構。輪廓為圖像中所有輪廓的 Python 列表。每個單獨的輪廓為一個物體的邊界點的 (x,y) 坐標的 Numpy 數組。

注意
我們將在后面討論第二個和第三個參數,以及層次結構的詳細內容。在那之前,代碼示例中給它們的值將適用于所有圖像。

如何繪制輪廓?

要想繪制輪廓,可以使用 cv.drawContours 函數。只要你有邊界點,它也可以用于繪制任何形狀。它的第一個參數是源圖像,第二個參數是應該以 Python 列表的形式傳入的輪廓,第三個參數是輪廓的索引(繪制單個輪廓時很有用。要繪制所有輪廓,請傳遞 -1),其余的參數為顏色,粗細度等等。

  • 在圖像中繪制所有輪廓的方法如下:
cv.drawContours(img, contours, -1, (0,255,0), 3)
  • 在圖像中繪制單個輪廓,比如第 4 個輪廓的方法如下:
cv.drawContours(img, contours, 3, (0,255,0), 3)
  • 但在大多數時候,下面的方法將很有用:
cnt = contours[4]
cv.drawContours(img, [cnt], 0, (0,255,0), 3)

在上面的示例代碼中添加在原圖像中繪制輪廓的邏輯,并顯示原圖像和包含輪廓的圖像的代碼如下:

import numpy as np
import cv2 as cv


def simple_find_contours():
    cv.samples.addSamplesDataSearchPath("/home/hanpfei/data/multimedia/opencv/samples/data")
    img = cv.imread(cv.samples.findFile('butterfly.jpg'))
    img_original = img.copy()
    imgray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(imgray, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)

    cv.drawContours(img, contours, -1, (0, 0, 255), 2)

    dst = cv.hconcat([img_original, img])
    cv.imshow("Butterfly contours", dst)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    simple_find_contours()

看到的結果應該像下面這樣:

Simple find contours

輪廓逼近法

這是 cv.findContours 函數的第三個參數。它實際上表示什么呢?

上面,我們提到過輪廓是具有相同強度的形狀的邊界。它存儲形狀的邊界的 (x,y) 坐標。但它存儲所有的坐標么?這由輪廓逼近法指定。

如果傳入 cv.CHAIN_APPROX_NONE,則存儲所有的邊界點。但實際上我們需要所有的點么?比如,你找到了一條直線輪廓。你需要直線上的所有點來表示它么?不,我們只需要直線兩端的點即可。這就是 cv.CHAIN_APPROX_SIMPLE 所作的事情。它移除所有的冗余點并壓縮輪廓,從而節省內存。

下面的矩形圖像演示了這種技術。只需在輪廓數組中的所有坐標上畫一個圓圈(以藍色繪制)。第一張圖像顯示了我使用 cv.CHAIN_APPROX_NONE 獲得的點(734 個點),第二張圖像顯示了使用 cv.CHAIN_APPROX_SIMPLE 獲得的點(只有 4 個點)。

image

輪廓特征

目標

在這篇文章中,我們將學習

  • 找出輪廓的不同特征,如面積、周長、質心、邊界框等
  • 你將看到許多與輪廓相關的函數。

1. 矩

圖像矩可以幫助你計算一些特征,如物體的質心、物體的面積等。查看 圖像矩 的維基百科頁面。

函數 cv.moments() 給出了計算出的所有矩值的字典。 見下文:

import numpy as np
import cv2 as cv

def image_moments():
    img = cv.imread('/media/data2/Download/star.jpg', 0)
    ret, thresh = cv.threshold(img, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    M = cv.moments(cnt)
    print(M)


if __name__ == '__main__':
    image_moments()

從這個矩里,你可以提取有用的數據,比如面積、質心等。質心由這個關系給出,C_x = \frac{M_{10}}{M_{00}}C_y = \frac{M_{01}}{M_{00}}。這可以通過如下代碼完成:

cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])

如對于如下圖像 star.png

star.png

提取并繪制出質心的方法如下:

import numpy as np
import cv2 as cv


def image_moments():
    img = cv.imread('/media/data2/Download/star.png', 0)
    ret, thresh = cv.threshold(img, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    M = cv.moments(cnt)
    print(M)

    cx = int(M['m10'] / M['m00'])
    cy = int(M['m01'] / M['m00'])

    cv.circle(img, (cx, cy), 10, (0, 0, 255), -1)

    cv.imshow("Star", img)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    image_moments()

看到的結果應該像下面這樣:

Star Centroid

2. 輪廓面積

輪廓面積由 cv.contourArea() 函數給出,或來自于矩,M['m00']

area = cv.contourArea(cnt)

3. 輪廓周長

它也稱為弧長。可以使用 cv.arcLength() 函數找到它。第二個參數指定形狀是閉合輪廓(如果傳遞 True),還是只是曲線。

perimeter = cv.arcLength(cnt,True)

4. 輪廓逼近

它根據我們指定的精度將輪廓形狀近似為具有較少頂點數的另一個形狀。它是 Douglas-Peucker 算法 的一種實現。查看維基百科頁面以獲取算法和演示。

為了理解這一點,假設你試圖在圖像中找到一個正方形,但由于圖像中的一些問題,你沒有得到一個完美的正方形,而是一個“壞形狀”(如下圖第一張所示)。現在你可以使用這個函數來逼近形狀。在此,第二個參數稱為 epsilon,它是從輪廓到近似輪廓的最大距離。它是一個精度參數。需要明智地選擇 epsilon 以獲得正確的輸出。

epsilon = 0.1*cv.arcLength(cnt,True)
approx = cv.approxPolyDP(cnt,epsilon,True)

下面,在第二張圖片中,綠線顯示了 epsilon = 10% 弧長的近似曲線。第三張圖片顯示了相同的 epsilon = 1% 的弧長。第三個參數指定曲線是否閉合。

完整代碼如下所示:

import numpy as np
import cv2 as cv


def contour_approximation():
    img = cv.imread('/media/data2/Download/approx.png')

    gray_image = cv.imread('/media/data2/Download/approx.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

    image1 = img.copy()
    epsilon = 0.1 * cv.arcLength(cnt, True)
    approx = cv.approxPolyDP(cnt, epsilon, True)
    cv.drawContours(image1, [approx], -1, (0, 0, 255), 2)

    image2 = img.copy()
    epsilon = 0.01 * cv.arcLength(cnt, True)
    approx = cv.approxPolyDP(cnt, epsilon, True)
    cv.drawContours(image2, [approx], -1, (0, 0, 255), 2)

    dst = cv.hconcat([img, image1, image2])
    cv.imshow("Contour Approximation", dst)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    contour_approximation()

結果如下圖:

Contour Approximation

5. 凸包

凸包看起來類似于輪廓逼近,但實際上并非如此(在某些情況下兩者可能提供相同的結果)。在這里,cv.convexHull() 函數檢查曲線是否存在凸面缺陷并進行糾正。一般來說,凸曲線是總是凸出或至少平坦的曲線。如果它向內凸出,則稱為凸面缺陷。例如,檢查下面的手的圖像。紅線展示了手的凸包。雙邊箭頭標記表示凸面缺陷,即包絡與輪廓的局部最大偏差。

Image

關于它的語法有一點需要討論:

hull = cv.convexHull(points[, hull[, clockwise[, returnPoints]]])

參數詳情:

  • points 是我們傳入的輪廓。
  • hull 是輸出,通常我們會避免它。
  • clockwise:方向標志。如果為 True,則輸出凸包為順時針方向。否則,它是逆時針方向的。
  • returnPoints:默認情況下,為 True。然后它返回包絡點的坐標。如果為 False,則返回與包絡點對應的輪廓點的索引。

因此,要獲得如上圖所示的凸包,以下內容就足夠了:

hull = cv.convexHull(cnt)

但是如果要查找凸性缺陷,則需要通過 returnPoints = False。為了理解它,我們將使用上面的矩形圖像。首先我找到它的輪廓為cnt。現在我找到了 returnPoints = True 的凸包,我得到了以下值:[[[234 202]],[[ 51 202]],[[ 51 79]],[[234 79]]],它們是矩形四個角的點。現在如果設置 returnPoints = False 做同樣的事情,我會得到以下結果:[[129],[ 67],[ 0],[142]]。這些是輪廓中對應點的索引。比如,檢查第一個值:cnt[129] = [[234, 202]] 這與第一個結果相同(其它結果以此類推)。

當我們討論凸性缺陷時,你會再次看到它。

上面例子中,手的圖像如下:

Convex Hull Hands

完整代碼如下所示:

import numpy as np
import cv2 as cv


def convex_hull():
    img = cv.imread('/media/data2/Download/hands.png')

    gray_image = cv.imread('/media/data2/Download/hands.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

    image1 = img.copy()

    hull = cv.convexHull(cnt)
    cv.drawContours(image1, [hull], -1, (0, 0, 255), 2)

    dst = cv.hconcat([img, image1])
    cv.imshow("Convex Hull", dst)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    convex_hull()

結果如下圖:

Convex Hull

6. 檢查凸度

有一個函數來檢查曲線是否是凸的,cv.isContourConvex()。它只返回 True 或 False。沒有大礙。

k = cv.isContourConvex(cnt)

7. 邊界矩形

有兩種類型的邊界矩形。

7.a. 直邊界矩形

它是一個直的矩形,它不考慮物體的旋轉。所以邊界矩形的面積不會是最小的。它由函數 cv.boundingRect() 找到。

令 (x,y) 為矩形的左上角坐標, (w,h) 為其寬度和高度。

7.b. 旋轉矩形

在這里,邊界矩形是用最小面積繪制的,所以它也考慮了旋轉。用到的函數為 cv.minAreaRect()。它返回一個 Box2D 結構,其中包含以下詳細信息 - (中心 (x,y)、(寬度、高度)、旋轉角度)。但是要繪制這個矩形,我們需要矩形的 4 個角。由函數 cv.boxPoints() 獲得。

rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int0(box)
cv.drawContours(img,[box],0,(0,0,255),2)

兩個矩形都顯示在一個圖像中。綠色矩形顯示正常邊界矩形。紅色矩形是旋轉后的矩形。原始圖像如下:

Image

完整代碼如下所示:

import numpy as np
import cv2 as cv


def bounding_rectangle():
    img = cv.imread('/media/data2/Download/image.png')

    gray_image = cv.imread('/media/data2/Download/image.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

    image1 = img.copy()

    x, y, w, h = cv.boundingRect(cnt)
    cv.rectangle(image1, (x, y), (x + w, y + h), (0, 255, 0), 2)

    rect = cv.minAreaRect(cnt)
    box = cv.boxPoints(rect)
    box = np.int0(box)
    cv.drawContours(image1, [box], 0, (0, 0, 255), 2)

    cv.imshow("Bounding Rectangle", image1)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    bounding_rectangle()

結果如下圖:

Bounding Rectangle

8. 最小封閉圓

接下來我們使用函數 cv.minEnclosingCircle() 尋找物體的外接圓。它是完全覆蓋物體的面積最小的圓。

    (x, y), radius = cv.minEnclosingCircle(cnt)
    center = (int(x), int(y))
    radius = int(radius)
    cv.circle(img, center, radius, (0, 255, 0), 2)
Image

完整代碼如下所示:

import numpy as np
import cv2 as cv


def minimum_enclosing_circle():
    img = cv.imread('/media/data2/Download/image.png')

    gray_image = cv.imread('/media/data2/Download/image.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    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("Bounding Rectangle", img)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    minimum_enclosing_circle()

9. 擬合橢圓

下一個是將橢圓擬合到對象。它返回內接橢圓的旋轉矩形。

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

完整代碼如下所示:

import numpy as np
import cv2 as cv


def fitting_an_ellipse():
    img = cv.imread('/media/data2/Download/image.png')

    gray_image = cv.imread('/media/data2/Download/image.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

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

    cv.imshow("Bounding Rectangle", img)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    fitting_an_ellipse()

9. 擬合直線

同樣,我們可以將一條線擬合到一組點。 下圖包含一組白點。 我們可以近似為一條直線。

    rows, cols = img.shape[:2]
    [vx, vy, x, y] = cv.fitLine(cnt, cv.DIST_L2, 0, 0.01, 0.01)
    lefty = int((-x * vy / vx) + y)
    righty = int(((cols - x) * vy / vx) + y)
    cv.line(img, (cols - 1, righty), (0, lefty), (0, 255, 0), 2)
Image

完整代碼如下所示:

import numpy as np
import cv2 as cv


def fitting_a_ine():
    img = cv.imread('/media/data2/Download/image.png')

    gray_image = cv.imread('/media/data2/Download/image.png', 0)
    ret, thresh = cv.threshold(gray_image, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]

    rows, cols = img.shape[:2]
    [vx, vy, x, y] = cv.fitLine(cnt, cv.DIST_L2, 0, 0.01, 0.01)
    lefty = int((-x * vy / vx) + y)
    righty = int(((cols - x) * vy / vx) + y)
    cv.line(img, (cols - 1, righty), (0, lefty), (0, 255, 0), 2)

    cv.imshow("Bounding Rectangle", img)

    cv.waitKey(-1)
    cv.destroyAllWindows()


if __name__ == '__main__':
    fitting_a_ine()

其它資源

練習

參考文檔

Contours in OpenCV

Contours : Getting Started

Contour Features

Contour Properties

Contours : More Functions

Contours Hierarchy

Done.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容