OpenCV計算機視覺學習(8)——影像輪廓處理(輪廓繪製,輪廓檢索,輪廓填充,輪廓近似)

戰爭熱誠發表於2020-10-26

如果需要處理的原圖及程式碼,請移步小編的GitHub地址

  傳送門:請點選我

  如果點選有誤:https://github.com/LeBron-Jian/ComputerVisionPractice

1,簡單幾何影像繪製

  簡單幾何影像一般包括點,直線,矩陣,圓,橢圓,多邊形等等。

  下面學習一下 opencv對畫素點的定義。影像的一個畫素點有1或3個值,對灰度影像有一個灰度值,對彩色影像有3個值組成一個畫素值,他們表現出不同的顏色。

  其實有了點才能組成各種多邊形,才能對多邊形進行輪廓檢測,所以下面先練習一下簡單的幾何影像繪製。

1.1 繪製直線

  在OpenCV中,繪製直線使用的函式為 line() ,其函式原型如下:

def line(img, pt1, pt2, color, thickness=None, lineType=None, shift=None): # real signature unknown; restored from __doc__
    """
    line(img, pt1, pt2, color[, thickness[, lineType[, shift]]]) -> img
    .   @brief Draws a line segment connecting two points.
    .   
    .   The function line draws the line segment between pt1 and pt2 points in the image. The line is
    .   clipped by the image boundaries. For non-antialiased lines with integer coordinates, the 8-connected
    .   or 4-connected Bresenham algorithm is used. Thick lines are drawn with rounding endings. Antialiased
    .   lines are drawn using Gaussian filtering.
    .   
    .   @param img Image.
    .   @param pt1 First point of the line segment.
    .   @param pt2 Second point of the line segment.
    .   @param color Line color.
    .   @param thickness Line thickness.
    .   @param lineType Type of the line. See #LineTypes.
    .   @param shift Number of fractional bits in the point coordinates.
    """
    pass

   可以看到這個函式主要接受引數為兩個點的座標,線的顏色(其中灰色圖為一個數字,彩色圖為1*3的陣列)。

  實踐程式碼如下:

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 生成一個空灰度影像
img1 = np.zeros((400, 400), np.uint8)
img1 = cv2.line(img1, (0, 0), (400, 400), 255, 5)

# 生成一個空彩色影像
img3 = np.zeros((400, 400, 3), np.uint8)
img3 = cv2.line(img3, (0, 0), (400, 400), (0, 255, 0), 5)

titles = ['gray line image', 'color line image']
res = [img1, img3]

for i in range(2):
    plt.subplot(1, 2, i+1)
    plt.imshow(res[i]), plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

   效果如下:

   注意1:在這裡再強調一下,由於cv和matplotlib的讀取影像通道不同,導致灰度圖和彩色圖的顏色不一樣,如果想分開看,可以直接使用cv2.imshow()。

  注意2:繪製影像是在原圖上繪製,這裡我們寫的是專門在原圖上繪製,後面draw輪廓的話,可能需要 img.copy()了。不然我們的原圖會存在畫的輪廓。

1.2  繪製矩陣

  在OpenCV中,繪製直線使用的函式為 rectangel() ,其函式原型如下:

def rectangle(img, pt1, pt2, color, thickness=None, lineType=None, shift=None): # real signature unknown; restored from __doc__
    """
    rectangle(img, pt1, pt2, color[, thickness[, lineType[, shift]]]) -> img
    .   @brief Draws a simple, thick, or filled up-right rectangle.
    .   
    .   The function cv::rectangle draws a rectangle outline or a filled rectangle whose two opposite corners
    .   are pt1 and pt2.
    .   
    .   @param img Image.
    .   @param pt1 Vertex of the rectangle.
    .   @param pt2 Vertex of the rectangle opposite to pt1 .
    .   @param color Rectangle color or brightness (grayscale image).
    .   @param thickness Thickness of lines that make up the rectangle. Negative values, like #FILLED,
    .   mean that the function has to draw a filled rectangle.
    .   @param lineType Type of the line. See #LineTypes
    .   @param shift Number of fractional bits in the point coordinates.
    
    
    rectangle(img, rec, color[, thickness[, lineType[, shift]]]) -> img
    .   @overload
    .   
    .   use `rec` parameter as alternative specification of the drawn rectangle: `r.tl() and
    .   r.br()-Point(1,1)` are opposite corners
    """
    pass

引數解釋

  • 第一個引數img:img是原圖
  • 第二個引數pt1:(x,y)是矩陣的左上點座標
  • 第三個引數pt2:(x+w,y+h)是矩陣的右下點座標
  • 第四個引數color:(0,255,0)是畫線對應的rgb顏色
  • 第五個引數thickness:2是所畫的線的寬度

   cv2.rectangle(img, (10, 10), (390, 390), (255, 0, 0), 3),需要確定的就是矩形的兩個點(左上角與右下角),顏色,線的型別(不設定就預設)。

  程式碼如下:

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 生成一個空灰度影像
img1 = np.zeros((400, 400), np.uint8)
img1 = cv2.rectangle(img1, (40, 40), (350, 350), 255, 5)

# 生成一個空彩色影像
img3 = np.zeros((400, 400, 3), np.uint8)
img3 = cv2.rectangle(img3, (40, 40), (350, 350), (0, 255, 0), 5)

titles = ['gray rectangle image', 'color rectangle image']
res = [img1, img3]

for i in range(2):
    plt.subplot(1, 2, i+1)
    plt.imshow(res[i]), plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

   效果如下:

1.3  繪製圓形

  在OpenCV中,繪製直線使用的函式為 circle() ,其函式原型如下:

def circle(img, center, radius, color, thickness=None, lineType=None, shift=None): # real signature unknown; restored from __doc__
    """
    circle(img, center, radius, color[, thickness[, lineType[, shift]]]) -> img
    .   @brief Draws a circle.
    .   
    .   The function cv::circle draws a simple or filled circle with a given center and radius.
    .   @param img Image where the circle is drawn.
    .   @param center Center of the circle.
    .   @param radius Radius of the circle.
    .   @param color Circle color.
    .   @param thickness Thickness of the circle outline, if positive. Negative values, like #FILLED,
    .   mean that a filled circle is to be drawn.
    .   @param lineType Type of the circle boundary. See #LineTypes
    .   @param shift Number of fractional bits in the coordinates of the center and in the radius value.
    """
    pass

   繪製圓形也簡單,只需要確定圓心與半徑即可。

  實踐程式碼如下:

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 生成一個空灰度影像
img1 = np.zeros((400, 400), np.uint8)
img1 = cv2.circle(img1, (150, 150), 100, 255, 5)

# 生成一個空彩色影像
img3 = np.zeros((400, 400, 3), np.uint8)
img3 = cv2.circle(img3, (150, 150), 100, (0, 255, 0), 5)

titles = ['gray circle image', 'color circle image']
res = [img1, img3]

for i in range(2):
    plt.subplot(1, 2, i+1)
    plt.imshow(res[i]), plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

   效果如下:

1.4  繪製橢圓

  在OpenCV中,繪製直線使用的函式為 ellipse() ,其函式原型如下:

def ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness=None, lineType=None, shift=None): # real signature unknown; restored from __doc__
    """
    ellipse(img, center, axes, angle, startAngle, endAngle, color[, thickness[, lineType[, shift]]]) -> img
    .   @brief Draws a simple or thick elliptic arc or fills an ellipse sector.
    .   
    .   The function cv::ellipse with more parameters draws an ellipse outline, a filled ellipse, an elliptic
    .   arc, or a filled ellipse sector. The drawing code uses general parametric form.
    .   A piecewise-linear curve is used to approximate the elliptic arc
    .   boundary. If you need more control of the ellipse rendering, you can retrieve the curve using
    .   #ellipse2Poly and then render it with #polylines or fill it with #fillPoly. If you use the first
    .   variant of the function and want to draw the whole ellipse, not an arc, pass `startAngle=0` and
    .   `endAngle=360`. If `startAngle` is greater than `endAngle`, they are swapped. The figure below explains
    .   the meaning of the parameters to draw the blue arc.
    .   
    .   ![Parameters of Elliptic Arc](pics/ellipse.svg)
    .   
    .   @param img Image.
    .   @param center Center of the ellipse.
    .   @param axes Half of the size of the ellipse main axes.
    .   @param angle Ellipse rotation angle in degrees.
    .   @param startAngle Starting angle of the elliptic arc in degrees.
    .   @param endAngle Ending angle of the elliptic arc in degrees.
    .   @param color Ellipse color.
    .   @param thickness Thickness of the ellipse arc outline, if positive. Otherwise, this indicates that
    .   a filled ellipse sector is to be drawn.
    .   @param lineType Type of the ellipse boundary. See #LineTypes
    .   @param shift Number of fractional bits in the coordinates of the center and values of axes.
    
    
    
    ellipse(img, box, color[, thickness[, lineType]]) -> img
    .   @overload
    .   @param img Image.
    .   @param box Alternative ellipse representation via RotatedRect. This means that the function draws
    .   an ellipse inscribed in the rotated rectangle.
    .   @param color Ellipse color.
    .   @param thickness Thickness of the ellipse arc outline, if positive. Otherwise, this indicates that
    .   a filled ellipse sector is to be drawn.
    .   @param lineType Type of the ellipse boundary. See #LineTypes
    """
    pass

   這裡解釋一下引數:

  • img:影像
  • center:橢圓圓心座標
  • axes:軸的長度
  • angle:偏轉的角度
  • start_angle:圓弧起始角的角度
  • end_angle:圓弧終結角的角度
  • color:線條的顏色
  • thickness:線條的粗細程度
  • line_type:線條的型別,詳情見CVLINE的描述
  • shift:圓心座標點的數軸的精度

  影像化如下:

   實踐程式碼如下:

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 生成一個空灰度影像
img_origin1 = np.zeros((400, 400), np.uint8)
img_origin11 = img_origin1.copy()
# 引數依次是:影像,橢圓圓心座標,軸的長度,偏轉的角度, 圓弧起始角的角度,圓弧終結角的角度,線條的顏色,線條的粗細程度,線條的型別
img1 = cv2.ellipse(img_origin1, (150, 150), (150, 100), 30, 10, 190, 250)
img11 = cv2.ellipse(img_origin11, (150, 150), (150, 100), 30, 10, 190, 250, -1)

# 生成一個空彩色影像
img_origin3 = np.zeros((400, 400, 3), np.uint8)
img_origin33 = img_origin3.copy()
# 注意最後一個引數 -1,表示對影像進行填充,預設是不填充的,如果去掉,只有橢圓輪廓了
img3 = cv2.ellipse(img_origin3, (150, 150), (150, 100), 30, 0, 180, 250)
img33 = cv2.ellipse(img_origin33, (150, 150), (150, 100), 30, 0, 180, 250, -1)

titles = ['gray ellipse image', 'color ellipse image', 'gray ellipse padding', 'color ellipse padding']
res = [img1, img3, img11, img33]

for i in range(4):
    plt.subplot(2, 2, i+1)
    plt.imshow(res[i]), plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

   效果如下:

2,影像輪廓

  影像輪廓可以簡單認為成將連續的點(連著邊界)連在一起的曲線,具有相同的顏色或者灰度。輪廓在形狀分析和物體的檢測和識別中很有用。

  • 為了更加準確,要使用二值化影像。在尋找輪廓之前,要進行閾值化處理,或者Canny邊界檢測。
  • 查詢輪廓的函式會修改原始影像。如果你在找到輪廓之後還想使用原始影像的話,你應該將原始影像儲存到其他變數中。
  • 在OpenCV中,查詢輪廓就像在黑色背景中超白色物體。你應該記住要找的物體應該是白色而背景應該是黑色

2.1  cv2.findContours()函式

  那麼如何在一個二值化影像中查詢輪廓呢?這裡推薦使用函式cv2.findContours():

  函式cv2.findContours()函式的原型為:

cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]])  

  注意:opencv2返回兩個值:contours:hierarchy。而opencv3會返回三個值,分別是img(影像), countours(輪廓,是一個列表,裡面存貯著影像中所有的輪廓,每一個輪廓都是一個numpy陣列,包含物件邊界點(x, y)的座標), hierarchy(輪廓的層析結構)。

函式引數:

  第一個引數是尋找輪廓的影像,即輸入影像;

  第二個參數列示輪廓的檢索模式,有四種(本文介紹的都是新的cv2介面):

  •  cv2.RETR_EXTERNAL: 表示只檢測外輪廓
  • cv2.RETR_LIST: 表示檢測所有輪廓,檢測的輪廓不建立等級關係,並將其儲存到一條連結串列當中
  • cv2.RETR_CCOMP :表示檢測所有的輪廓,並將他們組織為兩層:頂層是各部分的外部邊界,第二次是空洞的邊界
  • cv2.RETR_TREE: 表示檢測所有輪廓,並重構巢狀輪廓的整個層次,建立一個等級樹結構的輪廓

  第三個引數method為輪廓的近似辦法

  • cv2.CHAIN_APPROX_NONE:以Freeman鏈碼的方式輸出輪廓,所有其他方法輸出多邊形(頂點的序列)。儲存所有的輪廓點,相鄰的兩個點的畫素位置差不超過1,即max(abs(x1-x2),abs(y2-y1))==1
  • cv2.CHAIN_APPROX_SIMPLE:壓縮水平方向,垂直方向,對角線方向的元素,只保留該方向的終點座標,例如一個矩形輪廓只需4個點來儲存輪廓資訊
  • cv2.CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似演算法

  這裡輪廓的近似的兩個方法我們可以從下面圖看更加明顯:

   一個是輸出所有輪廓(即所有頂點的序列),另一個函式只保留他們的終點部分。

函式返回值

  一般情況下,cv2.findContours()函式返回兩個值,一個是輪廓本身,還有一個是每條輪廓對應的屬性。當然特殊情況下返回三個值。即第一個是影像本身。

  contour返回值

  cv2.findContours()函式首先返回一個 list,list中每個元素都是影像中的一個輪廓,用numpy中的ndarray表示。這個概念非常重要,通過下面程式碼檢視:

print (type(contours))
print (type(contours[0]))
print (len(contours))
'''
結果如下:
    <class 'list'>
    <class 'numpy.ndarray'>
    2
'''

  這裡我們使用 contour.jpg 這幅影像舉個例子,圖如下:

  通過上述圖,我們會看到本例中有兩條輪廓,一個是五角星的,一個是矩形的。每個輪廓是一個 ndarray,每個 ndarray是輪廓上的點的集合,並且列印出list的長度為2。

  由於我們知道返回的輪廓有兩個,因此可以通過:

cv2.drawContours(img,contours[0],0,(0,0,255),3)

cv2.drawContours(img,contours[1],0,(0,0,255),3)

  分別繪製兩個輪廓,同時通過:

print(len(contours[0]))
print(len(contours[1]))
'''
結果如下:
        4
        368
'''

  輸出兩個輪廓中儲存的點的個數,可以看出,第一個輪廓中只有四個元素,這是因為輪廓中並不是儲存輪廓上所有的點,而是隻儲存可以用直線描述輪廓的點的個數,比如一個“正立”的矩形,只需要四個頂點就能描述輪廓了。而第二個輪廓卻有368個元素,因為它是不規整的影像。

  hiarachy返回值

  此外,該函式還可返回一個可選的hiararchy結果,這是一個ndarray,其中的元素個數和輪廓個數相同,每個輪廓contours[i]對應4個hierarchy元素hierarchy[i][0] ~hierarchy[i][3],分別表示後一個輪廓、前一個輪廓、父輪廓、內嵌輪廓的索引編號,如果沒有對應項,則該值為負數。

print (type(hierarchy))
print (hierarchy.ndim)
print (hierarchy[0].ndim)
print (hierarchy.shape)
'''
結果如下:
        <class 'numpy.ndarray'>
        3
        2
        (1, 2, 4)
'''

  可以看出,hierachy本身包含兩個ndarray,每個 ndarray對應一個輪廓,每個輪廓有四個屬性。

  完整程式碼如下:

import cv2

img = cv2.imread('contour.jpg')

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

print (type(contours))
print (type(contours[0]))
print (len(contours))
'''
結果如下:
    <class 'list'>
    <class 'numpy.ndarray'>
    2
'''
print(len(contours[0]))
print(len(contours[1]))
'''
結果如下:
        4
        368
'''
print (type(hierarchy))
print (hierarchy.ndim)
print (hierarchy[0].ndim)
print (hierarchy.shape)
'''
結果如下:
        <class 'numpy.ndarray'>
        3
        2
        (1, 2, 4)
'''

# cv2.imshow('thresh', thresh)
# cv2.waitKey(0)
# cv2.destroyWindow('thresh')

 

2.2  cv2.drawContours()

  OpenCV中通過 cv2.drawContours在影像上繪製輪廓。

  下面看一下cv2.drawContours()函式:

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

引數:

  • 第一個引數是指明在哪幅影像上繪製輪廓
  • 第二個引數是輪廓本身,在Python中是一個list。
  • 第三個引數指定繪製輪廓list中的哪條輪廓,如果是-1,則繪製其中的所有輪廓。後面的引數很簡單。其中thickness表明輪廓線的寬度,如果是-1(cv2.FILLED),則為填充模式。繪製引數將在以後獨立詳細介紹。

  下面看一個例項,在一幅影像上繪製所有的輪廓:

#_*_coding:utf-8_*_
import cv2
import numpy as np
 
img_path = 'contour.jpg'
img = cv2.imread(img_path)
img1 = img.copy()
img2 = img.copy()
img3 = img.copy()
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy= cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 繪製獨立輪廓,如第四個輪廓
img1 = cv2.drawContours(img1, contours, -1, (0, 255, 0), 3)
# 如果指定繪製幾個輪廓(確保數量在輪廓總數裡面),就會只繪製指定數量的輪廓
img2 = cv2.drawContours(img2, contours, 1, (0, 255, 0), 3)
img3 = cv2.drawContours(img3, contours, 0, (0, 255, 0), 3)

res = np.hstack((img, img1, img2))
cv2.imshow('img', img3)
cv2.waitKey(0)
cv2.destroyAllWindows()

  需要注意的是 cv2.findContours()函式接受的引數是二值圖,即黑白的(不是灰度圖),所以讀取的影像先要轉化成灰度圖,再轉化成二值圖,後面兩行程式碼分別是檢測輪廓,繪製輪廓。

  比如原圖如下:

   檢測到的所有輪廓圖如下(當指定繪製輪廓引數為 -1 ,預設繪製所有的輪廓):

  當指定繪製輪廓的引數為 0的時候,則會找到索引為0的影像的輪廓如下:

  同理,當指定繪製輪廓的引數為 1的時候,則會找到索引為1的影像的輪廓如下:

   注意:findcontours函式會“原地”修改輸入的影像,所以我們需要copy影像,不然原圖會變。。。。

2.3  cv2.boundingrect()函式

  矩形邊框(Bounding Rectangle)是說,用一個最小的矩形,把找到的形狀包起來。還有一個帶旋轉的矩形,面積會更小。

  首先介紹下cv2.boundingRect(img)這個函式,原始碼如下:

def boundingRect(array): # real signature unknown; restored from __doc__
    """
    boundingRect(array) -> retval
    .   @brief Calculates the up-right bounding rectangle of a point set or non-zero pixels of gray-scale image.
    .   
    .   The function calculates and returns the minimal up-right bounding rectangle for the specified point set or
    .   non-zero pixels of gray-scale image.
    .   
    .   @param array Input gray-scale image or 2D point set, stored in std::vector or Mat.
    """
    pass

  解釋一下引數的意義:img是一個二值圖,也就是它的引數;返回四個值,分別是x,y,w,h( x,y是矩陣左上點的座標,w,h是矩陣的寬和高);

  用下面函式解釋更加形象:

x, y, w, h = cv2.boudingrect(cnt) # 獲得外接矩形

引數說明:x,y, w, h 分別表示外接矩形的x軸和y軸的座標,以及矩形的寬和高, cnt表示輸入的輪廓值

  得到矩陣的座標後,然後利用cv2.rectangle(img, (x,y), (x+w,y+h), (0,255,0), 2)畫出矩行,我們前面有講這個函式,這裡不再贅述。

  下面舉個例子來看看如何找出不規則影像的外接矩陣,並畫出其矩陣,首先圖如下:

   我們的目的是找出這個不規則影像的外接矩陣,並展示出來,程式碼如下:

#_*_coding:utf-8_*_
import cv2
import numpy as np
 
img_path = 'contour2.png'
img = cv2.imread(img_path)
img1 = img.copy()
img2 = img.copy()
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy= cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
print('輪廓的總數為', len(contours))
# 輪廓的總數為 2

cnt = contours[0]
x, y, w, h = cv2.boundingRect(cnt)
img1 = cv2.rectangle(img1, (x,y), (x+w,y+h), (0, 255, 0), 2)

cv2.imshow('img', img1)
cv2.waitKey(0)
cv2.destroyAllWindows()

   效果如下:

2.4  cv2.contourArea()

  opencv中使用cv2.contourArea()來計算輪廓的面積。

  首先介紹下cv2.contourArea(cnt, True)這個函式,原始碼如下:

def contourArea(contour, oriented=None): # real signature unknown; restored from __doc__
    """
    contourArea(contour[, oriented]) -> retval
    .   @brief Calculates a contour area.
    .   
    .   The function computes a contour area. Similarly to moments , the area is computed using the Green
    .   formula. Thus, the returned area and the number of non-zero pixels, if you draw the contour using
    .   #drawContours or #fillPoly , can be different. Also, the function will most certainly give a wrong
    .   results for contours with self-intersections.
    .   
    .   Example:
    .   @code
    .       vector<Point> contour;
    .       contour.push_back(Point2f(0, 0));
    .       contour.push_back(Point2f(10, 0));
    .       contour.push_back(Point2f(10, 10));
    .       contour.push_back(Point2f(5, 4));
    .   
    .       double area0 = contourArea(contour);
    .       vector<Point> approx;
    .       approxPolyDP(contour, approx, 5, true);
    .       double area1 = contourArea(approx);
    .   
    .       cout << "area0 =" << area0 << endl <<
    .               "area1 =" << area1 << endl <<
    .               "approx poly vertices" << approx.size() << endl;
    .   @endcode
    .   @param contour Input vector of 2D points (contour vertices), stored in std::vector or Mat.
    .   @param oriented Oriented area flag. If it is true, the function returns a signed area value,
    .   depending on the contour orientation (clockwise or counter-clockwise). Using this feature you can
    .   determine orientation of a contour by taking the sign of an area. By default, the parameter is
    .   false, which means that the absolute value is returned.
    """
    pass

   引數含義如下:

  • contour:表示某輸入單個輪廓,為array
  • oriented:表示某個方向上輪廓的面積值,這裡指順時針或者逆時針。若為True,該函式返回一個帶符號的面積值,正負值取決於輪廓的方向(順時針還是逆時針),若為False,表示以絕對值返回

  面積的值與輸入點的順序有關,因為求的是按照點的順序連線構成的圖形的面積。

  下面實踐一下:

#_*_coding:utf-8_*_
import cv2
import numpy as np
  
img_path = 'contour2.png'
img = cv2.imread(img_path)
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy= cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
 
 
cnt = contours[0]
# 求輪廓的面積
area = cv2.contourArea(cnt)
print(img.shape)  # (306, 453, 3)
print(area)  # 57436.5
# 也可以看輪廓面積與邊界矩形比
x, y, w, h = cv2.boundingRect(cnt)
rect_area = w*h
extent = float(area) / rect_area
print('輪廓面積與邊界矩形比為', extent)
# 輪廓面積與邊界矩形比為 0.7800798598378357

 

2.5  cv2.arcLength()

  opencv中使用cv2.arcLength()來計算輪廓的周長。

  首先介紹下cv2.arcLength(cnt, True)這個函式,原始碼如下:

def arcLength(curve, closed): # real signature unknown; restored from __doc__
    """
    arcLength(curve, closed) -> retval
    .   @brief Calculates a contour perimeter or a curve length.
    .   
    .   The function computes a curve length or a closed contour perimeter.
    .   
    .   @param curve Input vector of 2D points, stored in std::vector or Mat.
    .   @param closed Flag indicating whether the curve is closed or not.
    """
    pass

   引數含義如下:

  • curve:輸入的二維點集(輪廓頂點),可以是 vector或者Mat型別
  • closed:用於指示曲線是否封閉

   下面舉個例子:

#_*_coding:utf-8_*_
import cv2
import numpy as np
 
img_path = 'contour2.png'
img = cv2.imread(img_path)
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy= cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)


cnt = contours[0]
# 求輪廓的周長
arcLength = cv2.arcLength(cnt, True)
print(img.shape)  # (306, 453, 3)
print(arcLength)  # 1265.9625457525253

 

2.6 cv2.approxPolyDP()

  cv2.approxPolyDP()函式是輪廓近似函式,是opencv中對指定的點集進行多邊形逼近的函式,其逼近的精度可通過引數設定。我們首先看一張圖:

   對於左邊這張圖,我們可以近似為中間和右邊的這張圖,具體如何近似呢?我們先不說,下面接著學。

  下面看看cv2.approxPolyDP()函式的原始碼:

def approxPolyDP(curve, epsilon, closed, approxCurve=None): # real signature unknown; restored from __doc__
    """
    approxPolyDP(curve, epsilon, closed[, approxCurve]) -> approxCurve
    .   @brief Approximates a polygonal curve(s) with the specified precision.
    .   
    .   The function cv::approxPolyDP approximates a curve or a polygon with another curve/polygon with less
    .   vertices so that the distance between them is less or equal to the specified precision. It uses the
    .   Douglas-Peucker algorithm <http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm>
    .   
    .   @param curve Input vector of a 2D point stored in std::vector or Mat
    .   @param approxCurve Result of the approximation. The type should match the type of the input curve.
    .   @param epsilon Parameter specifying the approximation accuracy. This is the maximum distance
    .   between the original curve and its approximation.
    .   @param closed If true, the approximated curve is closed (its first and last vertices are
    .   connected). Otherwise, it is not closed.
    """
    pass

   其引數含義:

  • curve:表示輸入的點集
  • epslion:指定的精度,也即原始曲線與近似曲線之間的最大距離,不過這個值我們一般按照周長的大小進行比較
  • close:若為True,則說明近似曲線為閉合的;反之,若為False,則斷開

  該函式採用的是道格拉斯—普克演算法(Douglas-Peucker)來實現。該演算法也以Douglas-Peucker 演算法和迭代終點擬合演算法為名。是將曲線近似表示為一系列點,並減少點的數量的一種演算法。該演算法的原始型別分別由烏爾斯-拉默(Urs Ramer)於 1972年以及大衛-道格拉斯(David Douglas)和托馬斯普克(Thomas Peucker)於 1973年提出,並在之後的數十年中由其他學者完善。

  經典的Douglas-Peucker 演算法描述如下:

  • 1,在曲線首位兩點A, B之間連線一條直線AB,該直線為曲線的弦
  • 2,得到曲線上離該直線段距離最大的點C,計算其與AB之間的距離d
  • 3,比較該距離與預先給定的閾值 threshold 的大小,如果小於 threshold,則該直線段作為曲線的近似,該段曲線處理完畢
  • 4,如果距離大於閾值,則用C將曲線分為兩段AC和BC,並分別對兩段取新進行1~3處理
  • 5,當所有曲線都處理完畢後,依次連線各個分割點形成的折線,即可以作為曲線的近似

  示意圖如下:

   示例如下:

#_*_coding:utf-8_*_
import cv2
import numpy as np
 
img_path = 'contour2.png'
img = cv2.imread(img_path)
img1 = img.copy()
img2 = img.copy()
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy= cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

cnt = contours[0]
# 繪製獨立輪廓,如第四個輪廓
img1 = cv2.drawContours(img1, [cnt], -1, (0, 255, 0), 3)

epsilon = 0.1*cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
img2 = cv2.drawContours(img2, [approx], -1, (0, 255, 0), 3)

res = np.hstack((img, img1, img2))
cv2.imshow('img', res)
cv2.waitKey(0)
cv2.destroyAllWindows()

   效果如下:

 

2.7 cv2.minEnclosingCircle()

  在opencv中也可以實現輪廓的外接圓,它是函式cv2.minEnclosingCircle()。

  下面我們看一下cv2.minEnclosingCircle()的原始碼:

def minEnclosingCircle(points): # real signature unknown; restored from __doc__
    """
    minEnclosingCircle(points) -> center, radius
    .   @brief Finds a circle of the minimum area enclosing a 2D point set.
    .   
    .   The function finds the minimal enclosing circle of a 2D point set using an iterative algorithm.
    .   
    .   @param points Input vector of 2D points, stored in std::vector\<\> or Mat
    .   @param center Output center of the circle.
    .   @param radius Output radius of the circle.
    """
    pass

  引數意思也很明瞭,這裡不再贅述。

  實踐程式碼如下:

#_*_coding:utf-8_*_
import cv2
import numpy as np
  
img_path = 'contour2.png'
img = cv2.imread(img_path)
img1 = img.copy()
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy= cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
 
 
cnt = contours[0]
# 求輪廓的外接圓
(x, y), radius = cv2.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
img1 = cv2.circle(img1, center, radius, (0, 255, 0), 2)
res = np.hstack((img, img1))
cv2.imshow('img', res)
cv2.waitKey(0)
cv2.destroyAllWindows()

  效果如下:

 

2.8  cv2.fillConvexPoly()與cv2.fillPoly()填充多邊形

  opencv中沒有旋轉矩形,也沒有填充矩陣,但是它可以使用填充多邊形函式 fillPoly()來填充。上面兩個函式的區別就在於 fillConvexPoly() 畫了一個凸多邊形,這個函式要快得多,不過需要指定凸多邊形的座標。而fillPoly()則不僅可以填充凸多邊形,任何單調多邊形都可以填充。

  cv2.fillConvexPoly()函式可以用來填充凸多邊形,只需要提供凸多邊形的頂點即可。

  下面看看cv2.fillConvexPoly()函式的原始碼:

def fillConvexPoly(img, points, color, lineType=None, shift=None): # real signature unknown; restored from __doc__
    """
    fillConvexPoly(img, points, color[, lineType[, shift]]) -> img
    .   @brief Fills a convex polygon.
    .   
    .   The function cv::fillConvexPoly draws a filled convex polygon. This function is much faster than the
    .   function #fillPoly . It can fill not only convex polygons but any monotonic polygon without
    .   self-intersections, that is, a polygon whose contour intersects every horizontal line (scan line)
    .   twice at the most (though, its top-most and/or the bottom edge could be horizontal).
    .   
    .   @param img Image.
    .   @param points Polygon vertices.
    .   @param color Polygon color.
    .   @param lineType Type of the polygon boundaries. See #LineTypes
    .   @param shift Number of fractional bits in the vertex coordinates.
    """
    pass

  示例如下:

#_*_coding:utf-8_*_
import cv2
import numpy as np

img = np.zeros((500, 500, 3), np.uint8)
triangle = np.array([[50, 50], [50, 400], [400, 450]])
cv2.fillConvexPoly(img, triangle, (0, 255, 0))
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

   我們使用綠色填充,效果如下:

    cv2.fillPoly()函式可以用來填充任意形狀的圖型.可以用來繪製多邊形,工作中也經常使用非常多個邊來近似的畫一條曲線.cv2.fillPoly()函式可以一次填充多個圖型

  下面看看cv2.fillPoly()函式的原始碼:

def fillPoly(img, pts, color, lineType=None, shift=None, offset=None): # real signature unknown; restored from __doc__
    """
    fillPoly(img, pts, color[, lineType[, shift[, offset]]]) -> img
    .   @brief Fills the area bounded by one or more polygons.
    .   
    .   The function cv::fillPoly fills an area bounded by several polygonal contours. The function can fill
    .   complex areas, for example, areas with holes, contours with self-intersections (some of their
    .   parts), and so forth.
    .   
    .   @param img Image.
    .   @param pts Array of polygons where each polygon is represented as an array of points.
    .   @param color Polygon color.
    .   @param lineType Type of the polygon boundaries. See #LineTypes
    .   @param shift Number of fractional bits in the vertex coordinates.
    .   @param offset Optional offset of all points of the contours.
    """
    pass

   效果如下:

#_*_coding:utf-8_*_
import cv2
import numpy as np

img = np.zeros((500, 500, 3), np.uint8)
area1 = np.array([[50, 50], [50, 400], [100, 450]])
area2 = np.array([[300, 300],[450, 300], [450, 450], [300, 450]])
cv2.fillPoly(img, [area1, area2], (255, 0, 0))
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

  效果如下:

3,輪廓處理實戰

  下面舉一個實際的例子來鞏固一下學習的知識點。

  問題是這樣的,假設我相對這張圖的左邊面積做處理,我希望將其填充為白色(任何想要的顏色)。

  也就是黑色圈外的顏色填充為白色,希望能完全利用上面學到的函式。

  下面依次分析,首先對影像進行K-Means聚類,效果如下:

  然後檢測輪廓,這裡儘量將所有的輪廓檢測出來,如下:

   然後對需要的輪廓進行填充,結果如下:

  上圖為最終的效果,程式碼如下:

import cv2
import numpy as np
import matplotlib.pyplot as plt


def show_image(img):
    cv2.imshow('image', img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


def image_processing(filename):
    img = cv2.imread(filename)
    img = cv2.resize(img, dsize=(100, 100))
    data = img.reshape((-1, 3))
    data = np.float32(data)
    # 定義中心(tyep, max_iter, epsilon)
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
    # 設定標籤
    flags = cv2.KMEANS_RANDOM_CENTERS
    # K-means 聚類,聚整合2類
    compactness, labels2, centers2 = cv2.kmeans(data, 2, None, criteria, 10, flags)

    # 2 類 影像轉換回 uint8 二維型別
    centers2 = np.uint8(centers2)
    res2 = centers2[labels2.flatten()]
    dst2 = res2.reshape(img.shape)

    gray = cv2.cvtColor(dst2, cv2.COLOR_BGR2GRAY)
    _, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
    contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 第一個引數是指明在哪副影像上繪製輪廓,第二個引數是輪廓本身,在Python中是list
    # 第三個引數指定繪製輪廓list中那條輪廓,如果是-1,則繪製其中的所有輪廓。。
    # dst3 = cv2.drawContours(img, contours, -1, (0, 255, 0), 3)

    # show_image(dst3)
    for ind, contour in enumerate(contours):
        print('總共有幾個輪廓:%s' % len(contours))

        # 其中x,y,w,h分佈表示外接矩陣的x軸和y軸的座標,以及矩陣的寬和高,contour表示輸入的輪廓值
        x, y, w, h = cv2.boundingRect(contour)
        print(x, y, w, h)
        if w > 80 or h > 80:
            print(contours[ind])
            print(type(contours[ind]), contours[ind].shape)
            # cv2.fillConvexPoly()函式可以用來填充凸多邊形,只需要提供凸多邊形的頂點即可。
            cv2.fillConvexPoly(img, contours[ind], (255, 255, 255))
    show_image(img)

    # # 用來正常顯示中文標籤
    # plt.rcParams['font.sans-serif'] = ['SimHei']
    #
    # # 顯示圖形
    # titles = [u'原圖', u'聚類影像 K=2']
    # images = [img,  dst2]
    # for i in range(len(images)):
    #     plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    #     plt.title(titles[i])
    #     plt.xticks([]), plt.yticks([])
    # plt.show()


if __name__ == '__main__':
    filename1 = 'test.png'
    image_processing(filename)

 

 

 

 openCV Contours詳解:https://www.pianshen.com/article/5989350739/

參考文獻:https://blog.csdn.net/hjxu2016/article/details/77833336

https://blog.csdn.net/sunny2038/article/details/12889059#(寫的好)

 https://www.cnblogs.com/Ph-one/p/12082692.html

部落格園函式:https://www.cnblogs.com/Undo-self-blog/p/8438808.html#top

https://blog.csdn.net/on2way/article/details/46793911

相關文章