開發環境:
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])<max_area_lmit \
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)<max_area_lmit and i.shape[0]>2):
# 排除面積太小或太大的塊輪廓 而且排除 "點數不足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引入,解決細胞的分類識別,解決團塊的分類識別(兩個粘在一起算一個類,三個在一起又算一個類,四個都又一類,如此類推,這樣才能更準確地進行計數)