mosamatic2 2.0.24__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- models.py +259 -0
- mosamatic2/__init__.py +0 -0
- mosamatic2/app.py +32 -0
- mosamatic2/cli.py +50 -0
- mosamatic2/commands/__init__.py +0 -0
- mosamatic2/commands/boadockerpipeline.py +48 -0
- mosamatic2/commands/calculatemaskstatistics.py +59 -0
- mosamatic2/commands/calculatescores.py +73 -0
- mosamatic2/commands/createdicomsummary.py +61 -0
- mosamatic2/commands/createpngsfromsegmentations.py +65 -0
- mosamatic2/commands/defaultdockerpipeline.py +84 -0
- mosamatic2/commands/defaultpipeline.py +70 -0
- mosamatic2/commands/dicom2nifti.py +55 -0
- mosamatic2/commands/liveranalysispipeline.py +61 -0
- mosamatic2/commands/rescaledicomimages.py +54 -0
- mosamatic2/commands/segmentmusclefatl3tensorflow.py +55 -0
- mosamatic2/commands/selectslicefromscans.py +66 -0
- mosamatic2/commands/totalsegmentator.py +77 -0
- mosamatic2/constants.py +27 -0
- mosamatic2/core/__init__.py +0 -0
- mosamatic2/core/data/__init__.py +5 -0
- mosamatic2/core/data/dicomimage.py +27 -0
- mosamatic2/core/data/dicomimageseries.py +26 -0
- mosamatic2/core/data/dixonseries.py +22 -0
- mosamatic2/core/data/filedata.py +26 -0
- mosamatic2/core/data/multidicomimage.py +30 -0
- mosamatic2/core/data/multiniftiimage.py +26 -0
- mosamatic2/core/data/multinumpyimage.py +26 -0
- mosamatic2/core/data/niftiimage.py +13 -0
- mosamatic2/core/data/numpyimage.py +13 -0
- mosamatic2/core/managers/__init__.py +0 -0
- mosamatic2/core/managers/logmanager.py +45 -0
- mosamatic2/core/managers/logmanagerlistener.py +3 -0
- mosamatic2/core/pipelines/__init__.py +4 -0
- mosamatic2/core/pipelines/boadockerpipeline/__init__.py +0 -0
- mosamatic2/core/pipelines/boadockerpipeline/boadockerpipeline.py +70 -0
- mosamatic2/core/pipelines/defaultdockerpipeline/__init__.py +0 -0
- mosamatic2/core/pipelines/defaultdockerpipeline/defaultdockerpipeline.py +28 -0
- mosamatic2/core/pipelines/defaultpipeline/__init__.py +0 -0
- mosamatic2/core/pipelines/defaultpipeline/defaultpipeline.py +90 -0
- mosamatic2/core/pipelines/liveranalysispipeline/__init__.py +0 -0
- mosamatic2/core/pipelines/liveranalysispipeline/liveranalysispipeline.py +48 -0
- mosamatic2/core/pipelines/pipeline.py +14 -0
- mosamatic2/core/singleton.py +9 -0
- mosamatic2/core/tasks/__init__.py +13 -0
- mosamatic2/core/tasks/applythresholdtosegmentationstask/__init__.py +0 -0
- mosamatic2/core/tasks/applythresholdtosegmentationstask/applythresholdtosegmentationstask.py +117 -0
- mosamatic2/core/tasks/calculatemaskstatisticstask/__init__.py +0 -0
- mosamatic2/core/tasks/calculatemaskstatisticstask/calculatemaskstatisticstask.py +104 -0
- mosamatic2/core/tasks/calculatescorestask/__init__.py +0 -0
- mosamatic2/core/tasks/calculatescorestask/calculatescorestask.py +152 -0
- mosamatic2/core/tasks/createdicomsummarytask/__init__.py +0 -0
- mosamatic2/core/tasks/createdicomsummarytask/createdicomsummarytask.py +88 -0
- mosamatic2/core/tasks/createpngsfromsegmentationstask/__init__.py +0 -0
- mosamatic2/core/tasks/createpngsfromsegmentationstask/createpngsfromsegmentationstask.py +101 -0
- mosamatic2/core/tasks/dicom2niftitask/__init__.py +0 -0
- mosamatic2/core/tasks/dicom2niftitask/dicom2niftitask.py +45 -0
- mosamatic2/core/tasks/rescaledicomimagestask/__init__.py +0 -0
- mosamatic2/core/tasks/rescaledicomimagestask/rescaledicomimagestask.py +64 -0
- mosamatic2/core/tasks/segmentationnifti2numpytask/__init__.py +0 -0
- mosamatic2/core/tasks/segmentationnifti2numpytask/segmentationnifti2numpytask.py +57 -0
- mosamatic2/core/tasks/segmentationnumpy2niftitask/__init__.py +0 -0
- mosamatic2/core/tasks/segmentationnumpy2niftitask/segmentationnumpy2niftitask.py +86 -0
- mosamatic2/core/tasks/segmentmusclefatl3tensorflowtask/__init__.py +0 -0
- mosamatic2/core/tasks/segmentmusclefatl3tensorflowtask/paramloader.py +39 -0
- mosamatic2/core/tasks/segmentmusclefatl3tensorflowtask/segmentmusclefatl3tensorflowtask.py +122 -0
- mosamatic2/core/tasks/segmentmusclefatt4pytorchtask/__init__.py +0 -0
- mosamatic2/core/tasks/segmentmusclefatt4pytorchtask/paramloader.py +39 -0
- mosamatic2/core/tasks/segmentmusclefatt4pytorchtask/segmentmusclefatt4pytorchtask.py +128 -0
- mosamatic2/core/tasks/selectslicefromscanstask/__init__.py +0 -0
- mosamatic2/core/tasks/selectslicefromscanstask/selectslicefromscanstask.py +249 -0
- mosamatic2/core/tasks/task.py +50 -0
- mosamatic2/core/tasks/totalsegmentatortask/__init__.py +0 -0
- mosamatic2/core/tasks/totalsegmentatortask/totalsegmentatortask.py +75 -0
- mosamatic2/core/utils.py +405 -0
- mosamatic2/server.py +146 -0
- mosamatic2/ui/__init__.py +0 -0
- mosamatic2/ui/mainwindow.py +426 -0
- mosamatic2/ui/resources/VERSION +1 -0
- mosamatic2/ui/resources/icons/mosamatic2.icns +0 -0
- mosamatic2/ui/resources/icons/mosamatic2.ico +0 -0
- mosamatic2/ui/resources/icons/spinner.gif +0 -0
- mosamatic2/ui/resources/images/body-composition.jpg +0 -0
- mosamatic2/ui/settings.py +62 -0
- mosamatic2/ui/utils.py +36 -0
- mosamatic2/ui/widgets/__init__.py +0 -0
- mosamatic2/ui/widgets/dialogs/__init__.py +0 -0
- mosamatic2/ui/widgets/dialogs/dialog.py +16 -0
- mosamatic2/ui/widgets/dialogs/helpdialog.py +9 -0
- mosamatic2/ui/widgets/panels/__init__.py +0 -0
- mosamatic2/ui/widgets/panels/defaultpanel.py +31 -0
- mosamatic2/ui/widgets/panels/logpanel.py +65 -0
- mosamatic2/ui/widgets/panels/mainpanel.py +82 -0
- mosamatic2/ui/widgets/panels/pipelines/__init__.py +0 -0
- mosamatic2/ui/widgets/panels/pipelines/boadockerpipelinepanel.py +195 -0
- mosamatic2/ui/widgets/panels/pipelines/defaultdockerpipelinepanel.py +314 -0
- mosamatic2/ui/widgets/panels/pipelines/defaultpipelinepanel.py +302 -0
- mosamatic2/ui/widgets/panels/pipelines/liveranalysispipelinepanel.py +187 -0
- mosamatic2/ui/widgets/panels/pipelines/pipelinepanel.py +6 -0
- mosamatic2/ui/widgets/panels/settingspanel.py +16 -0
- mosamatic2/ui/widgets/panels/stackedpanel.py +22 -0
- mosamatic2/ui/widgets/panels/tasks/__init__.py +0 -0
- mosamatic2/ui/widgets/panels/tasks/applythresholdtosegmentationstaskpanel.py +271 -0
- mosamatic2/ui/widgets/panels/tasks/calculatemaskstatisticstaskpanel.py +215 -0
- mosamatic2/ui/widgets/panels/tasks/calculatescorestaskpanel.py +238 -0
- mosamatic2/ui/widgets/panels/tasks/createdicomsummarytaskpanel.py +206 -0
- mosamatic2/ui/widgets/panels/tasks/createpngsfromsegmentationstaskpanel.py +247 -0
- mosamatic2/ui/widgets/panels/tasks/dicom2niftitaskpanel.py +183 -0
- mosamatic2/ui/widgets/panels/tasks/rescaledicomimagestaskpanel.py +184 -0
- mosamatic2/ui/widgets/panels/tasks/segmentationnifti2numpytaskpanel.py +192 -0
- mosamatic2/ui/widgets/panels/tasks/segmentationnumpy2niftitaskpanel.py +213 -0
- mosamatic2/ui/widgets/panels/tasks/segmentmusclefatl3tensorflowtaskpanel.py +216 -0
- mosamatic2/ui/widgets/panels/tasks/segmentmusclefatt4pytorchtaskpanel.py +217 -0
- mosamatic2/ui/widgets/panels/tasks/selectslicefromscanstaskpanel.py +193 -0
- mosamatic2/ui/widgets/panels/tasks/taskpanel.py +6 -0
- mosamatic2/ui/widgets/panels/tasks/totalsegmentatortaskpanel.py +195 -0
- mosamatic2/ui/widgets/panels/visualizations/__init__.py +0 -0
- mosamatic2/ui/widgets/panels/visualizations/liversegmentvisualization/__init__.py +0 -0
- mosamatic2/ui/widgets/panels/visualizations/liversegmentvisualization/liversegmentpicker.py +96 -0
- mosamatic2/ui/widgets/panels/visualizations/liversegmentvisualization/liversegmentviewer.py +130 -0
- mosamatic2/ui/widgets/panels/visualizations/liversegmentvisualization/liversegmentvisualization.py +120 -0
- mosamatic2/ui/widgets/panels/visualizations/sliceselectionvisualization/__init__.py +0 -0
- mosamatic2/ui/widgets/panels/visualizations/sliceselectionvisualization/sliceselectionviewer.py +61 -0
- mosamatic2/ui/widgets/panels/visualizations/sliceselectionvisualization/sliceselectionvisualization.py +133 -0
- mosamatic2/ui/widgets/panels/visualizations/sliceselectionvisualization/slicetile.py +63 -0
- mosamatic2/ui/widgets/panels/visualizations/slicevisualization/__init__.py +0 -0
- mosamatic2/ui/widgets/panels/visualizations/slicevisualization/custominteractorstyle.py +80 -0
- mosamatic2/ui/widgets/panels/visualizations/slicevisualization/sliceviewer.py +116 -0
- mosamatic2/ui/widgets/panels/visualizations/slicevisualization/slicevisualization.py +141 -0
- mosamatic2/ui/widgets/panels/visualizations/visualization.py +6 -0
- mosamatic2/ui/widgets/splashscreen.py +101 -0
- mosamatic2/ui/worker.py +29 -0
- mosamatic2-2.0.24.dist-info/METADATA +43 -0
- mosamatic2-2.0.24.dist-info/RECORD +136 -0
- mosamatic2-2.0.24.dist-info/WHEEL +4 -0
- mosamatic2-2.0.24.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import nibabel as nb
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
from mosamatic2.core.tasks.task import Task
|
|
7
|
+
from mosamatic2.core.managers.logmanager import LogManager
|
|
8
|
+
|
|
9
|
+
LOG = LogManager()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CalculateMaskStatisticsTask(Task):
|
|
13
|
+
INPUTS = ['scans', 'masks']
|
|
14
|
+
PARAMS = []
|
|
15
|
+
|
|
16
|
+
def __init__(self, inputs, params, output, overwrite):
|
|
17
|
+
super(CalculateMaskStatisticsTask, self).__init__(inputs, params, output, overwrite)
|
|
18
|
+
|
|
19
|
+
def load_scans(self):
|
|
20
|
+
scans = []
|
|
21
|
+
for f in os.listdir(self.input('scans')):
|
|
22
|
+
if f.endswith('.nii.gz'):
|
|
23
|
+
scan = os.path.join(self.input('scans'), f)
|
|
24
|
+
if os.path.isfile(scan):
|
|
25
|
+
scans.append(scan)
|
|
26
|
+
return scans
|
|
27
|
+
|
|
28
|
+
def load_mask_files(self):
|
|
29
|
+
mask_files = []
|
|
30
|
+
for f in os.listdir(self.input('masks')):
|
|
31
|
+
if f.endswith('.nii.gz') and 'liver_segment' in f:
|
|
32
|
+
mask_file = os.path.join(self.input('masks'), f)
|
|
33
|
+
if os.path.isfile(mask_file):
|
|
34
|
+
mask_files.append(mask_file)
|
|
35
|
+
return mask_files
|
|
36
|
+
|
|
37
|
+
def collect_scan_mask_file_pairs(self, scans, mask_files):
|
|
38
|
+
scan_mask_file_pairs = []
|
|
39
|
+
for scan in scans:
|
|
40
|
+
scan_name = os.path.split(scan)[1][:-7]
|
|
41
|
+
for mask_file in mask_files:
|
|
42
|
+
mask_file_name = os.path.split(mask_file)[1][:-7]
|
|
43
|
+
if scan_name in mask_file_name:
|
|
44
|
+
scan_mask_file_pairs.append((scan, mask_file))
|
|
45
|
+
return scan_mask_file_pairs
|
|
46
|
+
|
|
47
|
+
def get_masked_voxels(self, scan_mask_file_pair):
|
|
48
|
+
scan_image_file = scan_mask_file_pair[0]
|
|
49
|
+
scan_image = nb.load(scan_image_file)
|
|
50
|
+
scan_image_data = scan_image.get_fdata()
|
|
51
|
+
mask_file_image_file = scan_mask_file_pair[1]
|
|
52
|
+
mask_file_image_file_name = os.path.split(mask_file_image_file)[1]
|
|
53
|
+
mask_file_image = nb.load(mask_file_image_file)
|
|
54
|
+
mask_file_image_data = mask_file_image.get_fdata()
|
|
55
|
+
mask = mask_file_image_data > 0
|
|
56
|
+
masked_voxels = scan_image_data[mask]
|
|
57
|
+
return masked_voxels, mask_file_image_file_name
|
|
58
|
+
|
|
59
|
+
def calculate_volume_in_mL(self, mask_file):
|
|
60
|
+
nifti_image = nb.load(mask_file)
|
|
61
|
+
nifti_image_data = nifti_image.get_fdata()
|
|
62
|
+
voxel_dims = nifti_image.header.get_zooms()[:3]
|
|
63
|
+
voxel_volume = np.prod(voxel_dims)
|
|
64
|
+
num_voxels = np.sum(nifti_image_data > 0.5)
|
|
65
|
+
mask_volume = num_voxels * voxel_volume / 1000.0
|
|
66
|
+
return mask_volume
|
|
67
|
+
|
|
68
|
+
def create_histogram_png(self, masked_voxels, file_name):
|
|
69
|
+
histogram_png_file = os.path.join(self.output(), f'{file_name}.png')
|
|
70
|
+
plt.figure(figsize=(8,6))
|
|
71
|
+
plt.hist(masked_voxels, bins=100, color='steelblue', edgecolor='black')
|
|
72
|
+
plt.title(f'Histogram of HU values inside {file_name}')
|
|
73
|
+
plt.xlabel('HU')
|
|
74
|
+
plt.ylabel('Frequency')
|
|
75
|
+
plt.savefig(histogram_png_file, dpi=300, bbox_inches='tight')
|
|
76
|
+
plt.close()
|
|
77
|
+
|
|
78
|
+
def save_data_to_file(self, data):
|
|
79
|
+
csv_file_path = os.path.join(self.output(), 'statistics.csv')
|
|
80
|
+
xls_file_path = os.path.join(self.output(), 'statistics.xlsx')
|
|
81
|
+
df = pd.DataFrame(data=data)
|
|
82
|
+
df.to_csv(csv_file_path, index=False, sep=';')
|
|
83
|
+
df.to_excel(xls_file_path, index=False, engine='openpyxl')
|
|
84
|
+
|
|
85
|
+
def run(self):
|
|
86
|
+
scans = self.load_scans()
|
|
87
|
+
mask_files = self.load_mask_files()
|
|
88
|
+
scan_mask_file_pairs = self.collect_scan_mask_file_pairs(scans, mask_files)
|
|
89
|
+
nr_steps = len(scan_mask_file_pairs)
|
|
90
|
+
data = {
|
|
91
|
+
'file': [],
|
|
92
|
+
'mean_HU': [],
|
|
93
|
+
'std_HU': [],
|
|
94
|
+
'volume_mL': [],
|
|
95
|
+
}
|
|
96
|
+
for step in range(nr_steps):
|
|
97
|
+
masked_voxels, file_name = self.get_masked_voxels(scan_mask_file_pairs[step])
|
|
98
|
+
data['file'].append(file_name)
|
|
99
|
+
data['mean_HU'].append(round(np.mean(masked_voxels)))
|
|
100
|
+
data['std_HU'].append(round(np.std(masked_voxels)))
|
|
101
|
+
data['volume_mL'].append(round(self.calculate_volume_in_mL(scan_mask_file_pairs[step][1])))
|
|
102
|
+
self.create_histogram_png(masked_voxels, file_name)
|
|
103
|
+
self.set_progress(step, nr_steps)
|
|
104
|
+
self.save_data_to_file(data)
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from mosamatic2.core.tasks.task import Task
|
|
5
|
+
from mosamatic2.core.managers.logmanager import LogManager
|
|
6
|
+
from mosamatic2.core.data.multidicomimage import MultiDicomImage
|
|
7
|
+
from mosamatic2.core.data.numpyimage import NumpyImage
|
|
8
|
+
from mosamatic2.core.utils import (
|
|
9
|
+
get_pixels_from_dicom_object,
|
|
10
|
+
calculate_area,
|
|
11
|
+
calculate_mean_radiation_attenuation,
|
|
12
|
+
calculate_lama_percentage,
|
|
13
|
+
get_pixels_from_tag_file,
|
|
14
|
+
MUSCLE,
|
|
15
|
+
SAT,
|
|
16
|
+
VAT,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
LOG = LogManager()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CalculateScoresTask(Task):
|
|
23
|
+
INPUTS = [
|
|
24
|
+
'images',
|
|
25
|
+
'segmentations'
|
|
26
|
+
]
|
|
27
|
+
PARAMS = ['file_type']
|
|
28
|
+
|
|
29
|
+
def __init__(self, inputs, params, output, overwrite=True):
|
|
30
|
+
super(CalculateScoresTask, self).__init__(inputs, params, output, overwrite)
|
|
31
|
+
|
|
32
|
+
def collect_img_seg_pairs(self, images, segmentations, file_type='npy'):
|
|
33
|
+
file_type = '.tag' if file_type == 'tag' else '.seg.npy'
|
|
34
|
+
img_seg_pairs = []
|
|
35
|
+
for image in images:
|
|
36
|
+
f_img_path = image.path()
|
|
37
|
+
f_img_name = os.path.split(f_img_path)[1]
|
|
38
|
+
for f_seg_path in segmentations:
|
|
39
|
+
f_seg_name = os.path.split(f_seg_path)[1]
|
|
40
|
+
if file_type == '.seg.npy':
|
|
41
|
+
f_seg_name = f_seg_name.removesuffix(file_type)
|
|
42
|
+
if f_seg_name == f_img_name:
|
|
43
|
+
img_seg_pairs.append((image, f_seg_path))
|
|
44
|
+
elif file_type == '.tag':
|
|
45
|
+
f_seg_name = f_seg_name.removesuffix(file_type).removesuffix('.dcm')
|
|
46
|
+
f_img_name = f_img_name.removesuffix('.dcm')
|
|
47
|
+
if f_seg_name == f_img_name:
|
|
48
|
+
img_seg_pairs.append((image, f_seg_path))
|
|
49
|
+
else:
|
|
50
|
+
raise RuntimeError('Unknown file type')
|
|
51
|
+
return img_seg_pairs
|
|
52
|
+
|
|
53
|
+
def load_images(self):
|
|
54
|
+
image_data = MultiDicomImage()
|
|
55
|
+
image_data.set_path(self.input('images'))
|
|
56
|
+
if image_data.load():
|
|
57
|
+
return image_data
|
|
58
|
+
raise RuntimeError('Could not load images')
|
|
59
|
+
|
|
60
|
+
def load_pixels_and_spacing(self, image):
|
|
61
|
+
p = image.object()
|
|
62
|
+
pixels = get_pixels_from_dicom_object(p, normalize=True)
|
|
63
|
+
return pixels, p.PixelSpacing
|
|
64
|
+
|
|
65
|
+
def load_segmentations(self, file_type='npy'):
|
|
66
|
+
file_type = '.tag' if file_type == 'tag' else '.seg.npy'
|
|
67
|
+
segmentations = []
|
|
68
|
+
for f in os.listdir(self.input('segmentations')):
|
|
69
|
+
f_path = os.path.join(self.input('segmentations'), f)
|
|
70
|
+
if f.endswith(file_type):
|
|
71
|
+
segmentations.append(f_path)
|
|
72
|
+
if len(segmentations) == 0:
|
|
73
|
+
raise RuntimeError('Input directory has no segmentation files')
|
|
74
|
+
return segmentations
|
|
75
|
+
|
|
76
|
+
def load_segmentation(self, f, file_type='npy'):
|
|
77
|
+
if file_type == 'npy':
|
|
78
|
+
segmentation = NumpyImage()
|
|
79
|
+
segmentation.set_path(f)
|
|
80
|
+
if segmentation.load():
|
|
81
|
+
return segmentation.object()
|
|
82
|
+
LOG.error(f'Could not load segmentation file {f}')
|
|
83
|
+
if file_type == 'tag':
|
|
84
|
+
pixels = get_pixels_from_tag_file(f)
|
|
85
|
+
try:
|
|
86
|
+
pixels = pixels.reshape(512, 512)
|
|
87
|
+
return pixels
|
|
88
|
+
except Exception:
|
|
89
|
+
LOG.warning(f'Could not reshape TAG pixels to (512, 512), skipping...')
|
|
90
|
+
return None
|
|
91
|
+
raise RuntimeError('Unknown file type')
|
|
92
|
+
|
|
93
|
+
def run(self):
|
|
94
|
+
image_data = self.load_images()
|
|
95
|
+
images = image_data.images()
|
|
96
|
+
file_type = self.param('file_type')
|
|
97
|
+
segmentations = self.load_segmentations(file_type)
|
|
98
|
+
img_seg_pairs = self.collect_img_seg_pairs(images, segmentations, file_type)
|
|
99
|
+
# Create empty data dictionary
|
|
100
|
+
data = {
|
|
101
|
+
'file': [],
|
|
102
|
+
'muscle_area': [], 'muscle_idx': [], 'muscle_ra': [], 'muscle_lama_perc': [],
|
|
103
|
+
'vat_area': [], 'vat_idx': [], 'vat_ra': [],
|
|
104
|
+
'sat_area': [], 'sat_idx': [], 'sat_ra': []
|
|
105
|
+
}
|
|
106
|
+
nr_steps = len(img_seg_pairs)
|
|
107
|
+
for step in range(nr_steps):
|
|
108
|
+
# Get image and its pixel spacing
|
|
109
|
+
image, pixel_spacing = self.load_pixels_and_spacing(img_seg_pairs[step][0])
|
|
110
|
+
if image is None:
|
|
111
|
+
raise RuntimeError(f'Could not load DICOM image for file {img_seg_pairs[step][0]}')
|
|
112
|
+
# Get segmentation for this image
|
|
113
|
+
segmentation = self.load_segmentation(img_seg_pairs[step][1], file_type)
|
|
114
|
+
if segmentation is None:
|
|
115
|
+
LOG.warning(f'Could not load segmentation for file {img_seg_pairs[step][1]}')
|
|
116
|
+
continue
|
|
117
|
+
# Calculate metrics
|
|
118
|
+
file_name = os.path.split(img_seg_pairs[step][0].path())[1]
|
|
119
|
+
muscle_area = calculate_area(segmentation, MUSCLE, pixel_spacing)
|
|
120
|
+
muscle_idx = 0
|
|
121
|
+
muscle_ra = calculate_mean_radiation_attenuation(image, segmentation, MUSCLE)
|
|
122
|
+
muscle_lama_perc = calculate_lama_percentage(image, segmentation, MUSCLE)
|
|
123
|
+
vat_area = calculate_area(segmentation, VAT, pixel_spacing)
|
|
124
|
+
vat_idx = 0
|
|
125
|
+
vat_ra = calculate_mean_radiation_attenuation(image, segmentation, VAT)
|
|
126
|
+
sat_area = calculate_area(segmentation, SAT, pixel_spacing)
|
|
127
|
+
sat_idx = 0
|
|
128
|
+
sat_ra = calculate_mean_radiation_attenuation(image, segmentation, SAT)
|
|
129
|
+
LOG.info(f'file: {file_name}, ' +
|
|
130
|
+
f'muscle_area: {muscle_area}, muscle_idx: {muscle_idx}, muscle_ra: {muscle_ra}, ' +
|
|
131
|
+
f'vat_area: {vat_area}, vat_idx: {vat_idx}, vat_ra: {vat_ra}, ' +
|
|
132
|
+
f'sat_area: {sat_area}, sat_idx: {sat_idx}, sat_ra: {sat_ra}')
|
|
133
|
+
# Update dataframe data
|
|
134
|
+
data['file'].append(file_name)
|
|
135
|
+
data['muscle_area'].append(muscle_area)
|
|
136
|
+
data['muscle_idx'].append(muscle_idx)
|
|
137
|
+
data['muscle_ra'].append(muscle_ra)
|
|
138
|
+
data['muscle_lama_perc'].append(muscle_lama_perc)
|
|
139
|
+
data['vat_area'].append(vat_area)
|
|
140
|
+
data['vat_idx'].append(vat_idx)
|
|
141
|
+
data['vat_ra'].append(vat_ra)
|
|
142
|
+
data['sat_area'].append(sat_area)
|
|
143
|
+
data['sat_idx'].append(sat_idx)
|
|
144
|
+
data['sat_ra'].append(sat_ra)
|
|
145
|
+
# Update progress
|
|
146
|
+
self.set_progress(step, nr_steps)
|
|
147
|
+
# Build dataframe and return the CSV file as output
|
|
148
|
+
csv_file_path = os.path.join(self.output(), 'bc_scores.csv')
|
|
149
|
+
xls_file_path = os.path.join(self.output(), 'bc_scores.xlsx')
|
|
150
|
+
df = pd.DataFrame(data=data)
|
|
151
|
+
df.to_csv(csv_file_path, index=False, sep=';')
|
|
152
|
+
df.to_excel(xls_file_path, index=False, engine='openpyxl')
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from mosamatic2.core.tasks.task import Task
|
|
4
|
+
from mosamatic2.core.managers.logmanager import LogManager
|
|
5
|
+
from mosamatic2.core.utils import (
|
|
6
|
+
is_dicom,
|
|
7
|
+
load_dicom,
|
|
8
|
+
current_time_in_seconds,
|
|
9
|
+
elapsed_time_in_seconds,
|
|
10
|
+
duration,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
LOG = LogManager()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CreateDicomSummaryTask(Task):
|
|
17
|
+
INPUTS = ['directory']
|
|
18
|
+
PARAMS = []
|
|
19
|
+
|
|
20
|
+
def __init__(self, inputs, params, output, overwrite):
|
|
21
|
+
super(CreateDicomSummaryTask, self).__init__(inputs, params, output, overwrite)
|
|
22
|
+
|
|
23
|
+
def get_values(self, files, tag_name):
|
|
24
|
+
values = []
|
|
25
|
+
for f in files:
|
|
26
|
+
if tag_name in f.keys():
|
|
27
|
+
if f[tag_name] not in values:
|
|
28
|
+
values.append(f[tag_name])
|
|
29
|
+
return values
|
|
30
|
+
|
|
31
|
+
def get_tag_value(self, p, tag_name):
|
|
32
|
+
return p.data_element(tag_name).value
|
|
33
|
+
|
|
34
|
+
def run(self):
|
|
35
|
+
series = {}
|
|
36
|
+
start_time = current_time_in_seconds()
|
|
37
|
+
for root, dirs, files in os.walk(self.input('directory')):
|
|
38
|
+
for f in files:
|
|
39
|
+
f_path = os.path.join(root, f)
|
|
40
|
+
if is_dicom(f_path):
|
|
41
|
+
p = load_dicom(f_path, stop_before_pixels=True)
|
|
42
|
+
if p:
|
|
43
|
+
patient_id = self.get_tag_value(p, 'PatientID')
|
|
44
|
+
if patient_id:
|
|
45
|
+
if patient_id not in series.keys():
|
|
46
|
+
series[patient_id] = {}
|
|
47
|
+
series_instance_uid = self.get_tag_value(p, 'SeriesInstanceUID')
|
|
48
|
+
if series_instance_uid:
|
|
49
|
+
if series_instance_uid not in series[patient_id].keys():
|
|
50
|
+
series[patient_id][series_instance_uid] = []
|
|
51
|
+
series[patient_id][series_instance_uid].append({
|
|
52
|
+
'series_description': self.get_tag_value(p, 'SeriesDescription'),
|
|
53
|
+
'slice_thickness': self.get_tag_value(p, 'SliceThickness'),
|
|
54
|
+
'rows': self.get_tag_value(p, 'Rows'),
|
|
55
|
+
'columns': self.get_tag_value(p, 'Columns'),
|
|
56
|
+
'pixel_spacing': self.get_tag_value(p, 'PixelSpacing'),
|
|
57
|
+
'modality': self.get_tag_value(p, 'Modality'),
|
|
58
|
+
'image_type': self.get_tag_value(p, 'ImageType'),
|
|
59
|
+
})
|
|
60
|
+
data = {
|
|
61
|
+
'patient_id': [],
|
|
62
|
+
'series_description': [],
|
|
63
|
+
'nr_images': [],
|
|
64
|
+
'slice_thickness': [],
|
|
65
|
+
'rows': [],
|
|
66
|
+
'columns': [],
|
|
67
|
+
'pixel_spacing': [],
|
|
68
|
+
'modality': [],
|
|
69
|
+
'image_type': [],
|
|
70
|
+
'series_instance_uid': [],
|
|
71
|
+
}
|
|
72
|
+
for patient_id, v1 in series.items():
|
|
73
|
+
for series_instance_uid, v2 in v1.items():
|
|
74
|
+
data['patient_id'].append(patient_id)
|
|
75
|
+
data['series_description'].append(self.get_values(v2, tag_name='series_description'))
|
|
76
|
+
data['nr_images'].append(len(v2))
|
|
77
|
+
data['slice_thickness'].append(self.get_values(v2, tag_name='slice_thickness'))
|
|
78
|
+
data['rows'].append(self.get_values(v2, tag_name='rows'))
|
|
79
|
+
data['columns'].append(self.get_values(v2, tag_name='columns'))
|
|
80
|
+
data['pixel_spacing'].append(self.get_values(v2, tag_name='pixel_spacing'))
|
|
81
|
+
data['modality'].append(self.get_values(v2, tag_name='modality'))
|
|
82
|
+
data['image_type'].append(self.get_values(v2, tag_name='image_type'))
|
|
83
|
+
data['series_instance_uid'].append(series_instance_uid)
|
|
84
|
+
df = pd.DataFrame(data=data)
|
|
85
|
+
df.to_csv(os.path.join(self.output(), 'summary.csv'), index=False, sep=';')
|
|
86
|
+
df.to_excel(os.path.join(self.output(), 'summary.xlsx'), index=False, engine='openpyxl')
|
|
87
|
+
LOG.info(df)
|
|
88
|
+
LOG.info(f'Elapsed time: {duration(elapsed_time_in_seconds(start_time))}')
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from mosamatic2.core.tasks.task import Task
|
|
3
|
+
from mosamatic2.core.managers.logmanager import LogManager
|
|
4
|
+
from mosamatic2.core.data.multidicomimage import MultiDicomImage
|
|
5
|
+
from mosamatic2.core.utils import (
|
|
6
|
+
convert_numpy_array_to_png_image,
|
|
7
|
+
convert_muscle_mask_to_myosteatosis_map,
|
|
8
|
+
get_pixels_from_dicom_object,
|
|
9
|
+
AlbertaColorMap,
|
|
10
|
+
load_numpy_array,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
LOG = LogManager()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CreatePngsFromSegmentationsTask(Task):
|
|
17
|
+
INPUTS = ['images', 'segmentations']
|
|
18
|
+
PARAMS = [
|
|
19
|
+
'fig_width',
|
|
20
|
+
'fig_height',
|
|
21
|
+
'hu_low',
|
|
22
|
+
'hu_high',
|
|
23
|
+
'alpha',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
def __init__(self, inputs, params, output, overwrite=True):
|
|
27
|
+
super(CreatePngsFromSegmentationsTask, self).__init__(inputs, params, output, overwrite)
|
|
28
|
+
|
|
29
|
+
def load_images(self):
|
|
30
|
+
image_data = MultiDicomImage()
|
|
31
|
+
image_data.set_path(self.input('images'))
|
|
32
|
+
if image_data.load():
|
|
33
|
+
return image_data
|
|
34
|
+
raise RuntimeError('Could not load images')
|
|
35
|
+
|
|
36
|
+
def load_segmentations(self):
|
|
37
|
+
segmentations = []
|
|
38
|
+
for f in os.listdir(self.input('segmentations')):
|
|
39
|
+
f_path = os.path.join(self.input('segmentations'), f)
|
|
40
|
+
if f.endswith('.seg.npy'):
|
|
41
|
+
segmentations.append(f_path)
|
|
42
|
+
if len(segmentations) == 0:
|
|
43
|
+
raise RuntimeError('Input directory has no segmentation files')
|
|
44
|
+
return segmentations
|
|
45
|
+
|
|
46
|
+
def collect_img_seg_pairs(self, images, segmentations, file_type='npy'):
|
|
47
|
+
file_type = '.tag' if file_type == 'tag' else '.seg.npy'
|
|
48
|
+
img_seg_pairs = []
|
|
49
|
+
for image in images:
|
|
50
|
+
f_img_path = image.path()
|
|
51
|
+
f_img_name = os.path.split(f_img_path)[1]
|
|
52
|
+
for f_seg_path in segmentations:
|
|
53
|
+
f_seg_name = os.path.split(f_seg_path)[1]
|
|
54
|
+
if file_type == '.seg.npy':
|
|
55
|
+
f_seg_name = f_seg_name.removesuffix(file_type)
|
|
56
|
+
if f_seg_name == f_img_name:
|
|
57
|
+
img_seg_pairs.append((image, f_seg_path))
|
|
58
|
+
elif file_type == '.tag':
|
|
59
|
+
f_seg_name = f_seg_name.removesuffix(file_type).removesuffix('.dcm')
|
|
60
|
+
f_img_name = f_img_name.removesuffix('.dcm')
|
|
61
|
+
if f_seg_name == f_img_name:
|
|
62
|
+
img_seg_pairs.append((image, f_seg_path))
|
|
63
|
+
else:
|
|
64
|
+
raise RuntimeError('Unknown file type')
|
|
65
|
+
return img_seg_pairs
|
|
66
|
+
|
|
67
|
+
def run(self):
|
|
68
|
+
images = self.load_images()
|
|
69
|
+
segmentations = self.load_segmentations()
|
|
70
|
+
img_seg_pairs = self.collect_img_seg_pairs(images.images(), segmentations, 'npy')
|
|
71
|
+
# nr_steps = len(segmentations)
|
|
72
|
+
nr_steps = len(img_seg_pairs)
|
|
73
|
+
for step in range(nr_steps):
|
|
74
|
+
segmentation = img_seg_pairs[step][1]
|
|
75
|
+
segmentation_name = os.path.split(segmentation)[1]
|
|
76
|
+
segmentation_image = load_numpy_array(segmentation)
|
|
77
|
+
if segmentation_image is not None:
|
|
78
|
+
png_file_name = segmentation_name + '.png'
|
|
79
|
+
convert_numpy_array_to_png_image(
|
|
80
|
+
segmentation_image,
|
|
81
|
+
self.output(),
|
|
82
|
+
AlbertaColorMap(),
|
|
83
|
+
png_file_name,
|
|
84
|
+
fig_width=int(self.param('fig_width')),
|
|
85
|
+
fig_height=int(self.param('fig_height')),
|
|
86
|
+
)
|
|
87
|
+
image = img_seg_pairs[step][0]
|
|
88
|
+
image = get_pixels_from_dicom_object(image.object(), normalize=True)
|
|
89
|
+
png_file_name = segmentation_name + '_myosteatosis.png'
|
|
90
|
+
convert_muscle_mask_to_myosteatosis_map(
|
|
91
|
+
image,
|
|
92
|
+
segmentation_image,
|
|
93
|
+
self.output(),
|
|
94
|
+
png_file_name,
|
|
95
|
+
hu_low=self.param('hu_low'),
|
|
96
|
+
hu_high=self.param('hu_high'),
|
|
97
|
+
alpha=self.param('alpha'),
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
LOG.warning(f'File {segmentation} is not a valid NumPy array file')
|
|
101
|
+
self.set_progress(step, nr_steps)
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import dicom2nifti
|
|
3
|
+
from mosamatic2.core.tasks.task import Task
|
|
4
|
+
from mosamatic2.core.managers.logmanager import LogManager
|
|
5
|
+
from mosamatic2.core.utils import is_dicom
|
|
6
|
+
|
|
7
|
+
LOG = LogManager()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Dicom2NiftiTask(Task):
|
|
11
|
+
"""
|
|
12
|
+
This task converts DICOM scans to NIFTI. Each scan should be stored in a separate
|
|
13
|
+
directory (scan_dir). This task does not convert a single DICOM image!!
|
|
14
|
+
"""
|
|
15
|
+
INPUTS = ['scans']
|
|
16
|
+
PARAMS = ['compressed']
|
|
17
|
+
|
|
18
|
+
def __init__(self, inputs, params, output, overwrite):
|
|
19
|
+
super(Dicom2NiftiTask, self).__init__(inputs, params, output, overwrite)
|
|
20
|
+
|
|
21
|
+
def load_scan_dirs(self):
|
|
22
|
+
scan_dirs = []
|
|
23
|
+
for d in os.listdir(self.input('scans')):
|
|
24
|
+
scan_dir = os.path.join(self.input('scans'), d)
|
|
25
|
+
if os.path.isdir(scan_dir):
|
|
26
|
+
scan_dirs.append(scan_dir)
|
|
27
|
+
return scan_dirs
|
|
28
|
+
|
|
29
|
+
def run(self):
|
|
30
|
+
scan_dirs = self.load_scan_dirs()
|
|
31
|
+
nr_steps = len(scan_dirs)
|
|
32
|
+
for step in range(nr_steps):
|
|
33
|
+
scan_dir = scan_dirs[step]
|
|
34
|
+
scan_name = os.path.split(scan_dir)[1]
|
|
35
|
+
if self.param('compressed'):
|
|
36
|
+
nifti_file_name = scan_name + '.nii.gz'
|
|
37
|
+
else:
|
|
38
|
+
nifti_file_name = scan_name + '.nii'
|
|
39
|
+
LOG.info(f'Converting DICOM series in {scan_dir} to {nifti_file_name}')
|
|
40
|
+
dicom2nifti.dicom_series_to_nifti(
|
|
41
|
+
scan_dir,
|
|
42
|
+
os.path.join(self.output(), nifti_file_name),
|
|
43
|
+
reorient_nifti=True,
|
|
44
|
+
)
|
|
45
|
+
self.set_progress(step, nr_steps)
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import numpy as np
|
|
4
|
+
from mosamatic2.core.tasks.task import Task
|
|
5
|
+
from mosamatic2.core.managers.logmanager import LogManager
|
|
6
|
+
from mosamatic2.core.data.multidicomimage import MultiDicomImage
|
|
7
|
+
from mosamatic2.core.data.dicomimage import DicomImage
|
|
8
|
+
from mosamatic2.core.data.multidicomimage import MultiDicomImage
|
|
9
|
+
from scipy.ndimage import zoom
|
|
10
|
+
|
|
11
|
+
LOG = LogManager()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RescaleDicomImagesTask(Task):
|
|
15
|
+
INPUTS = ['images']
|
|
16
|
+
PARAMS = ['target_size']
|
|
17
|
+
|
|
18
|
+
def __init__(self, inputs, params, output=None, overwrite=True):
|
|
19
|
+
super(RescaleDicomImagesTask, self).__init__(inputs, params, output, overwrite)
|
|
20
|
+
|
|
21
|
+
def rescale_image(self, p, target_size):
|
|
22
|
+
pixel_array = p.pixel_array
|
|
23
|
+
hu_array = pixel_array * p.RescaleSlope + p.RescaleIntercept
|
|
24
|
+
hu_air = -1000
|
|
25
|
+
new_rows = max(p.Rows, p.Columns)
|
|
26
|
+
new_cols = max(p.Rows, p.Columns)
|
|
27
|
+
padded_hu_array = np.full((new_rows, new_cols), hu_air, dtype=hu_array.dtype)
|
|
28
|
+
padded_hu_array[:pixel_array.shape[0], :pixel_array.shape[1]] = hu_array
|
|
29
|
+
pixel_array_padded = (padded_hu_array - p.RescaleIntercept) / p.RescaleSlope
|
|
30
|
+
pixel_array_padded = pixel_array_padded.astype(pixel_array.dtype) # Image now has largest dimensions
|
|
31
|
+
pixel_array_rescaled = zoom(pixel_array_padded, zoom=(target_size / new_rows), order=3) # Cubic interpolation
|
|
32
|
+
pixel_array_rescaled = pixel_array_rescaled.astype(pixel_array.dtype)
|
|
33
|
+
original_pixel_spacing = p.PixelSpacing
|
|
34
|
+
new_pixel_spacing = [ps * (new_rows / target_size) for ps in original_pixel_spacing]
|
|
35
|
+
p.PixelSpacing = new_pixel_spacing
|
|
36
|
+
p.PixelData = pixel_array_rescaled.tobytes()
|
|
37
|
+
p.Rows = target_size
|
|
38
|
+
p.Columns = target_size
|
|
39
|
+
return p
|
|
40
|
+
|
|
41
|
+
def run(self):
|
|
42
|
+
image_data = MultiDicomImage()
|
|
43
|
+
image_data.set_path(self.input('images'))
|
|
44
|
+
if image_data.load():
|
|
45
|
+
images = image_data.images()
|
|
46
|
+
nr_steps = len(images)
|
|
47
|
+
for step in range(nr_steps):
|
|
48
|
+
source = images[step]
|
|
49
|
+
assert isinstance(source, DicomImage)
|
|
50
|
+
p = source.object()
|
|
51
|
+
if len(p.pixel_array.shape) == 2:
|
|
52
|
+
source_name = os.path.split(source.path())[1]
|
|
53
|
+
if p.Rows != self.param('target_size') or p.Columns != self.param('target_size'):
|
|
54
|
+
p = self.rescale_image(p, self.param('target_size'))
|
|
55
|
+
target = os.path.join(self.output(), source_name)
|
|
56
|
+
p.save_as(target)
|
|
57
|
+
else:
|
|
58
|
+
target = os.path.join(self.output(), source_name)
|
|
59
|
+
shutil.copy(source.path(), target)
|
|
60
|
+
else:
|
|
61
|
+
LOG.warning(f'Shape of pixel data in file {source.path()} should be 2D but is {len(p.pixel_array.shape)}D')
|
|
62
|
+
self.set_progress(step, nr_steps)
|
|
63
|
+
else:
|
|
64
|
+
LOG.error('Error loading multi-DICOM image data')
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import nibabel as nib
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from mosamatic2.core.tasks.task import Task
|
|
6
|
+
from mosamatic2.core.managers.logmanager import LogManager
|
|
7
|
+
from mosamatic2.core.utils import (
|
|
8
|
+
convert_numpy_array_to_png_image,
|
|
9
|
+
AlbertaColorMap,
|
|
10
|
+
)
|
|
11
|
+
LOG = LogManager()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SegmentationNifti2NumpyTask(Task):
|
|
15
|
+
INPUTS = ['segmentations']
|
|
16
|
+
PARAMS = ['png']
|
|
17
|
+
|
|
18
|
+
def __init__(self, inputs, params, output, overwrite):
|
|
19
|
+
super(SegmentationNifti2NumpyTask, self).__init__(inputs, params, output, overwrite)
|
|
20
|
+
|
|
21
|
+
def load_segmentations(self):
|
|
22
|
+
segmentations = []
|
|
23
|
+
for f in os.listdir(self.input('segmentations')):
|
|
24
|
+
if f.endswith('.seg.npy.nii.gz'):
|
|
25
|
+
f_path = os.path.join(self.input('segmentations'), f)
|
|
26
|
+
segmentations.append(f_path)
|
|
27
|
+
return segmentations
|
|
28
|
+
|
|
29
|
+
def load_segmentation_as_nifti(self, segmentation):
|
|
30
|
+
nifti = nib.load(segmentation)
|
|
31
|
+
narray = np.asanyarray(nifti.dataobj)
|
|
32
|
+
narray = narray[..., 0]
|
|
33
|
+
narray = np.rot90(narray, k=3, axes=(0, 1))
|
|
34
|
+
return narray
|
|
35
|
+
|
|
36
|
+
def create_png_from_array(self, data, file_path):
|
|
37
|
+
png_file_name = os.path.split(file_path)[1] + '.png'
|
|
38
|
+
convert_numpy_array_to_png_image(
|
|
39
|
+
data,
|
|
40
|
+
self.output(),
|
|
41
|
+
AlbertaColorMap(),
|
|
42
|
+
png_file_name,
|
|
43
|
+
fig_width=10, fig_height=10,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def run(self):
|
|
47
|
+
segmentations = self.load_segmentations()
|
|
48
|
+
nr_steps = len(segmentations)
|
|
49
|
+
for step in range(nr_steps):
|
|
50
|
+
segmentation = segmentations[step]
|
|
51
|
+
segmentation_narray = self.load_segmentation_as_nifti(segmentation)
|
|
52
|
+
segmentation_narray_name = os.path.split(segmentation)[1][:-7]
|
|
53
|
+
segmentation_narray_path = os.path.join(self.output(), segmentation_narray_name)
|
|
54
|
+
np.save(segmentation_narray_path, segmentation_narray)
|
|
55
|
+
if self.param('png'):
|
|
56
|
+
self.create_png_from_array(segmentation_narray, segmentation_narray_path)
|
|
57
|
+
self.set_progress(step, nr_steps)
|
|
File without changes
|