【小林的OpenCV基礎課 15】剪刀手/分水嶺分割法

所謂青春,大概只有經歷過的人才懂吧

圖像分割是圖像處理的一種常用手段,通俗地講就是傳說中的「摳圖」。在CV中,進行圖像分割可以有效地分辨圖像中地信息,對視頻處理和機器視覺中地語義分析有著重要的作用。分水嶺分割法是一種基礎的分割方法。

首先講為什麼叫分水嶺。廣袤的大地上有這樣一種地貌現象,一座山坡上出現了一個低洼的盆地,盆地里積滿了水,而分水嶺將盆地和山坡分隔開。

說得康娜想去旅行了(づ ̄ 3 ̄)づ

低洼的盆地與高聳的山峰

以前景和背景為例:

  • 從顏色的角度講,前景和背景的顏色不同
  • 從像素的角度講,前景的像素變化比背景的像素變化要豐富。由此我們將前景類比成山峰,背景類比成盆地,前景與背景的邊緣類比成分水嶺
  • 從數學的角度的講,分水嶺是局部極大值,盆地是局部極小值,同學們一定要注意極值和最值的區別。

在進行分水嶺演算法時,我們將這種分水嶺的地貌縮小成沙盤模型,在底部鑽孔,然後浸入水中,低洼的盆地最先被水淹沒,然後不知所措。而山峰受分水嶺的阻擋並不會被淹沒。現象是,背景部分被淹沒,前景部分並沒有被水掩膜,所以就把前景摳出來了。

分水嶺比較經典的計算方法是L.Vincent於1991年在PAMI上提出的。然而基於梯度圖像的直接分水嶺演算法容易導致圖像的過分割,即經典的分水嶺演算法對梯度運算肥腸敏感,圖像都被割碎了。因此出現了改進的分水嶺演算法,這種改進演算法的中心思想是貼標籤,根據標籤來判斷「地貌」。這種演算法的優點是解決了經典演算法造成的過分割,缺點是很多時候需要手動「貼標籤」。OpenCV中的watershed函數使用的就是這種改進的演算法。

下面講一下OpenCV分水嶺中的掩膜。假設我們要在圖像中分割出兩個區域,那麼最多存在三種區域,即兩個目標區域A、B和未知區域U。區域與區域的關係就是山峰與盆地的關係(比喻而已,山峰與盆地是相對於分水嶺講的),區域A和區域B的交界處為邊界,即分水嶺,而分分水嶺的區域通常是未知區域。

那麼如何標記呢?我們默認圖像是8位的,那麼未知區域U的標籤是0,區域A和區域B分別取半開半閉區間(0,255]的不同值,比如A取128,B取255,那麼機器就知道128和255是不同的區域,0是未知區域(我們眼中的分水嶺),區域之間存在分水嶺。

其實在OpenCV中使用分水嶺演算法時,也是默認0是未知區域的。

然後講一下分水嶺演算法的輸出圖像。首先要明確,OpenCV中分水嶺演算法watershed輸出的圖像深度是CV_32S,通常需要變換到CV_8U。在CV32S的輸出圖像中,像素值為-1的像素是邊界,我們需要將他修改為某個值(1.以便與區域A、B和U區分,2.方便轉換到CV8U)。轉換到CV_8U後的圖像中,區域A和B的像素值就是在處理掩膜時設置的值,前文講到A和B分別為128和255,那麼輸出圖像中A和B依舊為128和255。

API:

markers=cv2.watershed(image, markers)

  • image:待處理圖像,8位
  • markers:生成的掩膜,為32位單通道圖像

OpenCV中分水嶺法做圖像分割的一般步驟:

  • 圖像預處理,通常需要得到灰度圖像
  • 對需要分割的區域和未知區域做數字標記
  • 使用watershed函數進行處理
  • 根據需要處理後續圖像

下面看一個經典的Demo,出自官方指導,具體任務是分割硬幣。圖像中很多硬幣都是相互連接的。小林先跑了,看看買的基金的指數有沒有變化。

import cv2import numpy as npimg = cv2.imread(water_coins.jpg)gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)cv2.imshow(Binary Image, thresh)# 使用形態學運算濾除噪點kernel = np.ones((3,3),np.uint8)opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)# 靠近目標中心的是前景 遠離目標中心的是背景 硬幣邊緣是未知區域# 確定背景sure_bg = cv2.dilate(opening,kernel,iterations=3)cv2.imshow(Background Image, sure_bg)# 確定前景# 使用距離轉換讓硬幣之間分開# 如果只是單純摳前景 則可以不使用距離變換dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)ret, sure_fg = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)cv2.imshow(Foreground Image, sure_fg)# 確定未知區域# 背景圖減去前景圖得到未知區域# 類似於同心圓中大圓減去小圓得到圓環 圓環就是未知區域sure_fg = np.uint8(sure_fg)unknown = cv2.subtract(sure_bg,sure_fg)cv2.imshow(Unknown, unknown)# 從0開始進行標記# 這個函數可以將各個連通域從0開始標號 同一個連通域的像素的標籤相同ret, markers = cv2.connectedComponents(sure_fg)# 因為0是未知區域 所有標籤自增1markers = markers+1# 標記未知區域 這裡unknown中的白色的環狀區域為未知區域markers[unknown==255] = 0markers = cv2.watershed(img,markers)img[markers == -1] = [255,0,0]cv2.imshow(Result Image, img)cv2.waitKey()cv2.destroyAllWindows()

源圖

膨脹處理的背景圖

前景圖

未知圖像 白色為未知區域

效果圖

再來一個Demo,將圖片中的無人機分離出來,並對分割後的無人機用紅框框出來。我們將分水嶺的相關操作封裝到了一個叫做Segmenter的類裡面。

這段代碼的大致流程:

  • 圖像預處理
  • 配合閾值化和形態學運算獲得前景和背景,合成後得到掩膜
  • 將watershed函數輸出的圖像進行轉換,便於我們觀察
  • 對分水嶺處理後的前景圖像進行邊緣檢測

import cv2import numpy as npclass Segmenter(object): def __init__(self): self._mask_32S = None self._waterImg = None# 將掩膜轉化為CV_32S def setMark(self, mask): self._mask_32S = np.int32(mask)# 進行分水嶺操作 def waterProcess(self, img): self._waterImg = cv2.watershed(img, self._mask_32S)# 獲取分割後的8點陣圖像 def getSegmentationImg(self): segmentationImg = np.uint8(self._waterImg) return segmentationImg# 處理分割後圖像的邊界值 def getWaterSegmentationImg(self): waterSegmentationImg = np.copy(self._waterImg) waterSegmentationImg[self._waterImg == -1] = 1 waterSegmentationImg = np.uint8(waterSegmentationImg) return waterSegmentationImg# 將分水嶺演算法得到的圖像與源圖像合併 實現摳圖效果 def mergeSegmentationImg(self, waterSegmentationImg, isWhite = False): _, segmentMask = cv2.threshold(waterSegmentationImg, 250, 1, cv2.THRESH_BINARY) segmentMask = cv2.cvtColor(segmentMask, cv2.COLOR_GRAY2BGR) mergeImg = cv2.multiply(img, segmentMask) if isWhite is True: mergeImg[mergeImg == 0] = 255 return mergeImgdef getBoundingRect(img, pattern): _, contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) x, y, w, h = cv2.boundingRect(contours[1]) cv2.rectangle(pattern, (x, y), (x + w, y + h), (0, 0, 200), 2)img = cv2.imread(Drone.jpg)mySegmenter = Segmenter()# 獲取前景圖片grayImg = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)blurImg = cv2.blur(grayImg, (3, 3))_, binImg = cv2.threshold(blurImg, 30, 255, cv2.THRESH_BINARY_INV)kernel1 = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))fgImg = cv2.morphologyEx(binImg, cv2.MORPH_CLOSE, kernel1)# 獲取背景圖片kernel2 = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))dilateImg = cv2.dilate(binImg, kernel2, iterations=4)_, bgImg = cv2.threshold(dilateImg, 1, 128, cv2.THRESH_BINARY_INV)# 合成掩膜maskImg = cv2.add(fgImg, bgImg)mySegmenter.setMark(maskImg)# 進行分水嶺操作 並獲得分割圖像mySegmenter.waterProcess(img)waterSegmentationImg = mySegmenter.getWaterSegmentationImg()outputImgWhite = mySegmenter.mergeSegmentationImg(waterSegmentationImg,True)kernel3 = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 20))dilateImg = cv2.dilate(waterSegmentationImg, kernel3)_, dilateImg = cv2.threshold(dilateImg, 130, 255, cv2.THRESH_BINARY)# 尋找輪廓getBoundingRect(dilateImg, img)cv2.imshow(Contours Image, dilateImg)cv2.imshow(White Image, outputImgWhite)cv2.imshow(Mask Image, maskImg)cv2.imshow(Output Image, img)cv2.waitKey()cv2.destroyAllWindows()

源圖

背景圖

前景圖

前景圖和背景圖相加得到的掩膜圖

watershed函數的輸出圖像 邊緣處的像素值為1

白色底的摳圖效果

對摳圖進行輪廓檢測

這一話的代碼已經同步到了Github,對應Class 4 Image Processing下的C4 WaterSegmentation Coins.py和C4 WaterSegmentation Drone.py。點個Star,手有餘香(づ ̄ 3 ̄)づ

KobayashiLiu/Kobayashi_OpenCV_py?

github.com圖標


最後的最後

如果喜歡小林的專欄,就收藏了吧!してください!


推薦閱讀:

【小林的OpenCV基礎課 番外】霍夫變換原理
1.26【OpenCV圖像處理】模板匹配
關於opencv中對齊圖片的問題?
50行代碼實現人臉檢測
【小林的OpenCV基礎課 番外】色彩空間

TAG:計算機視覺 | OpenCV | Python |