###### tags: `OpenCV`,`圖像分割` # OpenCV 實作篇-圖像分割(GrabCut Technique) mask遮罩 mask是用一副二值化圖片(0和1)對另外一幅圖片進行局部的遮擋。 ![](https://i.imgur.com/b8Y8EPD.jpg) **原始像素x 0 = 0 原始像素x 1 = 原本的像素(要留下來的)** 把前景、背景分出來-grabCut語法 outputmask, bgModel, fgModel = cv2.grabCut(image, mask,rect, bgModel, fgModel, iterCount, mode) output的部分有三塊,最重要的是outputmask: 1. outputmask-會返回一塊跟輸入image一樣大小的灰階圖片 裡面每個pixel的值,有四種可能: Pixel value 0 => cv2.GC_BGD =>屬於背景 Pixel value 1 => cv2.GC_FGD =>屬於前景 Pixel value 2 => cv2.GC_PR_BGD=>可能屬於背景 Pixel value 3 => cv2.GC_PR_FGD=>可能屬於前景 input的部分 1.image-目標照片 2.bgModel,fgModel-就是兩個尺寸(1,65)的array,給算法存模型參數用的。 下面兩個參數,就是要告訴算式,你打算怎麼分前後景 3.mask-你可以用一張跟輸入圖像一樣大的二元圖像當設定值 4.rect-你也可以用框選的方式當設定值 5.iterCount-計算過程你想要跑多少次 (try & erro去試) 6.mode-你可以用這個參數,決定你要用前面的mask當初始值,還是用rect當初始值,比如你用rect當初始值,model就知道主題在rect裡面,所以外面的部分先自動變成背景了。 cv2.GC_INIT_WITH_RECT cv2.GC_INIT_WITH_MASK **grabcut會把算出來的結果存回去 mask、bgdModel、fgdModel** ```python #選取我家newnew照片 import cv2 import numpy as np imgnew = cv2.imread("new33.jpg") newCopy = imgnew.copy() #imgnew.shape #(600, 450, 3) b_Model = np.zeros((1,65),np.float64) f_Model = np.zeros((1,65),np.float64) rect1 = cv2.selectROI(imgnew) #選擇有興趣的區域 框住然後按空白鍵 cv2.destroyAllWindows() ``` ![](https://i.imgur.com/mAYMqpv.jpg) ```python x1,y1,w1,h1 = rect1 #rect1是一個list print(x1,y1,w1,h1) #57 54 302 510 (x,y座標 ,w1:寬,h1:高) mask_new,b_model,f_model=cv2.grabCut(imgnew,None,rect1,b_Model,f_Model,5,cv2.GC_INIT_WITH_RECT) #沒有用mask所以第二個參數要填none,不然會拋錯,數字5是指跑5次 # grabcut會把算出來的結果存回去 mask_new、b_Model、f_Model mask_new,b_model,f_model #2d array ``` ![](https://i.imgur.com/8BdJnVT.jpg) ```python mask_new[200,:] ##可以確認一下圖片主要部分的資料結構轉換成怎樣 ``` Pixel value 0 => cv2.GC_BGD =>屬於背景 Pixel value 1 => cv2.GC_FGD =>屬於前景 Pixel value 2 => cv2.GC_PR_BGD=>可能屬於背景 Pixel value 3 => cv2.GC_PR_FGD=>可能屬於前景 ![](https://i.imgur.com/3gAK0iZ.jpg) ```python= #做一個mask要區分背景或前景用 mask_new2 = np.where((mask_new==0)|(mask_new==2),0,1).astype('uint8') #mask_new==0 或 mask_new==2,0&2屬於背景就轉為0,其餘轉為1 mask_new2[200,:] ``` ![](https://i.imgur.com/x5uJQ3h.jpg) ```python= #確認mask資料型態 np.unique(mask_new) #array([0, 2, 3], dtype=uint8) np.unique(mask_new2) #array([0, 1], dtype=uint8) 做好mask ``` ```python= cv2.imshow("Mask_new",mask_new*60) #數字60,255,80,255純粹把數值拉開,對比較為明顯 cv2.imshow("Mask_new2",mask_new2*255) cv2.imwrite("mask_new.jpg",mask_new*80) cv2.imwrite("mask_new2.jpg",mask_new2*255) cv2.waitKey(0) cv2.destroyAllWindows() ``` ![](https://i.imgur.com/ZzMNMgH.jpg) ```python= #mask_new2[:,:,np.newaxis].shape #check資料大小(要確保資料大小一致才能相乘) #透過相乘,乘0的歸0,乘1的留下來 img_new = imgnew*mask_new2[:,:,np.newaxis] cv2.imwrite("grabcut-new.jpg",img_new) cv2.imshow("grabcut-new",img_new) cv2.waitKey(0) cv2.destroyAllWindows() ``` ![](https://i.imgur.com/8WqkvWF.jpg) :star: 去背完成啦~~~ ```python= ##############################試試看調整iterCount,看看結果 import cv2 import numpy as np img_le = cv2.imread("lele2.jpg") #使用我家弟弟的照片 img_le = cv2.resize(img_le, (0,0), fx=0.3, fy=0.3) #照片太大先resize一下 print(img_le.shape) imgCopy_le = img_le.copy() mask_le = np.zeros(img_le.shape[:2], np.uint8)#跟上面作法不同(這個先做一個空的mask) bgd_Model = np.zeros((1,65),np.float64) fgd_Model = np.zeros((1,65),np.float64) rect_le = cv2.selectROI(img_le) cv2.destroyAllWindows() for i in range(5,11,2): #調整iterCount,看看結果 (利用變數i去帶不同參數) mask_le = np.zeros(img_le.shape[:2], np.uint8) bgd_Model = np.zeros((1,65),np.float64) fgd_Model = np.zeros((1,65),np.float64) cv2.grabCut(img_le,mask_le,rect_le,bgd_Model,fgd_Model,i,cv2.GC_INIT_WITH_RECT) mask_le2 = np.where((mask_le==0)|(mask_le==2),0,1).astype('uint8') #做一個只有0 & 1新的mask # print(np.unique(mask_le2)) img_copy_le = img_le.copy() img_grabcut_le = img_copy_le*mask_le2[:,:,np.newaxis] cv2.imshow(f"Image{i}",img_grabcut_le) cv2.imshow("Original",img_le) cv2.waitKey(0) cv2.destroyAllWindows() ``` ![](https://i.imgur.com/T0QtCwS.jpg) :star:**結果就是背景比較複雜的話,grabcut效果就比較差,座椅就無法去除掉,但藉由不同次數的計算顯示,推車的手還是可以被去背** **手動調整啦!! 做一支自己的修圖筆刷** ```python= import cv2 import numpy as np img = cv2.imread("lele2.jpg") img = cv2.resize(img, (0,0), fx=0.3, fy=0.3) imgCopy = img.copy() mask = np.zeros(img.shape[:2], np.uint8) bgdModel = np.zeros((1,65),np.float64) fgdModel = np.zeros((1,65),np.float64) rect = cv2.selectROI(img) x,y,w,h = rect cv2.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT) mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8') cv2.imshow("Mask2",mask2*255) # cv2.imwrite("mask.png",mask*80) # cv2.imwrite("mask2.png",mask2*255) cv2.waitKey(0) cv2.destroyAllWindows() ``` ```python= imgResult = img*mask2[:,:,np.newaxis] cv2.imshow("Image",imgResult) cv2.waitKey(0) cv2.destroyAllWindows() ``` ```python= img_mask = img.copy() mask2 = mask2*255 mask_copy = mask2.copy() ``` ```python= class Sketcher: def __init__(self, windowname, dests, colors_func): #視窗名稱 self.windowname = windowname #顯示的圖片目標 #會塞兩張圖進來 顯示cut圖和mask圖 self.dests = dests #顯示的顏色 self.colors_func = colors_func #一啟動就要顯示結果 self.show() #要記錄滑鼠座標位置 self.prev_pt = None #把滑鼠的callback function裝上來 #windowname是設定要把function裝在哪個視窗 #on_mouse是下面設計的function,這個function是要給cv2.setMouseCallback呼叫的,每逢有滑鼠訊號,cv2就會把參數傳給我們的on_mouse cv2.setMouseCallback(self.windowname, self.on_mouse) def show(self): #就是顯示圖 #會塞兩張圖進來 顯示cut圖和mask圖 cv2.imshow(self.windowname , self.dests[0]) cv2.imshow(self.windowname + ": mask" , self.dests[1]) # onMouse function for Mouse Handling # 處理滑鼠動作的指令 def on_mouse(self, event, x, y, flags, param): #point座標 pt = (x, y) #當滑鼠剛按下去,就先記錄一下這個點的座標,代表畫線任務開始,只要prev_pt不是None 下面的if判斷式就會進行 if event == cv2.EVENT_LBUTTONDOWN: self.prev_pt = pt elif event == cv2.EVENT_LBUTTONUP: #鬆開就忘記座標,歸0,那下面線就畫不出來了,代表這個畫線任務結束 self.prev_pt = None #self.prev_pt如果沒有歸零,就代表是同一個畫線任務仍在進行中 #CV_EVENT_FLAG_LBUTTON 的意思是 有左鍵拖曳的事件發生 #cv2.line(影像, 開始座標, 結束座標, 顏色, 線條寬度) if self.prev_pt and cv2.EVENT_FLAG_LBUTTON: #針對兩個圖分別處理標示 for dst, color in zip(self.dests, self.colors_func()): #((0,0,255), 0) #隨著你畫線,會開始在兩張圖裡面開始標註color,第一張圖就會配你設計的(0,0,255),如果第二張圖就會用後面那個0了 cv2.line(dst, self.prev_pt, pt, color, 5) #每次畫了線就更新一下起點,那你就會畫出一條線,如果這句刪掉會怎樣?就會每次原點出發 self.prev_pt = pt self.show() ``` ```python= while True: #把蒐集到鍵盤的訊號存起來,下面就開始根據不同的訊號,開始不同的處理 ch = cv2.waitKey() #Quit #按esc if ch == 27: print("exiting...") #存照片,不接受中文名稱 cv2.imwrite("cut_result.png",img_mask) cv2.imwrite("mask_grabcut.png",mask2) break # 按r的時候Reset elif ch == ord('r'): print("重新開始...") img_mask = img.copy() mask2 = mask_copy.copy() #處理成0或1 mask2 = mask2//255 #切割、重分類 #沒有設定rect,改用None cv2.grabCut(img,mask2,None,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_MASK) mask2 = np.where((mask2==2)|(mask2==0),0,1).astype('uint8') #準備兩個要顯示的圖 img_mask = img*mask2[:,:,np.newaxis] mask2 = mask2*255 #'image'是sketcher show的時候,視窗的名稱 # [img_mask, mask2] 是等一下要顯示的cut圖和mask圖 # lambda是等一下sketch顯示的顏色 sketch = Sketcher('image', [img_mask, mask2], lambda : ((255,0,0), 255)) sketch.show() # 按了b,表示改成背景選取 elif ch == ord('b'): print("處理背景...") sketch = Sketcher('image', [img_mask, mask2], lambda : ((0,0,255), 0)) #這裡lambda的設定就是(img_mask的顏色,mask2的顏色),因為這是畫背景,所以mask2的顏色是0 sketch.show() # 按了f,表示改成前景選取 elif ch == ord('f'): print("處理前景...") sketch = Sketcher('image', [img_mask, mask2], lambda : ((255,0,0), 255)) #這裡lambda的設定就是(img_mask的顏色,mask2的顏色),因為這是畫前景,所以mask2的顏色是1 sketch.show() else: # print("進行 grabcut...") #處理成0或1 mask2 = mask2//255 #沒有設定rect,改用None cv2.grabCut(img,mask2,None,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_MASK) mask2 = np.where((mask2==2)|(mask2==0),0,1).astype('uint8') img_mask = img*mask2[:,:,np.newaxis] mask2 = mask2*255 # print("處理好了...前台看看...") sketch = Sketcher('image', [img_mask, mask2], lambda : ((255,0,0), 255)) sketch.show() cv2.destroyAllWindows() ``` ![](https://i.imgur.com/i2iS9jF.jpg) 可以透過手動結合滑鼠去做細節的修改 最後圖片就儲存如下啦!!! ![](https://i.imgur.com/VYYVSWK.png)