以下的记录是科研小白使用OpenCV的一个小实战。
声明
我的文章和代码主要是来自 vSLAM算法攻城狮 的这篇文章:利用opencv提取目标区域,还加了一些基于自己的图片的其他处理。可惜的是,这位大佬写的博客有代码没有图,所以我又找到了这个博客。
据我推测,后面的这个博客应该是完全复制的之前那位大佬的文章(前者是2018年发表的,后者是2020年),但是后者补充上了过程中的图。所以我边观察着图,边照着代码做一个小实战。
目的
提取MRI图像中的乳腺癌部分,图片是我从百度上随便找的。预期效果如下图所示:
步骤
- 转灰度图,并二值化
- 腐蚀和膨胀
- 找轮廓
1.转灰度图,并二值化
根据原文,作者直接进行了转灰度图并二值化的操作,但是我按照作者的步骤尝试之后,发现我的图片中,与乳腺组织相连接的人体其他部位中会产生很多噪音,将不利于后续的分割病灶点。所以我首先制作了掩模,只保留有效区域。
制作掩模时,要确定掩模的形状。我要做一个不规则的多边形,所以首先来确定多边形的点的坐标。最后达到如下图所示的掩模,将掩模与原图做“与”操作,就可以只保留有效区域。
寻找不规则图形的点的坐标的代码如下:
(代码主体来自:Python cv2 图片中鼠标点击获取像素点坐标)
import cv2
img = cv2.imread('breast.jpg')
a = []
b = []
def on_EVENT_LBUTTONDOWN(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
xy = "%d,%d" % (x, y)
a.append(x)
b.append(y)
cv2.circle(img, (x, y), 1, (0, 0, 255), thickness=-1)
cv2.putText(img, xy, (x, y), cv2.FONT_HERSHEY_PLAIN,
1.0, (0, 0, 255), thickness=1)
cv2.imshow("image", img)
print(x,y)
cv2.namedWindow("image")
cv2.setMouseCallback("image", on_EVENT_LBUTTONDOWN)
cv2.imshow("image", img)
cv2.waitKey(0)
print(a[0], b[0])
产生的效果大致如图:
输入的值如下:
由于用鼠标点坐标点的时候,图片边缘部分的点会有一点偏差,所以到时候输入的时候需要根据自己图片的大小做一些校正,最后一个点出现在图片的外侧,可以直接看打印出来的结果。例如,我的图片大小是500x393,则第一个点校正为(0,245),最后一个点校正为(500,277),x,y坐标轴的问题参考opencv-图像的坐标系。
除了打印出来的6个点,而且此时还缺两个顶点,分别是最右上的像素点(500,0)和最左上(0,0),此时一共有8个点。大致如下图所示。依据下图8个点的顺序将其输入到掩模的代码中。
有了掩模的坐标点之后,就可以做出掩模了。此时参考的是这位朋友的代码【OpenCV 】图像掩模。代码如下:
import cv2
import numpy as np
img = cv2.imread('breast.jpg')
print(img.shape[:2]) #获取图片的前两个元素:高,宽
# 输入图像是RGB图像,故构造一个三维数组,8个二维数组是mask八个点的坐标,
#1-8,每个点的位置
site = np.array([[[0,245],[78,195],[186,192],[255,214],[366,198],[500,277],[500,0],[0,0]]], dtype=np.int32)
im = np.zeros(img.shape[:2], dtype="uint8") # 生成image大小的黑色空白图(全黑图)
cv2.polylines(im, site, 1, 255) # 在im上画site大小的线,1表示线段闭合,255表示线段颜色(白色)
cv2.fillPoly(im, site, 255) # 在im的site区域,填充颜色为255(白色)
mask = im
cv2.namedWindow('Mask')
cv2.imshow("Mask", mask)
masked = cv2.bitwise_and(img, img, mask=mask) # 在模板mask上,将image和image做“与”操作
cv2.namedWindow('Mask to Image')
cv2.imshow("Mask to Image", masked)
cv2.waitKey(0) # 图像一直显示,键盘按任意键即可关闭窗口
cv2.imwrite('breast_mask.jpg',masked)
最终结果的掩模和我最初画的有一定的差距,但是大致形状是对应的。有了掩模之后如下图所示。此时,我们就只保留了有效的区域。最后使用imread()函数将右边的图保存,取名为breast_mask.jpg。
此时可以使用breast_mask.jpg进行转灰度图并二值化的操作了。
核心函数是:
img = cv2.threshold(blurred, thresh, 255, cv2.THRESH_BINARY)[1]
原文中的图片目标区域是比较暗的,周边背景很亮,所以采用的是cv2.THRESH_BINARY_INV这个参数。而我的图片是目标区域亮,周围暗,所以我使用的参数是cv2.THRESH_BINARY。
为了更方便的调整阈值,原文中设立了一个滚动条,我觉得非常方便,效果如下:
代码如下:
import cv2
import numpy as np
def on_trace_bar_changed(args):
pass
real = cv2.imread('breast.jpg') #原图
img_masked = cv2.imread('breast_mask.jpg')
# cv2.namedWindow("real")
# cv2.imshow("real", real)
gray = cv2.cvtColor(img_masked, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
cv2.namedWindow("Image", cv2.WINDOW_NORMAL)
#使用进度条寻找阈值
cv2.createTrackbar('thres', 'Image', 0, 255, on_trace_bar_changed)
while True:
cv2.imshow("Image", img_masked)
thresh = cv2.getTrackbarPos('thres', 'Image')
img_masked = cv2.threshold(blurred, thresh, 255, cv2.THRESH_BINARY)[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'): #按 q退出
break
cv2.destroyAllWindows()
经过测试后阈值设定在176比较合适。(此时两个癌细胞团都比较明显的显示了出来)
2.腐蚀和膨胀
可以看做膨胀(dilate)是将白色区域扩大,腐蚀(erode)是将黑色区域扩大。腐蚀和膨胀的次数和顺序需要手动调试,原文中写了个小文件,按e就进行erode,同时pring ‘erode’,便于统计,按d就就行dilate,按r恢复到二值图像。按e和d的次数会被打印出来,方便统计。效果如下图:(右边是经过先经过2次erode,再经过2次dilate处理过后的图)
代码如下:
img_masked = cv2.imread('breast_mask.jpg')
# cv2.namedWindow("real")
# cv2.imshow("real", real)
gray = cv2.cvtColor(img_masked, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 之前的结果表示最佳阈值大概是176
thresh = cv2.threshold(blurred, 176, 255, cv2.THRESH_BINARY)[1]
cv2.namedWindow("thresh")
cv2.imshow('thresh',thresh)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img = thresh
while True:
cv2.imshow("change", 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()
3.找轮廓
使用findContour函数。最后效果如下:
代码如下:
# 2次腐蚀,2次膨胀
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
thresh = cv2.erode(thresh, kernel, iterations=2)
thresh = cv2.dilate(thresh, kernel, iterations=2)
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0]
result = cv2.drawContours(real, cnts, -1, (0, 255, 0), 2)
cv2.imshow("Image", result)
cv2.waitKey()
cv2.destroyAllWindows()
原文中还将背景图改为黑色,但是我这张图本来就是黑色,所以不需要修改了。
完整代码
我分成了两个python文件,分别是mask.py和opencv_breast.py。其中mask.py用于制作掩模,保留有效区域,最后生成带掩模的图片breast_mask.jpg。opencv_breast.py用于进行乳腺癌的分割。
mask.py的完整代码如下:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('breast.jpg')
#寻找坐标点
""" a = [] b = [] def on_EVENT_LBUTTONDOWN(event, x, y, flags, param): if event == cv2.EVENT_LBUTTONDOWN: xy = "%d,%d" % (x, y) a.append(x) b.append(y) cv2.circle(img, (x, y), 1, (0, 0, 255), thickness=-1) cv2.putText(img, xy, (x, y), cv2.FONT_HERSHEY_PLAIN, 1.0, (0, 0, 255), thickness=1) cv2.imshow("image", img) print(x,y) cv2.namedWindow("image") cv2.setMouseCallback("image", on_EVENT_LBUTTONDOWN) cv2.imshow("image", img) cv2.waitKey(0) print(a[0], b[0]) """
print(img.shape[:2]) #获取图片的前两个元素:高,宽
# 输入图像是RGB图像,故构造一个三维数组,8个二维数组是mask8个点的坐标,
site = np.array([[[0,245],[78,195],[186,192],[255,214],[366,198],[500,277],[500,0],[0,0]]], dtype=np.int32) #1-8,每个点的位置
im = np.zeros(img.shape[:2], dtype="uint8") # 生成image大小的黑色空白图(全黑图)
cv2.polylines(im, site, 1, 255) # 在im上画site大小的线,1表示线段闭合,255表示线段颜色(白色)
cv2.fillPoly(im, site, 255) # 在im的site区域,填充颜色为255(白色)
mask = im
cv2.namedWindow('Mask')
cv2.imshow("Mask", mask)
masked = cv2.bitwise_and(img, img, mask=mask) # 在模板mask上,将image和image做“与”操作
cv2.namedWindow('Mask to Image')
cv2.imshow("Mask to Image", masked)
cv2.waitKey(0) # 图像一直显示,键盘按任意键即可关闭窗口
cv2.imwrite('breast_mask.jpg',masked)
opencv_breast.py的完整代码如下:
import cv2
import numpy as np
def on_trace_bar_changed(args):
pass
real = cv2.imread('breast.jpg') #原图
img_masked = cv2.imread('breast_mask.jpg') #带掩模的图
# cv2.namedWindow("real")
# cv2.imshow("real", real)
gray = cv2.cvtColor(img_masked, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
""" cv2.namedWindow("Image", cv2.WINDOW_NORMAL) #使用进度条寻找阈值 cv2.createTrackbar('thres', 'Image', 0, 255, on_trace_bar_changed) while True: cv2.imshow("Image", img_masked) thresh = cv2.getTrackbarPos('thres', 'Image') img_masked = cv2.threshold(blurred, thresh, 255, cv2.THRESH_BINARY)[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 """
# 之前的结果表示最佳阈值大概是176
thresh = cv2.threshold(blurred, 176, 255, cv2.THRESH_BINARY)[1]
# cv2.namedWindow("thresh")
# cv2.imshow('thresh',thresh)
""" kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) img = thresh while True: cv2.imshow("change", 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() """
# 2次腐蚀,2次膨胀
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
thresh = cv2.erode(thresh, kernel, iterations=2)
thresh = cv2.dilate(thresh, kernel, iterations=2)
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0]
result = cv2.drawContours(real, cnts, -1, (0, 255, 0), 2)
cv2.imshow("Image", result)
cv2.waitKey()
cv2.destroyAllWindows()
总结
仅看这张图的话效果还不错。但是固定阈值的方式泛化能力差,不能应用到大数据集上。考虑继续学习其他算法