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
mosamatic2/ui/widgets/panels/visualizations/liversegmentvisualization/liversegmentvisualization.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from PySide6.QtWidgets import (
|
|
3
|
+
QLineEdit,
|
|
4
|
+
QHBoxLayout,
|
|
5
|
+
QVBoxLayout,
|
|
6
|
+
QFormLayout,
|
|
7
|
+
QPushButton,
|
|
8
|
+
QFileDialog,
|
|
9
|
+
)
|
|
10
|
+
from mosamatic2.ui.widgets.panels.visualizations.visualization import Visualization
|
|
11
|
+
from mosamatic2.ui.widgets.panels.visualizations.liversegmentvisualization.liversegmentviewer import LiverSegmentViewer
|
|
12
|
+
from mosamatic2.core.managers.logmanager import LogManager
|
|
13
|
+
from mosamatic2.ui.settings import Settings
|
|
14
|
+
from mosamatic2.ui.utils import is_macos
|
|
15
|
+
|
|
16
|
+
LOG = LogManager()
|
|
17
|
+
PANEL_TITLE = 'LiverSegmentVisualization'
|
|
18
|
+
PANEL_NAME = 'liversegmentvisualization'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LiverSegmentVisualization(Visualization):
|
|
22
|
+
def __init__(self):
|
|
23
|
+
super(LiverSegmentVisualization, self).__init__()
|
|
24
|
+
self.set_title(PANEL_TITLE)
|
|
25
|
+
self._liver_segment_actors = {}
|
|
26
|
+
self._liver_volumes = {}
|
|
27
|
+
self._liver_segments_dir_line_edit = None
|
|
28
|
+
self._liver_segments_dir_select_button = None
|
|
29
|
+
self._liver_volumes_file_line_edit = None
|
|
30
|
+
self._liver_volumes_file_select_button = None
|
|
31
|
+
self._load_liver_data_button = None
|
|
32
|
+
self._liver_segment_viewer = None
|
|
33
|
+
self._form_layout = None
|
|
34
|
+
self._settings = None
|
|
35
|
+
self.init_layout()
|
|
36
|
+
|
|
37
|
+
def liver_segments_dir_line_edit(self):
|
|
38
|
+
if not self._liver_segments_dir_line_edit:
|
|
39
|
+
self._liver_segments_dir_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/liver_segments_dir'))
|
|
40
|
+
return self._liver_segments_dir_line_edit
|
|
41
|
+
|
|
42
|
+
def liver_segments_dir_select_button(self):
|
|
43
|
+
if not self._liver_segments_dir_select_button:
|
|
44
|
+
self._liver_segments_dir_select_button = QPushButton('Select directory')
|
|
45
|
+
self._liver_segments_dir_select_button.clicked.connect(self.handle_liver_segments_dir_select_button)
|
|
46
|
+
return self._liver_segments_dir_select_button
|
|
47
|
+
|
|
48
|
+
def liver_volumes_file_line_edit(self):
|
|
49
|
+
if not self._liver_volumes_file_line_edit:
|
|
50
|
+
self._liver_volumes_file_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/liver_volumes_file'))
|
|
51
|
+
return self._liver_volumes_file_line_edit
|
|
52
|
+
|
|
53
|
+
def liver_volumes_file_select_button(self):
|
|
54
|
+
if not self._liver_volumes_file_select_button:
|
|
55
|
+
self._liver_volumes_file_select_button = QPushButton('Select file')
|
|
56
|
+
self._liver_volumes_file_select_button.clicked.connect(self.handle_liver_volumes_file_select_button)
|
|
57
|
+
return self._liver_volumes_file_select_button
|
|
58
|
+
|
|
59
|
+
def load_liver_data_button(self):
|
|
60
|
+
if not self._load_liver_data_button:
|
|
61
|
+
self._load_liver_data_button = QPushButton('Load')
|
|
62
|
+
self._load_liver_data_button.clicked.connect(self.handle_load_liver_data_button)
|
|
63
|
+
return self._load_liver_data_button
|
|
64
|
+
|
|
65
|
+
def liver_segment_viewer(self):
|
|
66
|
+
if not self._liver_segment_viewer:
|
|
67
|
+
self._liver_segment_viewer = LiverSegmentViewer()
|
|
68
|
+
return self._liver_segment_viewer
|
|
69
|
+
|
|
70
|
+
def form_layout(self):
|
|
71
|
+
if not self._form_layout:
|
|
72
|
+
self._form_layout = QFormLayout()
|
|
73
|
+
if is_macos():
|
|
74
|
+
self._form_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
|
75
|
+
return self._form_layout
|
|
76
|
+
|
|
77
|
+
def settings(self):
|
|
78
|
+
if not self._settings:
|
|
79
|
+
self._settings = Settings()
|
|
80
|
+
return self._settings
|
|
81
|
+
|
|
82
|
+
def init_layout(self):
|
|
83
|
+
liver_segments_layout = QHBoxLayout()
|
|
84
|
+
liver_segments_layout.addWidget(self.liver_segments_dir_line_edit())
|
|
85
|
+
liver_segments_layout.addWidget(self.liver_segments_dir_select_button())
|
|
86
|
+
self.form_layout().addRow('Liver segments directory', liver_segments_layout)
|
|
87
|
+
liver_volumes_layout = QHBoxLayout()
|
|
88
|
+
liver_volumes_layout.addWidget(self.liver_volumes_file_line_edit())
|
|
89
|
+
liver_volumes_layout.addWidget(self.liver_volumes_file_select_button())
|
|
90
|
+
self.form_layout().addRow('Liver volumes file', liver_volumes_layout)
|
|
91
|
+
layout = QVBoxLayout()
|
|
92
|
+
layout.addLayout(self.form_layout())
|
|
93
|
+
layout.addWidget(self.load_liver_data_button())
|
|
94
|
+
layout.addWidget(self.liver_segment_viewer())
|
|
95
|
+
self.setLayout(layout)
|
|
96
|
+
self.setObjectName(PANEL_NAME)
|
|
97
|
+
|
|
98
|
+
def handle_liver_segments_dir_select_button(self):
|
|
99
|
+
last_directory = self.settings().get('last_directory')
|
|
100
|
+
dir_path = QFileDialog.getExistingDirectory(dir=last_directory)
|
|
101
|
+
if dir_path:
|
|
102
|
+
self.liver_segments_dir_line_edit().setText(dir_path)
|
|
103
|
+
self.settings().set('last_directory', dir_path)
|
|
104
|
+
|
|
105
|
+
def handle_liver_volumes_file_select_button(self):
|
|
106
|
+
last_directory = self.settings().get('last_directory')
|
|
107
|
+
file_path, _ = QFileDialog.getOpenFileName(dir=last_directory)
|
|
108
|
+
if file_path:
|
|
109
|
+
self.liver_volumes_file_line_edit().setText(file_path)
|
|
110
|
+
self.settings().set('last_directory', os.path.dirname(file_path))
|
|
111
|
+
|
|
112
|
+
def handle_load_liver_data_button(self):
|
|
113
|
+
self.liver_segment_viewer().load_segments_and_volumes(
|
|
114
|
+
liver_segments_dir=self.liver_segments_dir_line_edit().text(),
|
|
115
|
+
liver_volumes_file=self.liver_volumes_file_line_edit().text(),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def save_inputs_and_parameters(self):
|
|
119
|
+
self.settings().set(f'{PANEL_NAME}/liver_segments_dir', self.liver_segments_dir_line_edit().text())
|
|
120
|
+
self.settings().set(f'{PANEL_NAME}/liver_volumes_file', self.liver_volumes_file_line_edit().text())
|
|
File without changes
|
mosamatic2/ui/widgets/panels/visualizations/sliceselectionvisualization/sliceselectionviewer.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from PySide6.QtWidgets import QWidget, QVBoxLayout, QMessageBox, QScrollArea, QGridLayout
|
|
3
|
+
from PySide6.QtCore import Qt, Signal, QSize
|
|
4
|
+
from mosamatic2.ui.widgets.panels.visualizations.sliceselectionvisualization.slicetile import SliceTile
|
|
5
|
+
from mosamatic2.ui.widgets.panels.visualizations.sliceselectionvisualization.slicetile import SliceItem
|
|
6
|
+
|
|
7
|
+
# https://chatgpt.com/c/69494292-b8fc-8327-863f-438838247bd4
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SliceSelectionViewer(QWidget):
|
|
11
|
+
selection_changed = Signal()
|
|
12
|
+
|
|
13
|
+
def __init__(self, columns: int = 4, tile_size: QSize = QSize(220, 220), parent=None):
|
|
14
|
+
super(SliceSelectionViewer, self).__init__()
|
|
15
|
+
self._columns = columns
|
|
16
|
+
self._tile_size = tile_size
|
|
17
|
+
self._tiles: list[SliceTile] = []
|
|
18
|
+
self._scroll_area = QScrollArea()
|
|
19
|
+
self._scroll_area.setWidgetResizable(True)
|
|
20
|
+
self._container = QWidget()
|
|
21
|
+
self._grid = QGridLayout(self._container)
|
|
22
|
+
self._grid.setContentsMargins(10, 10, 10, 10)
|
|
23
|
+
self._grid.setSpacing(10)
|
|
24
|
+
self._scroll_area.setWidget(self._container)
|
|
25
|
+
self._root = QVBoxLayout(self)
|
|
26
|
+
self._root.addWidget(self._scroll_area)
|
|
27
|
+
|
|
28
|
+
def clear(self):
|
|
29
|
+
# remove widgets from layout
|
|
30
|
+
while self._grid.count():
|
|
31
|
+
item = self._grid.takeAt(0)
|
|
32
|
+
w = item.widget()
|
|
33
|
+
if w:
|
|
34
|
+
w.setParent(None)
|
|
35
|
+
self._tiles.clear()
|
|
36
|
+
|
|
37
|
+
def load_images(self, images_dir):
|
|
38
|
+
images = []
|
|
39
|
+
for f in os.listdir(images_dir):
|
|
40
|
+
if f.endswith('_sagittal.png'):
|
|
41
|
+
f_path = os.path.join(images_dir, f)
|
|
42
|
+
images.append(f_path)
|
|
43
|
+
self.clear()
|
|
44
|
+
for i, p in enumerate(images):
|
|
45
|
+
tile = SliceTile(SliceItem(p), self._tile_size)
|
|
46
|
+
tile.toggled.connect(self._on_tile_toggled)
|
|
47
|
+
self._tiles.append(tile)
|
|
48
|
+
r, c = divmod(i, self._columns)
|
|
49
|
+
self._grid.addWidget(tile, r, c)
|
|
50
|
+
# Optional: keep grid left-aligned by adding stretch
|
|
51
|
+
self._grid.setColumnStretch(self._columns, 1)
|
|
52
|
+
|
|
53
|
+
def selected_paths(self) -> list[str]:
|
|
54
|
+
return [t.item.path for t in self._tiles if t.is_checked()]
|
|
55
|
+
|
|
56
|
+
def set_all_checked(self, checked: bool):
|
|
57
|
+
for t in self._tiles:
|
|
58
|
+
t.set_checked(checked)
|
|
59
|
+
|
|
60
|
+
def _on_tile_toggled(self, _path: str, _checked: bool):
|
|
61
|
+
self.selection_changed.emit()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from PySide6.QtWidgets import (
|
|
3
|
+
QLineEdit,
|
|
4
|
+
QLabel,
|
|
5
|
+
QSpinBox,
|
|
6
|
+
QHBoxLayout,
|
|
7
|
+
QVBoxLayout,
|
|
8
|
+
QFormLayout,
|
|
9
|
+
QPushButton,
|
|
10
|
+
QFileDialog,
|
|
11
|
+
)
|
|
12
|
+
from mosamatic2.ui.widgets.panels.visualizations.visualization import Visualization
|
|
13
|
+
from mosamatic2.ui.widgets.panels.visualizations.sliceselectionvisualization.sliceselectionviewer import SliceSelectionViewer
|
|
14
|
+
from mosamatic2.core.managers.logmanager import LogManager
|
|
15
|
+
from mosamatic2.ui.settings import Settings
|
|
16
|
+
from mosamatic2.ui.utils import is_macos
|
|
17
|
+
|
|
18
|
+
LOG = LogManager()
|
|
19
|
+
PANEL_TITLE = 'SliceSelectionVisualization'
|
|
20
|
+
PANEL_NAME = 'sliceselectionvisualization'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SliceSelectionVisualization(Visualization):
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super(SliceSelectionVisualization, self).__init__()
|
|
26
|
+
self.set_title(PANEL_TITLE)
|
|
27
|
+
self._images_line_edit = None
|
|
28
|
+
self._images_dir_select_button = None
|
|
29
|
+
self._output_dir_line_edit = None
|
|
30
|
+
self._output_dir_select_button = None
|
|
31
|
+
self._load_images_button = None
|
|
32
|
+
self._copy_selected_images_button = None
|
|
33
|
+
self._slice_selection_viewer = None
|
|
34
|
+
self._form_layout = None
|
|
35
|
+
self._settings = None
|
|
36
|
+
self.init_layout()
|
|
37
|
+
|
|
38
|
+
def images_line_edit(self):
|
|
39
|
+
if not self._images_line_edit:
|
|
40
|
+
self._images_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/images'))
|
|
41
|
+
return self._images_line_edit
|
|
42
|
+
|
|
43
|
+
def images_dir_select_button(self):
|
|
44
|
+
if not self._images_dir_select_button:
|
|
45
|
+
self._images_dir_select_button = QPushButton('Select directory')
|
|
46
|
+
self._images_dir_select_button.clicked.connect(self.handle_images_dir_select_button)
|
|
47
|
+
return self._images_dir_select_button
|
|
48
|
+
|
|
49
|
+
def output_dir_line_edit(self):
|
|
50
|
+
if not self._output_dir_line_edit:
|
|
51
|
+
self._output_dir_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/output_dir'))
|
|
52
|
+
return self._output_dir_line_edit
|
|
53
|
+
|
|
54
|
+
def output_dir_select_button(self):
|
|
55
|
+
if not self._output_dir_select_button:
|
|
56
|
+
self._output_dir_select_button = QPushButton('Select directory')
|
|
57
|
+
self._output_dir_select_button.clicked.connect(self.handle_output_dir_select_button)
|
|
58
|
+
return self._output_dir_select_button
|
|
59
|
+
|
|
60
|
+
def copy_selected_images_button(self):
|
|
61
|
+
if not self._copy_selected_images_button:
|
|
62
|
+
self._copy_selected_images_button = QPushButton("Copy selected images to output directory")
|
|
63
|
+
self._copy_selected_images_button.clicked.connect(self.handle_copy_selected_images_button)
|
|
64
|
+
return self._copy_selected_images_button
|
|
65
|
+
|
|
66
|
+
def load_images_button(self):
|
|
67
|
+
if not self._load_images_button:
|
|
68
|
+
self._load_images_button = QPushButton('Load')
|
|
69
|
+
self._load_images_button.clicked.connect(self.handle_load_images_button)
|
|
70
|
+
return self._load_images_button
|
|
71
|
+
|
|
72
|
+
def slice_selection_viewer(self):
|
|
73
|
+
if not self._slice_selection_viewer:
|
|
74
|
+
self._slice_selection_viewer = SliceSelectionViewer()
|
|
75
|
+
return self._slice_selection_viewer
|
|
76
|
+
|
|
77
|
+
def form_layout(self):
|
|
78
|
+
if not self._form_layout:
|
|
79
|
+
self._form_layout = QFormLayout()
|
|
80
|
+
if is_macos():
|
|
81
|
+
self._form_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
|
82
|
+
return self._form_layout
|
|
83
|
+
|
|
84
|
+
def settings(self):
|
|
85
|
+
if not self._settings:
|
|
86
|
+
self._settings = Settings()
|
|
87
|
+
return self._settings
|
|
88
|
+
|
|
89
|
+
def init_layout(self):
|
|
90
|
+
image_layout = QHBoxLayout()
|
|
91
|
+
image_layout.addWidget(self.images_line_edit())
|
|
92
|
+
image_layout.addWidget(self.images_dir_select_button())
|
|
93
|
+
output_dir_layout = QHBoxLayout()
|
|
94
|
+
output_dir_layout.addWidget(self.output_dir_line_edit())
|
|
95
|
+
output_dir_layout.addWidget(self.output_dir_select_button())
|
|
96
|
+
self.form_layout().addRow('PNG images directory', image_layout)
|
|
97
|
+
self.form_layout().addRow('Output directory', output_dir_layout)
|
|
98
|
+
layout = QVBoxLayout()
|
|
99
|
+
layout.addLayout(self.form_layout())
|
|
100
|
+
layout.addWidget(self.load_images_button())
|
|
101
|
+
layout.addWidget(self.copy_selected_images_button())
|
|
102
|
+
layout.addWidget(self.slice_selection_viewer())
|
|
103
|
+
self.setLayout(layout)
|
|
104
|
+
self.setObjectName(PANEL_NAME)
|
|
105
|
+
|
|
106
|
+
def handle_images_dir_select_button(self):
|
|
107
|
+
last_directory = self.settings().get('last_directory')
|
|
108
|
+
dir_path = QFileDialog.getExistingDirectory(dir=last_directory)
|
|
109
|
+
if dir_path:
|
|
110
|
+
self.images_line_edit().setText(dir_path)
|
|
111
|
+
self.settings().set('last_directory', dir_path)
|
|
112
|
+
|
|
113
|
+
def handle_output_dir_select_button(self):
|
|
114
|
+
last_directory = self.settings().get('last_directory')
|
|
115
|
+
dir_path = QFileDialog.getExistingDirectory(dir=last_directory)
|
|
116
|
+
if dir_path:
|
|
117
|
+
self.output_dir_line_edit().setText(dir_path)
|
|
118
|
+
self.settings().set('last_directory', dir_path)
|
|
119
|
+
|
|
120
|
+
def handle_load_images_button(self):
|
|
121
|
+
self.slice_selection_viewer().load_images(self.images_line_edit().text())
|
|
122
|
+
|
|
123
|
+
def handle_copy_selected_images_button(self):
|
|
124
|
+
selected_paths = self.slice_selection_viewer().selected_paths()
|
|
125
|
+
for p in selected_paths:
|
|
126
|
+
p_dcm = p[:-13] + '.dcm'
|
|
127
|
+
target_dir = self.output_dir_line_edit().text()
|
|
128
|
+
LOG.info(f'Copying {p_dcm} to {target_dir}')
|
|
129
|
+
shutil.copy(p_dcm, target_dir)
|
|
130
|
+
|
|
131
|
+
def save_inputs_and_parameters(self):
|
|
132
|
+
self.settings().set(f'{PANEL_NAME}/images', self.images_line_edit().text())
|
|
133
|
+
self.settings().set(f'{PANEL_NAME}/output_dir', self.output_dir_line_edit().text())
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from PySide6.QtCore import Qt, Signal, QSize
|
|
5
|
+
from PySide6.QtGui import QPixmap
|
|
6
|
+
from PySide6.QtWidgets import (
|
|
7
|
+
QApplication, QWidget, QLabel, QCheckBox, QVBoxLayout, QGridLayout,
|
|
8
|
+
QScrollArea, QMainWindow, QFileDialog, QPushButton, QHBoxLayout
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class SliceItem:
|
|
14
|
+
path: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SliceTile(QWidget):
|
|
18
|
+
toggled = Signal(str, bool) # path, checked
|
|
19
|
+
|
|
20
|
+
def __init__(self, item: SliceItem, display_size: QSize, parent=None):
|
|
21
|
+
super().__init__(parent)
|
|
22
|
+
self.item = item
|
|
23
|
+
self.display_size = display_size
|
|
24
|
+
|
|
25
|
+
self.image_label = QLabel()
|
|
26
|
+
self.image_label.setAlignment(Qt.AlignCenter)
|
|
27
|
+
self.image_label.setFixedSize(display_size)
|
|
28
|
+
self.image_label.setStyleSheet("QLabel { background: #222; border: 1px solid #444; }")
|
|
29
|
+
|
|
30
|
+
dcm_name = os.path.basename(item.path)[:-13] + '.dcm'
|
|
31
|
+
self.checkbox = QCheckBox(dcm_name)
|
|
32
|
+
self.checkbox.setChecked(True)
|
|
33
|
+
self.checkbox.toggled.connect(self._on_toggled)
|
|
34
|
+
|
|
35
|
+
layout = QVBoxLayout(self)
|
|
36
|
+
layout.setContentsMargins(6, 6, 6, 6)
|
|
37
|
+
layout.setSpacing(6)
|
|
38
|
+
layout.addWidget(self.image_label)
|
|
39
|
+
layout.addWidget(self.checkbox)
|
|
40
|
+
|
|
41
|
+
self._load_and_scale()
|
|
42
|
+
|
|
43
|
+
def _load_and_scale(self):
|
|
44
|
+
pix = QPixmap(self.item.path)
|
|
45
|
+
if pix.isNull():
|
|
46
|
+
self.image_label.setText("Failed to load")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
scaled = pix.scaled(
|
|
50
|
+
self.display_size,
|
|
51
|
+
Qt.KeepAspectRatio,
|
|
52
|
+
Qt.SmoothTransformation
|
|
53
|
+
)
|
|
54
|
+
self.image_label.setPixmap(scaled)
|
|
55
|
+
|
|
56
|
+
def _on_toggled(self, checked: bool):
|
|
57
|
+
self.toggled.emit(self.item.path, checked)
|
|
58
|
+
|
|
59
|
+
def is_checked(self) -> bool:
|
|
60
|
+
return self.checkbox.isChecked()
|
|
61
|
+
|
|
62
|
+
def set_checked(self, checked: bool):
|
|
63
|
+
self.checkbox.setChecked(checked)
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import vtk
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CustomInteractorStyle(vtk.vtkInteractorStyleImage):
|
|
5
|
+
def __init__(self, image_data, slice_mapper, status_actor, slice_obj, orientation="axial"):
|
|
6
|
+
super(CustomInteractorStyle, self).__init__()
|
|
7
|
+
self.AddObserver("MouseWheelForwardEvent", self.move_slice_forward)
|
|
8
|
+
self.AddObserver("MouseWheelBackwardEvent", self.move_slice_backward)
|
|
9
|
+
self.AddObserver("MouseMoveEvent", self.update_overlay, 1.0)
|
|
10
|
+
self.AddObserver("KeyPressEvent", self.key_press_event)
|
|
11
|
+
|
|
12
|
+
self.image_data = image_data
|
|
13
|
+
self.slice_mapper = slice_mapper
|
|
14
|
+
self.status_actor = status_actor
|
|
15
|
+
self.slice_obj = slice_obj
|
|
16
|
+
self.orientation = orientation
|
|
17
|
+
self._color_window = 0
|
|
18
|
+
self._color_level = 0
|
|
19
|
+
|
|
20
|
+
xmin, xmax, ymin, ymax, zmin, zmax = image_data.GetExtent()
|
|
21
|
+
if orientation == "axial":
|
|
22
|
+
self.min_slice, self.max_slice = zmin, zmax
|
|
23
|
+
elif orientation == "sagittal":
|
|
24
|
+
self.min_slice, self.max_slice = xmin, xmax
|
|
25
|
+
elif orientation == "coronal":
|
|
26
|
+
self.min_slice, self.max_slice = ymin, ymax
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError(f"Unknown orientation: {orientation}")
|
|
29
|
+
|
|
30
|
+
self.slice = (self.min_slice + self.max_slice) // 2
|
|
31
|
+
self.slice_mapper.SetSliceNumber(self.slice)
|
|
32
|
+
self.update_status_message()
|
|
33
|
+
|
|
34
|
+
def color_window(self):
|
|
35
|
+
return self._color_window
|
|
36
|
+
|
|
37
|
+
def set_color_window(self, color_window):
|
|
38
|
+
self._color_window = color_window
|
|
39
|
+
self.slice_obj.GetProperty().SetColorWindow(self._color_window)
|
|
40
|
+
self.GetInteractor().GetRenderWindow().Render()
|
|
41
|
+
|
|
42
|
+
def color_level(self):
|
|
43
|
+
return self._color_level
|
|
44
|
+
|
|
45
|
+
def set_color_level(self, color_level):
|
|
46
|
+
self._color_level = color_level
|
|
47
|
+
self.slice_obj.GetProperty().SetColorLevel(self._color_level)
|
|
48
|
+
self.GetInteractor().GetRenderWindow().Render()
|
|
49
|
+
|
|
50
|
+
def update_status_message(self):
|
|
51
|
+
window = int(self.slice_obj.GetProperty().GetColorWindow())
|
|
52
|
+
level = int(self.slice_obj.GetProperty().GetColorLevel())
|
|
53
|
+
message = f'Slice {self.slice + 1}/{self.max_slice + 1} | W: {window} L: {level}'
|
|
54
|
+
self.status_actor.GetMapper().SetInput(message)
|
|
55
|
+
|
|
56
|
+
def move_slice_forward(self, obj, event):
|
|
57
|
+
if self.slice < self.max_slice:
|
|
58
|
+
self.slice += 1
|
|
59
|
+
self.slice_mapper.SetSliceNumber(self.slice)
|
|
60
|
+
self.update_status_message()
|
|
61
|
+
self.GetInteractor().GetRenderWindow().Render()
|
|
62
|
+
|
|
63
|
+
def move_slice_backward(self, obj, event):
|
|
64
|
+
if self.slice > self.min_slice:
|
|
65
|
+
self.slice -= 1
|
|
66
|
+
self.slice_mapper.SetSliceNumber(self.slice)
|
|
67
|
+
self.update_status_message()
|
|
68
|
+
self.GetInteractor().GetRenderWindow().Render()
|
|
69
|
+
|
|
70
|
+
def key_press_event(self, obj, event):
|
|
71
|
+
key = self.GetInteractor().GetKeySym()
|
|
72
|
+
if key == "Up":
|
|
73
|
+
self.move_slice_forward(obj, event)
|
|
74
|
+
elif key == "Down":
|
|
75
|
+
self.move_slice_backward(obj, event)
|
|
76
|
+
|
|
77
|
+
def update_overlay(self, obj, event):
|
|
78
|
+
super(CustomInteractorStyle, self).OnMouseMove()
|
|
79
|
+
self.update_status_message()
|
|
80
|
+
self.GetInteractor().GetRenderWindow().Render()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import vtk
|
|
3
|
+
import pydicom
|
|
4
|
+
from PySide6.QtWidgets import QWidget, QVBoxLayout, QMessageBox
|
|
5
|
+
from mosamatic2.ui.widgets.panels.visualizations.slicevisualization.custominteractorstyle import CustomInteractorStyle
|
|
6
|
+
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
|
|
7
|
+
|
|
8
|
+
COLOR_WINDOW = 400
|
|
9
|
+
COLOR_LEVEL = 40
|
|
10
|
+
IMAGE_SHIFT_SCALE = -1000
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SliceViewer(QWidget):
|
|
14
|
+
def __init__(self, color_window=COLOR_WINDOW, color_level=COLOR_LEVEL):
|
|
15
|
+
super(SliceViewer, self).__init__()
|
|
16
|
+
self._nifti_file_or_dicom_dir = None
|
|
17
|
+
self._view_orientation = 'axial'
|
|
18
|
+
self._color_window = color_window
|
|
19
|
+
self._color_level = color_level
|
|
20
|
+
self._vtk_widget = QVTKRenderWindowInteractor(self)
|
|
21
|
+
self._render_window = self._vtk_widget.GetRenderWindow()
|
|
22
|
+
self._interactor = self._render_window.GetInteractor()
|
|
23
|
+
self._interactor_style = None
|
|
24
|
+
layout = QVBoxLayout()
|
|
25
|
+
layout.addWidget(self._vtk_widget)
|
|
26
|
+
self.setLayout(layout)
|
|
27
|
+
self._default_renderer = vtk.vtkRenderer()
|
|
28
|
+
self._default_renderer.SetBackground(0.0, 0.0, 0.0) # black
|
|
29
|
+
self._render_window.AddRenderer(self._default_renderer)
|
|
30
|
+
self._render_window.Render()
|
|
31
|
+
|
|
32
|
+
def color_window(self):
|
|
33
|
+
return self._color_window
|
|
34
|
+
|
|
35
|
+
def set_color_window(self, color_window):
|
|
36
|
+
self._color_window = color_window
|
|
37
|
+
if self._interactor_style:
|
|
38
|
+
self._interactor_style.set_color_window(self._color_window)
|
|
39
|
+
|
|
40
|
+
def color_level(self):
|
|
41
|
+
return self._color_level
|
|
42
|
+
|
|
43
|
+
def set_color_level(self, color_level):
|
|
44
|
+
self._color_level = color_level
|
|
45
|
+
if self._interactor_style:
|
|
46
|
+
self._interactor_style.set_color_level(self._color_level)
|
|
47
|
+
|
|
48
|
+
def nifti_file_or_dicom_dir(self):
|
|
49
|
+
return self._nifti_file_or_dicom_dir
|
|
50
|
+
|
|
51
|
+
def set_nifti_file_or_dicom_dir(self, nifti_file_or_dicom_dir):
|
|
52
|
+
self._nifti_file_or_dicom_dir = nifti_file_or_dicom_dir
|
|
53
|
+
|
|
54
|
+
def create_text_actor(self, text, x, y, font_size, align_bottom=False, normalized=False):
|
|
55
|
+
text_prop = vtk.vtkTextProperty()
|
|
56
|
+
text_prop.SetFontFamilyToCourier()
|
|
57
|
+
text_prop.SetFontSize(font_size)
|
|
58
|
+
text_prop.SetVerticalJustificationToBottom() if align_bottom else text_prop.SetVerticalJustificationToTop()
|
|
59
|
+
text_prop.SetJustificationToLeft()
|
|
60
|
+
text_mapper = vtk.vtkTextMapper()
|
|
61
|
+
text_mapper.SetInput(text)
|
|
62
|
+
text_mapper.SetTextProperty(text_prop)
|
|
63
|
+
text_actor = vtk.vtkActor2D()
|
|
64
|
+
text_actor.SetMapper(text_mapper)
|
|
65
|
+
if normalized:
|
|
66
|
+
text_actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay()
|
|
67
|
+
text_actor.SetPosition(x, y)
|
|
68
|
+
return text_actor
|
|
69
|
+
|
|
70
|
+
def is_nifti_file(self, file_path):
|
|
71
|
+
return file_path.endswith('.nii') or file_path.endswith('.nii.gz')
|
|
72
|
+
|
|
73
|
+
def is_dicom_dir(self, dir_path):
|
|
74
|
+
first_dicom_file = os.path.join(dir_path, os.listdir(dir_path)[0])
|
|
75
|
+
return pydicom.dcmread(first_dicom_file, stop_before_pixels=True)
|
|
76
|
+
|
|
77
|
+
def load_image(self):
|
|
78
|
+
if not self.nifti_file_or_dicom_dir():
|
|
79
|
+
QMessageBox.warning(self, 'Warning', 'No NIFTI file or DICOM directory set')
|
|
80
|
+
return
|
|
81
|
+
if self.is_nifti_file(self.nifti_file_or_dicom_dir()):
|
|
82
|
+
reader = vtk.vtkNIFTIImageReader()
|
|
83
|
+
reader.SetFileName(self.nifti_file_or_dicom_dir())
|
|
84
|
+
elif self.is_dicom_dir(self.nifti_file_or_dicom_dir()):
|
|
85
|
+
reader = vtk.vtkDICOMImageReader()
|
|
86
|
+
reader.SetDirectoryName(self.nifti_file_or_dicom_dir())
|
|
87
|
+
reader.Update()
|
|
88
|
+
image_data = reader.GetOutput()
|
|
89
|
+
xmin, xmax, ymin, ymax, zmin, zmax = image_data.GetExtent()
|
|
90
|
+
axial_index = (zmin + zmax) // 2
|
|
91
|
+
slice_mapper = vtk.vtkImageSliceMapper()
|
|
92
|
+
slice_mapper.SetInputData(image_data)
|
|
93
|
+
slice_mapper.SetOrientationToZ() # axial orientation
|
|
94
|
+
slice_mapper.SetSliceNumber(axial_index)
|
|
95
|
+
slice = vtk.vtkImageSlice()
|
|
96
|
+
slice.GetProperty().SetColorWindow(self.color_window())
|
|
97
|
+
slice.GetProperty().SetColorLevel(self.color_level())
|
|
98
|
+
slice.SetMapper(slice_mapper)
|
|
99
|
+
slice_text_actor = self.create_text_actor("", 0.01, 0.01, 12, align_bottom=True, normalized=True)
|
|
100
|
+
usage_text_actor = self.create_text_actor(
|
|
101
|
+
"- Slice with mouse wheel or Up/Down keys (first click inside viewer)\n"
|
|
102
|
+
"- Zoom with pressed right mouse button while dragging\n"
|
|
103
|
+
"- Pan with Shift key and left mouse button while dragging\n"
|
|
104
|
+
"- Change contrast/brightness with pressed left mouse while dragging",
|
|
105
|
+
0.01, 0.99, 12, normalized=True)
|
|
106
|
+
ren = vtk.vtkRenderer()
|
|
107
|
+
ren.AddActor2D(slice_text_actor)
|
|
108
|
+
ren.AddActor2D(usage_text_actor)
|
|
109
|
+
ren.AddViewProp(slice)
|
|
110
|
+
ren.ResetCamera()
|
|
111
|
+
self._render_window.RemoveRenderer(self._default_renderer)
|
|
112
|
+
self._render_window.AddRenderer(ren)
|
|
113
|
+
self._interactor_style = CustomInteractorStyle(image_data, slice_mapper, slice_text_actor, slice)
|
|
114
|
+
self._interactor.SetInteractorStyle(self._interactor_style)
|
|
115
|
+
self._interactor.Initialize()
|
|
116
|
+
self._render_window.Render()
|