-
Notifications
You must be signed in to change notification settings - Fork 280
/
tutorial57_Cell Nuclei analysis using watershed.py
136 lines (104 loc) · 5.67 KB
/
tutorial57_Cell Nuclei analysis using watershed.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#Video Playlist: https://www.youtube.com/playlist?list=PLHae9ggVvqPgyRQQOtENr6hK0m1UquGaG
"""
https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_watershed/py_watershed.html
This code performs cell counting and size distribution analysis and dumps results into a csv file.
It uses watershed segmentation for better segmentation, separating touching nuclei.
"""
import cv2
import numpy as np
from matplotlib import pyplot as plt
from scipy import ndimage
from skimage import measure, color, io
img = cv2.imread("images/Osteosarcoma_01.tif")
#Extract only blue channel as DAPI / nuclear (blue) staining is the best
#channel to perform cell count.
cells=img[:,:,0] #Blue channel. Image equivalent to grey image.
pixels_to_um = 0.454 # 1 pixel = 454 nm (got this from the metadata of original image)
#Threshold image to binary using OTSU. ALl thresholded pixels will be set to 255
ret1, thresh = cv2.threshold(cells, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
# Morphological operations to remove small noise - opening
#To remove holes we can use closing
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
from skimage.segmentation import clear_border
opening = clear_border(opening) #Remove edge touching grains
plt.imshow(opening, cmap='gray') #This is our image to be segmented further using watershed
#Check the total regions found before and after applying this.
#STEP 1: Sude background
#Now we know that the regions at the center of cells is for sure cells
#The region far away is background.
#We need to extract sure regions. For that erode a few times.
#But we have cells touching, so erode alone will not work.
#To separate touching objects, the best approach would be distance transform and then thresholding.
# let us start by identifying sure background area
# dilating pixes a few times increases cell boundary to background.
# This way whatever is remaining for sure will be background.
#The area in between sure background and foreground is our ambiguous area.
#Watershed should find this area for us.
sure_bg = cv2.dilate(opening,kernel,iterations=10)
plt.imshow(sure_bg, cmap='gray') #Dark region is our sure background
# Finding sure foreground area using distance transform and thresholding
#intensities of the points inside the foreground regions are changed to
#distance their respective distances from the closest 0 value (boundary).
#https://www.tutorialspoint.com/opencv/opencv_distance_transformation.htm
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
plt.imshow(dist_transform, cmap='gray') #Dist transformed img.
#Let us threshold the dist transform by starting at 1/2 its max value.
print(dist_transform.max()) #gives about 21.9
ret2, sure_fg = cv2.threshold(dist_transform,0.5*dist_transform.max(),255,0)
plt.imshow(sure_fg, cmap='gray')
#Later you realize that 0.25* max value will not separate the cells well.
#High value like 0.7 will not recognize some cells. 0.5 seems to be a good compromize
# Unknown ambiguous region is nothing but bkground - foreground
sure_fg = np.uint8(sure_fg) #Convert to uint8 from float
unknown = cv2.subtract(sure_bg,sure_fg)
plt.imshow(unknown, cmap='gray')
#Now we create a marker and label the regions inside.
# For sure regions, both foreground and background will be labeled with positive numbers.
# Unknown regions will be labeled 0.
#For markers let us use ConnectedComponents.
#Connected components labeling scans an image and groups its pixels into components
#based on pixel connectivity, i.e. all pixels in a connected component share
#similar pixel intensity values and are in some way connected with each other.
#Once all groups have been determined, each pixel is labeled with a graylevel
# or a color (color labeling) according to the component it was assigned to.
ret3, markers = cv2.connectedComponents(sure_fg)
plt.imshow(markers)
#One problem rightnow is that the entire background pixels is given value 0.
#This means watershed considers this region as unknown.
#So let us add 10 to all labels so that sure background is not 0, but 10
markers = markers+10
# Now, mark the region of unknown with zero
markers[unknown==255] = 0
plt.imshow(markers, cmap='jet') #Look at the 3 distinct regions.
#Now we are ready for watershed filling.
markers = cv2.watershed(img,markers)
#Let us color boundaries in yellow.
#Remember that watershed assigns boundaries a value of -1
img[markers == -1] = [0,255,255]
#label2rgb - Return an RGB image where color-coded labels are painted over the image.
img2 = color.label2rgb(markers, bg_label=0)
plt.imshow(img2)
cv2.imshow('Overlay on original image', img)
cv2.imshow('Colored Grains', img2)
cv2.waitKey(0)
#####################################################################################
#Now, time to extract properties of detected cells
#Directly capturing props to pandas dataframe
props = measure.regionprops_table(markers, cells,
properties=['label',
'area', 'equivalent_diameter',
'mean_intensity', 'solidity', 'orientation',
'perimeter'])
import pandas as pd
df = pd.DataFrame(props)
print(df.head())
#To delete small regions...
df = df[df['area'] > 50]
print(df.head())
#######################################################
#Convert to micron scale
df['area_sq_microns'] = df['area'] * (pixels_to_um**2)
df['equivalent_diameter_microns'] = df['equivalent_diameter'] * (pixels_to_um)
print(df.head())
df.to_csv('data/cast_iron_measurements.csv')