mosamatic2 2.0.4__tar.gz → 2.0.6__tar.gz

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.

Potentially problematic release.


This version of mosamatic2 might be problematic. Click here for more details.

Files changed (86) hide show
  1. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/PKG-INFO +3 -1
  2. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/pyproject.toml +3 -1
  3. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/cli.py +2 -0
  4. mosamatic2-2.0.6/src/mosamatic2/commands/selectslicefromscans.py +65 -0
  5. mosamatic2-2.0.6/src/mosamatic2/core/data/multinumpyimage.py +26 -0
  6. mosamatic2-2.0.6/src/mosamatic2/core/data/numpyimage.py +13 -0
  7. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/__init__.py +2 -1
  8. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/calculatescorestask/calculatescorestask.py +6 -6
  9. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/segmentmusclefatl3tensorflowtask/segmentmusclefatl3tensorflowtask.py +1 -3
  10. mosamatic2-2.0.6/src/mosamatic2/core/tasks/selectslicefromscanstask/selectslicefromscanstask.py +109 -0
  11. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/utils.py +10 -2
  12. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/server.py +17 -0
  13. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/mainwindow.py +34 -0
  14. mosamatic2-2.0.6/src/mosamatic2/ui/resources/VERSION +1 -0
  15. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/panels/pipelines/defaultpipelinepanel.py +2 -2
  16. mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/pipelines/pipelinepanel.py +6 -0
  17. mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/tasks/__init__.py +0 -0
  18. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/panels/tasks/calculatescorestaskpanel.py +1 -1
  19. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/panels/tasks/createpngsfromsegmentationstaskpanel.py +1 -1
  20. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/panels/tasks/dicom2niftitaskpanel.py +1 -1
  21. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/panels/tasks/rescaledicomimagestaskpanel.py +1 -1
  22. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/panels/tasks/segmentmusclefatl3tensorflowtaskpanel.py +1 -1
  23. mosamatic2-2.0.4/src/mosamatic2/ui/widgets/panels/tasks/selectslicefromscantaskpanel.py → mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/tasks/selectslicefromscanstaskpanel.py +26 -17
  24. mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/visualizations/__init__.py +0 -0
  25. mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/visualizations/slicevisualization/__init__.py +0 -0
  26. mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/visualizations/slicevisualization/custominteractorstyle.py +62 -0
  27. mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/visualizations/slicevisualization/sliceviewer.py +98 -0
  28. mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/visualizations/slicevisualization/slicevisualization.py +106 -0
  29. mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/visualizations/visualization.py +6 -0
  30. mosamatic2-2.0.4/src/mosamatic2/ui/resources/VERSION +0 -1
  31. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/README.md +0 -0
  32. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/models.py +0 -0
  33. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/__init__.py +0 -0
  34. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/app.py +0 -0
  35. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/commands/__init__.py +0 -0
  36. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/commands/calculatescores.py +0 -0
  37. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/commands/createpngsfromsegmentations.py +0 -0
  38. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/commands/dicom2nifti.py +0 -0
  39. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/commands/rescaledicomimages.py +0 -0
  40. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/commands/segmentmusclefatl3tensorflow.py +0 -0
  41. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/constants.py +0 -0
  42. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/__init__.py +0 -0
  43. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/data/__init__.py +0 -0
  44. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/data/dicomimage.py +0 -0
  45. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/data/dicomimageseries.py +0 -0
  46. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/data/dixonseries.py +0 -0
  47. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/data/filedata.py +0 -0
  48. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/data/multidicomimage.py +0 -0
  49. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/managers/__init__.py +0 -0
  50. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/managers/logmanager.py +0 -0
  51. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/managers/logmanagerlistener.py +0 -0
  52. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/pipelines/__init__.py +0 -0
  53. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/pipelines/defaultpipeline.py +0 -0
  54. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/pipelines/pipeline.py +0 -0
  55. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/singleton.py +0 -0
  56. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/calculatescorestask/__init__.py +0 -0
  57. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/createpngsfromsegmentationstask/__init__.py +0 -0
  58. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/createpngsfromsegmentationstask/createpngsfromsegmentationstask.py +0 -0
  59. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/dicom2niftitask/__init__.py +0 -0
  60. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/dicom2niftitask/dicom2niftitask.py +0 -0
  61. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/rescaledicomimagestask/__init__.py +0 -0
  62. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/rescaledicomimagestask/rescaledicomimagestask.py +0 -0
  63. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/segmentmusclefatl3tensorflowtask/__init__.py +0 -0
  64. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/segmentmusclefatl3tensorflowtask/paramloader.py +0 -0
  65. {mosamatic2-2.0.4/src/mosamatic2/ui → mosamatic2-2.0.6/src/mosamatic2/core/tasks/selectslicefromscanstask}/__init__.py +0 -0
  66. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/core/tasks/task.py +0 -0
  67. {mosamatic2-2.0.4/src/mosamatic2/ui/widgets → mosamatic2-2.0.6/src/mosamatic2/ui}/__init__.py +0 -0
  68. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/resources/icons/mosamatic2.icns +0 -0
  69. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/resources/icons/mosamatic2.ico +0 -0
  70. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/resources/icons/spinner.gif +0 -0
  71. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/resources/images/body-composition.jpg +0 -0
  72. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/settings.py +0 -0
  73. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/utils.py +0 -0
  74. {mosamatic2-2.0.4/src/mosamatic2/ui/widgets/dialogs → mosamatic2-2.0.6/src/mosamatic2/ui/widgets}/__init__.py +0 -0
  75. {mosamatic2-2.0.4/src/mosamatic2/ui/widgets/panels → mosamatic2-2.0.6/src/mosamatic2/ui/widgets/dialogs}/__init__.py +0 -0
  76. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/dialogs/dialog.py +0 -0
  77. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/dialogs/helpdialog.py +0 -0
  78. {mosamatic2-2.0.4/src/mosamatic2/ui/widgets/panels/pipelines → mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels}/__init__.py +0 -0
  79. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/panels/defaultpanel.py +0 -0
  80. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/panels/logpanel.py +0 -0
  81. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/panels/mainpanel.py +0 -0
  82. {mosamatic2-2.0.4/src/mosamatic2/ui/widgets/panels/tasks → mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/pipelines}/__init__.py +0 -0
  83. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/panels/stackedpanel.py +0 -0
  84. {mosamatic2-2.0.4/src/mosamatic2/ui/widgets/panels → mosamatic2-2.0.6/src/mosamatic2/ui/widgets/panels/tasks}/taskpanel.py +0 -0
  85. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/widgets/splashscreen.py +0 -0
  86. {mosamatic2-2.0.4 → mosamatic2-2.0.6}/src/mosamatic2/ui/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mosamatic2
3
- Version: 2.0.4
3
+ Version: 2.0.6
4
4
  Summary:
5
5
  Author: Ralph Brecheisen
6
6
  Author-email: r.brecheisen@maastrichtuniversity.nl
@@ -17,6 +17,7 @@ Requires-Dist: pandas (>=2.3.2)
17
17
  Requires-Dist: pendulum (>=3.1.0)
18
18
  Requires-Dist: pillow (>=11.3.0)
19
19
  Requires-Dist: pydicom (>=3.0.1)
20
+ Requires-Dist: pyqtgraph (>=0.13.7)
20
21
  Requires-Dist: pyside6-essentials (>=6.9)
21
22
  Requires-Dist: python-gdcm (>=3.0.26)
22
23
  Requires-Dist: scipy (>=1.15.3)
@@ -30,6 +31,7 @@ Requires-Dist: tensorflow-macos (==2.15.0) ; platform_system == "Darwin" and pla
30
31
  Requires-Dist: torch (>=2.8.0)
31
32
  Requires-Dist: torchvision (>=0.23.0)
32
33
  Requires-Dist: totalsegmentator (>=2.11.0)
34
+ Requires-Dist: vtk (>=9.5.1)
33
35
  Description-Content-Type: text/markdown
34
36
 
35
37
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mosamatic2"
3
- version = "2.0.4"
3
+ version = "2.0.6"
4
4
  description = ""
5
5
  authors = [
6
6
  {name = "Ralph Brecheisen", email = "r.brecheisen@maastrichtuniversity.nl"}
@@ -31,6 +31,8 @@ dependencies = [
31
31
  "totalsegmentator>=2.11.0",
32
32
  "flask>=3.1.2",
33
33
  "antspyx>=0.5.4",
34
+ "vtk>=9.5.1",
35
+ "pyqtgraph>=0.13.7",
34
36
  ]
35
37
 
36
38
  [tool.poetry]
@@ -5,6 +5,7 @@ from mosamatic2.commands import (
5
5
  segmentmusclefatl3tensorflow,
6
6
  createpngsfromsegmentations,
7
7
  dicom2nifti,
8
+ selectslicefromscans,
8
9
  )
9
10
  from mosamatic2.core.utils import show_doc_command
10
11
 
@@ -31,4 +32,5 @@ main.add_command(rescaledicomimages.rescaledicomimages)
31
32
  main.add_command(segmentmusclefatl3tensorflow.segmentmusclefatl3tensorflow)
32
33
  main.add_command(createpngsfromsegmentations.createpngsfromsegmentations)
33
34
  main.add_command(dicom2nifti.dicom2nifti)
35
+ main.add_command(selectslicefromscans.selectslicefromscans)
34
36
  main.add_command(show_doc_command(main)) # Special command to show long description for command
@@ -0,0 +1,65 @@
1
+ import click
2
+
3
+ from mosamatic2.core.tasks import SelectSliceFromScansTask
4
+
5
+
6
+ @click.command(help='Selects specific slice from CT scans')
7
+ @click.option(
8
+ '--scans',
9
+ required=True,
10
+ type=click.Path(exists=True),
11
+ help='Directory with scans (each patient separate subdirectory)',
12
+ )
13
+ @click.option(
14
+ '--output',
15
+ required=True,
16
+ type=click.Path(),
17
+ help='Output directory'
18
+ )
19
+ @click.option(
20
+ '--vertebra',
21
+ required=True,
22
+ help='Vertebral level for selecting slice (default: "L3")'
23
+ )
24
+ @click.option(
25
+ '--overwrite',
26
+ type=click.BOOL,
27
+ default=False,
28
+ help='Overwrite [true|false]'
29
+ )
30
+ def selectslicefromscans(scans, vertebra, output, overwrite):
31
+ """
32
+ Selects specific slice from list of CT scans
33
+
34
+ Parameters
35
+ ----------
36
+ --scans : str
37
+ Directory to scans. Each patient's scan should be in a separate
38
+ subdirectory. For example:
39
+
40
+ /scans
41
+ /scans/patient1
42
+ /scans/patient1/file1.dcm
43
+ /scans/patient1/file2.dcm
44
+ ...
45
+ /scans/patient2
46
+ ...
47
+
48
+ --output : str
49
+ Path to output directory where selected slices will be placed. Each
50
+ slice's file name will be the same as the scan directory name, so in
51
+ the example above that would be "patient1", "patient2", etc.
52
+
53
+ --vertebra : str
54
+ Vertebral level where to take slice [L3|T4]
55
+
56
+ --overwrite : bool
57
+ Overwrite contents output directory [true|false]
58
+ """
59
+ task = SelectSliceFromScansTask(
60
+ inputs={'scans': scans},
61
+ params={'vertebra': vertebra},
62
+ output=output,
63
+ overwrite=overwrite,
64
+ )
65
+ task.run()
@@ -0,0 +1,26 @@
1
+ import os
2
+ from mosamatic2.core.managers.logmanager import LogManager
3
+ from mosamatic2.core.data.filedata import FileData
4
+ from mosamatic2.core.data.numpyimage import NumPyImage
5
+
6
+ LOG = LogManager()
7
+
8
+
9
+ class MultiNumPyImage(FileData):
10
+ def __init__(self):
11
+ super(MultiNumPyImage, self).__init__()
12
+ self._images = []
13
+
14
+ def images(self):
15
+ return self._images
16
+
17
+ def load(self):
18
+ if self.path():
19
+ for f in os.listdir(self.path()):
20
+ f_path = os.path.join(self.path(), f)
21
+ image = NumPyImage()
22
+ image.set_path(f_path)
23
+ if image.load():
24
+ self._images.append(image)
25
+ return True
26
+ return False
@@ -0,0 +1,13 @@
1
+ from mosamatic2.core.data.filedata import FileData
2
+ from mosamatic2.core.utils import (
3
+ is_numpy,
4
+ load_numpy_array,
5
+ )
6
+
7
+ class NumPyImage(FileData):
8
+ def load(self):
9
+ if self.path():
10
+ if is_numpy(self.path()):
11
+ self.set_object(load_numpy_array(self.path()))
12
+ return True
13
+ return False
@@ -2,4 +2,5 @@ from mosamatic2.core.tasks.rescaledicomimagestask.rescaledicomimagestask import
2
2
  from mosamatic2.core.tasks.segmentmusclefatl3tensorflowtask.segmentmusclefatl3tensorflowtask import SegmentMuscleFatL3TensorFlowTask
3
3
  from mosamatic2.core.tasks.calculatescorestask.calculatescorestask import CalculateScoresTask
4
4
  from mosamatic2.core.tasks.createpngsfromsegmentationstask.createpngsfromsegmentationstask import CreatePngsFromSegmentationsTask
5
- from mosamatic2.core.tasks.dicom2niftitask.dicom2niftitask import Dicom2NiftiTask
5
+ from mosamatic2.core.tasks.dicom2niftitask.dicom2niftitask import Dicom2NiftiTask
6
+ from mosamatic2.core.tasks.selectslicefromscanstask.selectslicefromscanstask import SelectSliceFromScansTask
@@ -4,10 +4,8 @@ import pandas as pd
4
4
  from mosamatic2.core.tasks.task import Task
5
5
  from mosamatic2.core.managers.logmanager import LogManager
6
6
  from mosamatic2.core.data.multidicomimage import MultiDicomImage
7
+ from mosamatic2.core.data.numpyimage import NumPyImage
7
8
  from mosamatic2.core.utils import (
8
- is_dicom,
9
- load_dicom,
10
- is_jpeg2000_compressed,
11
9
  get_pixels_from_dicom_object,
12
10
  calculate_area,
13
11
  calculate_mean_radiation_attenuation,
@@ -41,13 +39,11 @@ class CalculateScoresTask(Task):
41
39
  if file_type == '.seg.npy':
42
40
  f_seg_name = f_seg_name.removesuffix(file_type)
43
41
  if f_seg_name == f_img_name:
44
- # img_seg_pairs.append((f_img_path, f_seg_path))
45
42
  img_seg_pairs.append((image, f_seg_path))
46
43
  elif file_type == '.tag':
47
44
  f_seg_name = f_seg_name.removesuffix(file_type).removesuffix('.dcm')
48
45
  f_img_name = f_img_name.removesuffix('.dcm')
49
46
  if f_seg_name == f_img_name:
50
- # img_seg_pairs.append((f_img_path, f_seg_path))
51
47
  img_seg_pairs.append((image, f_seg_path))
52
48
  else:
53
49
  raise RuntimeError('Unknown file type')
@@ -78,7 +74,11 @@ class CalculateScoresTask(Task):
78
74
 
79
75
  def load_segmentation(self, f, file_type='npy'):
80
76
  if file_type == 'npy':
81
- return np.load(f)
77
+ segmentation = NumPyImage()
78
+ segmentation.set_path(f)
79
+ if segmentation.load():
80
+ return segmentation.object()
81
+ LOG.error(f'Could not load segmentation file {f}')
82
82
  if file_type == 'tag':
83
83
  pixels = get_pixels_from_tag_file(f)
84
84
  try:
@@ -28,7 +28,7 @@ class SegmentMuscleFatL3TensorFlowTask(Task):
28
28
  def __init__(self, inputs, params, output, overwrite=True):
29
29
  super(SegmentMuscleFatL3TensorFlowTask, self).__init__(inputs, params, output, overwrite)
30
30
 
31
- def load_images(self):
31
+ def load_images(self):
32
32
  image_data = MultiDicomImage()
33
33
  image_data.set_path(self.input('images'))
34
34
  if image_data.load():
@@ -55,7 +55,6 @@ class SegmentMuscleFatL3TensorFlowTask(Task):
55
55
  import tensorflow as tf
56
56
  tfLoaded = True
57
57
  with tempfile.TemporaryDirectory() as model_dir_unzipped:
58
- # model_dir_unzipped = os.path.join(os.path.split(f_path)[0], 'model_unzipped')
59
58
  os.makedirs(model_dir_unzipped, exist_ok=True)
60
59
  with zipfile.ZipFile(f_path) as zipObj:
61
60
  zipObj.extractall(path=model_dir_unzipped)
@@ -65,7 +64,6 @@ class SegmentMuscleFatL3TensorFlowTask(Task):
65
64
  import tensorflow as tf
66
65
  tfLoaded = True
67
66
  with tempfile.TemporaryDirectory() as contour_model_dir_unzipped:
68
- # contour_model_dir_unzipped = os.path.join(os.path.split(f_path)[0], 'contour_model_unzipped')
69
67
  os.makedirs(contour_model_dir_unzipped, exist_ok=True)
70
68
  with zipfile.ZipFile(f_path) as zipObj:
71
69
  zipObj.extractall(path=contour_model_dir_unzipped)
@@ -0,0 +1,109 @@
1
+ import os
2
+ import math
3
+ import tempfile
4
+ import shutil
5
+ import nibabel as nib
6
+ import numpy as np
7
+ from totalsegmentator.python_api import totalsegmentator
8
+ from mosamatic2.core.tasks.task import Task
9
+ from mosamatic2.core.managers.logmanager import LogManager
10
+ from mosamatic2.core.utils import load_dicom
11
+
12
+ LOG = LogManager()
13
+
14
+ TOTAL_SEGMENTATOR_OUTPUT_DIR = os.path.join(tempfile.gettempdir(), 'total_segmentator_output')
15
+ TOTAL_SEGMENTATOR_TASK = 'total'
16
+ Z_DELTA_OFFSETS = {
17
+ 'vertebrae_L3': 0.333,
18
+ 'vertebrae_T4': 0.5,
19
+ }
20
+
21
+
22
+ class SelectSliceFromScansTask(Task):
23
+ INPUTS = ['scans']
24
+ PARAMS = ['vertebra']
25
+
26
+ def __init__(self, inputs, params, output, overwrite):
27
+ super(SelectSliceFromScansTask, self).__init__(inputs, params, output, overwrite)
28
+
29
+ def load_scan_dirs(self):
30
+ scan_dirs = []
31
+ for d in os.listdir(self.input('scans')):
32
+ scan_dir = os.path.join(self.input('scans'), d)
33
+ if os.path.isdir(scan_dir):
34
+ scan_dirs.append(scan_dir)
35
+ return scan_dirs
36
+
37
+ def extract_masks(self, scan_dir):
38
+ os.makedirs(TOTAL_SEGMENTATOR_OUTPUT_DIR, exist_ok=True)
39
+ totalsegmentator(input=scan_dir, output=TOTAL_SEGMENTATOR_OUTPUT_DIR, fast=True)
40
+ # os.system(f'TotalSegmentator -i {scan_dir} -o {TOTAL_SEGMENTATOR_OUTPUT_DIR} --fast')
41
+
42
+ def delete_total_segmentator_output(self):
43
+ if os.path.exists(TOTAL_SEGMENTATOR_OUTPUT_DIR):
44
+ shutil.rmtree(TOTAL_SEGMENTATOR_OUTPUT_DIR)
45
+
46
+ def get_z_delta_offset_for_mask(self, mask_name):
47
+ if mask_name not in Z_DELTA_OFFSETS.keys():
48
+ return None
49
+ return Z_DELTA_OFFSETS[mask_name]
50
+
51
+ def find_slice(self, scan_dir, vertebra):
52
+ if vertebra == 'L3':
53
+ vertebral_level = 'vertebrae_L3'
54
+ elif vertebra == 'T4':
55
+ vertebral_level = 'vertebrae_T4'
56
+ else:
57
+ raise RuntimeError(f'Unknown vertbra {vertebra}. Options are "L3" and "T4"')
58
+ # Find Z-positions DICOM images
59
+ z_positions = {}
60
+ for f in os.listdir(scan_dir):
61
+ f_path = os.path.join(scan_dir, f)
62
+ p = load_dicom(f_path, stop_before_pixels=True)
63
+ if p is not None:
64
+ z_positions[p.ImagePositionPatient[2]] = f_path
65
+ # Find Z-position L3 image
66
+ mask_file = os.path.join(TOTAL_SEGMENTATOR_OUTPUT_DIR, f'{vertebral_level}.nii.gz')
67
+ mask_obj = nib.load(mask_file)
68
+ mask = mask_obj.get_fdata()
69
+ affine_transform = mask_obj.affine
70
+ indexes = np.array(np.where(mask == 1))
71
+ index_min = indexes.min(axis=1)
72
+ index_max = indexes.max(axis=1)
73
+ world_min = nib.affines.apply_affine(affine_transform, index_min)
74
+ world_max = nib.affines.apply_affine(affine_transform, index_max)
75
+ z_direction = affine_transform[:3, 2][2]
76
+ z_sign = math.copysign(1, z_direction)
77
+ z_delta_offset = self.get_z_delta_offset_for_mask(vertebral_level)
78
+ if z_delta_offset is None:
79
+ return None
80
+ z_delta = 0.333 * abs(world_max[2] - world_min[2]) # This needs to be vertebra-specific perhaps
81
+ z_l3 = world_max[2] - z_sign * z_delta
82
+ # Find closest L3 image in DICOM set
83
+ positions = sorted(z_positions.keys())
84
+ closest_file = None
85
+ for z1, z2 in zip(positions[:-1], positions[1:]):
86
+ if min(z1, z2) <= z_l3 <= max(z1, z2):
87
+ closest_z = min(z_positions.keys(), key=lambda z: abs(z - z_l3))
88
+ closest_file = z_positions[closest_z]
89
+ LOG.info(f'Closest image: {closest_file}')
90
+ break
91
+ return closest_file
92
+
93
+ def run(self):
94
+ scan_dirs = self.load_scan_dirs()
95
+ vertebra = self.param('vertebra')
96
+ nr_steps = len(scan_dirs)
97
+ for step in range(nr_steps):
98
+ scan_dir = scan_dirs[step]
99
+ scan_name = os.path.split(scan_dir)[1]
100
+ self.extract_masks(scan_dir)
101
+ file_path = self.find_slice(scan_dir, vertebra)
102
+ if file_path is not None:
103
+ extension = '' if file_path.endswith('.dcm') else '.dcm'
104
+ target_file_path = os.path.join(self.output(), vertebra + '_' + scan_name + extension)
105
+ shutil.copyfile(file_path, target_file_path)
106
+ else:
107
+ LOG.error(f'Could not find slice for vertebral level: {vertebra}')
108
+ self.delete_total_segmentator_output()
109
+ self.set_progress(step, nr_steps)
@@ -119,10 +119,18 @@ def is_numpy_array(value):
119
119
  return isinstance(value, np.array)
120
120
 
121
121
 
122
- def load_numpy_array(f):
122
+ def is_numpy(f):
123
123
  try:
124
+ np.load(f)
125
+ return True
126
+ except:
127
+ return False
128
+
129
+
130
+ def load_numpy_array(f):
131
+ if is_numpy(f):
124
132
  return np.load(f)
125
- except Exception as e:
133
+ else:
126
134
  return None
127
135
 
128
136
 
@@ -6,6 +6,7 @@ from mosamatic2.core.tasks import SegmentMuscleFatL3TensorFlowTask
6
6
  from mosamatic2.core.tasks import CalculateScoresTask
7
7
  from mosamatic2.core.tasks import CreatePngsFromSegmentationsTask
8
8
  from mosamatic2.core.tasks import Dicom2NiftiTask
9
+ from mosamatic2.core.tasks import SelectSliceFromScansTask
9
10
 
10
11
  app = Flask(__name__)
11
12
 
@@ -105,6 +106,22 @@ def run_dicom2nifti():
105
106
  return 'PASSED'
106
107
 
107
108
 
109
+ @app.route('/selectslicefromscans')
110
+ def run_selectslicefromscans():
111
+ scans = request.args.get('scans')
112
+ vertebra = request.args.get('vertebra')
113
+ output = request.args.get('output')
114
+ overwrite = request.args.get('overwrite', default=True, type=bool)
115
+ task = SelectSliceFromScansTask(
116
+ inputs={'scans': scans},
117
+ params={'vertebra': vertebra},
118
+ output=output,
119
+ overwrite=overwrite,
120
+ )
121
+ task.run()
122
+ return 'PASSED'
123
+
124
+
108
125
  def main():
109
126
  parser = argparse.ArgumentParser()
110
127
  parser.add_argument('--port', type=int, default=constants.MOSAMATIC2_SERVER_PORT)
@@ -19,7 +19,9 @@ from mosamatic2.ui.widgets.panels.tasks.segmentmusclefatl3tensorflowtaskpanel im
19
19
  from mosamatic2.ui.widgets.panels.tasks.createpngsfromsegmentationstaskpanel import CreatePngsFromSegmentationsTaskPanel
20
20
  from mosamatic2.ui.widgets.panels.tasks.calculatescorestaskpanel import CalculateScoresTaskPanel
21
21
  from mosamatic2.ui.widgets.panels.tasks.dicom2niftitaskpanel import Dicom2NiftiTaskPanel
22
+ from mosamatic2.ui.widgets.panels.tasks.selectslicefromscanstaskpanel import SelectSliceFromScansTaskPanel
22
23
  from mosamatic2.ui.widgets.panels.pipelines.defaultpipelinepanel import DefaultPipelinePanel
24
+ from mosamatic2.ui.widgets.panels.visualizations.slicevisualization.slicevisualization import SliceVisualization
23
25
 
24
26
  LOG = LogManager()
25
27
 
@@ -36,7 +38,9 @@ class MainWindow(QMainWindow):
36
38
  self._create_pngs_from_segmentations_task_panel = None
37
39
  self._calculate_scores_task_panel = None
38
40
  self._dicom2nifti_task_panel = None
41
+ self._select_slice_from_scans_task_panel = None
39
42
  self._default_pipeline_panel = None
43
+ self._slice_visualization = None
40
44
  self.init_window()
41
45
 
42
46
  def init_window(self):
@@ -55,6 +59,7 @@ class MainWindow(QMainWindow):
55
59
  self.init_app_menu()
56
60
  self.init_tasks_menu()
57
61
  self.init_pipelines_menu()
62
+ self.init_visualizations_menu()
58
63
  if is_macos():
59
64
  self.menuBar().setNativeMenuBar(False)
60
65
 
@@ -75,12 +80,15 @@ class MainWindow(QMainWindow):
75
80
  create_pngs_from_segmentations_task_action.triggered.connect(self.handle_create_pngs_from_segmentations_task_action)
76
81
  dicom2nifti_task_action = QAction('Dicom2NiftiTask', self)
77
82
  dicom2nifti_task_action.triggered.connect(self.handle_dicom2nifti_task_action)
83
+ select_slice_from_scans_task_action = QAction('SelectSliceFromScansTask', self)
84
+ select_slice_from_scans_task_action.triggered.connect(self.handle_select_slice_from_scans_task_action)
78
85
  tasks_menu = self.menuBar().addMenu('Tasks')
79
86
  tasks_menu.addAction(rescale_dicom_images_task_action)
80
87
  tasks_menu.addAction(segment_muscle_fat_l3_tensorflow_task_action)
81
88
  tasks_menu.addAction(calculate_scores_task_action)
82
89
  tasks_menu.addAction(create_pngs_from_segmentations_task_action)
83
90
  tasks_menu.addAction(dicom2nifti_task_action)
91
+ tasks_menu.addAction(select_slice_from_scans_task_action)
84
92
 
85
93
  def init_pipelines_menu(self):
86
94
  default_pipeline_action = QAction('DefaultPipeline', self)
@@ -88,6 +96,12 @@ class MainWindow(QMainWindow):
88
96
  pipelines_menu = self.menuBar().addMenu('Pipelines')
89
97
  pipelines_menu.addAction(default_pipeline_action)
90
98
 
99
+ def init_visualizations_menu(self):
100
+ slice_visualization_action = QAction('SliceVisualization', self)
101
+ slice_visualization_action.triggered.connect(self.handle_slice_visualization_action)
102
+ visualizations_menu = self.menuBar().addMenu('Visualizations')
103
+ visualizations_menu.addAction(slice_visualization_action)
104
+
91
105
  def init_status_bar(self):
92
106
  self.set_status('Ready')
93
107
 
@@ -106,7 +120,9 @@ class MainWindow(QMainWindow):
106
120
  self._main_panel.add_panel(self.create_pngs_from_segmentations_task_panel(), 'createpngsfromsegmentationstaskpanel')
107
121
  self._main_panel.add_panel(self.calculate_scores_task_panel(), 'calculatescorestaskpanel')
108
122
  self._main_panel.add_panel(self.dicom2nifti_task_panel(), 'dicom2niftitaskpanel')
123
+ self._main_panel.add_panel(self.select_slice_from_scans_task_panel(), 'selectslicefromscanstaskpanel')
109
124
  self._main_panel.add_panel(self.default_pipeline_panel(), 'defaultpipelinepanel')
125
+ self._main_panel.add_panel(self.slice_visualization(), 'slicevisualization')
110
126
  self._main_panel.select_panel('defaultpipelinepanel')
111
127
  return self._main_panel
112
128
 
@@ -142,11 +158,21 @@ class MainWindow(QMainWindow):
142
158
  if not self._dicom2nifti_task_panel:
143
159
  self._dicom2nifti_task_panel = Dicom2NiftiTaskPanel()
144
160
  return self._dicom2nifti_task_panel
161
+
162
+ def select_slice_from_scans_task_panel(self):
163
+ if not self._select_slice_from_scans_task_panel:
164
+ self._select_slice_from_scans_task_panel = SelectSliceFromScansTaskPanel()
165
+ return self._select_slice_from_scans_task_panel
145
166
 
146
167
  def default_pipeline_panel(self):
147
168
  if not self._default_pipeline_panel:
148
169
  self._default_pipeline_panel = DefaultPipelinePanel()
149
170
  return self._default_pipeline_panel
171
+
172
+ def slice_visualization(self):
173
+ if not self._slice_visualization:
174
+ self._slice_visualization = SliceVisualization()
175
+ return self._slice_visualization
150
176
 
151
177
  # SETTERS
152
178
 
@@ -170,9 +196,15 @@ class MainWindow(QMainWindow):
170
196
  def handle_dicom2nifti_task_action(self):
171
197
  self.main_panel().select_panel('dicom2niftitaskpanel')
172
198
 
199
+ def handle_select_slice_from_scans_task_action(self):
200
+ self.main_panel().select_panel('selectslicefromscanstaskpanel')
201
+
173
202
  def handle_default_pipeline_action(self):
174
203
  self.main_panel().select_panel('defaultpipelinepanel')
175
204
 
205
+ def handle_slice_visualization_action(self):
206
+ self.main_panel().select_panel('slicevisualization')
207
+
176
208
  def showEvent(self, event):
177
209
  return super().showEvent(event)
178
210
 
@@ -184,7 +216,9 @@ class MainWindow(QMainWindow):
184
216
  self.create_pngs_from_segmentations_task_panel().save_inputs_and_parameters()
185
217
  self.calculate_scores_task_panel().save_inputs_and_parameters()
186
218
  self.dicom2nifti_task_panel().save_inputs_and_parameters()
219
+ self.select_slice_from_scans_task_panel().save_inputs_and_parameters()
187
220
  self.default_pipeline_panel().save_inputs_and_parameters()
221
+ self.slice_visualization().save_inputs_and_parameters()
188
222
  return super().closeEvent(event)
189
223
 
190
224
  def load_geometry_and_state(self):
@@ -18,7 +18,7 @@ from PySide6.QtCore import (
18
18
  )
19
19
 
20
20
  from mosamatic2.core.managers.logmanager import LogManager
21
- from mosamatic2.ui.widgets.panels.defaultpanel import DefaultPanel
21
+ from mosamatic2.ui.widgets.panels.pipelines.pipelinepanel import PipelinePanel
22
22
  from mosamatic2.ui.settings import Settings
23
23
  from mosamatic2.ui.utils import is_macos
24
24
  from mosamatic2.ui.worker import Worker
@@ -32,7 +32,7 @@ MODEL_TYPE_ITEM_NAMES = ['tensorflow', 'pytorch']
32
32
  MODEL_VERSION_ITEM_NAMES = ['1.0', '2.2']
33
33
 
34
34
 
35
- class DefaultPipelinePanel(DefaultPanel):
35
+ class DefaultPipelinePanel(PipelinePanel):
36
36
  def __init__(self):
37
37
  super(DefaultPipelinePanel, self).__init__()
38
38
  self.set_title(PANEL_TITLE)
@@ -0,0 +1,6 @@
1
+ from mosamatic2.ui.widgets.panels.defaultpanel import DefaultPanel
2
+
3
+
4
+ class PipelinePanel(DefaultPanel):
5
+ def __init__(self):
6
+ super(PipelinePanel, self).__init__()
@@ -17,7 +17,7 @@ from PySide6.QtCore import (
17
17
  )
18
18
 
19
19
  from mosamatic2.core.managers.logmanager import LogManager
20
- from mosamatic2.ui.widgets.panels.taskpanel import TaskPanel
20
+ from mosamatic2.ui.widgets.panels.tasks.taskpanel import TaskPanel
21
21
  from mosamatic2.ui.settings import Settings
22
22
  from mosamatic2.ui.utils import is_macos
23
23
  from mosamatic2.ui.worker import Worker
@@ -17,7 +17,7 @@ from PySide6.QtCore import (
17
17
  )
18
18
 
19
19
  from mosamatic2.core.managers.logmanager import LogManager
20
- from mosamatic2.ui.widgets.panels.taskpanel import TaskPanel
20
+ from mosamatic2.ui.widgets.panels.tasks.taskpanel import TaskPanel
21
21
  from mosamatic2.ui.settings import Settings
22
22
  from mosamatic2.ui.utils import is_macos
23
23
  from mosamatic2.ui.worker import Worker
@@ -17,7 +17,7 @@ from PySide6.QtCore import (
17
17
  )
18
18
 
19
19
  from mosamatic2.core.managers.logmanager import LogManager
20
- from mosamatic2.ui.widgets.panels.taskpanel import TaskPanel
20
+ from mosamatic2.ui.widgets.panels.tasks.taskpanel import TaskPanel
21
21
  from mosamatic2.ui.settings import Settings
22
22
  from mosamatic2.ui.utils import is_macos
23
23
  from mosamatic2.ui.worker import Worker
@@ -17,7 +17,7 @@ from PySide6.QtCore import (
17
17
  )
18
18
 
19
19
  from mosamatic2.core.managers.logmanager import LogManager
20
- from mosamatic2.ui.widgets.panels.taskpanel import TaskPanel
20
+ from mosamatic2.ui.widgets.panels.tasks.taskpanel import TaskPanel
21
21
  from mosamatic2.ui.settings import Settings
22
22
  from mosamatic2.ui.utils import is_macos
23
23
  from mosamatic2.ui.worker import Worker
@@ -16,7 +16,7 @@ from PySide6.QtCore import (
16
16
  )
17
17
 
18
18
  from mosamatic2.core.managers.logmanager import LogManager
19
- from mosamatic2.ui.widgets.panels.taskpanel import TaskPanel
19
+ from mosamatic2.ui.widgets.panels.tasks.taskpanel import TaskPanel
20
20
  from mosamatic2.ui.settings import Settings
21
21
  from mosamatic2.ui.utils import is_macos
22
22
  from mosamatic2.ui.worker import Worker
@@ -3,7 +3,7 @@ import os
3
3
  from PySide6.QtWidgets import (
4
4
  QLineEdit,
5
5
  QCheckBox,
6
- QSpinBox,
6
+ QComboBox,
7
7
  QHBoxLayout,
8
8
  QVBoxLayout,
9
9
  QFormLayout,
@@ -16,26 +16,26 @@ from PySide6.QtCore import (
16
16
  Slot,
17
17
  )
18
18
 
19
- from mosamaticdesktop.core.utils.logmanager import LogManager
20
- from mosamaticdesktop.ui.panels.taskpanel import TaskPanel
21
- from mosamaticdesktop.ui.settings import Settings
22
- from mosamaticdesktop.ui.utils import is_macos
23
- from mosamaticdesktop.ui.worker import Worker
24
-
25
- from mosamatic.tasks import SelectSliceFromScanTask
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
26
25
 
27
26
  LOG = LogManager()
28
27
 
29
- PANEL_TITLE = 'Automatically select L3 or T4 slice from full CT scan'
30
- PANEL_NAME = 'selectslicefromscantaskpanel'
28
+ PANEL_TITLE = 'Automatically select L3 slice from full CT scan'
29
+ PANEL_NAME = 'selectslicefromscanstaskpanel'
31
30
 
32
31
 
33
- class SelectSliceFromScanTaskPanel(TaskPanel):
32
+ class SelectSliceFromScansTaskPanel(TaskPanel):
34
33
  def __init__(self):
35
- super(SelectSliceFromScanTaskPanel, self).__init__()
34
+ super(SelectSliceFromScansTaskPanel, self).__init__()
36
35
  self.set_title(PANEL_TITLE)
37
36
  self._scans_dir_line_edit = None
38
37
  self._scans_dir_select_button = None
38
+ self._vertebra_combobox = None
39
39
  self._output_dir_line_edit = None
40
40
  self._output_dir_select_button = None
41
41
  self._overwrite_checkbox = None
@@ -58,6 +58,13 @@ class SelectSliceFromScanTaskPanel(TaskPanel):
58
58
  self._scans_dir_select_button.clicked.connect(self.handle_scans_dir_select_button)
59
59
  return self._scans_dir_select_button
60
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
+
61
68
  def output_dir_line_edit(self):
62
69
  if not self._output_dir_line_edit:
63
70
  self._output_dir_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/output_dir'))
@@ -101,6 +108,7 @@ class SelectSliceFromScanTaskPanel(TaskPanel):
101
108
  output_dir_layout.addWidget(self.output_dir_line_edit())
102
109
  output_dir_layout.addWidget(self.output_dir_select_button())
103
110
  self.form_layout().addRow('Scans directory', scans_dir_layout)
111
+ # self.form_layout().addRow('Vertebra', self.vertebra_combobox())
104
112
  self.form_layout().addRow('Output directory', output_dir_layout)
105
113
  self.form_layout().addRow('Overwrite', self.overwrite_checkbox())
106
114
  layout = QVBoxLayout()
@@ -134,11 +142,11 @@ class SelectSliceFromScanTaskPanel(TaskPanel):
134
142
  LOG.info('Running task...')
135
143
  self.run_task_button().setEnabled(False)
136
144
  self.save_inputs_and_parameters()
137
- self._task = SelectSliceFromScanTask(
138
- self.scans_dir_line_edit().text(),
139
- self.output_dir_line_edit().text(),
140
- 'vertebrae_L3',
141
- self.overwrite_checkbox().isChecked()
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(),
142
150
  )
143
151
  self._worker = Worker(self._task)
144
152
  self._thread = QThread()
@@ -180,5 +188,6 @@ class SelectSliceFromScanTaskPanel(TaskPanel):
180
188
 
181
189
  def save_inputs_and_parameters(self):
182
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())
183
192
  self.settings().set(f'{PANEL_NAME}/output_dir', self.output_dir_line_edit().text())
184
193
  self.settings().set(f'{PANEL_NAME}/overwrite', self.overwrite_checkbox().isChecked())
@@ -0,0 +1,62 @@
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
+
18
+ xmin, xmax, ymin, ymax, zmin, zmax = image_data.GetExtent()
19
+ if orientation == "axial":
20
+ self.min_slice, self.max_slice = zmin, zmax
21
+ elif orientation == "sagittal":
22
+ self.min_slice, self.max_slice = xmin, xmax
23
+ elif orientation == "coronal":
24
+ self.min_slice, self.max_slice = ymin, ymax
25
+ else:
26
+ raise ValueError(f"Unknown orientation: {orientation}")
27
+
28
+ self.slice = (self.min_slice + self.max_slice) // 2
29
+ self.slice_mapper.SetSliceNumber(self.slice)
30
+ self.update_status_message()
31
+
32
+ def update_status_message(self):
33
+ window = int(self.slice_obj.GetProperty().GetColorWindow())
34
+ level = int(self.slice_obj.GetProperty().GetColorLevel())
35
+ message = f'Slice {self.slice + 1}/{self.max_slice + 1} | W: {window} L: {level}'
36
+ self.status_actor.GetMapper().SetInput(message)
37
+
38
+ def move_slice_forward(self, obj, event):
39
+ if self.slice < self.max_slice:
40
+ self.slice += 1
41
+ self.slice_mapper.SetSliceNumber(self.slice)
42
+ self.update_status_message()
43
+ self.GetInteractor().GetRenderWindow().Render()
44
+
45
+ def move_slice_backward(self, obj, event):
46
+ if self.slice > self.min_slice:
47
+ self.slice -= 1
48
+ self.slice_mapper.SetSliceNumber(self.slice)
49
+ self.update_status_message()
50
+ self.GetInteractor().GetRenderWindow().Render()
51
+
52
+ def key_press_event(self, obj, event):
53
+ key = self.GetInteractor().GetKeySym()
54
+ if key == "Up":
55
+ self.move_slice_forward(obj, event)
56
+ elif key == "Down":
57
+ self.move_slice_backward(obj, event)
58
+
59
+ def update_overlay(self, obj, event):
60
+ super(CustomInteractorStyle, self).OnMouseMove()
61
+ self.update_status_message()
62
+ self.GetInteractor().GetRenderWindow().Render()
@@ -0,0 +1,98 @@
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):
15
+ super(SliceViewer, self).__init__()
16
+ self._nifti_file_or_dicom_dir = None
17
+ self._view_orientation = 'axial'
18
+ self._vtk_widget = QVTKRenderWindowInteractor(self)
19
+ self._render_window = self._vtk_widget.GetRenderWindow()
20
+ self._interactor = self._render_window.GetInteractor()
21
+ self._interactor_style = None
22
+ layout = QVBoxLayout()
23
+ layout.addWidget(self._vtk_widget)
24
+ self.setLayout(layout)
25
+ self._default_renderer = vtk.vtkRenderer()
26
+ self._default_renderer.SetBackground(0.0, 0.0, 0.0) # black
27
+ self._render_window.AddRenderer(self._default_renderer)
28
+ self._render_window.Render()
29
+
30
+ def nifti_file_or_dicom_dir(self):
31
+ return self._nifti_file_or_dicom_dir
32
+
33
+ def set_nifti_file_or_dicom_dir(self, nifti_file_or_dicom_dir):
34
+ self._nifti_file_or_dicom_dir = nifti_file_or_dicom_dir
35
+
36
+ def create_text_actor(self, text, x, y, font_size, align_bottom=False, normalized=False):
37
+ text_prop = vtk.vtkTextProperty()
38
+ text_prop.SetFontFamilyToCourier()
39
+ text_prop.SetFontSize(font_size)
40
+ text_prop.SetVerticalJustificationToBottom() if align_bottom else text_prop.SetVerticalJustificationToTop()
41
+ text_prop.SetJustificationToLeft()
42
+ text_mapper = vtk.vtkTextMapper()
43
+ text_mapper.SetInput(text)
44
+ text_mapper.SetTextProperty(text_prop)
45
+ text_actor = vtk.vtkActor2D()
46
+ text_actor.SetMapper(text_mapper)
47
+ if normalized:
48
+ text_actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay()
49
+ text_actor.SetPosition(x, y)
50
+ return text_actor
51
+
52
+ def is_nifti_file(self, file_path):
53
+ return file_path.endswith('.nii') or file_path.endswith('.nii.gz')
54
+
55
+ def is_dicom_dir(self, dir_path):
56
+ first_dicom_file = os.path.join(dir_path, os.listdir(dir_path)[0])
57
+ return pydicom.dcmread(first_dicom_file, stop_before_pixels=True)
58
+
59
+ def load_image(self):
60
+ if not self.nifti_file_or_dicom_dir():
61
+ QMessageBox.warning(self, 'Warning', 'No NIFTI file or DICOM directory set')
62
+ return
63
+ if self.is_nifti_file(self.nifti_file_or_dicom_dir()):
64
+ reader = vtk.vtkNIFTIImageReader()
65
+ reader.SetFileName(self.nifti_file_or_dicom_dir())
66
+ elif self.is_dicom_dir(self.nifti_file_or_dicom_dir()):
67
+ reader = vtk.vtkDICOMImageReader()
68
+ reader.SetDirectoryName(self.nifti_file_or_dicom_dir())
69
+ reader.Update()
70
+ image_data = reader.GetOutput()
71
+ xmin, xmax, ymin, ymax, zmin, zmax = image_data.GetExtent()
72
+ axial_index = (zmin + zmax) // 2
73
+ slice_mapper = vtk.vtkImageSliceMapper()
74
+ slice_mapper.SetInputData(image_data)
75
+ slice_mapper.SetOrientationToZ() # axial orientation
76
+ slice_mapper.SetSliceNumber(axial_index)
77
+ slice = vtk.vtkImageSlice()
78
+ slice.GetProperty().SetColorWindow(400)
79
+ slice.GetProperty().SetColorLevel(40)
80
+ slice.SetMapper(slice_mapper)
81
+ slice_text_actor = self.create_text_actor("", 0.01, 0.01, 12, align_bottom=True, normalized=True)
82
+ usage_text_actor = self.create_text_actor(
83
+ "- Slice with mouse wheel or Up/Down-Key (first click inside viewer)\n"
84
+ "- Zoom with pressed right mouse button while dragging\n"
85
+ "- Pan with middle mouse button while dragging\n"
86
+ "- Change contrast/brightness with pressed left mouse while dragging",
87
+ 0.01, 0.99, 12, normalized=True)
88
+ ren = vtk.vtkRenderer()
89
+ ren.AddActor2D(slice_text_actor)
90
+ ren.AddActor2D(usage_text_actor)
91
+ ren.AddViewProp(slice)
92
+ ren.ResetCamera()
93
+ self._render_window.RemoveRenderer(self._default_renderer)
94
+ self._render_window.AddRenderer(ren)
95
+ self._interactor_style = CustomInteractorStyle(image_data, slice_mapper, slice_text_actor, slice)
96
+ self._interactor.SetInteractorStyle(self._interactor_style)
97
+ self._interactor.Initialize()
98
+ self._render_window.Render()
@@ -0,0 +1,106 @@
1
+ from PySide6.QtWidgets import (
2
+ QLineEdit,
3
+ QHBoxLayout,
4
+ QVBoxLayout,
5
+ QFormLayout,
6
+ QPushButton,
7
+ QFileDialog,
8
+ )
9
+ from mosamatic2.ui.widgets.panels.visualizations.visualization import Visualization
10
+ from mosamatic2.ui.widgets.panels.visualizations.slicevisualization.sliceviewer import SliceViewer
11
+ from mosamatic2.core.managers.logmanager import LogManager
12
+ from mosamatic2.ui.settings import Settings
13
+ from mosamatic2.ui.utils import is_macos
14
+
15
+ LOG = LogManager()
16
+ PANEL_TITLE = 'SliceVisualization'
17
+ PANEL_NAME = 'slicevisualization'
18
+
19
+
20
+ class SliceVisualization(Visualization):
21
+ def __init__(self):
22
+ super(SliceVisualization, self).__init__()
23
+ self.set_title(PANEL_TITLE)
24
+ self._image_line_edit = None
25
+ self._image_select_button = None
26
+ self._image_dir_select_button = None
27
+ self._load_image_button = None
28
+ self._slice_viewer = None
29
+ self._form_layout = None
30
+ self._settings = None
31
+ self.init_layout()
32
+
33
+ def image_line_edit(self):
34
+ if not self._image_line_edit:
35
+ self._image_line_edit = QLineEdit(self.settings().get(f'{PANEL_NAME}/image'))
36
+ return self._image_line_edit
37
+
38
+ def image_select_button(self):
39
+ if not self._image_select_button:
40
+ self._image_select_button = QPushButton('Select Image')
41
+ self._image_select_button.clicked.connect(self.handle_image_select_button)
42
+ return self._image_select_button
43
+
44
+ def image_dir_select_button(self):
45
+ if not self._image_dir_select_button:
46
+ self._image_dir_select_button = QPushButton('Select Directory')
47
+ self._image_dir_select_button.clicked.connect(self.handle_image_dir_select_button)
48
+ return self._image_dir_select_button
49
+
50
+ def load_image_button(self):
51
+ if not self._load_image_button:
52
+ self._load_image_button = QPushButton('Load')
53
+ self._load_image_button.clicked.connect(self.handle_load_image_button)
54
+ return self._load_image_button
55
+
56
+ def slice_viewer(self):
57
+ if not self._slice_viewer:
58
+ self._slice_viewer = SliceViewer()
59
+ return self._slice_viewer
60
+
61
+ def form_layout(self):
62
+ if not self._form_layout:
63
+ self._form_layout = QFormLayout()
64
+ if is_macos():
65
+ self._form_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
66
+ return self._form_layout
67
+
68
+ def settings(self):
69
+ if not self._settings:
70
+ self._settings = Settings()
71
+ return self._settings
72
+
73
+ def init_layout(self):
74
+ image_layout = QHBoxLayout()
75
+ image_layout.addWidget(self.image_line_edit())
76
+ image_layout.addWidget(self.image_select_button())
77
+ image_layout.addWidget(self.image_dir_select_button())
78
+ self.form_layout().addRow('NIFTI file or DICOM directory', image_layout)
79
+ layout = QVBoxLayout()
80
+ layout.addLayout(self.form_layout())
81
+ layout.addWidget(self.load_image_button())
82
+ layout.addWidget(self.slice_viewer())
83
+ self.setLayout(layout)
84
+ self.setObjectName(PANEL_NAME)
85
+
86
+ def handle_image_select_button(self):
87
+ last_directory = self.settings().get('last_directory')
88
+ file_path, _ = QFileDialog.getOpenFileName(dir=last_directory)
89
+ if file_path:
90
+ self.image_line_edit().setText(file_path)
91
+ self.settings().set('last_directory', file_path)
92
+
93
+ def handle_image_dir_select_button(self):
94
+ last_directory = self.settings().get('last_directory')
95
+ dir_path = QFileDialog.getExistingDirectory(dir=last_directory)
96
+ if dir_path:
97
+ self.image_line_edit().setText(dir_path)
98
+ self.settings().set('last_directory', dir_path)
99
+
100
+ def handle_load_image_button(self):
101
+ self.slice_viewer().set_nifti_file_or_dicom_dir(self.image_line_edit().text())
102
+ # self.slice_viewer().set_view_orientation('axial')
103
+ self.slice_viewer().load_image()
104
+
105
+ def save_inputs_and_parameters(self):
106
+ self.settings().set(f'{PANEL_NAME}/image', self.image_line_edit().text())
@@ -0,0 +1,6 @@
1
+ from mosamatic2.ui.widgets.panels.defaultpanel import DefaultPanel
2
+
3
+
4
+ class Visualization(DefaultPanel):
5
+ def __init__(self):
6
+ super(Visualization, self).__init__()
@@ -1 +0,0 @@
1
- 2.0.4
File without changes
File without changes