|
a |
|
b/lung.py |
|
|
1 |
# Copyright 2020 University of New South Wales, University of Sydney, Ingham Institute |
|
|
2 |
|
|
|
3 |
# Licensed under the Apache License, Version 2.0 (the "License"); |
|
|
4 |
# you may not use this file except in compliance with the License. |
|
|
5 |
# You may obtain a copy of the License at |
|
|
6 |
|
|
|
7 |
# http://www.apache.org/licenses/LICENSE-2.0 |
|
|
8 |
|
|
|
9 |
# Unless required by applicable law or agreed to in writing, software |
|
|
10 |
# distributed under the License is distributed on an "AS IS" BASIS, |
|
|
11 |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
|
12 |
# See the License for the specific language governing permissions and |
|
|
13 |
# limitations under the License. |
|
|
14 |
|
|
|
15 |
import SimpleITK as sitk |
|
|
16 |
|
|
|
17 |
|
|
|
18 |
def detect_holes(img, lower_threshold=-10000, upper_threshold=-400): |
|
|
19 |
""" |
|
|
20 |
Detect all (air) holes in given image. Default threshold values given are defined for CT. |
|
|
21 |
|
|
|
22 |
Args: |
|
|
23 |
img (sitk.Image): The image in which to detect holes. |
|
|
24 |
lower_threshold (int, optional): The lower threshold of intensities. Defaults to -10000. |
|
|
25 |
upper_threshold (int, optional): The upper threshold of intensities. Defaults to -400. |
|
|
26 |
|
|
|
27 |
Returns: |
|
|
28 |
label_image: image containing labels of all holes detected |
|
|
29 |
labels: a list of holes detected and some of their properties |
|
|
30 |
""" |
|
|
31 |
|
|
|
32 |
# Threshold to get holes |
|
|
33 |
btif = sitk.BinaryThresholdImageFilter() |
|
|
34 |
btif.SetInsideValue(1) |
|
|
35 |
btif.SetOutsideValue(0) |
|
|
36 |
btif.SetLowerThreshold(lower_threshold) |
|
|
37 |
btif.SetUpperThreshold(upper_threshold) |
|
|
38 |
holes = btif.Execute(img) |
|
|
39 |
|
|
|
40 |
# Get connected components |
|
|
41 |
ccif = sitk.ConnectedComponentImageFilter() |
|
|
42 |
label_image = ccif.Execute(holes) |
|
|
43 |
|
|
|
44 |
labels = [] |
|
|
45 |
|
|
|
46 |
lssif = sitk.LabelShapeStatisticsImageFilter() |
|
|
47 |
lssif.Execute(label_image) |
|
|
48 |
for region in range(1, ccif.GetObjectCount()): |
|
|
49 |
label = { |
|
|
50 |
"label": region, |
|
|
51 |
"phys_size": lssif.GetPhysicalSize(region), |
|
|
52 |
"elongation": lssif.GetElongation(region), |
|
|
53 |
"roundness": lssif.GetRoundness(region), |
|
|
54 |
"perimeter": lssif.GetPerimeter(region), |
|
|
55 |
"flatness": lssif.GetFlatness(region), |
|
|
56 |
} |
|
|
57 |
labels.append(label) |
|
|
58 |
|
|
|
59 |
# Sort by size |
|
|
60 |
labels = sorted(labels, key=lambda i: i["phys_size"], reverse=True) |
|
|
61 |
|
|
|
62 |
return label_image, labels |
|
|
63 |
|
|
|
64 |
|
|
|
65 |
def get_external_mask(label_image, labels, kernel_radius=5): |
|
|
66 |
"""Get a External mask given a label image and labels computed with detect_holes |
|
|
67 |
|
|
|
68 |
Args: |
|
|
69 |
label_image (sitk.Image): Label image from detect_holes |
|
|
70 |
labels (list): List of labels from detect_holes |
|
|
71 |
kernel_radius (int, optional): The width of the radius used by binary close. Defaults to 5. |
|
|
72 |
|
|
|
73 |
Returns: |
|
|
74 |
sitk.Image: External mask |
|
|
75 |
""" |
|
|
76 |
|
|
|
77 |
bmcif = sitk.BinaryMorphologicalClosingImageFilter() |
|
|
78 |
bmcif.SetKernelType(sitk.sitkBall) |
|
|
79 |
bmcif.SetKernelRadius(kernel_radius) |
|
|
80 |
|
|
|
81 |
# Save the largest as the external contour |
|
|
82 |
external_mask = sitk.BinaryThreshold(label_image, labels[0]["label"], labels[0]["label"]) |
|
|
83 |
external_mask = bmcif.Execute(external_mask) |
|
|
84 |
|
|
|
85 |
return external_mask |
|
|
86 |
|
|
|
87 |
|
|
|
88 |
def get_lung_mask(label_image, labels, kernel_radius=2): |
|
|
89 |
"""Get a Lung mask given a label image and labels computed with detect_holes |
|
|
90 |
|
|
|
91 |
Args: |
|
|
92 |
label_image (sitk.Image): Label image from detect_holes |
|
|
93 |
labels (list): List of labels from detect_holes |
|
|
94 |
kernel_radius (int, optional): The width of the radius used by binary close. Defaults to 2. |
|
|
95 |
|
|
|
96 |
Returns: |
|
|
97 |
sitk.Image: Combined lung mask |
|
|
98 |
""" |
|
|
99 |
# Find potential lung regions (typically the 2nd and 3rd largest regions after the external air) |
|
|
100 |
lung_candidates = [] |
|
|
101 |
for idx in range(1, min(5, len(labels))): # Check among top 5 largest regions |
|
|
102 |
if 1.5 < labels[idx]["flatness"] < 3.0 and labels[idx]["phys_size"] > 1000: |
|
|
103 |
lung_candidates.append(labels[idx]["label"]) |
|
|
104 |
if len(lung_candidates) == 2: # Stop after finding 2 lung regions |
|
|
105 |
break |
|
|
106 |
|
|
|
107 |
if len(lung_candidates) < 2: |
|
|
108 |
print("Warning: Could not detect both lungs!") |
|
|
109 |
return None |
|
|
110 |
|
|
|
111 |
bmcif = sitk.BinaryMorphologicalClosingImageFilter() |
|
|
112 |
bmcif.SetKernelType(sitk.sitkBall) |
|
|
113 |
bmcif.SetKernelRadius(kernel_radius) |
|
|
114 |
|
|
|
115 |
# Create masks for both lungs and combine them |
|
|
116 |
lung_mask = sitk.BinaryThreshold(label_image, lung_candidates[0], lung_candidates[0]) |
|
|
117 |
lung_mask2 = sitk.BinaryThreshold(label_image, lung_candidates[1], lung_candidates[1]) |
|
|
118 |
lung_mask = sitk.Or(lung_mask, lung_mask2) |
|
|
119 |
lung_mask = bmcif.Execute(lung_mask) |
|
|
120 |
|
|
|
121 |
return lung_mask |
|
|
122 |
|
|
|
123 |
|
|
|
124 |
def fill_holes(img, label_image, external_mask, lung_mask, fill_value=50): |
|
|
125 |
"""Returns the input image with all holes filled (except for the external and lung holes). |
|
|
126 |
|
|
|
127 |
Args: |
|
|
128 |
img (sitk.Image): The image to fill |
|
|
129 |
label_image (sitk.Image): Label image from detect_holes |
|
|
130 |
external_mask (sitk.Image): The external mask of the patient. |
|
|
131 |
lung_mask (sitk.Image): The lung mask of the patient. |
|
|
132 |
fill_value (int): The value to use to fill the holes. Defaults to 50. |
|
|
133 |
|
|
|
134 |
Returns: |
|
|
135 |
sitk.Image: The image with holes filled |
|
|
136 |
""" |
|
|
137 |
|
|
|
138 |
img_array = sitk.GetArrayFromImage(img) |
|
|
139 |
|
|
|
140 |
bdif = sitk.BinaryDilateImageFilter() |
|
|
141 |
bdif.SetKernelType(sitk.sitkBall) |
|
|
142 |
bdif.SetKernelRadius(3) |
|
|
143 |
|
|
|
144 |
mask = sitk.BinaryThreshold(label_image, 1, int(img_array.max())) |
|
|
145 |
mask = sitk.Subtract(mask, external_mask) |
|
|
146 |
mask = sitk.Subtract(mask, lung_mask) |
|
|
147 |
mask = bdif.Execute(mask) |
|
|
148 |
|
|
|
149 |
mask_array = sitk.GetArrayFromImage(mask) |
|
|
150 |
img_array[mask_array == 1] = fill_value |
|
|
151 |
|
|
|
152 |
filled_img = sitk.GetImageFromArray(img_array) |
|
|
153 |
filled_img.CopyInformation(img) |
|
|
154 |
|
|
|
155 |
return filled_img |