利用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本書籍的百度雲連結
問題
發現這個固定閾值的方式泛化效果很差,
- 漫水演算法
- 分水嶺演算法
- 顏色梯度演算法
- 求亮度最大值,然後設定比例