Skip to content

Commit c4e74a3

Browse files
committed
updated the spine model and added visualizations to the aortic calcium algorithm
1 parent a414f3f commit c4e74a3

5 files changed

Lines changed: 675 additions & 136 deletions

File tree

bin/C2C

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def AorticCalciumPipelineBuilder(path, args):
120120
pipeline = InferencePipeline(
121121
[
122122
io.DicomToNifti(path),
123-
spine.SpineSegmentation(model_name="ts_spine"),
123+
spine.SpineSegmentation(model_name=args.spine_model),
124124
orientation.ToCanonical(),
125125
aortic_calcium.AortaSegmentation(),
126126
orientation.ToCanonical(),
@@ -210,6 +210,12 @@ def argument_parser():
210210
aortic_calcium.add_argument(
211211
"--threshold", default="adaptive", type=str
212212
)
213+
aortic_calcium.add_argument(
214+
"--spine-model", default="ts_spine", type=str, help='Chose the model to perfom the spine segmentation'
215+
)
216+
aortic_calcium.add_argument(
217+
"--mosaic-type", default='all', type=str, help='Chose the the type of axial mosaic in the overview image'
218+
)
213219

214220
# Contrast phase
215221
contrast_phase_parser = subparsers.add_parser(

comp2comp/aortic_calcium/aortic_calcium.py

Lines changed: 62 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,6 @@ def aorta_seg(
8686
nora_tag="None",
8787
preview=False,
8888
task="total",
89-
# roi_subset = [
90-
# "vertebrae_T12",
91-
# "vertebrae_L1",
92-
# "vertebrae_L2",
93-
# "vertebrae_L3",
94-
# "vertebrae_L4",
95-
# "vertebrae_L5",
96-
# ],
9789
roi_subset=None,
9890
statistics=False,
9991
radiomics=False,
@@ -150,6 +142,36 @@ class AorticCalciumSegmentation(InferenceClass):
150142

151143
def __init__(self):
152144
super().__init__()
145+
# Label number for each vertebra
146+
self.vertebrae_num = {
147+
26: "vertebrae_S1",
148+
27: "vertebrae_L5",
149+
28: "vertebrae_L4",
150+
29: "vertebrae_L3",
151+
30: "vertebrae_L2",
152+
31: "vertebrae_L1",
153+
32: "vertebrae_T12",
154+
33: "vertebrae_T11",
155+
34: "vertebrae_T10",
156+
35: "vertebrae_T9",
157+
36: "vertebrae_T8",
158+
37: "vertebrae_T7",
159+
38: "vertebrae_T6",
160+
39: "vertebrae_T5",
161+
40: "vertebrae_T4",
162+
41: "vertebrae_T3",
163+
42: "vertebrae_T2",
164+
43: "vertebrae_T1",
165+
44: "vertebrae_C7",
166+
45: "vertebrae_C6",
167+
46: "vertebrae_C5",
168+
47: "vertebrae_C4",
169+
48: "vertebrae_C3",
170+
49: "vertebrae_C2",
171+
50: "vertebrae_C1"}
172+
173+
self.vertebrae_name = {v: k for k, v in self.vertebrae_num.items()}
174+
153175

154176
def __call__(self, inference_pipeline):
155177

@@ -171,9 +193,17 @@ def __call__(self, inference_pipeline):
171193
if not os.path.exists(os.path.join(self.output_dir, "metrics/")):
172194
os.makedirs(os.path.join(self.output_dir, "metrics/"))
173195

174-
ct = inference_pipeline.medical_volume.get_fdata()
175-
aorta_mask = inference_pipeline.segmentation.get_fdata().astype(np.int8) == 52
176-
spine_mask = inference_pipeline.spine_segmentation.get_fdata() > 0
196+
inference_pipeline.ct = inference_pipeline.medical_volume.get_fdata()
197+
inference_pipeline.aorta_mask = (inference_pipeline.segmentation.get_fdata().round().astype(np.int8) == 52)
198+
inference_pipeline.spine_mask = inference_pipeline.spine_segmentation.get_fdata().round().astype(np.uint8)
199+
200+
# convert to the index of TotalSegmentator
201+
if inference_pipeline.spine_model_name == 'stanford_spine_v0.0.1':
202+
tmp_mask = inference_pipeline.spine_mask > 0
203+
inference_pipeline.spine_mask[tmp_mask] = inference_pipeline.spine_mask[tmp_mask] + 11
204+
del tmp_mask
205+
206+
spine_mask_bin = inference_pipeline.spine_mask > 0
177207

178208
# Determine the target number of pixels
179209
pix_size = np.array(inference_pipeline.medical_volume.header.get_zooms())
@@ -186,11 +216,11 @@ def __call__(self, inference_pipeline):
186216

187217
# Run calcification detection pipeline
188218
calcification_results = self.detectCalcifications(
189-
ct,
190-
aorta_mask,
191-
exclude_mask=spine_mask,
219+
inference_pipeline.ct,
220+
inference_pipeline.aorta_mask,
221+
exclude_mask=spine_mask_bin,
192222
remove_size=3,
193-
return_dilated_mask=True,
223+
return_dilated_mask=True,
194224
return_eroded_aorta=True,
195225
threshold=inference_pipeline.args.threshold,
196226
dilation_iteration=target_aorta_dil,
@@ -200,6 +230,7 @@ def __call__(self, inference_pipeline):
200230

201231
inference_pipeline.calc_mask = calcification_results["calc_mask"]
202232
inference_pipeline.calcium_threshold = calcification_results["threshold"]
233+
inference_pipeline.dilated_aorta_mask = calcification_results["dilated_mask"]
203234

204235
# save masks
205236
inference_pipeline.saveArrToNifti(
@@ -227,21 +258,21 @@ def __call__(self, inference_pipeline):
227258
)
228259

229260
inference_pipeline.saveArrToNifti(
230-
spine_mask,
261+
inference_pipeline.spine_mask,
231262
os.path.join(
232263
inference_pipeline.output_dir_segmentation_masks, "spine_mask.nii.gz"
233264
),
234265
)
235266

236267
inference_pipeline.saveArrToNifti(
237-
aorta_mask,
268+
inference_pipeline.aorta_mask,
238269
os.path.join(
239270
inference_pipeline.output_dir_segmentation_masks, "aorta_mask.nii.gz"
240271
),
241272
)
242273

243274
inference_pipeline.saveArrToNifti(
244-
ct,
275+
inference_pipeline.ct,
245276
os.path.join(inference_pipeline.output_dir_segmentation_masks, "ct.nii.gz"),
246277
)
247278

@@ -393,7 +424,6 @@ def detectCalcifications(
393424
"""
394425
Choose threshold
395426
"""
396-
397427
if threshold == "adaptive":
398428
# calc_thres = eroded_ct_points.max()
399429

@@ -450,7 +480,7 @@ def detectCalcifications(
450480
struct=struct,
451481
num_iteration=dilation_iteration,
452482
operation="dilate",
453-
)
483+
).astype(np.int8)
454484

455485
if show_time:
456486
print("dilation mask time: {:.2f}".format(time.time() - t0))
@@ -627,33 +657,9 @@ def __init__(self):
627657

628658
def __call__(self, inference_pipeline):
629659
calc_mask = inference_pipeline.calc_mask
630-
spine_mask = inference_pipeline.spine_segmentation.get_fdata().astype(np.int8)
631-
""" 26: "vertebrae_S1",
632-
27: "vertebrae_L5",
633-
28: "vertebrae_L4",
634-
29: "vertebrae_L3",
635-
30: "vertebrae_L2",
636-
31: "vertebrae_L1",
637-
32: "vertebrae_T12",
638-
33: "vertebrae_T11",
639-
34: "vertebrae_T10",
640-
35: "vertebrae_T9",
641-
36: "vertebrae_T8",
642-
37: "vertebrae_T7",
643-
38: "vertebrae_T6",
644-
39: "vertebrae_T5",
645-
40: "vertebrae_T4",
646-
41: "vertebrae_T3",
647-
42: "vertebrae_T2",
648-
43: "vertebrae_T1",
649-
44: "vertebrae_C7",
650-
45: "vertebrae_C6",
651-
46: "vertebrae_C5",
652-
47: "vertebrae_C4",
653-
48: "vertebrae_C3",
654-
49: "vertebrae_C2",
655-
50: "vertebrae_C1","""
656-
660+
spine_mask = inference_pipeline.spine_mask
661+
aorta_mask = inference_pipeline.aorta_mask
662+
657663
t12_level = np.where((spine_mask == 32).sum(axis=(0, 1)))[0]
658664
l1_level = np.where((spine_mask == 31).sum(axis=(0, 1)))[0]
659665

@@ -666,7 +672,7 @@ def __call__(self, inference_pipeline):
666672
print("WARNNG: could not locate L1, using T12 only..")
667673
sep_plane = t12_level[0]
668674
else:
669-
raise ValueError("Could not locate either T12 or L1, aborting..")
675+
raise ValueError("Could not locate T12 and L1, aborting..")
670676

671677
planes = np.zeros_like(spine_mask, dtype=np.int8)
672678
planes[:, :, sep_plane] = 1
@@ -679,7 +685,8 @@ def __call__(self, inference_pipeline):
679685
inference_pipeline.output_dir_segmentation_masks, "t12_plane.nii.gz"
680686
),
681687
)
682-
688+
inference_pipeline.t12_plane = planes
689+
683690
inference_pipeline.pix_dims = inference_pipeline.medical_volume.header[
684691
"pixdim"
685692
][1:4]
@@ -696,9 +703,11 @@ def __call__(self, inference_pipeline):
696703
if i == 0:
697704
calc_mask_region = calc_mask[:, :, :sep_plane]
698705
ct = ct_full[:, :, :sep_plane]
706+
aorta_mask_region = aorta_mask[:, :, :sep_plane]
699707
elif i == 1:
700708
calc_mask_region = calc_mask[:, :, sep_plane:]
701709
ct = ct_full[:, :, sep_plane:]
710+
aorta_mask_region = aorta_mask[:, :, sep_plane:]
702711

703712
labelled_calc, num_lesions = ndimage.label(calc_mask_region)
704713

@@ -732,6 +741,10 @@ def __call__(self, inference_pipeline):
732741
metrics["volume_total"] = calc_vol
733742

734743
metrics["num_calc"] = num_lesions
744+
745+
# percent of the aorta calcificed
746+
metrics['perc_calcified'] = (calc_mask_region.sum() / aorta_mask_region.sum()) * 100
747+
735748

736749
if inference_pipeline.args.threshold == "agatston":
737750
if num_lesions == 0:

comp2comp/aortic_calcium/aortic_calcium_visualization.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import numpy as np
44

55
from comp2comp.inference_class_base import InferenceClass
6-
6+
from comp2comp.aortic_calcium.visualization_utils import createMipPlot, createCalciumMosaic, mergeMipAndMosaic
77

88
class AorticCalciumVisualizer(InferenceClass):
99
def __init__(self):
@@ -16,7 +16,35 @@ def __call__(self, inference_pipeline):
1616

1717
if not os.path.exists(self.output_dir_images_organs):
1818
os.makedirs(self.output_dir_images_organs)
19-
19+
20+
# Create MIP part of the overview plot
21+
createMipPlot(
22+
inference_pipeline.ct,
23+
inference_pipeline.calc_mask,
24+
inference_pipeline.aorta_mask,
25+
inference_pipeline.t12_plane == 1,
26+
inference_pipeline.calcium_threshold,
27+
inference_pipeline.pix_dims,
28+
inference_pipeline.metrics,
29+
self.output_dir_images_organs,
30+
)
31+
32+
# Create mosaic part of the overview plot
33+
createCalciumMosaic(
34+
inference_pipeline.ct,
35+
inference_pipeline.calc_mask,
36+
inference_pipeline.dilated_aorta_mask, # the dilated mask is used here
37+
inference_pipeline.spine_mask,
38+
inference_pipeline.pix_dims,
39+
self.output_dir_images_organs,
40+
inference_pipeline.args.mosaic_type,
41+
)
42+
43+
# Merge the two images created above for the final report
44+
mergeMipAndMosaic(
45+
self.output_dir_images_organs
46+
)
47+
2048
return {}
2149

2250

@@ -113,6 +141,9 @@ def __call__(self, inference_pipeline):
113141
f.write(
114142
"{},{:.3f}\n".format("Min volume (cm³):", np.min(metrics["volume"]))
115143
)
144+
f.write(
145+
"{},{:.3f}\n".format("% Calcified aorta:", metrics["perc_calcified"])
146+
)
116147

117148
if inference_pipeline.args.threshold == "agatston":
118149
f.write("Agatston score,{:.1f}\n".format(metrics["agatston_score"]))
@@ -187,6 +218,14 @@ def __call__(self, inference_pipeline):
187218
inference_pipeline.calcium_threshold,
188219
)
189220
)
221+
print(
222+
"{:<{}}{:.3f}".format(
223+
"% Calcified aorta",
224+
distance,
225+
metrics["perc_calcified"],
226+
)
227+
)
228+
190229
if inference_pipeline.args.threshold == "agatston":
191230
print(
192231
"{:<{}}{:.1f}".format(

0 commit comments

Comments
 (0)