利用opencv提取目标区域


title: 图像分割
categories: 机器学习
mathjax: true
date: 2017-08-28 13:45:26


需求

将肿瘤区域提取,应用在camelyon16数据集中,具体如下图

相关项目

  1. 漫水算法
    https://stackoverflow.com/questions/11294859/how-to-define-the-markers-for-watershed-in-opencv/11438165#11438165

  2. OpenCV 形状分析(上):计算轮廓中心
    http://python.jobbole.com/85600/

  3. 用 Python 和 OpenCV 检测图片上的条形码
    http://blog.jobbole.com/80448/

  4. opencv3编程入门第6章
    一定要看

  5. 根据轮廓画方框
    http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html#exercises

理论部分

这里设计的是形态学部分:膨胀,腐蚀,开运算,闭运算
在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三次,最后的结果


找轮廓

这部分可以看看 http://blog.csdn.net/vicdd/article/details/60478975
对findContour讲解的非常详细

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. 求亮度最大值,然后设定比例

分水岭算法 主要适用于在前景分割之后对前景进一步分割,如多个硬币
http://www.jianshu.com/p/de81d6029235

推荐阅读更多精彩内容

  • 技术要点分析:此次项目中主要的技术划分为身份证号码区域提取和光学字符识别。身份证号码区域的提取涉及有:图像灰度化阀...
    __Mars阅读 4,874评论 14 20
  • 这篇文章总结比较全面:http://blog.csdn.net/timidsmile/article/detail...
    rogerwu1228阅读 431评论 0 3
  • 不同图像灰度不同,边界处一般会有明显的边缘,利用此特征可以分割图像。需要说明的是:边缘和物体间的边界并不等同,边缘...
    大川无敌阅读 8,160评论 0 27
  • 前言opencv在图像处理中使用广泛,许多常见的应用场景例如人脸识别,车牌识别等都是基于opencv开发的。本文是...
    肖丹晨阅读 1,855评论 0 2
  • 1、阈值分割 1.1 简介 图像阈值化分割是一种传统的最常用的图像分割方法,因其实现简单、计算量小、性能较稳定而成...
    木夜溯阅读 10,647评论 9 13