圖像的分割與修復
圖像分割的基本概念
- 什麼是圖像分割
將前景物體從背景中分離出來。
- 圖像分割的方法
之前的很多方法都是圖像分割的前置步驟,比如腐蝕、膨脹、二值化等等。圖像分割方法又分爲傳統的圖像分割方法和基於深度學習的圖像分割方法。
- 傳統的圖像分割方法
- 分水嶺法
- GrabCut方法
- MeanShift法
- 背景扣除
- 分水嶺法原理
上圖表示圖像有一定的梯度,0代表黑色,代表比較低窪的地方,白色是255,代表一個峯點。當我們使用不同的顏色向低窪處灌水。
當兩個低窪處的水要打通的時候,此時這個顏色就會產生一個衝突。在衝突點的地方設置一個邊界點,這樣就將不同的區域給分割開來。但它也是有問題的。
當圖像中存在過多的極小區域而產生許多小的集水盆,但實際上我們真正想要的是一個大塊,視爲一個整體。但通過分水嶺法就可能分成很多的小塊。對於傳統的分水嶺法可能會把一張圖切割的很碎,不利於我們後面的處理。
但幸運的是,OpenCV的分水嶺法可以將一大塊分成一整塊。
- 分水嶺法的處理步驟
- 標記背景
- 標記前景
- 標記未知域
- 進行分割
現在我們要對這張圖片進行分割
import cv2 import numpy as np if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/9527.png") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化,cv2.THRESH_OTSU表示自適應閾值 ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 進行兩次開運算 open = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2) # 進行一次膨脹 bg = cv2.dilate(open, kernel, iterations=1) # 獲取前景物體,計算非0值到離它距離最近的0值之間的距離 # cv2.DIST_L2爲距離的計算方式,這裏爲歐式距離 # maskSize:掃描時的卷積核大小,L1時用3*3,L2時用5*5 dist = cv2.distanceTransform(open, cv2.DIST_L2, 5) ret, fg = cv2.threshold(dist, 0.7 * dist.max(), 255, cv2.THRESH_BINARY) fg = np.uint8(fg) # 獲取未知區域 unknow = cv2.subtract(bg, fg) # 創建連通域,求所有非0相同像素的連通域,它還有一個參數爲連通周圍的4個還是 # 8個,默認爲8個 ret, marker = cv2.connectedComponents(fg) marker += 1 marker[unknow == 255] = 0 # 進行圖像分割 result = cv2.watershed(img, marker) img[result == -1] = [0, 0, 255] while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
GrabCut方法
通過交互的方式獲得前景物體。當我們有一張圖片的時候可以通過鍵盤或者鼠標對其進行指定,通過框住我們需要分離的物品,就可以將物品進行分離。
- GrabCut原理
- 用戶指定前景的大體區域,剩下的爲背景區域。
- 用戶還可以明確指定某些地方爲前景或背景。
- GrabCut採用分段迭代的方法分析前景物體形成模型樹。
- 最後根據權重決定某個像素是前景還是背景。
現在我們使用
來進行圖像的分割
import cv2 import numpy as np class App: def __init__(self, img): self.img = img self.img2 = img.copy() self.rect = (0, 0, 0, 0) self.flag_rectangle = False self.startx = 0 self.starty = 0 self.mask = np.zeros(self.img.shape[:2], np.uint8) self.output = np.zeros(self.img.shape, np.uint8) def onmouse(self, event, x, y, flags, param): if event == cv2.EVENT_LBUTTONDOWN: self.flag_rectangle = True self.startx = x self.starty = y elif event == cv2.EVENT_LBUTTONUP: self.flag_rectangle = False cv2.rectangle(self.img, (self.startx, self.starty), (x, y), (0, 0, 255), 5) self.rect = (min(self.startx, x), min(self.starty, y), abs(self.startx - x), abs(self.starty - y)) elif event == cv2.EVENT_MOUSEMOVE: if self.flag_rectangle: # 每次繪製都在新的圖像中繪製 self.img = self.img2.copy() cv2.rectangle(self.img, (self.startx, self.starty), (x, y), (255, 0, 0), 5) def run(self): cv2.setMouseCallback('img', self.onmouse) while True: cv2.imshow('img', self.img) cv2.imshow('output', self.output) key = cv2.waitKey(100) if key & 0xFF == ord('q'): break if key & 0xFF == ord('g'): bgdmodel = np.zeros((1, 65), np.float64) fgdmodel = np.zeros((1, 65), np.float64) # 進行grabCut分割 # self.mask是分割之後產生的掩碼,BGD背景0,FGD前景1, # PR_BGD可能是背景2,PR_FGD可能是前景3 # self.rect爲我們選取的區域, # bgdmodel, fgdmodel是兩個固定值 # 1是迭代次數, # cv2.GC_INIT_WITH_RECT初始化時分離前後景使用的mode # 還有一個cv2.GC_INIT_WITH_MASK在後續迭代中使用的mode cv2.grabCut(self.img2, self.mask, self.rect, bgdmodel, fgdmodel, 1, cv2.GC_INIT_WITH_RECT) mask2 = np.where((self.mask == 1) | (self.mask == 3), 255, 0).astype('uint8') self.output = cv2.bitwise_and(self.img2, self.img2, mask=mask2) cv2.destroyAllWindows() if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) cv2.namedWindow('output', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/111.jpeg") app = App(img) app.run()
運行結果,當我們繪製區域,並按下g鍵時
meanshift圖像分割
- MeanShift原理
- 嚴格來說該方法並不是用來對圖像分割的,而是在色彩層面的平滑濾波。
- 它會中和色彩分佈相近的顏色,平滑色彩細節,侵蝕掉面積較小的顏色區域。
- 它以圖像上任一點P爲圓心,半徑爲sp,色彩幅值(色彩相近的範圍)爲sr進行不斷迭代。
我們以這張圖爲例來看看效果
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/10086.png") # 對圖像進行meanshift處理,20爲半徑,30爲幅值 mean_img = cv2.pyrMeanShiftFiltering(img, 20, 30) while True: cv2.imshow('img', mean_img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這樣我們就可以看到樹葉和部分紅花就給同化了,變成了一個抽象畫。不過這樣並不能完成圖像分割,我們還要做進一步處理。
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/10086.png") # 對圖像進行meanshift處理,20爲半徑,30爲幅值 mean_img = cv2.pyrMeanShiftFiltering(img, 20, 30) # 對meanshift處理後的圖像邊緣 imgcanny = cv2.Canny(mean_img, 150, 300) # 獲取邊緣圖像的最外層輪廓 contours, _ = cv2.findContours(imgcanny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 在願圖像中繪製輪廓 cv2.drawContours(img, contours, -1, (0, 0, 255), 3) while True: cv2.imshow('img', img) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
圖像修復
這裏我們需要準備兩張圖,一張是帶有失真的圖片,如
另外一張爲與上圖中失真部分一模一樣的灰度圖
現在就可以對失真的圖進行修復
import cv2 if __name__ == "__main__": cv2.namedWindow('img', cv2.WINDOW_NORMAL) img = cv2.imread("/Users/admin/Documents/2122.png") mask = cv2.imread("/Users/admin/Documents/2121.png", 0) # 圖像修復,5爲每個點的圓型鄰域半徑 # cv2.INPAINT_TELEA計算破損周圍像素點的加權平均值 # 當作它恢復的像素值 # 還有一個cv2.INPAINT_NS算法較爲複雜 dst = cv2.inpaint(img, mask, 5, cv2.INPAINT_TELEA) while True: cv2.imshow('img', dst) key = cv2.waitKey() if key & 0xFF == ord('q'): break cv2.destroyAllWindows()
運行結果
這是一種比較傳統的圖像修復技術,而且對摳圖的技術要求比較高。只能做一些簡單的修復,要做比較大塊的修復是不可能的。