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,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,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())
|
|
File without changes
|
|
File without changes
|
|
@@ -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()
|