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