Diff of /lung.py [000000] .. [05a195]

Switch to unified view

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