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.
Files changed (136) hide show
  1. models.py +259 -0
  2. mosamatic2/__init__.py +0 -0
  3. mosamatic2/app.py +32 -0
  4. mosamatic2/cli.py +50 -0
  5. mosamatic2/commands/__init__.py +0 -0
  6. mosamatic2/commands/boadockerpipeline.py +48 -0
  7. mosamatic2/commands/calculatemaskstatistics.py +59 -0
  8. mosamatic2/commands/calculatescores.py +73 -0
  9. mosamatic2/commands/createdicomsummary.py +61 -0
  10. mosamatic2/commands/createpngsfromsegmentations.py +65 -0
  11. mosamatic2/commands/defaultdockerpipeline.py +84 -0
  12. mosamatic2/commands/defaultpipeline.py +70 -0
  13. mosamatic2/commands/dicom2nifti.py +55 -0
  14. mosamatic2/commands/liveranalysispipeline.py +61 -0
  15. mosamatic2/commands/rescaledicomimages.py +54 -0
  16. mosamatic2/commands/segmentmusclefatl3tensorflow.py +55 -0
  17. mosamatic2/commands/selectslicefromscans.py +66 -0
  18. mosamatic2/commands/totalsegmentator.py +77 -0
  19. mosamatic2/constants.py +27 -0
  20. mosamatic2/core/__init__.py +0 -0
  21. mosamatic2/core/data/__init__.py +5 -0
  22. mosamatic2/core/data/dicomimage.py +27 -0
  23. mosamatic2/core/data/dicomimageseries.py +26 -0
  24. mosamatic2/core/data/dixonseries.py +22 -0
  25. mosamatic2/core/data/filedata.py +26 -0
  26. mosamatic2/core/data/multidicomimage.py +30 -0
  27. mosamatic2/core/data/multiniftiimage.py +26 -0
  28. mosamatic2/core/data/multinumpyimage.py +26 -0
  29. mosamatic2/core/data/niftiimage.py +13 -0
  30. mosamatic2/core/data/numpyimage.py +13 -0
  31. mosamatic2/core/managers/__init__.py +0 -0
  32. mosamatic2/core/managers/logmanager.py +45 -0
  33. mosamatic2/core/managers/logmanagerlistener.py +3 -0
  34. mosamatic2/core/pipelines/__init__.py +4 -0
  35. mosamatic2/core/pipelines/boadockerpipeline/__init__.py +0 -0
  36. mosamatic2/core/pipelines/boadockerpipeline/boadockerpipeline.py +70 -0
  37. mosamatic2/core/pipelines/defaultdockerpipeline/__init__.py +0 -0
  38. mosamatic2/core/pipelines/defaultdockerpipeline/defaultdockerpipeline.py +28 -0
  39. mosamatic2/core/pipelines/defaultpipeline/__init__.py +0 -0
  40. mosamatic2/core/pipelines/defaultpipeline/defaultpipeline.py +90 -0
  41. mosamatic2/core/pipelines/liveranalysispipeline/__init__.py +0 -0
  42. mosamatic2/core/pipelines/liveranalysispipeline/liveranalysispipeline.py +48 -0
  43. mosamatic2/core/pipelines/pipeline.py +14 -0
  44. mosamatic2/core/singleton.py +9 -0
  45. mosamatic2/core/tasks/__init__.py +13 -0
  46. mosamatic2/core/tasks/applythresholdtosegmentationstask/__init__.py +0 -0
  47. mosamatic2/core/tasks/applythresholdtosegmentationstask/applythresholdtosegmentationstask.py +117 -0
  48. mosamatic2/core/tasks/calculatemaskstatisticstask/__init__.py +0 -0
  49. mosamatic2/core/tasks/calculatemaskstatisticstask/calculatemaskstatisticstask.py +104 -0
  50. mosamatic2/core/tasks/calculatescorestask/__init__.py +0 -0
  51. mosamatic2/core/tasks/calculatescorestask/calculatescorestask.py +152 -0
  52. mosamatic2/core/tasks/createdicomsummarytask/__init__.py +0 -0
  53. mosamatic2/core/tasks/createdicomsummarytask/createdicomsummarytask.py +88 -0
  54. mosamatic2/core/tasks/createpngsfromsegmentationstask/__init__.py +0 -0
  55. mosamatic2/core/tasks/createpngsfromsegmentationstask/createpngsfromsegmentationstask.py +101 -0
  56. mosamatic2/core/tasks/dicom2niftitask/__init__.py +0 -0
  57. mosamatic2/core/tasks/dicom2niftitask/dicom2niftitask.py +45 -0
  58. mosamatic2/core/tasks/rescaledicomimagestask/__init__.py +0 -0
  59. mosamatic2/core/tasks/rescaledicomimagestask/rescaledicomimagestask.py +64 -0
  60. mosamatic2/core/tasks/segmentationnifti2numpytask/__init__.py +0 -0
  61. mosamatic2/core/tasks/segmentationnifti2numpytask/segmentationnifti2numpytask.py +57 -0
  62. mosamatic2/core/tasks/segmentationnumpy2niftitask/__init__.py +0 -0
  63. mosamatic2/core/tasks/segmentationnumpy2niftitask/segmentationnumpy2niftitask.py +86 -0
  64. mosamatic2/core/tasks/segmentmusclefatl3tensorflowtask/__init__.py +0 -0
  65. mosamatic2/core/tasks/segmentmusclefatl3tensorflowtask/paramloader.py +39 -0
  66. mosamatic2/core/tasks/segmentmusclefatl3tensorflowtask/segmentmusclefatl3tensorflowtask.py +122 -0
  67. mosamatic2/core/tasks/segmentmusclefatt4pytorchtask/__init__.py +0 -0
  68. mosamatic2/core/tasks/segmentmusclefatt4pytorchtask/paramloader.py +39 -0
  69. mosamatic2/core/tasks/segmentmusclefatt4pytorchtask/segmentmusclefatt4pytorchtask.py +128 -0
  70. mosamatic2/core/tasks/selectslicefromscanstask/__init__.py +0 -0
  71. mosamatic2/core/tasks/selectslicefromscanstask/selectslicefromscanstask.py +249 -0
  72. mosamatic2/core/tasks/task.py +50 -0
  73. mosamatic2/core/tasks/totalsegmentatortask/__init__.py +0 -0
  74. mosamatic2/core/tasks/totalsegmentatortask/totalsegmentatortask.py +75 -0
  75. mosamatic2/core/utils.py +405 -0
  76. mosamatic2/server.py +146 -0
  77. mosamatic2/ui/__init__.py +0 -0
  78. mosamatic2/ui/mainwindow.py +426 -0
  79. mosamatic2/ui/resources/VERSION +1 -0
  80. mosamatic2/ui/resources/icons/mosamatic2.icns +0 -0
  81. mosamatic2/ui/resources/icons/mosamatic2.ico +0 -0
  82. mosamatic2/ui/resources/icons/spinner.gif +0 -0
  83. mosamatic2/ui/resources/images/body-composition.jpg +0 -0
  84. mosamatic2/ui/settings.py +62 -0
  85. mosamatic2/ui/utils.py +36 -0
  86. mosamatic2/ui/widgets/__init__.py +0 -0
  87. mosamatic2/ui/widgets/dialogs/__init__.py +0 -0
  88. mosamatic2/ui/widgets/dialogs/dialog.py +16 -0
  89. mosamatic2/ui/widgets/dialogs/helpdialog.py +9 -0
  90. mosamatic2/ui/widgets/panels/__init__.py +0 -0
  91. mosamatic2/ui/widgets/panels/defaultpanel.py +31 -0
  92. mosamatic2/ui/widgets/panels/logpanel.py +65 -0
  93. mosamatic2/ui/widgets/panels/mainpanel.py +82 -0
  94. mosamatic2/ui/widgets/panels/pipelines/__init__.py +0 -0
  95. mosamatic2/ui/widgets/panels/pipelines/boadockerpipelinepanel.py +195 -0
  96. mosamatic2/ui/widgets/panels/pipelines/defaultdockerpipelinepanel.py +314 -0
  97. mosamatic2/ui/widgets/panels/pipelines/defaultpipelinepanel.py +302 -0
  98. mosamatic2/ui/widgets/panels/pipelines/liveranalysispipelinepanel.py +187 -0
  99. mosamatic2/ui/widgets/panels/pipelines/pipelinepanel.py +6 -0
  100. mosamatic2/ui/widgets/panels/settingspanel.py +16 -0
  101. mosamatic2/ui/widgets/panels/stackedpanel.py +22 -0
  102. mosamatic2/ui/widgets/panels/tasks/__init__.py +0 -0
  103. mosamatic2/ui/widgets/panels/tasks/applythresholdtosegmentationstaskpanel.py +271 -0
  104. mosamatic2/ui/widgets/panels/tasks/calculatemaskstatisticstaskpanel.py +215 -0
  105. mosamatic2/ui/widgets/panels/tasks/calculatescorestaskpanel.py +238 -0
  106. mosamatic2/ui/widgets/panels/tasks/createdicomsummarytaskpanel.py +206 -0
  107. mosamatic2/ui/widgets/panels/tasks/createpngsfromsegmentationstaskpanel.py +247 -0
  108. mosamatic2/ui/widgets/panels/tasks/dicom2niftitaskpanel.py +183 -0
  109. mosamatic2/ui/widgets/panels/tasks/rescaledicomimagestaskpanel.py +184 -0
  110. mosamatic2/ui/widgets/panels/tasks/segmentationnifti2numpytaskpanel.py +192 -0
  111. mosamatic2/ui/widgets/panels/tasks/segmentationnumpy2niftitaskpanel.py +213 -0
  112. mosamatic2/ui/widgets/panels/tasks/segmentmusclefatl3tensorflowtaskpanel.py +216 -0
  113. mosamatic2/ui/widgets/panels/tasks/segmentmusclefatt4pytorchtaskpanel.py +217 -0
  114. mosamatic2/ui/widgets/panels/tasks/selectslicefromscanstaskpanel.py +193 -0
  115. mosamatic2/ui/widgets/panels/tasks/taskpanel.py +6 -0
  116. mosamatic2/ui/widgets/panels/tasks/totalsegmentatortaskpanel.py +195 -0
  117. mosamatic2/ui/widgets/panels/visualizations/__init__.py +0 -0
  118. mosamatic2/ui/widgets/panels/visualizations/liversegmentvisualization/__init__.py +0 -0
  119. mosamatic2/ui/widgets/panels/visualizations/liversegmentvisualization/liversegmentpicker.py +96 -0
  120. mosamatic2/ui/widgets/panels/visualizations/liversegmentvisualization/liversegmentviewer.py +130 -0
  121. mosamatic2/ui/widgets/panels/visualizations/liversegmentvisualization/liversegmentvisualization.py +120 -0
  122. mosamatic2/ui/widgets/panels/visualizations/sliceselectionvisualization/__init__.py +0 -0
  123. mosamatic2/ui/widgets/panels/visualizations/sliceselectionvisualization/sliceselectionviewer.py +61 -0
  124. mosamatic2/ui/widgets/panels/visualizations/sliceselectionvisualization/sliceselectionvisualization.py +133 -0
  125. mosamatic2/ui/widgets/panels/visualizations/sliceselectionvisualization/slicetile.py +63 -0
  126. mosamatic2/ui/widgets/panels/visualizations/slicevisualization/__init__.py +0 -0
  127. mosamatic2/ui/widgets/panels/visualizations/slicevisualization/custominteractorstyle.py +80 -0
  128. mosamatic2/ui/widgets/panels/visualizations/slicevisualization/sliceviewer.py +116 -0
  129. mosamatic2/ui/widgets/panels/visualizations/slicevisualization/slicevisualization.py +141 -0
  130. mosamatic2/ui/widgets/panels/visualizations/visualization.py +6 -0
  131. mosamatic2/ui/widgets/splashscreen.py +101 -0
  132. mosamatic2/ui/worker.py +29 -0
  133. mosamatic2-2.0.24.dist-info/METADATA +43 -0
  134. mosamatic2-2.0.24.dist-info/RECORD +136 -0
  135. mosamatic2-2.0.24.dist-info/WHEEL +4 -0
  136. mosamatic2-2.0.24.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,50 @@
1
+ import os
2
+ import shutil
3
+ from mosamatic2.core.managers.logmanager import LogManager
4
+ from mosamatic2.core.utils import create_name_with_timestamp, mosamatic_output_dir
5
+
6
+ LOG = LogManager()
7
+
8
+
9
+ class Task:
10
+ INPUTS = []
11
+ PARAMS = []
12
+ OUTPUT = 'output'
13
+
14
+ def __init__(self, inputs, params, output, overwrite=True):
15
+ self._inputs = inputs
16
+ self._params = params
17
+ self._output = os.path.join(output, self.__class__.__name__.lower())
18
+ self._overwrite = overwrite
19
+ if self._overwrite and os.path.isdir(self._output):
20
+ shutil.rmtree(self._output)
21
+ os.makedirs(self._output, exist_ok=self._overwrite)
22
+ # Check that the inputs match specification and type
23
+ assert isinstance(self._inputs, dict)
24
+ assert len(self._inputs.keys()) == len(self.__class__.INPUTS)
25
+ for k, v in self._inputs.items():
26
+ assert k in self.__class__.INPUTS
27
+ assert isinstance(v, str)
28
+ # Check that param names match specification (if not None)
29
+ if self._params:
30
+ assert len(self._params.keys()) == len(self.__class__.PARAMS)
31
+ for k in self._params.keys():
32
+ assert k in self.__class__.PARAMS
33
+
34
+ def input(self, name):
35
+ return self._inputs[name]
36
+
37
+ def param(self, name):
38
+ return self._params[name]
39
+
40
+ def output(self):
41
+ return self._output
42
+
43
+ def overwrite(self):
44
+ return self._overwrite
45
+
46
+ def set_progress(self, step, nr_steps):
47
+ LOG.info(f'[{self.__class__.__name__}] step {step} from {nr_steps}')
48
+
49
+ def run(self):
50
+ raise NotImplementedError()
File without changes
@@ -0,0 +1,75 @@
1
+ import os
2
+ import shutil
3
+ import tempfile
4
+ from totalsegmentator.python_api import totalsegmentator
5
+ from mosamatic2.core.tasks.task import Task
6
+ from mosamatic2.core.managers.logmanager import LogManager
7
+
8
+ LOG = LogManager()
9
+ TOTAL_SEGMENTATOR_OUTPUT_DIR = os.path.join(tempfile.gettempdir(), 'total_segmentator_output')
10
+
11
+
12
+ class TotalSegmentatorTask(Task):
13
+ INPUTS = ['scans']
14
+ PARAMS = ['tasks', 'format']
15
+
16
+ def __init__(self, inputs, params, output, overwrite):
17
+ super(TotalSegmentatorTask, self).__init__(inputs, params, output, overwrite)
18
+ LOG.info(f'Using temporary output directory: {TOTAL_SEGMENTATOR_OUTPUT_DIR}')
19
+
20
+ def load_scan_dirs(self):
21
+ scan_dirs = []
22
+ for d in os.listdir(self.input('scans')):
23
+ scan_dir = os.path.join(self.input('scans'), d)
24
+ if os.path.isdir(scan_dir):
25
+ scan_dirs.append(scan_dir)
26
+ return scan_dirs
27
+
28
+ def load_scans(self):
29
+ scans = []
30
+ for f in os.listdir(self.input('scans')):
31
+ if f.endswith('.nii.gz'):
32
+ scan = os.path.join(self.input('scans'), f)
33
+ if os.path.isfile(scan):
34
+ scans.append(scan)
35
+ return scans
36
+
37
+ def extract_masks(self, scan_dir_or_file):
38
+ os.makedirs(TOTAL_SEGMENTATOR_OUTPUT_DIR, exist_ok=True)
39
+ tasks = self.param('tasks').split(",") if self.param('tasks') else []
40
+ for task in tasks:
41
+ LOG.info(f'Running task {task}...')
42
+ totalsegmentator(input=scan_dir_or_file, output=TOTAL_SEGMENTATOR_OUTPUT_DIR, task=task)
43
+
44
+ def delete_total_segmentator_output(self):
45
+ if os.path.exists(TOTAL_SEGMENTATOR_OUTPUT_DIR):
46
+ shutil.rmtree(TOTAL_SEGMENTATOR_OUTPUT_DIR)
47
+
48
+ def run(self):
49
+ if self.param('format') == 'dicom':
50
+ scan_dirs_or_files = self.load_scan_dirs()
51
+ elif self.param('format') == 'nifti':
52
+ scan_dirs_or_files = self.load_scans()
53
+ else:
54
+ LOG.error('Unknown format: {}'.format(self.param('format')))
55
+ return
56
+ nr_steps = len(scan_dirs_or_files)
57
+ for step in range(nr_steps):
58
+ scan_dir_or_file = scan_dirs_or_files[step]
59
+ scan_dir_or_file_name = os.path.split(scan_dir_or_file)[1]
60
+ if self.param('format') == 'nifti':
61
+ scan_dir_or_file_name = scan_dir_or_file_name[:-7]
62
+ try:
63
+ self.extract_masks(scan_dir_or_file)
64
+ except Exception as e:
65
+ LOG.error(f'{scan_dir_or_file}: Could not extract masks [{str(e)}]. Skipping scan...')
66
+ self.set_progress(step, nr_steps)
67
+ LOG.info(f'Copying temporary output to final output directory...')
68
+ for f in os.listdir(TOTAL_SEGMENTATOR_OUTPUT_DIR):
69
+ if f.endswith('.nii') or f.endswith('.nii.gz'):
70
+ f_path = os.path.join(TOTAL_SEGMENTATOR_OUTPUT_DIR, f)
71
+ target_file = os.path.join(self.output(), f'{scan_dir_or_file_name}_{f}')
72
+ shutil.copyfile(f_path, target_file)
73
+ LOG.info(f'Copied {f} to {target_file}')
74
+ LOG.info('Cleaning up Total Segmentator temporary output...')
75
+ self.delete_total_segmentator_output()
@@ -0,0 +1,405 @@
1
+ import time
2
+ import os
3
+ import click
4
+ import textwrap
5
+ import math
6
+ import pendulum
7
+ import numpy as np
8
+ import nibabel as nb
9
+ import struct
10
+ import binascii
11
+ import pydicom
12
+ import warnings
13
+ from pathlib import Path
14
+ from pydicom.uid import (
15
+ ExplicitVRLittleEndian, ImplicitVRLittleEndian, ExplicitVRBigEndian
16
+ )
17
+ from PIL import Image
18
+ from mosamatic2.core.managers.logmanager import LogManager
19
+
20
+ warnings.filterwarnings("ignore", message="Invalid value for VR UI:", category=UserWarning)
21
+
22
+ MUSCLE, VAT, SAT = 1, 5, 7
23
+ LOG = LogManager()
24
+
25
+
26
+ def create_name_with_timestamp(prefix: str='') -> str:
27
+ tz = pendulum.local_timezone()
28
+ timestamp = pendulum.now(tz).strftime('%Y%m%d%H%M%S%f')[:17]
29
+ if prefix != '' and not prefix.endswith('-'):
30
+ prefix = prefix + '-'
31
+ name = f'{prefix}{timestamp}'
32
+ return name
33
+
34
+
35
+ def show_doc_command(cli_group: click.Group) -> click.Command:
36
+ @click.command(name="showdoc")
37
+ @click.argument("command_name", required=False)
38
+ def show_doc(command_name):
39
+ commands = cli_group.commands
40
+ if command_name:
41
+ cmd = commands.get(command_name)
42
+ if cmd and hasattr(cmd, 'callback') and cmd.callback.__doc__:
43
+ print()
44
+ print(textwrap.dedent(cmd.callback.__doc__).strip())
45
+ else:
46
+ click.echo(f'No docstring found for command: {command_name}')
47
+ else:
48
+ click.echo('Available commands with docstrings:')
49
+ for name, cmd in commands.items():
50
+ if hasattr(cmd, 'callback') and cmd.callback.__doc__:
51
+ click.echo(f" {name}")
52
+ click.echo('\nUse: `mosamatic show-doc <command>` to view a commands docstring')
53
+ return show_doc
54
+
55
+
56
+ def home_dir():
57
+ return Path.home()
58
+
59
+
60
+ def mosamatic_dir():
61
+ d = os.path.join(home_dir(), '.mosamatic2')
62
+ os.makedirs(d, exist_ok=True)
63
+ return d
64
+
65
+
66
+ def mosamatic_data_dir():
67
+ data_dir = os.path.join(mosamatic_dir(), 'data')
68
+ os.makedirs(data_dir, exist_ok=True)
69
+ return data_dir
70
+
71
+
72
+ def mosamatic_output_dir():
73
+ output_dir = os.path.join(mosamatic_data_dir(), 'output')
74
+ os.makedirs(output_dir, exist_ok=True)
75
+ return output_dir
76
+
77
+
78
+ def current_time_in_milliseconds():
79
+ return int(round(time.time() * 1000))
80
+
81
+
82
+ def current_time_in_seconds() -> int:
83
+ return int(round(current_time_in_milliseconds() / 1000.0))
84
+
85
+
86
+ def elapsed_time_in_milliseconds(start_time_in_milliseconds):
87
+ return current_time_in_milliseconds() - start_time_in_milliseconds
88
+
89
+
90
+ def elapsed_time_in_seconds(start_time_in_seconds):
91
+ return current_time_in_seconds() - start_time_in_seconds
92
+
93
+
94
+ def duration(seconds):
95
+ h = int(math.floor(seconds/3600.0))
96
+ remainder = seconds - h * 3600
97
+ m = int(math.floor(remainder/60.0))
98
+ remainder = remainder - m * 60
99
+ s = int(math.floor(remainder))
100
+ return '{} hours, {} minutes, {} seconds'.format(h, m, s)
101
+
102
+
103
+ def is_dicom(f):
104
+ try:
105
+ pydicom.dcmread(f, stop_before_pixels=True)
106
+ return True
107
+ except pydicom.errors.InvalidDicomError:
108
+ pass
109
+ return False
110
+
111
+
112
+ def load_dicom(f, stop_before_pixels=False):
113
+ try:
114
+ return pydicom.dcmread(f, stop_before_pixels=stop_before_pixels)
115
+ except pydicom.errors.InvalidDicomError:
116
+ try:
117
+ p = pydicom.dcmread(f, stop_before_pixels=stop_before_pixels, force=True)
118
+ if hasattr(p, 'SOPClassUID'):
119
+ if not hasattr(p.file_meta, 'TransferSyntaxUID'):
120
+ LOG.warning(f'DICOM file {f} does not have FileMetaData/TransferSyntaxUID, trying to fix...')
121
+ p.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian
122
+ return p
123
+ except pydicom.errors.InvalidDicomError:
124
+ pass
125
+ return None
126
+
127
+
128
+ def is_jpeg2000_compressed(p):
129
+ if hasattr(p.file_meta, 'TransferSyntaxUID'):
130
+ return p.file_meta.TransferSyntaxUID not in [ExplicitVRLittleEndian, ImplicitVRLittleEndian, ExplicitVRBigEndian]
131
+ return False
132
+
133
+
134
+ def is_nifti(f):
135
+ return f.endswith('.nii') or f.endswith('.nii.gz')
136
+
137
+
138
+ def load_nifti(f):
139
+ if is_nifti(f):
140
+ return nb.load(f)
141
+ return None
142
+
143
+
144
+ def is_numpy_array(value):
145
+ return isinstance(value, np.array)
146
+
147
+
148
+ def is_numpy(f):
149
+ try:
150
+ np.load(f)
151
+ return True
152
+ except:
153
+ return False
154
+
155
+
156
+ def load_numpy_array(f):
157
+ if is_numpy(f):
158
+ return np.load(f)
159
+ return None
160
+
161
+
162
+ def get_pixels_from_tag_file(tag_file_path):
163
+ f = open(tag_file_path, 'rb')
164
+ f.seek(0)
165
+ byte = f.read(1)
166
+ # Make sure to check the byte-value in Python 3!!
167
+ while byte != b'':
168
+ byte_hex = binascii.hexlify(byte)
169
+ if byte_hex == b'0c':
170
+ break
171
+ byte = f.read(1)
172
+ values = []
173
+ f.read(1)
174
+ while byte != b'':
175
+ v = struct.unpack('b', byte)
176
+ values.append(v)
177
+ byte = f.read(1)
178
+ values = np.asarray(values)
179
+ values = values.astype(np.uint16)
180
+ return values
181
+
182
+
183
+ def get_rescale_params(p):
184
+ rescale_slope = getattr(p, 'RescaleSlope', None)
185
+ rescale_intercept = getattr(p, 'RescaleIntercept', None)
186
+ if rescale_slope is not None and rescale_intercept is not None:
187
+ return rescale_slope, rescale_intercept
188
+ # Try Enhanced DICOM structure
189
+ if 'SharedFunctionalGroupsSequence' in p:
190
+ fg = p.SharedFunctionalGroupsSequence[0]
191
+ if 'PixelValueTransformationSequence' in fg:
192
+ pvt = fg.PixelValueTransformationSequence[0]
193
+ rescale_slope = pvt.get('RescaleSlope', 1)
194
+ rescale_intercept = pvt.get('RescaleIntercept', 0)
195
+ return rescale_slope, rescale_intercept
196
+ return 1, 0
197
+
198
+
199
+ def get_pixels_from_dicom_object(p, normalize=True):
200
+ pixels = p.pixel_array
201
+ if not normalize:
202
+ return pixels
203
+ if normalize is True: # Map pixel values back to original HU values
204
+ rescale_slope, rescale_intercept = get_rescale_params(p)
205
+ return rescale_slope * pixels + rescale_intercept
206
+ if isinstance(normalize, int):
207
+ return (pixels + np.min(pixels)) / (np.max(pixels) - np.min(pixels)) * normalize
208
+ if isinstance(normalize, list):
209
+ return (pixels + np.min(pixels)) / (np.max(pixels) - np.min(pixels)) * normalize[1] + normalize[0]
210
+ return pixels
211
+
212
+
213
+ def convert_labels_to_157(label_image: np.array) -> np.array:
214
+ label_image157 = np.copy(label_image)
215
+ label_image157[label_image157 == 1] = 1
216
+ label_image157[label_image157 == 2] = 5
217
+ label_image157[label_image157 == 3] = 7
218
+ return label_image157
219
+
220
+
221
+ def normalize_between(img: np.array, min_bound: int, max_bound: int) -> np.array:
222
+ img = (img - min_bound) / (max_bound - min_bound)
223
+ # img[img > 1] = 1
224
+ img[img < 0] = 0
225
+ img[img > 1] = 0
226
+ c = (img - np.min(img))
227
+ d = (np.max(img) - np.min(img))
228
+ img = np.divide(c, d, np.zeros_like(c), where=d != 0)
229
+ return img
230
+
231
+
232
+ def apply_window_center_and_width(image: np.array, center: int, width: int) -> np.array:
233
+ image_min = center - width // 2
234
+ image_max = center + width // 2
235
+ windowed_image = np.clip(image, image_min, image_max)
236
+ windowed_image = ((windowed_image - image_min) / (image_max - image_min)) * 255.0
237
+ return windowed_image.astype(np.uint8)
238
+
239
+
240
+ def calculate_area(labels: np.array, label, pixel_spacing) -> float:
241
+ mask = np.copy(labels)
242
+ mask[mask != label] = 0
243
+ mask[mask == label] = 1
244
+ area = np.sum(mask) * (pixel_spacing[0] * pixel_spacing[1]) / 100.0
245
+ return area
246
+
247
+
248
+ def calculate_index(area: float, height: float) -> float:
249
+ return area / (height * height)
250
+
251
+
252
+ def calculate_mean_radiation_attenuation(image: np.array, labels: np.array, label: int) -> float:
253
+ mask = np.copy(labels)
254
+ mask[mask != label] = 0
255
+ mask[mask == label] = 1
256
+ subtracted = image * mask
257
+ mask_sum = np.sum(mask)
258
+ if mask_sum > 0.0:
259
+ mean_radiation_attenuation = np.sum(subtracted) / np.sum(mask)
260
+ else:
261
+ # print('Sum of mask pixels is zero, return zero radiation attenuation')
262
+ mean_radiation_attenuation = 0.0
263
+ return mean_radiation_attenuation
264
+
265
+
266
+ def calculate_lama_percentage(image: np.ndarray, labels: np.ndarray, label: int, threshold: float = 30.0) -> float:
267
+ roi = (labels == label)
268
+ n_roi = int(np.count_nonzero(roi))
269
+ if n_roi == 0:
270
+ return 0.0
271
+ lama = roi & (image < threshold)
272
+ lama_pct = (np.count_nonzero(lama) / n_roi) * 100.0
273
+ return int(lama_pct)
274
+
275
+
276
+ def calculate_dice_score(ground_truth: np.array, prediction: np.array, label: int) -> float:
277
+ numerator = prediction[ground_truth == label]
278
+ numerator[numerator != label] = 0
279
+ n = ground_truth[prediction == label]
280
+ n[n != label] = 0
281
+ if np.sum(numerator) != np.sum(n):
282
+ raise RuntimeError('Mismatch in Dice score calculation!')
283
+ denominator = (np.sum(prediction[prediction == label]) + np.sum(ground_truth[ground_truth == label]))
284
+ dice_score = np.sum(numerator) * 2.0 / denominator
285
+ return dice_score
286
+
287
+
288
+ def convert_dicom_to_numpy_array(dicom_file_path: str, window_level: int=50, window_width: int=400, normalize=True) -> np.array:
289
+ p = pydicom.dcmread(dicom_file_path)
290
+ pixels = p.pixel_array
291
+ pixels = pixels.reshape(p.Rows, p.Columns)
292
+ if normalize:
293
+ b = p.RescaleIntercept
294
+ m = p.RescaleSlope
295
+ pixels = m * pixels + b
296
+ pixels = apply_window_center_and_width(pixels, window_level, window_width)
297
+ return pixels
298
+
299
+
300
+ def convert_dicom_to_png_image(dicom_file_path: str, output_dir_path: str, window_level: int=50, window_width: int=400, normalize=True) -> str:
301
+ array = convert_dicom_to_numpy_array(dicom_file_path, window_level, window_width, normalize)
302
+ convert_numpy_array_to_png_image(
303
+ array,
304
+ output_dir_path,
305
+ None,
306
+ os.path.split(dicom_file_path)[1] + '.png',
307
+ 10, 10,
308
+ )
309
+
310
+
311
+ class ColorMap:
312
+ def __init__(self, name: str) -> None:
313
+ self._name = name
314
+ self._values = []
315
+
316
+ def name(self) -> str:
317
+ return self._name
318
+
319
+ def values(self):
320
+ return self._values
321
+
322
+
323
+ class GrayScaleColorMap(ColorMap):
324
+ def __init__(self) -> None:
325
+ super(GrayScaleColorMap, self).__init__(name='GrayScaleColorMap')
326
+ # Implement your own gray scale map or let NumPy do this more efficiently?
327
+ pass
328
+
329
+ class AlbertaColorMap(ColorMap):
330
+ def __init__(self) -> None:
331
+ super(AlbertaColorMap, self).__init__(name='AlbertaColorMap')
332
+ for i in range(256):
333
+ if i == 1: # muscle
334
+ self.values().append([255, 0, 0])
335
+ elif i == 2: # inter-muscular adipose tissue
336
+ self.values().append([0, 255, 0])
337
+ elif i == 5: # visceral adipose tissue
338
+ self.values().append([255, 255, 0])
339
+ elif i == 7: # subcutaneous adipose tissue
340
+ self.values().append([0, 255, 255])
341
+ elif i == 12: # unknown
342
+ self.values().append([0, 0, 255])
343
+ else:
344
+ self.values().append([0, 0, 0])
345
+
346
+
347
+ def apply_color_map(pixels: np.array, color_map: ColorMap) -> np.array:
348
+ pixels_new = np.zeros((*pixels.shape, 3), dtype=np.uint8)
349
+ np.take(color_map.values(), pixels, axis=0, out=pixels_new)
350
+ return pixels_new
351
+
352
+
353
+ def convert_numpy_array_to_png_image(
354
+ numpy_array_file_path_or_object: str, output_dir_path: str, color_map: ColorMap=None, png_file_name: str=None, fig_width: int=10, fig_height: int=10) -> str:
355
+ if isinstance(numpy_array_file_path_or_object, str):
356
+ numpy_array = np.load(numpy_array_file_path_or_object)
357
+ else:
358
+ numpy_array = numpy_array_file_path_or_object
359
+ if not png_file_name:
360
+ raise RuntimeError('PNG file name required for NumPy array object')
361
+ if color_map:
362
+ numpy_array = apply_color_map(pixels=numpy_array, color_map=color_map)
363
+ image = Image.fromarray(numpy_array)
364
+ if not png_file_name:
365
+ numpy_array_file_name = os.path.split(numpy_array_file_path_or_object)[1]
366
+ png_file_name = numpy_array_file_name + '.png'
367
+ elif not png_file_name.endswith('.png'):
368
+ png_file_name += '.png'
369
+ png_file_path = os.path.join(output_dir_path, png_file_name)
370
+ image.save(png_file_path)
371
+ return png_file_path
372
+
373
+
374
+ def convert_muscle_mask_to_myosteatosis_map(hu: np.array, mask: np.array, output_dir: str, png_file_name: str, hu_low: int = 30, hu_high: int = 200, alpha: float = 1.0) -> str:
375
+ muscle_mask = (mask == 1)
376
+ red = muscle_mask & (hu >= hu_low) & (hu <= hu_high)
377
+ yellow = muscle_mask & (hu < hu_low)
378
+ overlay = np.zeros((*hu.shape, 4), dtype=np.float32)
379
+ overlay[..., 3] = 1.0
380
+ overlay[red] = (1.0, 0.0, 0.0, alpha)
381
+ overlay[yellow] = (1.0, 1.0, 0.0, alpha)
382
+ overlay_u8 = (np.clip(overlay, 0.0, 1.0) * 255).astype(np.uint8)
383
+ png_file_path = os.path.join(output_dir, png_file_name)
384
+ # image = Image.fromarray(overlay)
385
+ image = Image.fromarray(overlay_u8, mode="RGBA")
386
+ image.save(png_file_path)
387
+ return overlay
388
+
389
+
390
+ def is_docker_running():
391
+ import docker
392
+ try:
393
+ client = docker.from_env()
394
+ client.ping()
395
+ return True
396
+ except Exception:
397
+ return False
398
+
399
+
400
+ def is_path_docker_compatible(path):
401
+ return not ' ' in path
402
+
403
+
404
+ def to_unix_path(path):
405
+ return path.replace("\\", "/").replace(" ", "\\ ")
mosamatic2/server.py ADDED
@@ -0,0 +1,146 @@
1
+ import argparse
2
+ import mosamatic2.constants as constants
3
+ from flask import Flask, request
4
+ from mosamatic2.core.tasks import RescaleDicomImagesTask
5
+ from mosamatic2.core.tasks import SegmentMuscleFatL3TensorFlowTask
6
+ from mosamatic2.core.tasks import CalculateScoresTask
7
+ from mosamatic2.core.tasks import CreatePngsFromSegmentationsTask
8
+ from mosamatic2.core.tasks import Dicom2NiftiTask
9
+ from mosamatic2.core.tasks import SelectSliceFromScansTask
10
+ from mosamatic2.core.tasks import CreateDicomSummaryTask
11
+
12
+ app = Flask(__name__)
13
+
14
+
15
+ @app.route('/test')
16
+ def run_tests():
17
+ return 'PASSED'
18
+
19
+
20
+ @app.route('/rescaledicomimages')
21
+ def run_rescaledicomimages():
22
+ images = request.args.get('images')
23
+ target_size = request.args.get('target_size', default=512, type=int)
24
+ output = request.args.get('output')
25
+ overwrite = request.args.get('overwrite', default=True, type=bool)
26
+ task = RescaleDicomImagesTask(
27
+ inputs={'images': images},
28
+ params={'target_size': target_size},
29
+ output=output,
30
+ overwrite=overwrite,
31
+ )
32
+ task.run()
33
+ return 'PASSED'
34
+
35
+
36
+ @app.route('/segmentmusclefatl3tensorflow')
37
+ def run_segmentmusclefatl3tensorflow():
38
+ images = request.args.get('images')
39
+ model_files = request.args.get('model_files')
40
+ output = request.args.get('output')
41
+ overwrite = request.args.get('overwrite', default=True, type=bool)
42
+ task = SegmentMuscleFatL3TensorFlowTask(
43
+ inputs={
44
+ 'images': images,
45
+ 'model_files': model_files,
46
+ },
47
+ params={'model_version': 1.0},
48
+ output=output,
49
+ overwrite=overwrite,
50
+ )
51
+ task.run()
52
+ return 'PASSED'
53
+
54
+
55
+ @app.route('/calculatescores')
56
+ def run_calculatescores():
57
+ images = request.args.get('images')
58
+ segmentations = request.args.get('segmentations')
59
+ file_type = request.args.get('file_type', default='npy', type=str)
60
+ output = request.args.get('output')
61
+ overwrite = request.args.get('overwrite', default=True, type=bool)
62
+ task = CalculateScoresTask(
63
+ inputs={
64
+ 'images': images,
65
+ 'segmentations': segmentations,
66
+ },
67
+ params={'file_type': file_type},
68
+ output=output,
69
+ overwrite=overwrite,
70
+ )
71
+ task.run()
72
+ return 'PASSED'
73
+
74
+
75
+ @app.route('/createpngsfromsegmentations')
76
+ def run_createpngsfromsegmentations():
77
+ segmentations = request.args.get('segmentations')
78
+ fig_width = request.args.get('fig_width', default=10, type=int)
79
+ fig_height = request.args.get('fig_height', default=10, type=int)
80
+ output = request.args.get('output')
81
+ overwrite = request.args.get('overwrite', default=True, type=bool)
82
+ task = CreatePngsFromSegmentationsTask(
83
+ inputs={'segmentations': segmentations},
84
+ params={
85
+ 'fig_width': fig_width,
86
+ 'fig_height': fig_height,
87
+ },
88
+ output=output,
89
+ overwrite=overwrite,
90
+ )
91
+ task.run()
92
+ return 'PASSED'
93
+
94
+
95
+ @app.route('/dicom2nifti')
96
+ def run_dicom2nifti():
97
+ images = request.args.get('images')
98
+ output = request.args.get('output')
99
+ overwrite = request.args.get('overwrite', default=True, type=bool)
100
+ task = Dicom2NiftiTask(
101
+ inputs={'images': images},
102
+ params=None,
103
+ output=output,
104
+ overwrite=overwrite,
105
+ )
106
+ task.run()
107
+ return 'PASSED'
108
+
109
+
110
+ @app.route('/selectslicefromscans')
111
+ def run_selectslicefromscans():
112
+ scans = request.args.get('scans')
113
+ vertebra = request.args.get('vertebra')
114
+ output = request.args.get('output')
115
+ overwrite = request.args.get('overwrite', default=True, type=bool)
116
+ task = SelectSliceFromScansTask(
117
+ inputs={'scans': scans},
118
+ params={'vertebra': vertebra},
119
+ output=output,
120
+ overwrite=overwrite,
121
+ )
122
+ task.run()
123
+ return 'PASSED'
124
+
125
+
126
+ @app.route('/createdicomsummary')
127
+ def run_createdicomsummary():
128
+ directory = request.args.get('directory')
129
+ output = request.args.get('output')
130
+ overwrite = request.args.get('overwrite', default=True, type=bool)
131
+ task = CreateDicomSummaryTask(
132
+ inputs={'directory': directory},
133
+ params=None,
134
+ output=output,
135
+ overwrite=overwrite,
136
+ )
137
+ task.run()
138
+ return 'PASSED'
139
+
140
+
141
+ def main():
142
+ parser = argparse.ArgumentParser()
143
+ parser.add_argument('--port', type=int, default=constants.MOSAMATIC2_SERVER_PORT)
144
+ parser.add_argument('--debug', type=bool, default=constants.MOSAMATIC2_SERVER_DEBUG)
145
+ args = parser.parse_args()
146
+ app.run(host='0.0.0.0', port=args.port, debug=args.debug)
File without changes