OpenCV – Contour輪廓

        剛開始我經常會搞糊塗Edge(邊緣)和Contour(輪廓)兩者有什麼不同,後來才知道在openCV的世界裏, 若Edge線條頭尾相連形成封閉的區塊,那麼它就是Contour,否則就只是Edge。

        對於一張結構簡單物體且背景色單純的圖片,我們可以直接使用灰階圖形取得該物體的Contour,但如果是一張複雜背景的圖片,就需要先透過edge detection或threshold預處理才行。一般來說,取得binary(黑白)圖片中的Contour會比從灰階圖片中取出來得更加容易且精確。

Contour的基本用法:

Contour的指令及用法如下,重要的是這兩個參數:cv2.RETR_LIST與cv2.CHAIN_APPROX_SIMPLE。

 

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

(_, cnts, _) = cv2.findContours(gray.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

clone = image.copy()

cv2.drawContours(clone, cnts, -1, (0, 255, 0), 2)

cv2.RETR_LIST表示我們要取得圖形中所有的Contour,如果將cv2.RETR_LIST改為CV_RETR_EXTERNAL,則表示只取外層的Contour(如果有其它Contour包在內部)。

cv2.CHAIN_APPROX_SIMPLE代表壓縮取回的Contour像素點,只取長寬及對角線的end points,而不傳回所有的點,如此可節省記憶體使用並加快速度。與此相反的則是CV_CHAIN_APPROX_NONE,表示要傳回該Contour所有的點。

我們先用小畫家畫一個簡單的向量圖形讓Contour處理看看,第二個參數分別指定為cv2.RETR_LIST與cv2_RETR_EXTERNAL,會發現後者的Contour數目少一個,但這卻是我們期望的結果。

取出物件

如果要將Contour物件取出來,我們需要使用mask指令,方式是依據指定的Contour建立一個mask,然後在原圖形上套用bitwise 對每個像素進行AND運算,如此一來,除了mask之外的其它區域都會是黑色,僅保留了mask區域也就是我們需要的Contour,如此一來就取得了需要的區域而去除了其它部份。

# 接續上方的程式,cnts為所有Contours,

# 並將上面程式中的cv2.RETR_LIST改為CV_RETR_EXTERNAL,只取外層的Contour。

for c in cnts:        

        mask = np.zeros(gray.shape, dtype="uint8″)  #依Contours圖形建立mask

        cv2.drawContours(mask, [c], -1, 255, -1) #255        白色, -1塗滿

        # show the images

        cv2.imshow(“Image", image)

        cv2.imshow(“Mask", mask)

#將mask與原圖形作AND運算

        cv2.imshow(“Image + Mask", cv2.bitwise_and(image, image, mask=mask))  

        cv2.waitKey(0)

依序取得圖片中的三個不同圖形

        

如果使用實際的相片呢?我找了一些形狀與顏色較為單純的物品集中在一起拍了一張,並丟給Contour試看看,結果挺另人失望的,應該僅有5個物品,不過卻傳回了高達315個物件。

這是因為Contour較適合處理Binary的圖形的緣故。在前例中我們用的是簡單幾何圖形,因此得出的結果還不差,但若套用到較為複雜的實際相片,往往就不是我們希望的結果了。

記得我們一開始提到,Contour較適合處理Binary的圖片嗎?所以一般會在使用Contour之前先以Threshold或Canny來處理(Threshold與Canny在前文有介紹過)。因此,我們插入如下的Gaussian模糊與Canny的codes在圖片轉為灰階與Contour指令之間(紅字部份)。

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

blurred = cv2.GaussianBlur(gray, (11, 11), 0)

binaryIMG = cv2.Canny(blurred, 20, 160)

(_, cnts, _) = cv2.findContours(binaryIMG.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

如此一來,便能正確的從圖片中取得需要的物件了。

 C:\Users\CHE7C6~1.TSE\AppData\Local\Temp\x10sctmp22.png C:\Users\CHE7C6~1.TSE\AppData\Local\Temp\x10sctmp23.png C:\Users\CHE7C6~1.TSE\AppData\Local\Temp\x10sctmp24.png C:\Users\CHE7C6~1.TSE\AppData\Local\Temp\x10sctmp25.png

針對圖片中Contour的處理:

一)標示中心點:

要取得Contour中心點,可使用OpenCV的moments(矩)函式,這是一個關於矩的計算函式。矩,又稱動差,英文為moment,這源自於物理學的數學理論對我實在太複雜又難懂,因此無法多作解釋,只要知道如何使用就可以了。

# 找出所有Contours

(cnts, _) = cv2.findContours(gray.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

clone = image.copy()

 

# 依次處理每個Contours

for c in cnts:

        # CV2.moments會傳回一系列的moments值,我們只要知道中點X, Y的取得方式是如下進行即可。

        M = cv2.moments(c)

        cX = int(M[“m10″] / M[“m00″])

        cY = int(M[“m01″] / M[“m00″])

 

        # 在中心點畫上黃色實心圓

        cv2.circle(clone, (cX, cY), 10, (1, 227, 254), -1)

C:\Users\CHE7C6~1.TSE\AppData\Local\Temp\x10sctmp26.png

二)計算面積與周長:

取得Contours面積的指令:contourArea(Contours物件),單位是pixel。

取得Contours周長的指令:cv2.arcLength(Contours物件, True)

cv2.arcLength後方的True是告訴電腦說該Contours邊界是closed沒有中斷,否則便傳入False。大部份情況我們都是使用True,例如上例圖中的5個Contours都是邊界closed。

area = cv2.contourArea(c)  #計算面積

perimeter = cv2.arcLength(c, True)    #計算周長

print “Contour #%d — area: %.2f, perimeter: %.2f" % (i + 1, area, perimeter)  #印出結果

#在該Contours印上其編號。

cv2.putText(clone, “#%d" % (i + 1), (cX – 20, cY), cv2.FONT_HERSHEY_SIMPLEX, 1.1, (252, 197, 5), 3)

C:\Users\CHE7C6~1.TSE\AppData\Local\Temp\x10sctmp27.png

從右側視窗中我們可看到各編號的contours其面積與周長,且這些面積與周長的資訊對於我們瞭解該物件在圖片中所佔大小比例以及物體特性很有幫助,例如,一個面積比其它物件小但周長卻最長,代表這個物件可能是狹長扭曲的形態(例如日本)。

三)擬合的矩形外框:

替contours加上邊框是用來標示圖片中的特定物體常用的手法,指令是cv2.boundingRect(contours物件),它會傳回一組(x, y, w, h),分別代表左上角的X與Y,以及寬和長。

(x, y, w, h) = cv2.boundingRect(c)

                cv2.rectangle(clone, (x, y), (x + w, y + h), (0, 255, 0), 2)

C:\Users\CHE7C6~1.TSE\AppData\Local\Temp\x10sctmp28.png

四)擬合的旋轉矩形外框:

直接使用cv2.boundingRect並沒有考慮到物體本身的角度。我們可以使用另一種方法來繪製邊框,使用到cv2.minAreaRect以及cv2.boxPoints兩個指令。

cv2.minAreaRect會傳回三個值,第一個是旋轉後距形的左上角X,Y值,第二個是寬及高,第三個是旋轉的角度\theta(或視為傾斜角度)。

另外,cv2.rectangle無法繪出傾斜的距形,可改為使用cv2.boxPoints,該指令會依據cv2.minAreaRect傳出的(X,Y)、(w,h)以及角度\theta來產生矩形。

box = cv2.minAreaRect(c)

        box = np.int0(cv2.boxPoints (box))  #–> int0會省略小數點後方的數字

        cv2.drawContours(clone, [box], -1, (0, 255, 0), 2)

C:\Users\CHE7C6~1.TSE\AppData\Local\Temp\x10sctmp29.png

        一般來說,如果我們要在圖片上標示或取出該物件會使用cv2.boundingRect,而cv2.minAreaRect則用於要產生該物件的mask以取得物該所在區域時使用。

五)擬合的圓形外框:

如果我們要用圓形而非矩形來繪製邊框,可用cv2.minEnclosingCircle指令。從下方的範例,知道minEnclosingCircle會傳回圓心X,Y以及半徑。

((x, y), radius) = cv2.minEnclosingCircle(c)

                cv2.circle(clone, (int(x), int(y)), int(radius), (0, 255, 0), 2)

C:\Users\CHE7C6~1.TSE\AppData\Local\Temp\x10sctmp30.png

六)擬合的橢圓外框:

類似旋轉角度的擬合矩形外框,製作擬合的橢圓外框指令是cv2.fitEllipse,然後使用cv2.ellipse將它繪出來:

                ellipse = cv2.fitEllipse(c)

                cv2.ellipse(clone, ellipse, (0, 255, 0), 2)

C:\Users\CHE7C6~1.TSE\AppData\Local\Temp\x10sctmp31.png

        在電腦視覺的應用中,最常使用的是第一種cv2.boundingRect。

5 Comments

  1. 想請問如果是像拼圖這種色彩度高的東西 要怎麼提取輪廓
    用色彩做出來跟背景無法有效的分離
    用canny拼圖的邊緣也會有太多的細小紋路
    想請問有做過類似的東西嗎 謝謝

迴響已關閉。