开发环境:
python3.6 + opencv-python 4.2
opencv-python 版的whl文件安装包下载地址: http://mirrors.aliyun.com/pypi/simple/opencv-python/
细胞计数的范例代码网上很多,但偏书面性和学习性,而且错误多形成误导,下面来点实用的.
一.快速上手一个最简范例:
上面左边是输入图像,右边是输出结果图(识别到的细胞总数)
下面代码运行的中间处理图像,用于调试观察:
主处理过程: 1.灰度化 -----> 2.二值化-----> 3.形态学运算-----> 4.轮廓查找
import cv2
import numpy as np
img=cv2.imread(r'111.png',cv2.IMREAD_COLOR) #读入一张RGB图片,忽略ALPHA通道
cv2.imshow("draw00",img)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #将图片转为灰度图片来分析
gray1 = cv2.GaussianBlur(gray,(3,3),0)# 高斯滤波(低通,去掉高频分量,图像平滑)
gray1 = cv2.GaussianBlur(gray1,(3,3),0)
#上面处理了多次,为消除细胞体内的浓淡深浅差别,避免在后续运算中产生孔洞
gray2=255-gray1 #0~255反相,为了适应腐蚀算法
cv2.imshow("draw11",gray2)
#ret, thresh = cv2.threshold(gray2, 80,255, cv2.THRESH_BINARY) # 固定 阈值处理 二值化图像
ret, thresh = cv2.threshold(gray2, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)#自动阈值处理
cv2.imshow("draw22",thresh)
#下面为了去除细胞之间的粘连,以免把两个细胞计算成一个
kernel=np.ones((2,2),np.uint8) #进行腐蚀操作,kernel=初值为1的2*2数组
erosion=cv2.erode(thresh,kernel,iterations=10) #腐蚀:卷积核沿着图像滑动,如果与卷积核对应的原图像的所有像素值都是1,那么中心元素就保持原来的像素值,否则就变为零。
cv2.imshow("draw33",erosion)
#下面为恢复一点SIZE,因为上面多次腐蚀,块太小了,所以这里膨胀几次
dilation = cv2.dilate(erosion,kernel,iterations = 5)
cv2.imshow("draw44",dilation)
contours,hirearchy=cv2.findContours(dilation, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 上面查找出块轮廓(实现细胞计数)
#对连通域面积进行比较
contours_out=[] #建立空list,放减去最小面积的数
for i in contours:
if (cv2.contourArea(i)>2 and cv2.contourArea(i)<3000 and i.shape[0]>2):
# 排除面积太小或太大的块轮廓 而且排除 "点数不足2"的离散点块
contours_out.append(i)
total_num=len(contours_out)
print("Count=%d"%total_num) #输出计算细包个数
#下面生成彩色结构的灰度图像,为的是在上面画彩色标注
color_of_gray_img=cv2.cvtColor(dilation,cv2.COLOR_GRAY2BGR)
cv2.drawContours(color_of_gray_img,contours_out,-1,(50,50,250),2) #用红色线,描绘块轮廓
#求连通域重心 以及 在重心坐标点描绘数字
for i,j in zip(contours_out,range(total_num)):
M = cv2.moments(i)
cX=int(M["m10"]/M["m00"])
cY=int(M["m01"]/M["m00"])
cv2.putText(color_of_gray_img, str(j+1), (cX+10, cY), 1,1, (50, 250, 50), 1) #在中心坐标点上描绘数字
strout="Count=%d"%total_num #输出计算细包个数
print(strout)
cv2.putText(color_of_gray_img, strout, (2, 12), 1,1, (250, 150, 150), 1)
#输出结果图片
cv2.imshow("draw55",color_of_gray_img)
cv2.waitKey()
#cv2.destroyWindow()
#输入图像你可以直接抓图(上面那张),存成jpg或png,和上面的python代码放在同一个目录下运行.
如果上面的源码你已经运行成功了,那么恭喜你,可以进行下一步了.
二.进阶篇,从学习阶段,向实用阶段的过渡:
上面三张图片差异很大,用上面的花枪代码就不行了,下面上兼容性强的代码,能一定程度上打断细胞粘连:
#-----全代码开始------#
import cv2
import numpy as np
import matplotlib.pyplot as plt
def fill_food(img):
#这是网上下的一个简要的孔洞填充函数,在边界上有问题,不能产品化,需要改进
#而且有的细胞没有完全闭合的孔洞它不能填好(在不能用闭运算的前提下,需另行单独开发填洞算子)
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
h,w=img.shape[:2]
max_area_lmit=h*w/4
min_area_lmit=2
len_contour = len(contours)
contour_list = []
drawing = np.zeros_like(img, np.uint8)
for i in range(len_contour):
if (cv2.contourArea(contours[i])>min_area_lmit and cv2.contourArea(contours[i])
and contours[i].shape[0]>2):
img_contour = cv2.drawContours(drawing, contours, i, (255, 255, 255), -1)
contour_list.append(img_contour)
out = sum(contour_list)
return out
#下面这个是固定长度的,也不适用于图像大小变化的情况,需要改进,而且计算速度慢,但切断粘连细胞的效果很好.
def clear_bridge_line(img,iterations): #清除桥接线,这里用的是9点模版,左右各2个点是桥头,中间5个点为桥身
h,w=img.shape[:2]
ibo=img.copy()
ib=ibo.copy()
for n in range(iterations):
for r in range(9,h-9):
for c in range(9,w-9):
if( (ib[r,c]==255 and ib[r,c+1]==255 and ib[r,c+2]==255 and ib[r,c-1]==255 and ib[r,c-2]==255) \
and ( ib[r,c+3]==255 and ib[r,c+4]==255 and ib[r,c-3]==255 and ib[r,c-4]==255 ) \
and ( (ib[r-1,c]==0 and ib[r-1,c+1]==0 and ib[r-1,c+2]==0 and ib[r-1,c-1]==0 and ib[r-1,c-2]==0) \
or (ib[r+1,c]==0 and ib[r+1,c+1]==0 and ib[r+1,c+2]==0 and ib[r+1,c-1]==0 and ib[r+1,c-2]==0) )):
ibo[r,c]= ibo[r,c+1]= ibo[r,c+2]= ibo[r,c-1]= ibo[r,c-2]=0 #删除横线桥
c+=2
for c in range(9,w-9):
for r in range(9,h-9):
if( (ib[r,c]==255 and ib[r+1,c]==255 and ib[r+2,c]==255 and ib[r-1,c]==255 and ib[r-2,c]==255) \
and ( ib[r+3,c]==255 and ib[r+4,c]==255 and ib[r-3,c]==255 and ib[r-4,c]==255 ) \
and ( (ib[r,c+1]==0 and ib[r+1,c+1]==0 and ib[r+2,c+1]==0 and ib[r-1,c+1]==0 and ib[r-2,c+1]==0) \
or (ib[r,c-1]==0 and ib[r+1,c-1]==0 and ib[r+2,c-1]==0 and ib[r-1,c-1]==0 and ib[r-2,c-1]==0) )):
ibo[r,c]= ibo[r+1,c]= ibo[r+2,c]= ibo[r-1,c]= ibo[r-2,c]=0#删除竖线桥
r+=2
print("del_bridge_line iterations step %g/%g ok." % (n+1,iterations))
ib=ibo.copy()
return ibo
#主函数
def main_call(_filename,_kernel_size=2,_gblur_count=1,_erode_count=8, _del_brdige_count=4,_open_count=3,_is_show_dbg_img=True):
#这里可以把图像换成111,也能正常计数,有较好的普适性,但上一个简单算法就不能正常计数222.png这个图像
img_src=cv2.imread(_filename,cv2.IMREAD_COLOR) #读入一张RGB图片,忽略ALPHA通道
if(_is_show_dbg_img):cv2.imshow("draw00",img_src)#绘制原图
KS=_kernel_size
img_gray = cv2.cvtColor(img_src,cv2.COLOR_BGR2GRAY) #将图片变为灰度图片来分析
histb = cv2.calcHist([img_gray], [0], None, [256], [0,255])
if(_is_show_dbg_img):plt.plot(histb)#绘制一下灰度直方图,用于辅助观察分析
'''
img_gray = cv2.equalizeHist(img_gray)
cv2.imshow("draw01",img_gray)#绘制均衡化之后的图像
histb2 = cv2.calcHist([img_gray], [0], None, [256], [0,255])
plt.plot(histb2)#绘制一下灰度直方图,用于辅助观察分析
'''
gblur_count=_gblur_count #可调节的参数->高斯滤波次数
img_blur=img_gray
for n in range(gblur_count):
img_blur = cv2.GaussianBlur(img_blur,(3,3),0)# 高斯滤波(低通,去掉高频分量,图像平滑)
# 可以按需多做一次或N次
img_inverse=255-img_blur #0~255反相,为了适应后面的腐蚀算法
if(_is_show_dbg_img):cv2.imshow("draw11",img_inverse)
#ret, img_thresh = cv2.threshold(img_inverse, 240,250, cv2.THRESH_BINARY) #固定阈值处理成二值化图像
ret, img_thresh = cv2.threshold(img_inverse, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
#上面用大律法,自动从直方图的波谷选一个阈值做二值切分
#但它不是通吃法,比如第三张红细胞样图,只有用固定阀值才能正确计数
if(_is_show_dbg_img):cv2.imshow("draw22",img_thresh)
print("开始进行孔洞填充.....")
img_fill=fill_food(img_thresh)#孔洞填充
if(_is_show_dbg_img):cv2.imshow("draw33",img_fill)
#下面为了去除细胞之间的粘连,以免把两个细胞计算成一个
kernel_std=np.ones((KS,KS),np.uint8) #进行腐蚀操作,kernel=初值为1的2*2数组
#old_ibo=cv2.erode(img_fill,kernel=kernel_std,iterations=10)#这个老算法没有先处理边界,效果不好看
#cv2.imshow("draw44_old",old_ibo)
print(img_fill.shape)
#下面为了在四边上做好卷积而做的padding
img_padding=cv2.copyMakeBorder(img_fill, top=2, bottom=2, left=2, right=2, borderType= cv2.BORDER_CONSTANT, value=0 )
erode_count=_erode_count #可调节的参数->首发腐蚀次数
print("开始进行%g次CV2卷积腐蚀....."%erode_count)
#注意:必要至少5次以上的连续腐蚀,才能形成中间连接宽度为>=5的细胞粘连桥接直线,与clear_bridge_line函数的固定长度相匹配
img_erode=cv2.erode(img_padding,kernel=kernel_std,iterations=erode_count) #腐蚀:卷积核沿着图像滑动,如果与卷积核对应的原图像的所有像素值都是1,那么中心元素就保持原来的像素值,否则就变为零。
if(_is_show_dbg_img):cv2.imshow("draw44_new",img_erode)
print(img_erode.shape)
nh,nw=img_erode.shape[:2]
img_no_padding=img_erode[2:nh-2,2:nw-2] #这里是padding还原
print(img_no_padding.shape)
del_brdige_count=_del_brdige_count #可调节的参数->删除细胞间的残连直线次数
print("请稍等,因为没有用多线程/CPU加速/GPU加速.....")
print("开始进行%g次细胞粘连桥接直线删除:"%del_brdige_count)
img_del_brdige=clear_bridge_line(img_no_padding,del_brdige_count)
if(_is_show_dbg_img):cv2.imshow('draw44_del_bridge_line_over', img_del_brdige)
#下面再来几次开运算操作,提高图像视觉圆滑效果(注意,迭代次数过多,或kernel面积过大,都可能会把一些小面积细胞弄消失)
open_count=_open_count #可调节的参数->最后开运算次数
kernel_open = np.ones((KS,KS), np.uint8)
print("开始进行%g次形态学开运算....."%open_count)
image_opening = cv2.morphologyEx(img_del_brdige, cv2.MORPH_OPEN, kernel_open,iterations=open_count)
if(_is_show_dbg_img):cv2.imshow("draw55",image_opening)
h,w=image_opening.shape[:2]
max_area_lmit=h*w/4
min_area_lmit=2
#加宽边界,为了多打字
img_padding=cv2.copyMakeBorder(image_opening, top=20, bottom=10, left=10, right=60, borderType= cv2.BORDER_CONSTANT, value=0 )
contours,hirearchy=cv2.findContours(img_padding, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)# 找出块轮廓
#对连通域面积进行比较
contours_out=[] #建立空list,放减去最小面积的数
for i in contours:
if (cv2.contourArea(i)>min_area_lmit and cv2.contourArea(i)
# 排除面积太小或太大的块轮廓 而且排除 "点数不足2"的离散点块
# 这里还可以利用面积和点数/密度,对细胞进行初分类,CV提供了丰富的块属性值供我们读取
contours_out.append(i)
#下面生成彩色结构的灰度图像,为的是在上面画彩色标注
color_of_gray_img=cv2.cvtColor(img_padding,cv2.COLOR_GRAY2BGR)
cv2.drawContours(color_of_gray_img,contours_out,-1,(50,50,250),2) #用红色线,描绘块轮廓
#求连通域重心 以及 在重心坐标点描绘数字
total_num=len(contours_out)
for i,j in zip(contours_out,range(total_num)):
M = cv2.moments(i)
cX=int(M["m10"]/M["m00"])
cY=int(M["m01"]/M["m00"])
cv2.putText(color_of_gray_img, str(j+1), (cX+10, cY), 1,1, (50, 250, 50), 1) #在中心坐标点上描绘数字
strout="Count=%d"%total_num #输出计算细包个数
#print(strout)
cv2.putText(color_of_gray_img, strout, (2, 12), 1,1, (250, 150, 150), 1)
#展示最终结果图片
if(_is_show_dbg_img):
cv2.imshow("draw66",color_of_gray_img)
#改变显示位置
cv2.moveWindow("draw00",0,0)
cv2.moveWindow("draw11",50,50)
cv2.moveWindow("draw22",100,100)
cv2.moveWindow("draw33",150,150)
cv2.moveWindow("draw44_new",200,200)
cv2.moveWindow("draw44_del_bridge_line_over",250,250)
cv2.moveWindow("draw55",300,300)
cv2.moveWindow("draw66",350,350)
return total_num
#可调节参数区
_gblur_count=1 #图像预处理时的高斯滤波次数
_kernel_size=2 #腐蚀与膨胀的核大小,根据图像中细胞的大小来调节
_erode_count=8 #腐蚀次数,少了细胞会连在一起,多个计算作一个;但腐蚀次数多了细胞会小到消失从而不能计入总数
_del_brdige_count=4 #删除细胞间的连接线的次数,多了速度慢(这个是最卡的),少了也会是细胞连在起,是否启用要看调试图上,是否有细胞连线存在
_open_count=3 #后期开运算次数,少了细胞会连在一起计算成一个;多了细胞会小到消失从而不能计入总数
_is_show_dbg_img=True #是否显示中间步骤调试图像
#可调节参数区
#主函数调用:
total_num=main_call("333.png",_kernel_size,_gblur_count,_erode_count,_del_brdige_count,_open_count,_is_show_dbg_img)
print("zhuwei->图像处理完成")
print("zhuwei->细胞计数(Count) = %g" % total_num )
cv2.waitKey()
#cv2.destroyWindow()
#-----全代码结束------#
注意:代码我是肯定全跑通了的,并测试了十张以上不同风格的细胞图像,都是微调参数而已,其中一张必要做固定阀值,如果运行报错,多半是代码中含隐形的ASCII编码,一个你要先做好对齐,一个就是想办法删除那些隐形编码(看起来像空格和TAB),然后再运行就OK了.
上图就是经过6次以上腐蚀后,形成的细胞桥接直线,用像素模板法clear_bridge_line清除,以保证正确的细胞计数
因为过多的腐蚀会让一些小细胞消失.
三.从实验到产品的过渡:
上面的代码也只能说勉强能应用到实践中,到了这一步,我们离真正的产品化都还有很长的距离:
首先就是解决算法的速度优化和调用接口及移植的问题.然后才是将深度学习网络模型,比如简单的alexnet或复杂的yolov3引入,解决细胞的分类识别,解决团块的分类识别(两个粘在一起算一个类,三个在一起又算一个类,四个都又一类,如此类推,这样才能更准确地进行计数)