|
a |
|
b/lung_segmentation.py |
|
|
1 |
import numpy as np |
|
|
2 |
import os |
|
|
3 |
|
|
|
4 |
from skimage import measure, morphology, segmentation |
|
|
5 |
from skimage.morphology import ball, disk, binary_erosion, binary_closing |
|
|
6 |
from skimage.measure import label, regionprops |
|
|
7 |
from skimage.filters import roberts |
|
|
8 |
from skimage.segmentation import clear_border |
|
|
9 |
|
|
|
10 |
from scipy import ndimage as ndi |
|
|
11 |
import matplotlib.pyplot as plt |
|
|
12 |
import scipy.misc |
|
|
13 |
|
|
|
14 |
import config |
|
|
15 |
|
|
|
16 |
|
|
|
17 |
# Optimal threshold, found in an article about segmentation algorithm using |
|
|
18 |
# morphological operations. This is in HU units. |
|
|
19 |
threshold = -420 |
|
|
20 |
nodule_threshold = -180 |
|
|
21 |
|
|
|
22 |
|
|
|
23 |
class SegmentationAlgorithm(object): |
|
|
24 |
def __init__(self, threshold): |
|
|
25 |
self._threshold = threshold |
|
|
26 |
|
|
|
27 |
def get_segmented_lungs(self, plot=False): |
|
|
28 |
pass |
|
|
29 |
|
|
|
30 |
def apply_threshold(self, scan): |
|
|
31 |
scan[scan < nodule_threshold] = 0 |
|
|
32 |
return scan |
|
|
33 |
|
|
|
34 |
def get_lung_nodules_candidates(self, patient_imgs): |
|
|
35 |
nodules = [self.apply_threshold(scan) for scan in patient_imgs] |
|
|
36 |
return np.stack([nodule for nodule in nodules if nodule.any()]) |
|
|
37 |
|
|
|
38 |
def get_slices_with_nodules(self, patient_imgs): |
|
|
39 |
return np.stack([slice_ for slice_ in patient_imgs if |
|
|
40 |
self._has_nodule(slice_)]) |
|
|
41 |
|
|
|
42 |
def _has_nodule(self, scan): |
|
|
43 |
scan_copy = scan.copy() |
|
|
44 |
scan_copy[scan_copy < nodule_threshold] = 0 |
|
|
45 |
return scan_copy.any() |
|
|
46 |
|
|
|
47 |
|
|
|
48 |
class MorphologicalSegmentation(SegmentationAlgorithm): |
|
|
49 |
def __init__(self, threshold=-420): |
|
|
50 |
super(MorphologicalSegmentation, self).__init__(threshold) |
|
|
51 |
|
|
|
52 |
def get_segmented_lungs(self, im, plot=False): |
|
|
53 |
''' |
|
|
54 |
This funtion segments the lungs from the given 2D slice. |
|
|
55 |
''' |
|
|
56 |
''' |
|
|
57 |
Step 1: Convert into a binary image. |
|
|
58 |
''' |
|
|
59 |
binary = im < self._threshold |
|
|
60 |
if plot == True: |
|
|
61 |
plt.imshow(binary, cmap=plt.cm.bone) |
|
|
62 |
plt.show() |
|
|
63 |
''' |
|
|
64 |
Step 2: Remove the blobs connected to the border of the image. |
|
|
65 |
''' |
|
|
66 |
cleared = clear_border(binary) |
|
|
67 |
if plot == True: |
|
|
68 |
plt.imshow(cleared, cmap=plt.cm.bone) |
|
|
69 |
plt.show() |
|
|
70 |
''' |
|
|
71 |
Step 3: Closure operation with a disk of radius 2. This operation is |
|
|
72 |
to keep nodules attached to the lung wall. |
|
|
73 |
''' |
|
|
74 |
selem = disk(2) |
|
|
75 |
binary = binary_closing(cleared, selem) |
|
|
76 |
if plot == True: |
|
|
77 |
plt.imshow(binary, cmap=plt.cm.bone) |
|
|
78 |
plt.show() |
|
|
79 |
''' |
|
|
80 |
Step 4: Label the image. |
|
|
81 |
''' |
|
|
82 |
label_image = label(binary) |
|
|
83 |
if plot == True: |
|
|
84 |
plt.imshow(label_image, cmap=plt.cm.bone) |
|
|
85 |
plt.show() |
|
|
86 |
''' |
|
|
87 |
Step 5: Keep the labels with 2 largest areas. |
|
|
88 |
''' |
|
|
89 |
areas = [r.area for r in regionprops(label_image)] |
|
|
90 |
|
|
|
91 |
areas.sort() |
|
|
92 |
if len(areas) > 2: |
|
|
93 |
for region in regionprops(label_image): |
|
|
94 |
if region.area < areas[-2]: |
|
|
95 |
for coordinates in region.coords: |
|
|
96 |
label_image[coordinates[0], coordinates[1]] = 0 |
|
|
97 |
binary = label_image > 0 |
|
|
98 |
if plot == True: |
|
|
99 |
plt.imshow(binary, cmap=plt.cm.bone) |
|
|
100 |
plt.show() |
|
|
101 |
''' |
|
|
102 |
Step 6: Closure operation with a disk of radius 2. This operation is |
|
|
103 |
to keep nodules attached to the lung wall. |
|
|
104 |
''' |
|
|
105 |
selem = disk(12) |
|
|
106 |
binary = binary_closing(binary, selem) |
|
|
107 |
if plot == True: |
|
|
108 |
plt.imshow(binary, cmap=plt.cm.bone) |
|
|
109 |
plt.show() |
|
|
110 |
''' |
|
|
111 |
Step 7: Fill in the small holes inside the binary mask of lungs. |
|
|
112 |
''' |
|
|
113 |
edges = roberts(binary) |
|
|
114 |
binary = ndi.binary_fill_holes(edges) |
|
|
115 |
if plot == True: |
|
|
116 |
plt.imshow(binary, cmap=plt.cm.bone) |
|
|
117 |
plt.show() |
|
|
118 |
''' |
|
|
119 |
Step 8: Erosion operation with a disk of radius 2. This operation is |
|
|
120 |
seperate the lung nodules attached to the blood vessels. |
|
|
121 |
''' |
|
|
122 |
selem = disk(2) |
|
|
123 |
binary = binary_erosion(binary, selem) |
|
|
124 |
if plot == True: |
|
|
125 |
plt.imshow(binary, cmap=plt.cm.bone) |
|
|
126 |
plt.show() |
|
|
127 |
|
|
|
128 |
''' |
|
|
129 |
Step 9: Superimpose the binary mask on the input image. |
|
|
130 |
''' |
|
|
131 |
get_high_vals = binary == 0 |
|
|
132 |
im[get_high_vals] = 0 |
|
|
133 |
if plot == True: |
|
|
134 |
plt.imshow(im, cmap=plt.cm.bone) |
|
|
135 |
plt.show() |
|
|
136 |
|
|
|
137 |
return im |
|
|
138 |
|
|
|
139 |
|
|
|
140 |
class WatershedSegmentation(SegmentationAlgorithm): |
|
|
141 |
def __init__(self, threshold=-400): |
|
|
142 |
super(WatershedSegmentation, self).__init__(threshold) |
|
|
143 |
|
|
|
144 |
def get_segmented_lungs(self, image, plot=False): |
|
|
145 |
# TODO: might add the logic for plotting the filters applied in the process |
|
|
146 |
#Creation of the markers as shown above: |
|
|
147 |
marker_internal, marker_external, marker_watershed = self.generate_markers(image) |
|
|
148 |
|
|
|
149 |
#Creation of the Sobel-Gradient |
|
|
150 |
sobel_filtered_dx = ndi.sobel(image, 1) |
|
|
151 |
sobel_filtered_dy = ndi.sobel(image, 0) |
|
|
152 |
sobel_gradient = np.hypot(sobel_filtered_dx, sobel_filtered_dy) |
|
|
153 |
sobel_gradient *= 255.0 / np.max(sobel_gradient) |
|
|
154 |
|
|
|
155 |
#Watershed algorithm |
|
|
156 |
watershed = morphology.watershed(sobel_gradient, marker_watershed) |
|
|
157 |
|
|
|
158 |
#Reducing the image created by the Watershed algorithm to its outline |
|
|
159 |
outline = ndi.morphological_gradient(watershed, size=(3,3)) |
|
|
160 |
outline = outline.astype(bool) |
|
|
161 |
|
|
|
162 |
#Performing Black-Tophat Morphology for reinclusion |
|
|
163 |
#Creation of the disk-kernel and increasing its size a bit |
|
|
164 |
blackhat_struct = [[0, 0, 1, 1, 1, 0, 0], |
|
|
165 |
[0, 1, 1, 1, 1, 1, 0], |
|
|
166 |
[1, 1, 1, 1, 1, 1, 1], |
|
|
167 |
[1, 1, 1, 1, 1, 1, 1], |
|
|
168 |
[1, 1, 1, 1, 1, 1, 1], |
|
|
169 |
[0, 1, 1, 1, 1, 1, 0], |
|
|
170 |
[0, 0, 1, 1, 1, 0, 0]] |
|
|
171 |
blackhat_struct = ndi.iterate_structure(blackhat_struct, 8) |
|
|
172 |
#Perform the Black-Hat |
|
|
173 |
outline += ndi.black_tophat(outline, structure=blackhat_struct) |
|
|
174 |
|
|
|
175 |
#Use the internal marker and the Outline that was just created to generate the lungfilter |
|
|
176 |
lungfilter = np.bitwise_or(marker_internal, outline) |
|
|
177 |
#Close holes in the lungfilter |
|
|
178 |
#fill_holes is not used here, since in some slices the heart would be reincluded by accident |
|
|
179 |
lungfilter = ndi.morphology.binary_closing(lungfilter, structure=np.ones((5,5)), iterations=3) |
|
|
180 |
|
|
|
181 |
#Apply the lungfilter (the filtered areas being assigned 0 HU |
|
|
182 |
segmented = np.where(lungfilter == 1, image, np.zeros(image.shape)) |
|
|
183 |
|
|
|
184 |
return segmented |
|
|
185 |
|
|
|
186 |
|
|
|
187 |
def generate_markers(self, image): |
|
|
188 |
#Creation of the internal Marker |
|
|
189 |
marker_internal = image < self._threshold |
|
|
190 |
marker_internal = segmentation.clear_border(marker_internal) |
|
|
191 |
marker_internal_labels = measure.label(marker_internal) |
|
|
192 |
areas = [r.area for r in measure.regionprops(marker_internal_labels)] |
|
|
193 |
areas.sort() |
|
|
194 |
if len(areas) > 2: |
|
|
195 |
for region in measure.regionprops(marker_internal_labels): |
|
|
196 |
if region.area < areas[-2]: |
|
|
197 |
for coordinates in region.coords: |
|
|
198 |
marker_internal_labels[coordinates[0], coordinates[1]] = 0 |
|
|
199 |
marker_internal = marker_internal_labels > 0 |
|
|
200 |
#Creation of the external Marker |
|
|
201 |
external_a = ndi.binary_dilation(marker_internal, iterations=10) |
|
|
202 |
external_b = ndi.binary_dilation(marker_internal, iterations=55) |
|
|
203 |
marker_external = external_b ^ external_a |
|
|
204 |
#Creation of the Watershed Marker matrix |
|
|
205 |
marker_watershed = np.zeros(image.shape, dtype=np.int) |
|
|
206 |
marker_watershed += marker_internal * 255 |
|
|
207 |
marker_watershed += marker_external * 128 |
|
|
208 |
|
|
|
209 |
return marker_internal, marker_external, marker_watershed |
|
|
210 |
|
|
|
211 |
|
|
|
212 |
def get_segmentation_algorithm(): |
|
|
213 |
if config.SEGMENTATION_ALGO == config.MORPHOLOGICAL_OPERATIONS: |
|
|
214 |
return MorphologicalSegmentation() |
|
|
215 |
if config.SEGMENTATION_ALGO == config.WATERSHED: |
|
|
216 |
return WatershedSegmentation() |
|
|
217 |
|
|
|
218 |
return WatershedSegmentation() |