1. 程式人生 > >利用opencv提取目標區域

利用opencv提取目標區域

title: 影象分割 categories: 機器學習 mathjax: true date: 2017-08-28 13:45:26

需求

將腫瘤區域提取,應用在camelyon16資料集中,具體如下圖

相關專案

理論部分

這裡設計的是形態學部分:膨脹,腐蝕,開運算,閉運算 在opencv裡經常是對灰度影象中的白色區域進行操作,顧名思義,dilate膨脹會使白色區域膨脹,erode腐蝕會減少白色區域.dilate是影象與一個核函式做求max運算,會使整體值更大,也就是更亮,erode則是求min運算,使整體值更小,也就是更暗,特別在黑白交界的地方,膨脹和腐蝕的現象非常直觀

開運算=erode+dilate,先腐蝕,讓黑色區域中的一些白色斑點消失,同時白色區域塊也會暗一些,再通過膨脹補回來 閉運算=dilate+erode,先膨脹會將一些散落的白色小塊通過擴增連線到一起,再腐蝕削減一點

個人覺得開運算和閉運算太固定了,不靈活,不如手動控制dilate和erode的次數高效.

難點

因為需要在輪廓之外為黑色,不是簡單的二值化就可以,需要先合理設定閾值二值化,然後通過腐蝕消除白色的噪聲斑點,再通過膨脹適當擴張白色區域,以避免影象樣本的損失,再找到輪廓,最後將輪廓之外的區域設為黑色

轉灰度圖加二值化

核心函式

img = cv2.threshold(blurred, thresh, 255, cv2.THRESH_BINARY_INV)[1]

中間的關鍵點是cv2.THRESH_BINARY_INV這個引數,因為原圖中目標區域是比較暗的,周邊背景很量,所以需要用這個引數,如果是目標區域量,周邊暗就用cv2.THRESH_BINARY

為了更方便的調整閾值,我設立了一個滾動條,效果如下

整體函式如下

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date    : 2017-08-28T06:52:38.406Z
# @Author  : CarryHJR

import cv2


def on_trace_bar_changed(args):
    pass


img = cv2.imread('test.png')
cv2.namedWindow("real")
cv2.imshow("real", img)
cv2.namedWindow("Image")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)


cv2.createTrackbar('thres', 'Image', 0, 255, on_trace_bar_changed)

while True:
    cv2.imshow("Image", img)
    thresh = cv2.getTrackbarPos('thres', 'Image')
    img = cv2.threshold(blurred, thresh, 255, cv2.THRESH_BINARY_INV)[1]
    # 鍵盤檢測函式,0xFF是因為64位機器
    # https: // stackoverflow.com / questions / 20539497 / opencv - python - waitkey d- dont - respond
    k = cv2.waitKey(1) & 0xFF
    # print k
    if k == ord('q'):
        break


cv2.destroyAllWindows()

經過測試後閾值設定在199比較合適

腐蝕和膨脹

這個腐蝕和膨脹的次數和順序得看自己手動除錯了,寫了個小檔案,按e就進行erode,同時pring 'erode',便於統計,按d就就行dilate,按m恢復到二值影象

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date    : 2017-08-28T06:52:38.406Z
# @Author  : CarryHJR
# @Link    : https://github.com/CarryHJR
# @Version : $Id$

import cv2

# 真實影象
real = cv2.imread('test.png')
cv2.namedWindow("real")
cv2.imshow("real", real)

# 前景影象
cv2.namedWindow("Image")
gray = cv2.cvtColor(real, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
thresh = cv2.threshold(blurred, 199, 255, cv2.THRESH_BINARY_INV)[1]

cv2.imshow("Image", thresh)

cv2.namedWindow("My")
img = thresh
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# 2次膨脹,3次腐蝕
while True:
    cv2.imshow("My", img)
    # 鍵盤檢測函式,0xFF是因為64位機器
    k = cv2.waitKey(1) & 0xFF
    # print k
    if k == ord('e'):
        # 加上iterations是為了記住這個引數,不加也行
        img = cv2.erode(img, kernel, iterations=1)
        print 'erode'
    if k == ord('d'):
        img = cv2.dilate(img, kernel, iterations=1)
        print 'dilate'
    if k == ord('r'):
        img = thresh
        print 'return threshold image'
    if k == ord('q'):
        break
cv2.destroyAllWindows()

個人採用erode兩次,dilate三次,最後的結果

找輪廓

cnts = cv2.findContours(img.copy(), cv2.RETR_EXTERNAL,
                        cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0]
cv2.drawContours(real, cnts, -1, (0, 255, 0), 2)
cv2.imwrite('contours.png', real)

效果如下

摳圖

得到contours需要將輪廓以外的部分設為黑色,這個功能我搜索了很長時間,最後在這裡發現了答案,關鍵是有個fillPoly函式我之前不知道

# 全黑
mask = np.zeros(real.shape).astype(real.dtype)
# 將contours裡填充白色
color = [255, 255, 255]
cv2.fillPoly(mask, cnts, color)
# mask與real相與
result = cv2.bitwise_and(real, mask)

最後結果:

result.jpg

把程式碼整理一下

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date    : 2017-08-28T06:52:38.406Z
# @Author  : CarryHJR

import numpy as np
import cv2

######################################################################
# 原始影象real
real = cv2.imread('test.png')
# cv2.namedWindow("real")
# cv2.imshow("real", real)

# 前景影象
# cv2.namedWindow("Image")
gray = cv2.cvtColor(real, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
img = cv2.threshold(blurred, 199, 255, cv2.THRESH_BINARY_INV)[1]
# cv2.imshow("Image", img)

# 2次腐蝕,3次膨脹
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img = cv2.erode(img, kernel, iterations=2)
img = cv2.dilate(img, kernel, iterations=3)


cnts = cv2.findContours(img.copy(), cv2.RETR_EXTERNAL,
                        cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0]

# loop over the contours
# for c in cnts:
#     cv2.drawContours(real, [c], -1, (0, 255, 0), 2)
# cv2.imshow("real", real)
# cv2.drawContours(real, cnts, -1, (0, 255, 0), 2)
# cv2.imwrite('contours.png', real

# https://stackoverflow.com/questions/37912928/fill-the-outside-of-contours-opencv
# 全黑
mask = np.zeros(real.shape).astype(real.dtype)
# 將contours裡填充白色
color = [255, 255, 255]
cv2.fillPoly(mask, cnts, color)
# mask與real相與
result = cv2.bitwise_and(real, mask)

# 最後結果reult
cv2.imwrite("result.jpg", result)
###################################################################

後記

這裡的圖片比較好,背景是全白,目標區域和背景區域的亮度差別很大,如果是像條形碼那張圖,就需要像文章裡的那樣進行sobel邊緣檢測了

彩蛋

中間的插圖用python合成的

# 將兩個圖片並排顯示
import matplotlib.pyplot as plt
import cv2

fig = plt.figure()
ax1 = plt.subplot(121)
img1 = cv2.imread('test.png')
ax1.imshow(img1)
ax1.set_title('input image')
ax1.axis('off')

ax2 = plt.subplot(122)
img2 = cv2.imread('2.png')
ax2.imshow(img2)
ax2.set_title('thresho image')
ax2.axis('off')

看到這的都是真愛,點贊私發參考文獻中的5本書籍的百度雲連結

問題

發現這個固定閾值的方式泛化效果很差,

  1. 漫水演算法
  2. 分水嶺演算法
  3. 顏色梯度演算法
  4. 求亮度最大值,然後設定比例