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,193 @@
1
+ import os
2
+
3
+ from PySide6.QtWidgets import (
4
+ QLineEdit,
5
+ QCheckBox,
6
+ QComboBox,
7
+ QHBoxLayout,
8
+ QVBoxLayout,
9
+ QFormLayout,
10
+ QPushButton,
11
+ QFileDialog,
12
+ QMessageBox,
13
+ )
14
+ from PySide6.QtCore import (
15
+ QThread,
16
+ Slot,
17
+ )
18
+
19
+ from mosamatic2.core.managers.logmanager import LogManager
20
+ from mosamatic2.ui.widgets.panels.tasks.taskpanel import TaskPanel
21
+ from mosamatic2.ui.settings import Settings
22
+ from mosamatic2.ui.utils import is_macos
23
+ from mosamatic2.ui.worker import Worker
24
+ from mosamatic2.core.tasks import SelectSliceFromScansTask
25
+
26
+ LOG = LogManager()
27
+
28
+ PANEL_TITLE = 'SelectSliceFromScansTaskPanel'
29
+ PANEL_NAME = 'selectslicefromscanstaskpanel'
30
+
31
+
32
+ class SelectSliceFromScansTaskPanel(TaskPanel):
33
+ def __init__(self):
34
+ super(SelectSliceFromScansTaskPanel, self).__init__()
35
+ self.set_title(PANEL_TITLE)
36
+ self._scans_dir_line_edit = None
37
+ self._scans_dir_select_button = None
38
+ self._vertebra_combobox = None
39
+ self._output_dir_line_edit = None
40
+ self._output_dir_select_button = None
41
+ self._overwrite_checkbox = None
42
+ self._form_layout = None
43
+ self._run_task_button = None
44
+ self._settings = None
45
+ self._task = None
46
+ self._worker = None
47
+ self._thread = None
48
+ self.init_layout()
49
+
50
+ def scans_dir_line_edit(self):
51
+ if not self._scans_dir_line_edit:
52
+ self._scans_dir_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/scans_dir'))
53
+ return self._scans_dir_line_edit
54
+
55
+ def scans_dir_select_button(self):
56
+ if not self._scans_dir_select_button:
57
+ self._scans_dir_select_button = QPushButton('Select')
58
+ self._scans_dir_select_button.clicked.connect(self.handle_scans_dir_select_button)
59
+ return self._scans_dir_select_button
60
+
61
+ def vertebra_combobox(self):
62
+ if not self._vertebra_combobox:
63
+ self._vertebra_combobox = QComboBox()
64
+ self._vertebra_combobox.addItems(['L3'])
65
+ self._vertebra_combobox.setCurrentText(self.settings().get(f'{PANEL_NAME}/vertebra'))
66
+ return self._vertebra_combobox
67
+
68
+ def output_dir_line_edit(self):
69
+ if not self._output_dir_line_edit:
70
+ self._output_dir_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/output_dir'))
71
+ return self._output_dir_line_edit
72
+
73
+ def output_dir_select_button(self):
74
+ if not self._output_dir_select_button:
75
+ self._output_dir_select_button = QPushButton('Select')
76
+ self._output_dir_select_button.clicked.connect(self.handle_output_dir_select_button)
77
+ return self._output_dir_select_button
78
+
79
+ def overwrite_checkbox(self):
80
+ if not self._overwrite_checkbox:
81
+ self._overwrite_checkbox = QCheckBox('')
82
+ self._overwrite_checkbox.setChecked(self.settings().get_bool(f'{PANEL_NAME}/overwrite', True))
83
+ return self._overwrite_checkbox
84
+
85
+ def form_layout(self):
86
+ if not self._form_layout:
87
+ self._form_layout = QFormLayout()
88
+ if is_macos():
89
+ self._form_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
90
+ return self._form_layout
91
+
92
+ def run_task_button(self):
93
+ if not self._run_task_button:
94
+ self._run_task_button = QPushButton('Run task')
95
+ self._run_task_button.clicked.connect(self.handle_run_task_button)
96
+ return self._run_task_button
97
+
98
+ def settings(self):
99
+ if not self._settings:
100
+ self._settings = Settings()
101
+ return self._settings
102
+
103
+ def init_layout(self):
104
+ scans_dir_layout = QHBoxLayout()
105
+ scans_dir_layout.addWidget(self.scans_dir_line_edit())
106
+ scans_dir_layout.addWidget(self.scans_dir_select_button())
107
+ output_dir_layout = QHBoxLayout()
108
+ output_dir_layout.addWidget(self.output_dir_line_edit())
109
+ output_dir_layout.addWidget(self.output_dir_select_button())
110
+ self.form_layout().addRow('Scans directory', scans_dir_layout)
111
+ # self.form_layout().addRow('Vertebra', self.vertebra_combobox())
112
+ self.form_layout().addRow('Output directory', output_dir_layout)
113
+ self.form_layout().addRow('Overwrite', self.overwrite_checkbox())
114
+ layout = QVBoxLayout()
115
+ layout.addLayout(self.form_layout())
116
+ layout.addWidget(self.run_task_button())
117
+ self.setLayout(layout)
118
+ self.setObjectName(PANEL_NAME)
119
+
120
+ def handle_scans_dir_select_button(self):
121
+ last_directory = self.settings().get('last_directory')
122
+ directory = QFileDialog.getExistingDirectory(dir=last_directory)
123
+ if directory:
124
+ self.scans_dir_line_edit().setText(directory)
125
+ self.settings().set('last_directory', directory)
126
+
127
+ def handle_output_dir_select_button(self):
128
+ last_directory = self.settings().get('last_directory')
129
+ directory = QFileDialog.getExistingDirectory(dir=last_directory)
130
+ if directory:
131
+ self.output_dir_line_edit().setText(directory)
132
+ self.settings().set('last_directory', directory)
133
+
134
+ def handle_run_task_button(self):
135
+ errors = self.check_inputs_and_parameters()
136
+ if len(errors) > 0:
137
+ error_message = 'Following errors were encountered:\n'
138
+ for error in errors:
139
+ error_message += f' - {error}\n'
140
+ QMessageBox.information(self, 'Error', error_message)
141
+ else:
142
+ LOG.info('Running task...')
143
+ self.run_task_button().setEnabled(False)
144
+ self.save_inputs_and_parameters()
145
+ self._task = SelectSliceFromScansTask(
146
+ inputs={'scans': self.scans_dir_line_edit().text()},
147
+ params={'vertebra': 'L3'},
148
+ output=self.output_dir_line_edit().text(),
149
+ overwrite=self.overwrite_checkbox().isChecked(),
150
+ )
151
+ self._worker = Worker(self._task)
152
+ self._thread = QThread()
153
+ self._worker.moveToThread(self._thread)
154
+ self._thread.started.connect(self._worker.run)
155
+ self._worker.progress.connect(self.handle_progress)
156
+ self._worker.status.connect(self.handle_status)
157
+ self._worker.finished.connect(self.handle_finished)
158
+ self._worker.finished.connect(self._thread.quit)
159
+ self._worker.finished.connect(self._worker.deleteLater)
160
+ self._thread.finished.connect(self._thread.deleteLater)
161
+ self._thread.start()
162
+
163
+ @Slot(int)
164
+ def handle_progress(self, progress):
165
+ LOG.info(f'Progress: {progress} / 100%')
166
+
167
+ @Slot(str)
168
+ def handle_status(self, status):
169
+ LOG.info(f'Status: {status}')
170
+
171
+ @Slot()
172
+ def handle_finished(self):
173
+ self.run_task_button().setEnabled(True)
174
+
175
+ # HELPERS
176
+
177
+ def check_inputs_and_parameters(self):
178
+ errors = []
179
+ if self.scans_dir_line_edit().text() == '':
180
+ errors.append('Empty scans directory path')
181
+ if not os.path.isdir(self.scans_dir_line_edit().text()):
182
+ errors.append('Scans directory does not exist')
183
+ if self.output_dir_line_edit().text() == '':
184
+ errors.append('Empty output directory path')
185
+ if os.path.isdir(self.output_dir_line_edit().text()) and not self.overwrite_checkbox().isChecked():
186
+ errors.append('Output directory exists but overwrite=False. Please remove output directory first')
187
+ return errors
188
+
189
+ def save_inputs_and_parameters(self):
190
+ self.settings().set(f'{PANEL_NAME}/scans_dir', self.scans_dir_line_edit().text())
191
+ self.settings().set(f'{PANEL_NAME}/vertebra', self.vertebra_combobox().currentText())
192
+ self.settings().set(f'{PANEL_NAME}/output_dir', self.output_dir_line_edit().text())
193
+ self.settings().set(f'{PANEL_NAME}/overwrite', self.overwrite_checkbox().isChecked())
@@ -0,0 +1,6 @@
1
+ from mosamatic2.ui.widgets.panels.defaultpanel import DefaultPanel
2
+
3
+
4
+ class TaskPanel(DefaultPanel):
5
+ def __init__(self):
6
+ super(TaskPanel, self).__init__()
@@ -0,0 +1,195 @@
1
+ import os
2
+
3
+ from PySide6.QtWidgets import (
4
+ QLineEdit,
5
+ QCheckBox,
6
+ QComboBox,
7
+ QHBoxLayout,
8
+ QVBoxLayout,
9
+ QFormLayout,
10
+ QPushButton,
11
+ QFileDialog,
12
+ QMessageBox,
13
+ )
14
+ from PySide6.QtCore import (
15
+ QThread,
16
+ Slot,
17
+ )
18
+
19
+ from mosamatic2.core.managers.logmanager import LogManager
20
+ from mosamatic2.ui.widgets.panels.tasks.taskpanel import TaskPanel
21
+ from mosamatic2.ui.settings import Settings
22
+ from mosamatic2.ui.utils import is_macos
23
+ from mosamatic2.ui.worker import Worker
24
+ from mosamatic2.core.tasks import TotalSegmentatorTask
25
+
26
+ LOG = LogManager()
27
+
28
+ PANEL_TITLE = 'TotalSegmentatorTaskPanel'
29
+ PANEL_NAME = 'totalsegmentatortaskpanel'
30
+
31
+
32
+ class TotalSegmentatorTaskPanel(TaskPanel):
33
+ def __init__(self):
34
+ super(TotalSegmentatorTaskPanel, self).__init__()
35
+ self.set_title(PANEL_TITLE)
36
+ self._scans_dir_line_edit = None
37
+ self._scans_dir_select_button = None
38
+ self._tasks_line_edit = None
39
+ self._output_dir_line_edit = None
40
+ self._output_dir_select_button = None
41
+ self._overwrite_checkbox = None
42
+ self._form_layout = None
43
+ self._run_task_button = None
44
+ self._settings = None
45
+ self._task = None
46
+ self._worker = None
47
+ self._thread = None
48
+ self.init_layout()
49
+
50
+ def scans_dir_line_edit(self):
51
+ if not self._scans_dir_line_edit:
52
+ self._scans_dir_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/scans_dir'))
53
+ return self._scans_dir_line_edit
54
+
55
+ def scans_dir_select_button(self):
56
+ if not self._scans_dir_select_button:
57
+ self._scans_dir_select_button = QPushButton('Select')
58
+ self._scans_dir_select_button.clicked.connect(self.handle_scans_dir_select_button)
59
+ return self._scans_dir_select_button
60
+
61
+ def tasks_line_edit(self):
62
+ if not self._tasks_line_edit:
63
+ self._tasks_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/tasks'))
64
+ if self._tasks_line_edit.text() == '':
65
+ self._tasks_line_edit.setText('total')
66
+ return self._tasks_line_edit
67
+
68
+ def output_dir_line_edit(self):
69
+ if not self._output_dir_line_edit:
70
+ self._output_dir_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/output_dir'))
71
+ return self._output_dir_line_edit
72
+
73
+ def output_dir_select_button(self):
74
+ if not self._output_dir_select_button:
75
+ self._output_dir_select_button = QPushButton('Select')
76
+ self._output_dir_select_button.clicked.connect(self.handle_output_dir_select_button)
77
+ return self._output_dir_select_button
78
+
79
+ def overwrite_checkbox(self):
80
+ if not self._overwrite_checkbox:
81
+ self._overwrite_checkbox = QCheckBox('')
82
+ self._overwrite_checkbox.setChecked(self.settings().get_bool(f'{PANEL_NAME}/overwrite', True))
83
+ return self._overwrite_checkbox
84
+
85
+ def form_layout(self):
86
+ if not self._form_layout:
87
+ self._form_layout = QFormLayout()
88
+ if is_macos():
89
+ self._form_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
90
+ return self._form_layout
91
+
92
+ def run_task_button(self):
93
+ if not self._run_task_button:
94
+ self._run_task_button = QPushButton('Run task')
95
+ self._run_task_button.clicked.connect(self.handle_run_task_button)
96
+ return self._run_task_button
97
+
98
+ def settings(self):
99
+ if not self._settings:
100
+ self._settings = Settings()
101
+ return self._settings
102
+
103
+ def init_layout(self):
104
+ scans_dir_layout = QHBoxLayout()
105
+ scans_dir_layout.addWidget(self.scans_dir_line_edit())
106
+ scans_dir_layout.addWidget(self.scans_dir_select_button())
107
+ output_dir_layout = QHBoxLayout()
108
+ output_dir_layout.addWidget(self.output_dir_line_edit())
109
+ output_dir_layout.addWidget(self.output_dir_select_button())
110
+ self.form_layout().addRow('Scans directory', scans_dir_layout)
111
+ self.form_layout().addRow('Tasks', self.tasks_line_edit())
112
+ self.form_layout().addRow('Output directory', output_dir_layout)
113
+ self.form_layout().addRow('Overwrite', self.overwrite_checkbox())
114
+ layout = QVBoxLayout()
115
+ layout.addLayout(self.form_layout())
116
+ layout.addWidget(self.run_task_button())
117
+ self.setLayout(layout)
118
+ self.setObjectName(PANEL_NAME)
119
+
120
+ def handle_scans_dir_select_button(self):
121
+ last_directory = self.settings().get('last_directory')
122
+ directory = QFileDialog.getExistingDirectory(dir=last_directory)
123
+ if directory:
124
+ self.scans_dir_line_edit().setText(directory)
125
+ self.settings().set('last_directory', directory)
126
+
127
+ def handle_output_dir_select_button(self):
128
+ last_directory = self.settings().get('last_directory')
129
+ directory = QFileDialog.getExistingDirectory(dir=last_directory)
130
+ if directory:
131
+ self.output_dir_line_edit().setText(directory)
132
+ self.settings().set('last_directory', directory)
133
+
134
+ def handle_run_task_button(self):
135
+ errors = self.check_inputs_and_parameters()
136
+ if len(errors) > 0:
137
+ error_message = 'Following errors were encountered:\n'
138
+ for error in errors:
139
+ error_message += f' - {error}\n'
140
+ QMessageBox.information(self, 'Error', error_message)
141
+ else:
142
+ LOG.info('Running task...')
143
+ self.run_task_button().setEnabled(False)
144
+ self.save_inputs_and_parameters()
145
+ self._task = TotalSegmentatorTask(
146
+ inputs={'scans': self.scans_dir_line_edit().text()},
147
+ params={'tasks': self.tasks_line_edit().text()},
148
+ output=self.output_dir_line_edit().text(),
149
+ overwrite=self.overwrite_checkbox().isChecked(),
150
+ )
151
+ self._worker = Worker(self._task)
152
+ self._thread = QThread()
153
+ self._worker.moveToThread(self._thread)
154
+ self._thread.started.connect(self._worker.run)
155
+ self._worker.progress.connect(self.handle_progress)
156
+ self._worker.status.connect(self.handle_status)
157
+ self._worker.finished.connect(self.handle_finished)
158
+ self._worker.finished.connect(self._thread.quit)
159
+ self._worker.finished.connect(self._worker.deleteLater)
160
+ self._thread.finished.connect(self._thread.deleteLater)
161
+ self._thread.start()
162
+
163
+ @Slot(int)
164
+ def handle_progress(self, progress):
165
+ LOG.info(f'Progress: {progress} / 100%')
166
+
167
+ @Slot(str)
168
+ def handle_status(self, status):
169
+ LOG.info(f'Status: {status}')
170
+
171
+ @Slot()
172
+ def handle_finished(self):
173
+ self.run_task_button().setEnabled(True)
174
+
175
+ # HELPERS
176
+
177
+ def check_inputs_and_parameters(self):
178
+ errors = []
179
+ if self.scans_dir_line_edit().text() == '':
180
+ errors.append('Empty scans directory path')
181
+ if not os.path.isdir(self.scans_dir_line_edit().text()):
182
+ errors.append('Scans directory does not exist')
183
+ if self.output_dir_line_edit().text() == '':
184
+ errors.append('Empty output directory path')
185
+ if os.path.isdir(self.output_dir_line_edit().text()) and not self.overwrite_checkbox().isChecked():
186
+ errors.append('Output directory exists but overwrite=False. Please remove output directory first')
187
+ if self.tasks_line_edit().text() == '':
188
+ errors.append('Tasks cannot be empty and must be comma-separated list of values')
189
+ return errors
190
+
191
+ def save_inputs_and_parameters(self):
192
+ self.settings().set(f'{PANEL_NAME}/scans_dir', self.scans_dir_line_edit().text())
193
+ self.settings().set(f'{PANEL_NAME}/tasks', self.tasks_line_edit().text())
194
+ self.settings().set(f'{PANEL_NAME}/output_dir', self.output_dir_line_edit().text())
195
+ self.settings().set(f'{PANEL_NAME}/overwrite', self.overwrite_checkbox().isChecked())
@@ -0,0 +1,96 @@
1
+ import vtk
2
+
3
+
4
+ class LiverSegmentPicker:
5
+ def __init__(self, renderer, interactor, actors, volumes, selected_volume_actor):
6
+ self.ren = renderer
7
+ self.iren = interactor
8
+ self.actors = actors
9
+ self.actor_keys = {v: k for k, v in actors.items()}
10
+ self.volumes = volumes
11
+ self.selected_volume_actor = selected_volume_actor
12
+ self.selected = set()
13
+ self._orig = {}
14
+ self._opacity_selected = 1.0
15
+ self._opacity_not_selected = 0.25
16
+
17
+ self.picker = vtk.vtkCellPicker()
18
+ self.picker.SetTolerance(0.001)
19
+
20
+ # Use trackball so it rotates only when you move
21
+ self.style = vtk.vtkInteractorStyleTrackballCamera()
22
+ self.iren.SetInteractorStyle(self.style)
23
+
24
+ # Hook events
25
+ self.iren.AddObserver("LeftButtonPressEvent", self.on_left_down)
26
+ self.iren.AddObserver("LeftButtonReleaseEvent", self.on_left_up)
27
+
28
+ self._allow_camera_drag = True
29
+
30
+ def _save(self, a):
31
+ if a in self._orig:
32
+ return
33
+
34
+ def _highlight(self, a, on):
35
+ self._save(a)
36
+ p = a.GetProperty()
37
+ if on:
38
+ p.SetOpacity(self._opacity_selected)
39
+ else:
40
+ p.SetOpacity(self._opacity_not_selected)
41
+
42
+ def clear(self):
43
+ for a in list(self.selected):
44
+ self._highlight(a, False)
45
+ self.selected.clear()
46
+
47
+ def toggle(self, a):
48
+ if a in self.selected:
49
+ self._highlight(a, False)
50
+ self.selected.remove(a)
51
+ else:
52
+ self._highlight(a, True)
53
+ self.selected.add(a)
54
+ total_selected_volume = 0
55
+ for a in self.selected:
56
+ a_key = self.actor_keys[a]
57
+ volume = int(self.volumes[a_key])
58
+ total_selected_volume += volume
59
+ self.selected_volume_actor.SetInput(f'Selected volume: {total_selected_volume} mL')
60
+
61
+
62
+ def on_left_down(self, iren, evt):
63
+ x, y = iren.GetEventPosition()
64
+
65
+ # Only do picking when Shift or Ctrl is held.
66
+ do_pick = (iren.GetShiftKey() or iren.GetControlKey())
67
+
68
+ if do_pick:
69
+ self.picker.Pick(x, y, 0, self.ren)
70
+ a = self.picker.GetActor()
71
+ a_key = self.actor_keys[a]
72
+
73
+ if a in self.actors.values():
74
+ # Additive (Shift/Ctrl): toggle selection
75
+ self.toggle(a)
76
+ else:
77
+ # Shift/Ctrl + empty click: clear selection (optional)
78
+ self.clear()
79
+
80
+ iren.GetRenderWindow().Render()
81
+ self._allow_camera_drag = False # don't also start a camera drag
82
+ return
83
+
84
+ # No modifier => normal rotate/pan/zoom interaction
85
+ self._allow_camera_drag = True
86
+ self.style.OnLeftButtonDown()
87
+
88
+ def on_left_up(self, iren, evt):
89
+ if self._allow_camera_drag:
90
+ self.style.OnLeftButtonUp()
91
+
92
+ def set_opacity_selected(self, opacity_selected):
93
+ self._opacity_selected = opacity_selected
94
+
95
+ def set_opacity_not_selected(self, opacity_not_selected):
96
+ self._opacity_not_selected = opacity_not_selected
@@ -0,0 +1,130 @@
1
+ import os
2
+ import vtk
3
+ import pandas as pd
4
+ from PySide6.QtWidgets import (
5
+ QWidget,
6
+ QVBoxLayout,
7
+ )
8
+ from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
9
+ from mosamatic2.ui.widgets.panels.visualizations.liversegmentvisualization.liversegmentpicker import LiverSegmentPicker
10
+ from mosamatic2.core.managers.logmanager import LogManager
11
+
12
+ LOG = LogManager()
13
+ SEGMENT_COLORS = [
14
+ (0.80, 0.25, 0.25), # muted red
15
+ (0.25, 0.55, 0.80), # steel blue
16
+ (0.30, 0.70, 0.45), # green
17
+ (0.85, 0.65, 0.25), # amber
18
+ (0.55, 0.40, 0.75), # purple
19
+ (0.90, 0.45, 0.15), # orange
20
+ (0.20, 0.75, 0.75), # teal
21
+ (0.75, 0.75, 0.30), # olive
22
+ ]
23
+
24
+ vtk.vtkObject.GlobalWarningDisplayOff()
25
+
26
+
27
+ class LiverSegmentViewer(QWidget):
28
+ def __init__(self):
29
+ super(LiverSegmentViewer, self).__init__()
30
+ self._total_volume = 0.0
31
+ self._selected_volume = 0.0
32
+ self._liver_segment_actors = {}
33
+ self._liver_volumes = {}
34
+ self._vtk_widget = QVTKRenderWindowInteractor(self)
35
+ self._render_window = self._vtk_widget.GetRenderWindow()
36
+ self._interactor = self._render_window.GetInteractor()
37
+ self._interactor.SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())
38
+ layout = QVBoxLayout()
39
+ layout.addWidget(self._vtk_widget)
40
+ self.setLayout(layout)
41
+ self._selected_volume_actor = vtk.vtkTextActor()
42
+ self._selected_volume_actor.SetInput(f'Selected volume: {self._selected_volume} mL')
43
+ self._selected_volume_actor.GetTextProperty().SetFontSize(18)
44
+ self._selected_volume_actor.GetTextProperty().SetColor(1, 1, 1) # RGB 0–1
45
+ self._selected_volume_actor.SetDisplayPosition(20, 50)
46
+ self._total_volume_actor = vtk.vtkTextActor()
47
+ self._total_volume_actor.SetInput(f'Total volume: {self._total_volume} mL')
48
+ self._total_volume_actor.GetTextProperty().SetFontSize(18)
49
+ self._total_volume_actor.GetTextProperty().SetColor(1, 1, 1) # RGB 0–1
50
+ self._total_volume_actor.SetDisplayPosition(20, 20)
51
+ self._renderer = vtk.vtkRenderer()
52
+ self._renderer.AddViewProp(self._total_volume_actor)
53
+ self._renderer.AddViewProp(self._selected_volume_actor)
54
+ self._render_window.AddRenderer(self._renderer)
55
+ self._render_window.Render()
56
+
57
+ def image_to_surface(self, image: vtk.vtkImageData, label=1) -> vtk.vtkPolyData:
58
+ dmc = vtk.vtkDiscreteMarchingCubes()
59
+ dmc.SetInputData(image)
60
+ dmc.SetValue(0, label)
61
+ dmc.Update()
62
+ cleaner = vtk.vtkCleanPolyData()
63
+ cleaner.SetInputConnection(dmc.GetOutputPort())
64
+ cleaner.Update()
65
+ smoother = vtk.vtkWindowedSincPolyDataFilter()
66
+ smoother.SetInputConnection(cleaner.GetOutputPort())
67
+ smoother.SetNumberOfIterations(20) # 10–30 typical
68
+ smoother.SetPassBand(0.1) # smaller = smoother (e.g. 0.05–0.2)
69
+ smoother.SetFeatureAngle(120.0) # preserve sharper edges
70
+ smoother.BoundarySmoothingOff() # usually best for closed organs
71
+ smoother.FeatureEdgeSmoothingOff() # keep features; turn On if you want more smoothing
72
+ smoother.NonManifoldSmoothingOn()
73
+ smoother.NormalizeCoordinatesOn() # helps numerical stability
74
+ smoother.Update()
75
+ normals = vtk.vtkPolyDataNormals()
76
+ normals.SetInputConnection(smoother.GetOutputPort())
77
+ normals.SetComputePointNormals(True)
78
+ normals.SetComputeCellNormals(False)
79
+ normals.SetAutoOrientNormals(True)
80
+ normals.SetConsistency(True)
81
+ normals.Update()
82
+ return normals.GetOutput()
83
+
84
+ def surface_to_actor(self, polydata: vtk.vtkPolyData) -> vtk.vtkActor:
85
+ mapper = vtk.vtkPolyDataMapper()
86
+ mapper.SetInputData(polydata)
87
+ mapper.ScalarVisibilityOff()
88
+ actor = vtk.vtkActor()
89
+ actor.SetMapper(mapper)
90
+ return actor
91
+
92
+ def load_segments_and_volumes(self, liver_segments_dir, liver_volumes_file):
93
+ # Load volumes
94
+ df = pd.read_csv(liver_volumes_file, sep=';')
95
+ for _, row in df.iterrows():
96
+ self._liver_volumes[row['file']] = row['volume_mL']
97
+ self._total_volume = sum(self._liver_volumes.values())
98
+ self._total_volume_actor.SetInput(f'Total volume: {self._total_volume} mL')
99
+ # Load liver segment actors
100
+ i = 0
101
+ for f in os.listdir(liver_segments_dir):
102
+ segment_name = f
103
+ f_path = os.path.join(liver_segments_dir, f)
104
+ reader = vtk.vtkNIFTIImageReader()
105
+ reader.SetFileName(f_path)
106
+ reader.Update()
107
+ image = reader.GetOutput()
108
+ surface = self.image_to_surface(image)
109
+ actor = self.surface_to_actor(surface)
110
+ prop = actor.GetProperty()
111
+ prop.SetSpecular(0.3)
112
+ prop.SetSpecularPower(20)
113
+ prop.SetDiffuse(0.7)
114
+ prop.SetAmbient(0.1)
115
+ prop.SetColor(*SEGMENT_COLORS[i])
116
+ prop.SetOpacity(0.5)
117
+ self._renderer.AddActor(actor)
118
+ self._liver_segment_actors[segment_name] = actor
119
+ i += 1
120
+ picker = LiverSegmentPicker(
121
+ self._renderer,
122
+ self._interactor,
123
+ self._liver_segment_actors,
124
+ self._liver_volumes,
125
+ self._selected_volume_actor,
126
+ )
127
+ self._renderer.SetBackground(0.1, 0.1, 0.12)
128
+ self._renderer.ResetCamera()
129
+ self._render_window.Render()
130
+ self._interactor.Start()