python-opencv 影像捕捉多個不規則輪廓,與輪廓內接區域(圓/矩形)思路-持續更新編輯中(會附上詳細的思路解釋和圖片)

Lorzen發表於2020-09-08

  整體思路:

  1.原圖灰度化  

  2.灰度圖擷取mask區域  

  3.mask區域二值化  

  4.二值化影像運算(開運算)  

  5.原灰圖輪廓提取 

  6.不規則輪廓校準(外接矩形/內接矩形)

注:程式碼依次頭尾連線哦!

0.第三方庫匯入

import cv2 as cv
import numpy as np
import imutils
import matplotlib.pyplot as plt
import MightexUSBcameraSDK as Cam
import math

 

1.原圖灰度化

1 img = cv.imread(r"D:\picture\p7.png", cv.COLOR_GRAY2BGR)
2 gray = cv.imread(r"D:\picture\p7.png", cv.COLOR_BGR2GRAY)

 

2.灰度圖擷取mask區域

 

 

 

 程式碼中省去了圖中右側的兩個畫素值分佈圖。從原灰度圖選取一個mask區間,區間是基於圖左兩張圖的XY畫素座標,擷取自己感興趣的mask區域,省去毫不關心並可能影響影像處理的區域。


mask = np.zeros(gray.shape[:2], np.uint8)
mask[0:430, 240:530] = 255 # 裁剪出mask區域
mask_hist = cv.calcHist([gray], [0], mask, [256], [0, 256]) # 計算mask的直方圖 灰度圖通道=[0]

# 8 通過位運算,計算有mask的灰度圖片
mask_img = cv.bitwise_and(gray, gray, mask=mask)
# show_image(mask_img, "gray image with mask", 3, "X pixel pos", "Y pixel pos")
# show_histogram(mask_hist, "histogram with masked gray image", 4,
# "m", "Gray value (black~white:0~255)", "Number of pixels")
# plt.show() # 顯示畫布

img = mask_img # img 作為原灰圖,mask_img 為mask區域的灰度圖

 

3.mask區域二值化

 

 

 

 影像二值化,可理解為將圖形與背景,通過一個閾值X,低於閾值X的視為黑色0,高於X的視為灰色1。

        # (1).灰度化影像
        gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
        # (2).高斯濾波
        blurred = cv.GaussianBlur(gray, (5, 5), 0)
        # thresh = cv.threshold(blurred, 64, 80, cv.THRESH_BINARY)[1]
        # (3).二值化影像(化為0,1)
        thresh = cv.threshold(blurred, 61, 80, cv.THRESH_BINARY)[1]     # 調整高/低閾值,以匹配最好的識別效果
        cv.imshow("thresh", thresh)     # thresh——二值化,具體將閉合輪廓整體填充為灰色,其餘(背景)為黑色

4.二值化影像運算(開運算)

 

 

  本人所關心的輪廓是基於類似橢圓狀的區域部分,所以應想辦法將尖端 "腐蝕" 掉。

  先腐蝕後膨脹——即為開運算;相對的反過來就是——閉運算。這兩類運算目的,是為了消除原圖拍攝時產生的輪廓(內/外)毛刺、尖端、或者噪音。此處我用開運算主要是為了腐蝕掉我不關心的輪廓尖端。

   很明顯通過腐蝕後再膨脹,輪廓變得更加圓滑,並消除了尖端。

 

 

 

        # (4).開運算[先膨脹-後腐蝕],嘗試去除噪聲(去除尖端)
        img2 = thresh.copy()
        k = np.ones((10, 10), np.uint8)     # 卷積核 如(10, 10)= 10X10的矩陣(或稱陣列)
        thresh_open = cv.morphologyEx(img2, cv.MORPH_OPEN, k)      # 開運算[先膨脹-後腐蝕]

        cv.imshow("open operation", thresh_open)    # 暫時遮蔽

 5.原灰圖輪廓提取

 

 

 

   cnts 返回所有輪廓集合,並計算輪廓質心(cX, cY),注意這裡指質心,而不是規則圖形的中心,質心會隨著圖中部分輪廓的凹陷而稍微偏移。c 很好理解,則是cnts遍歷出來的每一個輪廓。將 c 取出是為了對每個輪廓進行內接矩形的捕捉等等,從程式碼中可以看出 c 被當成一個引數傳入進行運算。

   可以print (M)進行列印檢視,就能理解cX,cY是通過輪廓總面積進行計算求取。

  x_min, x_max, y_min, y_max,則是求取輪廓的四點極值,也就是後面計算內接矩的一個標準範圍。

        # (5).下一步是使用輪廓檢測​​找到這些白色區域的位置:返回輪廓個數
        cnts = cv.findContours(thresh_open.copy(), cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)     # RETR_EXTERNAL
        cnts = imutils.grab_contours(cnts)     # 返回輪廓 contours —— cnts
        # (6).cnts 返回的是所有輪廓,所以需要for迴圈來遍歷每一個輪廓
        for i, c in enumerate(cnts):
            # 計算輪廓區域的影像矩。 在計算機視覺和影像處理中,影像矩通常用於表徵影像中物件的形狀。
            # 這些力矩捕獲了形狀的基本統計特性,包括物件的面積,質心(即,物件的中心(x,y)座標),
            # 方向以及其他所需的特性。
            M = cv.moments(c)
            # m00是影像面積(白色區域)的總和,或者說連通域的面積;而這時m10和m01是影像白色區域上x和y座標值的累計
            cX = int(M["m10"] / M["m00"])
            cY = int(M["m01"] / M["m00"])
            # 1. 繪製最大內接圓
            r = drawInCircle(thresh_open, img, c, cX, cY)

            # 2. 計算最小外接正矩形的四個頂點,是否繪製外矩形框
            x_min, x_max, y_min, y_max = drawOutRectgle(c, False)

            # 3. 最大內接矩形
            x1, x2, y1, y2 = drawInRectgle(img, c, cX, cY, x_min, x_max, y_min, y_max)
            cv.drawContours(img, [c], -1, (0, 255, 0), 1)       # 最外層輪廓繪製
            cv.circle(img, (cX, cY), 1, (255, 255, 255), -1)    # 輪廓中心點
            cv.putText(img, "center%d=%s avg=%d" % (i, bgr_val, gray_avg), (cX - 90, cY - 16),
                        cv.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)

6.不規則輪廓校準(外接矩形/內接矩形)

(1)最大內接圓

遍歷每個輪廓的所有座標,尋求基於中心座標(輪廓質心)最大圓直徑

 

 

  白字補充的分別是質心的灰度值,和白框矩形內所有亮度的平均灰度值,這裡可以忽略不是重點。 

 

        def drawInCircle(img_open, img, cont, cX, cY):
            # 繪製最大內接圓 # 最大內接圓——檢索輪廓的方式
            c = cont    # 單個輪廓
            contours = cv.findContours(img_open.copy(), cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
            src = img_open.copy()
            raw_dist = np.empty(src.shape, dtype=np.float32)
            for ii in range(src.shape[0]):
                for jj in range(src.shape[1]):
                    raw_dist[ii, jj] = cv.pointPolygonTest(c, (jj, ii), True)   # 檢測點座標,與c輪廓座標的距離
            minVal, maxVal, _, maxDistPt = cv.minMaxLoc(raw_dist)   # minMaxLoc查詢最小和最大元素值及其位置ma
            maxVal = abs(maxVal)
            cv.circle(img, (cX, cY), np.int(maxVal)-1, (255, 255, 255), 1, cv.LINE_8, 0)    # 最大內接圓
            height = np.int(maxVal)-1
            return np.int(maxVal)-1

(2)最大內接正矩形(中心延展演算法——個人理解)

  中心延展法,其實就是中心座標從 (cX, cY) 開始向四周延展。理解很簡單,即從單個畫素(cX, cY) 分別從第二、三象限,到第一、四象限(數學上以中心座標)進行+1延展,但是此處影像很明顯X:Y的比例大致將近 5:1或者4:1,故而演算法實現中不能Y延展1,再到X延展1來類推。通過比例進行Y延展1,再到X延展4,再判斷是否超出輪廓,若超出輪廓X部分需要遍歷返回步進1(即),直到到達輪廓。例如X向左延展需要同時判定,第二、三象限皆滿足條件(不超輪廓),向右延展則滿足一、四象限類推,Y也是如此。

  “# 取軸更長範圍作for迴圈”,這前部分程式碼是自動計算了輪廓的X:Y的比例關係,radio 為長軸比短軸的比例,也直接在延展中將X、Y的步進值與radio聯絡上,比較“人性化”的計算,所以當遇到Y比X長的輪廓,這個演算法也適用。這部分可以寫死簡化,看個人取捨。

  重點是理解核心演算法,位於註釋 "# 第二象限延展" 即為演算法的開端,當完成第二象限的延展時,返回一個標誌位提示完成;當四個象限延展判斷完成時,則提前終止for迴圈。

  有人會好奇,為何我用的不是雙for迴圈對X、Y的遍歷,其實只要理解中心延展法,其實如何寫就是自己的喜好;起初我也是用雙for,但是發現沒必要而且更復雜,就想到了比例延展的思路,沒必要每次只延展步進1,這樣效率也比較低;按比例延展,只要超出輪廓才進行遍歷返回(每次返回1直到回到輪廓上)。

        def drawInRectgle(img, cont, cX, cY, x_min, x_max, y_min, y_max):
            """繪製不規則最大內接正矩形"""
            # img 對應的是原圖, 四個極值座標對應的是最大外接矩形的四個頂點
            c = cont  # 單個輪廓
            # print(c)
            range_x, range_y = x_max - x_min, y_max - y_min   # 輪廓的X,Y的範圍
            x1, x2, y1, y2 = cX, cX, cY, cY     # 中心擴散矩形的四個頂點x,y
            cnt_range, radio = 0, 0
            shape_flag = 1                      # 1:輪廓X軸方向比Y長;0:輪廓Y軸方向比X長
            if range_x > range_y:                     # 判斷輪廓 X方向更長
                radio, shape_flag = int(range_x / range_y), 1
                range_x_left = cX - x_min
                range_x_right = x_max - cX
                if range_x_left >= range_x_right:   # 取軸更長範圍作for迴圈
                    cnt_range = int(range_x_left)
                if range_x_left < range_x_right:
                    cnt_range = int(range_x_right)
            else:                                   # 判斷輪廓 Y方向更長
                radio, shape_flag = int(range_y / range_x), 0
                range_y_top = cY - y_min
                range_y_bottom = y_max - cY
                if range_y_top >= range_y_bottom:   # 取軸更長範圍作for迴圈
                    cnt_range = int(range_y_top)
                if range_y_top < range_y_bottom:
                    cnt_range = int(range_y_bottom)
            print("X radio Y: %d " % radio)
            print("---------new drawing range: %d-------------------------------------" % cnt_range)
            flag_x1, flag_x2, flag_y1, flag_y2 = False, False, False, False
            radio = 5       # 暫時設5,統一比例X:Y=5:1 因為發現某些會出現X:Y=4:1, 某些會出現X:Y=5:1
            if shape_flag == 1:
                radio_x = radio - 1
                radio_y = 1
            else:
                radio_x = 1
                radio_y = radio - 1
            for ix in range(1, cnt_range, 1):      # X方向延展,假設X:Y=3:1,那延展步進值X:Y=3:1
                # 第二象限延展
                if flag_y1 == False:
                    y1 -= 1 * radio_y       # 假設X:Y=1:1,輪廓XY方向長度接近,可理解為延展步進X:Y=1:1
                    p_x1y1 = cv.pointPolygonTest(c, (x1, y1), False)
                    p_x2y1 = cv.pointPolygonTest(c, (x2, y1), False)
                    if p_x1y1 <= 0 or y1 <= y_min or p_x2y1 <= 0:  # 在輪廓外,只進行y運算,說明y超出範圍
                        for count in range(0, radio_y - 1, 1):    # 最長返回步進延展
                            y1 += 1     # y超出, 步進返回
                            p_x1y1 = cv.pointPolygonTest(c, (x1, y1), False)
                            if p_x1y1 <= 0 or y1 <= y_min or p_x2y1 <= 0:
                                pass
                            else:
                                break
                        # print("y1 = %d, P=%d" % (y1, p_x1y1))
                        flag_y1 = True

                if flag_x1 == False:
                    x1 -= 1 * radio_x
                    p_x1y1 = cv.pointPolygonTest(c, (x1, y1), False)    # 滿足第二象限的要求,畫素都在輪廓內
                    p_x1y2 = cv.pointPolygonTest(c, (x1, y2), False)    # 滿足第三象限的要求,畫素都在輪廓內
                    if p_x1y1 <= 0 or x1 <= x_min or p_x1y2 <= 0:       # 若X超出輪廓範圍
                        # x1 += 1  # x超出, 返回原點
                        for count in range(0, radio_x-1, 1):       #
                            x1 += 1         # x超出, 步進返回
                            p_x1y1 = cv.pointPolygonTest(c, (x1, y1), False)  # 滿足第二象限的要求,畫素都在輪廓內
                            p_x1y2 = cv.pointPolygonTest(c, (x1, y2), False)  # 滿足第三象限的要求,畫素都在輪廓內
                            if p_x1y1 <= 0 or x1 <= x_min or p_x1y2 <= 0:
                                pass
                            else:
                                break
                        # print("x1 = %d, P=%d" % (x1, p_x1y1))
                        flag_x1 = True              # X軸像左延展達到輪廓邊界,標誌=True
                # 第三象限延展
                if flag_y2 == False:
                    y2 += 1 * radio_y
                    p_x1y2 = cv.pointPolygonTest(c, (x1, y2), False)
                    p_x2y2 = cv.pointPolygonTest(c, (x2, y2), False)
                    if p_x1y2 <= 0 or y2 >= y_max or p_x2y2 <= 0:  # 在輪廓外,只進行y運算,說明y超出範圍
                        for count in range(0, radio_y - 1, 1):  # 最長返回步進延展
                            y2 -= 1     # y超出, 返回原點
                            p_x1y2 = cv.pointPolygonTest(c, (x1, y2), False)
                            if p_x1y2 <= 0 or y2 >= y_max or p_x2y2 <= 0:  # 在輪廓外,只進行y運算,說明y超出範圍
                                pass
                            else:
                                break
                        # print("y2 = %d, P=%d" % (y2, p_x1y2))
                        flag_y2 = True              # Y軸像左延展達到輪廓邊界,標誌=True
                # 第一象限延展
                if flag_x2 == False:
                    x2 += 1 * radio_x
                    p_x2y1 = cv.pointPolygonTest(c, (x2, y1), False)    # 滿足第一象限的要求,畫素都在輪廓內
                    p_x2y2 = cv.pointPolygonTest(c, (x2, y2), False)    # 滿足第四象限的要求,畫素都在輪廓內
                    if p_x2y1 <= 0 or x2 >= x_max or p_x2y2 <= 0:
                        for count in range(0, radio_x - 1, 1):  # 最長返回步進延展
                            x2 -= 1     # x超出, 返回原點
                            p_x2y1 = cv.pointPolygonTest(c, (x2, y1), False)  # 滿足第一象限的要求,畫素都在輪廓內
                            p_x2y2 = cv.pointPolygonTest(c, (x2, y2), False)  # 滿足第四象限的要求,畫素都在輪廓內
                            if p_x2y1 <= 0 or x2 >= x_max or p_x2y2 <= 0:
                                pass
                            elif p_x2y2 > 0:
                                break
                        # print("x2 = %d, P=%d" % (x2, p_x2y1))
                        flag_x2 = True
                if flag_y1 and flag_x1 and flag_y2 and flag_x2:
                    print("(x1,y1)=(%d,%d)" % (x1, y1))
                    print("(x2,y2)=(%d,%d)" % (x2, y2))
                    break
            # cv.line(img, (x1,y1), (x2,y1), (255, 0, 0))
            cv.rectangle(img, (x1, y1), (x2, y2), (255, 255, 255), 1, 8)

            return x1, x2, y1, y2

(3)最小外接正矩形

  最小外接正矩形,也就是常見的人臉識別的綠框框,是不是有點像帽子(滑稽)。這個opencv已經內建boundingRect()方法,可以直接引用。

        def drawOutRectgle(cont, isdrawing=False):
            # 最小外接正矩形————用於計算輪廓內每個畫素灰度值(去除 矩形-外輪廓)
            c = cont
            st_x, st_y, width, height = cv.boundingRect(c)      # 獲取外接正矩形的xy
            # 對應的四個頂點(0,1,2,3) 0:左上,1:右上,2:右下,3:左下
            bound_rect = np.array([[[st_x, st_y]], [[st_x + width, st_y]],
                                   [[st_x + width, st_y + height]], [[st_x, st_y + height]]])
            if isdrawing:
                cv.drawContours(img, [bound_rect], -1, (0, 0, 255), 1)      # 繪製最小外接正矩形
            x_min, x_max, y_min, y_max = st_x, st_x + width, st_y, st_y + height    # 矩形四頂點
            # 通過每一個最小外接正矩形(四個頂點座標),判斷矩形內累加座標畫素的灰度值,除去小於閾值的畫素(在輪廓外)
            return x_min, x_max, y_min, y_max

 

相關文章