OpenCV—輪廓操作一站式詳解:查找/篩選/繪製/形狀描述與重心標註(Python版)

OpenCV—輪廓操作一站式詳解:查找/篩選/繪製/形狀描述與重心標註(Python版)

爲了方便使用的Python同學,將上一篇文章用Python重寫了一遍,其中一些OpenCV的Python接口說明將後續持續更新。

輪廓是定義或限定形狀或對象的邊或線,是機器視覺中的常用的概念,多用於目標檢測、識別等任務。關於OpenCV輪廓操作,尤其是級別及如何使用輪廓級別進行篩選等問題,相關文章比較少,正好最近用到,因此將其總結成文。本文主要介紹OpenCV的查找輪廓函數findContours()繪製函數drawContours(),及其輪廓級別參數hierarchy,涉及到預處理、輪廓篩選等內容,並提供全部源代碼,希望能幫助大家理解基本概念並能借鑑示例代碼編寫自己的算法。

本文代碼:Python

Python版本3.5或以上

OpenCV版本3.4(4.1版本有一處接口修改,文中已註明)

本文包括如下內容:

  • 基本概念

1.查找和繪製輪廓函數findContous(),drawContours()

2.輪廓參數:輪廓級別、輪廓長度

3.輪廓的形狀描述子:最小覆蓋矩形、圓、多邊形逼近、凸包

  • 編程實戰

1.如何篩選輪廓:按輪廓級別和長度篩選

2.如何繪製輪廓的外接形狀

3.如何獲取輪廓的重心座標並標註

本文的目標:

1.從原始圖像中找到2架可回收火箭

2.標註目標的位置與重心座標

閱讀完成後,將能從原始圖像中找到2架火箭,並標註其位置與座標。如下圖所示:

目錄

OpenCV輪廓操作一站式詳解:查找/篩選/繪製/形狀描述與標註

1.查找、繪製輪廓函數

findContours()

drawContours()

2.預處理

3.查找輪廓

4.繪製輪廓

5.篩選輪廓

5.1 hierarchy輪廓級別詳解

contours與hierarchy的關係

什麼是層次結構hierarchy?

5.2 OpenCV中的層次結構表示

Next

Previous

First_Child

Parent

5.3 按hierarchy篩選輪廓

5.4 按長度篩選輪廓

6.聯通域分析

7.標註輪廓重心


1.查找、繪製輪廓函數


findContours()

函數原型:

cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]]) 
使用方式:
binary, contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

函數參數:

image

輸入:源圖像,一個8位單通道圖像,注意一定是CV_8UC1的單通道圖像,否則報錯。 非零像素被視爲1。 零像素保持爲0,因此圖像被視爲二進制。可以使用compare,inRange,threshold,adaptiveThreshold,Canny等來從灰度或彩色圖像中創建二進制圖像。如果mode爲RETR_CCOMP或RETR_FLOODFILL,則輸入也可以是標籤的32位整數圖像(CV_32SC1)。

contours

輸出:檢測到的輪廓。每個輪廓都存儲爲點向量(例如std :: vector <std :: vector <cv :: Point >>)。即由若干個cv::Point類型的點組成了單個輪廓std :: vector <cv :: Point >,再由若干個輪廓組成輸入圖像中的全部輪廓std::vector<std :: vector <cv :: Point >>

hierarchy

輸出:輪廓級別信息。Hierarchy爲可選輸出變量,是std::vector<cv::Vec4i>類型的向量(每個元素都是一個4個int值構成的向量)。包含有關圖像拓撲的信息。它具有與輪廓數量一樣多的元素。例如,第i個輪廓, hierarchy[i][0],hierarchy[i][1],hierarchy[i][2]和hierarchy[i][3]依次爲:第i個輪廓的[Next, Previous, First_Child, Parent],即輪廓i相同等級的下一輪廓、前一輪廓,第一個子輪廓和父輪廓(上一級輪廓)的索引號(即contours向量中的輪廓序號)。如果輪廓i沒有下一個,前一個,父級或嵌套輪廓,則層次結構[i]的相應元素將爲負數。這個參數我們將在下文中重點介紹。
mode 輸入:輪廓檢索模式, 詳見 RetrievalModes
method 輸入:輪廓近似法, 詳見ContourApproximationModes
offset 輸入:每個輪廓點移動的偏移量,可選參數,cv::Point()類型。如果從整幅圖像的某個ROI中提取輪廓,然後又在整個圖像中分析輪廓(將ROI中的輪廓座標恢復到整幅圖像中的座標),這個偏移量非常有用,可以免去我們自己寫代碼轉換座標系的麻煩。

mode參數:

RETR_EXTERNAL 

Python: cv.RETR_EXTERNAL

僅檢索極端外輪廓。 它爲所有輪廓設置hierarchy [i][2] = hierarchy [i][3] = - 1。

RETR_LIST 

Python: cv.RETR_LIST

檢索所有輪廓而不建立任何層次關係。

RETR_CCOMP 

Python: cv.RETR_CCOMP

檢索所有輪廓並將它們組織成兩級層次結構。在頂層輪廓是外部輪廓。在第二層輪廓是“洞”的輪廓。如果連接組件的洞內有另一個輪廓,它的級別仍然認定爲頂層。

RETR_TREE 

Python: cv.RETR_TREE

檢索所有輪廓並重建嵌套輪廓的完整層次結構。

RETR_FLOODFILL 

Python: cv.RETR_FLOODFILL

 

drawContours()

繪製輪廓輪廓或填充輪廓。

函數原型:

image = cv.drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]])

函數參數:

image

輸入:源圖像。單通道或3通道圖像。

contours

輸入:待繪製的輪廓。std :: vector <std :: vector <cv :: Point >>類型。
contourIdx 輸入:待繪製的輪廓序號。例如:0爲繪製第1個輪廓contours[0];1爲繪製第2個輪廓contours[1],依次類推;-1爲繪製所有輪廓。
color 輸入:輪廓顏色。cv::Scalar變量,例如:cv::Scalar(0,0,255)爲紅色輪廓,cv::Scalar::all(0)爲黑色輪廓
thickness 輸入:輪廓粗細。int型變量,默認爲1,值越大越粗
lineType 輸入:繪製輪廓的線型。默認LINE_8,8聯通線型(下一個點連接上一個點的邊或角)

hierarchy

輸入:待繪製的輪廓級別。std::vector<cv::Vec4i>類型的向量(每個元素都是一個4個int值構成的向量)。下一輪廓、前一輪廓,第一個子輪廓和父輪廓(上一級輪廓)的索引號。
maxLevel 輸入:待繪製的輪廓最大級別。
method 輸入:輪廓近似法, 詳見ContourApproximationModes
offset 輸入:每個輪廓點移動的偏移量,可選參數。

2.預處理


預處理目的是爲輪廓查找提供高質量的輸入源圖像。

預處理的主要步驟包括:

  • 灰度化:使用cv2.cvtColor()
  • 圖像去噪:使用高斯濾波cv2.GaussianBlur()
  • 二值化:使用cv2.threshold()
  • 形態學處理:cv2.morphologyEx()

其中灰度化可以將3通道圖像轉化爲單通道圖像,以便進行二值化門限分割;去噪可以有效剔除圖像中的異常獨立噪點;二值化是爲輪廓查找函數提供單通道圖像;形態學的某些處理通常可以剔除細小輪廓,聯通斷裂的輪廓。

讀取圖像代碼如下:

# 1.載入圖像
image = cv2.imread("spaceX2.jpg", 1)
# print(image.shape)
height, width, channel = image.shape
image = cv2.resize(image, (int(0.5*width), int(0.5*height)), interpolation=cv2.INTER_CUBIC)
cv2.imshow("original", image)
cv2.waitKey()

# 2.預處理
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# print(gray.shape)
cv2.imshow("gray", gray)

gray = cv2.GaussianBlur(gray, (3, 3), 1)
ret, binary = cv2.threshold(gray, 80, 255, cv2.THRESH_BINARY_INV)
cv2.imshow("binary", binary)

element = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))  # 3 * 3 正方形,8位uchar型,全1結構元素
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, element)

cv2.imshow("morphology", binary)
cv2.waitKey()

3.查找輪廓


使用findContours函數查找輪廓,並輸出找到的輪廓個數

# 3.查找輪廓
binary, contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)  # for opencv3.4
print("find", len(contours), "contours")

如果使用的是opencv4.1版本,則上述代碼中查找輪廓函數的返回值僅有2個,如下所示

# 3.查找輪廓
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)  # for opencv4.1
print("find", len(contours), "contours")

4.繪製輪廓


爲了方便查看輪廓查找結果,使用drawContours()來繪製輪廓並顯示。編寫函數drawMyContours()函數用於在白色背景中或者原圖上查看輪廓。

函數參數:

  • 窗口名字;
  • 原始圖像以及輪廓變量
  • 白色背景上還是在原圖上繪製輪廓的標誌位
# 4. 繪製輪廓函數
# 自定義繪製輪廓的函數(爲簡化操作)
# 輸入1:winName:窗口名
# 輸入2:image:原圖
# 輸入3:contours:輪廓
# 輸入4:draw_on_blank:繪製方式,True在白底上繪製,False:在原圖image上繪製
def drawMyContours(winName, image, contours, draw_on_blank):
    # cv2.drawContours(image, contours, index, color, line_width)
    # 輸入參數:
    # image:與原始圖像大小相同的畫布圖像(也可以爲原始圖像)
    # contours:輪廓(python列表)
    # index:輪廓的索引(當設置爲-1時,繪製所有輪廓)
    # color:線條顏色,
    # line_width:線條粗細
    # 返回繪製了輪廓的圖像image
    if (draw_on_blank): # 在白底上繪製輪廓
        temp = np.ones(image.shape, dtype=np.uint8) * 255
        cv2.drawContours(temp, contours, -1, (0, 0, 0), 2)
    else:
        temp = image.copy()
        cv2.drawContours(temp, contours, -1, (0, 0, 255), 2)
    cv2.imshow(winName, temp)
    cv2.waitKey()

在main函數中調用drawMyContours()如下:

# 4.繪製原始輪廓
drawMyContours("find contours", image, contours, True)

首次查找的輪廓變量contours中有21個向量,即找到21個輪廓。調試展開contours變量,可以看到每個元素都是一個由一系列輪廓上的點組成的。

調試展開hierarchy變量,可以看到其類型爲ndarray的n維數組,維度爲(1, 21, 4),第二維度對應輪廓的索引號(共21個輪廓),第三維度爲1×4的輪廓級別信息(每個輪廓包含4個級別描述量[Next,Previous,First_Child,Parent])。

通過繪製輪廓,可以看到這21個輪廓,除了兩個目標之外,還有云、地面背景以其中的“洞”輪廓。

5.篩選輪廓


查找到大輪廓顯然有許多不符合要求,因此可以通過某些準則進行輪廓篩選。通過觀察,發現上圖中有許多輪廓包含“洞”,即子輪廓,而這些子輪廓顯然也有父級輪廓,因此我們可以使用findContours的hierarchy輪廓級別參數刪除那些有子輪廓也有父輪廓的輪廓。

5.1hierarchy輪廓級別詳解

contours與hierarchy的關係

使用findContours()函數將返回contours輪廓向量以及對應的hierarchy輪廓級別向量(可選項)。兩者有相同的長度即contours.size() = hierarchy.size(),並且向量的序號表示找到的輪廓索引,且一一對應。

什麼是層次結構hierarchy?

通常我們使用findContours()函數來檢測圖像中的對象。有時對象位於不同的位置。但在某些情況下,某些形狀在其他形狀內(類似嵌套)。在這種情況下,我們將外部輪廓稱爲父級輪廓,將內部輪廓稱爲子輪廓。這樣,圖像中的輪廓彼此之間存在某種關係。並且我們可以指定一個輪廓如何相互連接,例如,它是某個其他輪廓的子項,還是父項等。此關係的表示就稱爲層次結構hierarchy。

考慮下面的示例圖片:

在上圖中,有一些形狀,我們從0-5編號這5個形狀。圖中2和2a表示最外側矩形的外部和內部輪廓。

輪廓0,1,2是最外部輪廓,三者爲同一級別。我們可以說,它們在層次結構0中,或者只是它們處於相同的層次結構級別。

接下來是輪廓-2a可以被認爲是輪廓-2的子輪廓(或者相反,輪廓-2是輪廓-2a的父級輪廓)所以讓它在層次結構-1中。 類似地,輪廓-3是輪廓-2a的子輪廓,它進入下一層次。 最後,輪廓4,5是輪廓-3a的子輪廓,它們位於最後的層次結構級別。 從編號框的方式,可以說輪廓-4是輪廓-3a的第一個子輪廓,當然輪廓-5也是輪廓-3a的子輪廓。

如果上面的描述看着頭暈,不要緊,一開始都這樣。

5.2 OpenCV中的層次結構表示

OpenCV中每個輪廓都有自己的信息,關於它是什麼層次結構,誰是它的子輪廓,誰是它的父輪廓等.OpenCV將它表示爲四個int值的數組,Python接口中類型爲1個1×4數組:

[Next,Previous,First_Child,Parent]

Next

Next表示同一級別的下一個輪廓索引。例如,在我們的圖片中取出輪廓-0。同一水平的下一個輪廓是輪廓-1。 所以簡單地說Next = 1。類似地,對於輪廓-1,next是輪廓-2。 所以Next = 2。

輪廓-2的同一級別沒有下一個輪廓,所以輪廓-2的Next = -1。輪廓-4呢?它與輪廓-5處於同一水平。所以它的下一個輪廓是輪廓-5,所以輪廓-4的Next = 5。

Previous

Previous表示同一級別的上一個輪廓索引。例如,輪廓-1的上一個輪廓在同一級別中爲輪廓-0。 類似地,對於輪廓-2,它的上一個輪廓是輪廓-1。而對於輪廓-0,沒有先前的,所以把它的Previous = -1。

First_Child

First_Child表示當前輪廓的第一個子輪廓索引。例如,對於輪廓-2,子輪廓是輪廓-2a。因此輪廓-2的First_Child爲輪廓-2a的相應索引值。輪廓-3a呢?它有兩個子輪廓。但hierarchy參數只記錄第一個子輪廓,因此它是輪廓-4的索引值。因此,對於輪廓-3a,First_Child = 4。

Parent

Parent表示當前輪廓的父輪廓索引。對於輪廓-4和輪廓-5,它們的父輪廓都是輪廓-3a。對於輪廓-3a,它的父輪廓是輪廓-3,依此類推。

注意:

  • Previous表示同一層級的前一個輪廓的索引;
  • Parent表示其父輪廓的索引;
  • 如果某個輪廓沒有子輪廓項或父輪廓,則對應的字段=-1.

5.3hierarchy篩選輪廓

有了上述對輪廓層級的理解,下面就可以根據需要篩選輪廓了。例如本文任務是找到兩個火箭,而首次查找輪廓有許多中間有空洞的輪廓不符合要求,下面就通過遍歷每一個輪廓的hierarchy級別參數的第3第4個參數來找到那些有子輪廓或者有父輪廓的輪廓,並刪除之。注意使用Python接口操作輪廓時,hierarchy級別參數與輪廓變量contours爲不同類型的數據。處理時需要分別對待。

首先自定義一個專門用於刪除指定序號輪廓的函數delete_contours(),以方便後續操作:

#  自定義函數:用於刪除列表指定序號的輪廓
#  輸入 1:contours:原始輪廓
#  輸入 2:delete_list:待刪除輪廓序號列表
#  返回值:contours:篩選後輪廓
def delet_contours(contours, delete_list):
    delta = 0
    for i in range(len(delete_list)):
        # print("i= ", i)
        del contours[delete_list[i] - delta]
        delta = delta + 1
    return contours

然後使用級別篩選與長度篩選分別獲得待刪除的輪廓序號列表delete_list,再調用上述函數進行處理,以確保操作時輪廓序號不越界。

# 5.篩選輪廓
# 5.1使用層級結構篩選輪廓
# hierarchy[i]: [Next,Previous,First_Child,Parent]
# 要求沒有父級輪廓
delete_list = []   # 新建待刪除的輪廓序號列表
c, row, col = hierarchy.shape
for i in range(row):
    if hierarchy[0, i, 2] > 0 or hierarchy[0, i, 3] > 0:  # 有父輪廓或子輪廓
    delete_list.append(i)

# 根據列表序號刪除不符合要求的輪廓
contours = delet_contours(contours, delete_list)

print(len(contours), "contours left after hierarchy filter")
drawMyContours("contours after hierarchy filtering", image, contours, True)

篩選過後的輪廓如下圖所示,左上角和右下角的雲層與地面的帶有空洞的輪廓都被刪除了。

5.4 按長度篩選輪廓

儘管上一個步驟已經剔除了天空與地面背景輪廓,但仍然殘留這一些細小的輪廓。下一步可以使用輪廓長度contours[i].size()來濾除過小或過大的輪廓:

 # 5.2使用輪廓長度濾波
min_size = 20
max_size = 500
delete_list = []
for i in range(len(contours)):
    if (cv2.arcLength(contours[i], True) < min_size) or (cv2.arcLength(contours[i], True) > max_size):
    delete_list.append(i)

# 根據列表序號刪除不符合要求的輪廓
contours = delet_contours(contours, delete_list)

print(len(contours), "contours left after length filter")
drawMyContours("contours after length filtering", image, contours, False)

再次感覺越來越接近真理了:)

6.聯通域分析


連通區域通常代表了場景中的某個物體。爲了識別該物體,或將它與其他圖像元素比較,需要對此區域進行測量,以提取部分特徵。本節介紹opencv的形狀描述子,用於描述連通區域的形狀。OpenCV中用於形狀描述的函數有很多。我們把其中幾個用到上節提取到的區域。

(1)矩形框x, y, w, h = cv2.boundingRect()

在表示和定位圖像中的區域方法中,邊界框可能是最簡潔的。它的定義是:能完整包含該形狀的最小垂直矩形。返回值是方框左上角座標以及方框的寬和高x,y,  w,  h.

(2)最小覆蓋圓cv2.minEnclosingCircle()

最小覆蓋圓通常用在只需要區域尺寸和位置的近似值的情況。

(3)多邊形逼近cv2.approxPolyDP()

如果要更緊湊地表示區域的形狀,可以採用多邊形逼近。在創建時需要設置精度參數,表示形狀與對應的簡化多邊形之間能接受的最大距離。它是cv::approxPolyDP(contours[1],poly,5,true)函數的第四個參數。返回結果是cv::Point類型的向量,表示多邊形頂點個數。在畫這個多邊形時,要迭代遍歷整個向量,並在頂點之間畫直線,把它們逐個連接起來。

(4)凸包cv2.convexHll()

凸包是包含該形狀的最小凸多邊形。可以把它看作一條繞在區域周圍的橡皮筋。在形狀輪廓中凹進去的位置,凸包輪廓會與原始輪廓發生偏離。

下面使用上述分析方法中的凸包與最小覆蓋矩形兩種方法分別對2架火箭的提取輪廓進行分析。

# 6.形狀描述子
# 6.1 最小覆蓋矩形
result = image.copy()
x, y, w, h = cv2.boundingRect(contours[0])  #(x,y)爲矩形左上角的座標,(w,h)是矩形的寬和高
cv2.rectangle(result, (x, y), (x+w, y+h), (0, 255, 255), 1)

# 6.2 凸包
hull = cv2.convexHull(contours[1])
cv2.polylines(result, [hull], True, (0, 255, 0), 1)

得到的結果如下圖所示:輪廓1爲最小覆蓋矩形(黃色線條),輪廓2爲凸包(綠色線條)

7.標註輪廓重心

終於到最後一步了。下面先求2個輪廓的重心,然後使用cv::Circle()與cv::putText()函數將重心位置與座標標註到畫面上。

# 7.畫重心
for i in range(len(contours)):
    mom = cv2.moments(contours[i])
    pt = (int(mom['m10'] / mom['m00']), int(mom['m01'] / mom['m00']))  # 使用前三個矩m00, m01和m10計算重心
    cv2.circle(result, pt, 2, (0, 0, 255), 2)  # 畫紅點
    text = "(" + str(pt[0]) + ", " + str(pt[1]) + ")"
    cv2.putText(result, text, (pt[0]+10, pt[1]+10), cv2.FONT_HERSHEY_PLAIN, 1.5, (255, 255, 255), 1, 8, 0);

cv2.imshow("center", result)
cv2.waitKey()

最終結果如下圖所示。

參考鏈接:

https://docs.opencv.org/3.1.0/d9/d8b/tutorial_py_contours_hierarchy.html

https://docs.opencv.org/3.4.1/d3/dc0/group__imgproc__shape.html#ga17ed9f5d79ae97bd4c7cf18403e1689a

本文更新鏈接:https://blog.csdn.net/iracer/article/details/90695914

轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章