---
# System prepended metadata

title: OpenCV 實作篇-圖像分割(GrabCut Technique)
tags: [圖像分割, OpenCV]

---

###### 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)

