醫學圖像處理之python opencv實用範例源碼

開發環境:

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引入,解決細胞的分類識別,解決團塊的分類識別(兩個粘在一起算一個類,三個在一起又算一個類,四個都又一類,如此類推,這樣才能更準確地進行計數)

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章