unveil-python 0.4.0__tar.gz → 0.5.1__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.
- unveil_python-0.5.1/PKG-INFO +74 -0
- unveil_python-0.5.1/README.md +55 -0
- {unveil_python-0.4.0 → unveil_python-0.5.1}/src/unveil/__init__.py +1 -1
- {unveil_python-0.4.0 → unveil_python-0.5.1}/src/unveil/core.py +401 -120
- unveil_python-0.5.1/src/unveil_python.egg-info/PKG-INFO +74 -0
- unveil_python-0.4.0/PKG-INFO +0 -40
- unveil_python-0.4.0/README.md +0 -21
- unveil_python-0.4.0/src/unveil_python.egg-info/PKG-INFO +0 -40
- {unveil_python-0.4.0 → unveil_python-0.5.1}/LICENSE +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.1}/pyproject.toml +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.1}/setup.cfg +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.1}/src/unveil/__main__.py +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.1}/src/unveil_python.egg-info/SOURCES.txt +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.1}/src/unveil_python.egg-info/dependency_links.txt +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.1}/src/unveil_python.egg-info/requires.txt +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.1}/src/unveil_python.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unveil-python
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: Implementation of UNVEIL
|
|
5
|
+
Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: GitHub, https://github.com/DelinteNicolas/UNVEIL
|
|
8
|
+
Keywords: tractography,viewer
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: dipy
|
|
15
|
+
Requires-Dist: pyvista
|
|
16
|
+
Requires-Dist: pyvistaqt
|
|
17
|
+
Requires-Dist: PyQt6
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# UNVEIL
|
|
21
|
+
[](https://pypi.org/project/unveil-python/)
|
|
22
|
+
[](https://pypi.org/project/unveil-python/)
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
UNVEIL is a Python-based visualization tool for interactive exploration of diffusion MRI tractography, anatomical volumes, cortical surfaces, and ROI segmentations. It combines a 3D PyVista viewer with an orthogonal slice viewer.
|
|
27
|
+
|
|
28
|
+
<img width="1297" height="757" alt="image" src="https://github.com/user-attachments/assets/28807e18-17fb-485b-a8ce-2179cdef796b" />
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
The GUI is launched with the following python command in a python console:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
import unveil.__main__
|
|
36
|
+
```
|
|
37
|
+
or with
|
|
38
|
+
```
|
|
39
|
+
python -c "import unveil.__main__"
|
|
40
|
+
```
|
|
41
|
+
from a terminal with a python environment activated.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **3D Tractography Visualization**
|
|
46
|
+
- Load and display `.trk` streamline files.
|
|
47
|
+
- RGB orientation coloring or scalar-based coloring from NIfTI volumes.
|
|
48
|
+
- Adjustable opacity and point/line rendering.
|
|
49
|
+
- **Volume Rendering**
|
|
50
|
+
- Load `.nii.gz` images.
|
|
51
|
+
- Interactive volume rendering and orthogonal slice planes.
|
|
52
|
+
- Adjustable opacity.
|
|
53
|
+
- **ROI Visualization**
|
|
54
|
+
- Load ROI masks from `.nii` / `.nii.gz`.
|
|
55
|
+
- Automatic surface extraction and smoothing.
|
|
56
|
+
- Individual visibility and color control.
|
|
57
|
+
- **Surface Visualization**
|
|
58
|
+
- Load GIFTI (`.gii`) cortical surfaces.
|
|
59
|
+
- Adjustable opacity and smooth shading.
|
|
60
|
+
- **Orthogonal Viewer**
|
|
61
|
+
- Axial, coronal, and sagittal views.
|
|
62
|
+
- Mouse-wheel slice navigation.
|
|
63
|
+
- Click-to-navigate crosshair synchronization.
|
|
64
|
+
- ROI overlay display.
|
|
65
|
+
- Screenshot export.
|
|
66
|
+
- **Scene Management**
|
|
67
|
+
- Actor visibility control.
|
|
68
|
+
- ROI color editing.
|
|
69
|
+
- Background color toggle.
|
|
70
|
+
- Color-blind-friendly tractography coloring.
|
|
71
|
+
- **Export**
|
|
72
|
+
- 3D screenshots.
|
|
73
|
+
- Orthogonal-view screenshots.
|
|
74
|
+
- Animated 360° GIF generation.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# UNVEIL
|
|
2
|
+
[](https://pypi.org/project/unveil-python/)
|
|
3
|
+
[](https://pypi.org/project/unveil-python/)
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
UNVEIL is a Python-based visualization tool for interactive exploration of diffusion MRI tractography, anatomical volumes, cortical surfaces, and ROI segmentations. It combines a 3D PyVista viewer with an orthogonal slice viewer.
|
|
8
|
+
|
|
9
|
+
<img width="1297" height="757" alt="image" src="https://github.com/user-attachments/assets/28807e18-17fb-485b-a8ce-2179cdef796b" />
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
The GUI is launched with the following python command in a python console:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
import unveil.__main__
|
|
17
|
+
```
|
|
18
|
+
or with
|
|
19
|
+
```
|
|
20
|
+
python -c "import unveil.__main__"
|
|
21
|
+
```
|
|
22
|
+
from a terminal with a python environment activated.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **3D Tractography Visualization**
|
|
27
|
+
- Load and display `.trk` streamline files.
|
|
28
|
+
- RGB orientation coloring or scalar-based coloring from NIfTI volumes.
|
|
29
|
+
- Adjustable opacity and point/line rendering.
|
|
30
|
+
- **Volume Rendering**
|
|
31
|
+
- Load `.nii.gz` images.
|
|
32
|
+
- Interactive volume rendering and orthogonal slice planes.
|
|
33
|
+
- Adjustable opacity.
|
|
34
|
+
- **ROI Visualization**
|
|
35
|
+
- Load ROI masks from `.nii` / `.nii.gz`.
|
|
36
|
+
- Automatic surface extraction and smoothing.
|
|
37
|
+
- Individual visibility and color control.
|
|
38
|
+
- **Surface Visualization**
|
|
39
|
+
- Load GIFTI (`.gii`) cortical surfaces.
|
|
40
|
+
- Adjustable opacity and smooth shading.
|
|
41
|
+
- **Orthogonal Viewer**
|
|
42
|
+
- Axial, coronal, and sagittal views.
|
|
43
|
+
- Mouse-wheel slice navigation.
|
|
44
|
+
- Click-to-navigate crosshair synchronization.
|
|
45
|
+
- ROI overlay display.
|
|
46
|
+
- Screenshot export.
|
|
47
|
+
- **Scene Management**
|
|
48
|
+
- Actor visibility control.
|
|
49
|
+
- ROI color editing.
|
|
50
|
+
- Background color toggle.
|
|
51
|
+
- Color-blind-friendly tractography coloring.
|
|
52
|
+
- **Export**
|
|
53
|
+
- 3D screenshots.
|
|
54
|
+
- Orthogonal-view screenshots.
|
|
55
|
+
- Animated 360° GIF generation.
|
|
@@ -10,14 +10,17 @@ import nibabel as nib
|
|
|
10
10
|
import pyvista as pv
|
|
11
11
|
from pyvistaqt import QtInteractor
|
|
12
12
|
from PyQt6.QtCore import Qt
|
|
13
|
-
from PyQt6.QtGui import QAction
|
|
14
13
|
from PyQt6.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout,
|
|
15
14
|
QPushButton, QFileDialog, QCheckBox, QSlider,
|
|
16
15
|
QLabel, QComboBox, QMainWindow, QLineEdit,
|
|
17
|
-
QDockWidget, QTreeWidget, QTreeWidgetItem
|
|
16
|
+
QDockWidget, QTreeWidget, QTreeWidgetItem,
|
|
17
|
+
QColorDialog, QTabWidget)
|
|
18
|
+
from PyQt6.QtGui import QAction, QColor
|
|
18
19
|
from dipy.io.streamline import load_tractogram
|
|
19
20
|
from matplotlib import pyplot as plt
|
|
20
21
|
from matplotlib.colors import LinearSegmentedColormap
|
|
22
|
+
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
|
|
23
|
+
from matplotlib.figure import Figure
|
|
21
24
|
# from unravel.utils import get_streamline_density
|
|
22
25
|
|
|
23
26
|
|
|
@@ -185,18 +188,34 @@ def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
|
|
|
185
188
|
class TrkViewer(QWidget):
|
|
186
189
|
def __init__(self):
|
|
187
190
|
super().__init__()
|
|
188
|
-
self.trk_file = None
|
|
189
|
-
self.nii_file = None
|
|
190
191
|
self.nii_data = None
|
|
191
192
|
self.grid = None
|
|
192
|
-
self.
|
|
193
|
+
self.actor_types = {}
|
|
193
194
|
self.x = 0
|
|
194
195
|
self.y = 0
|
|
195
196
|
self.z = 0
|
|
197
|
+
self.colormap_list = ['gray',
|
|
198
|
+
'viridis', 'plasma', 'inferno', 'magma', 'cividis',
|
|
199
|
+
'Greys', 'Purples', 'Blues', 'Greens', 'Oranges',
|
|
200
|
+
'Reds', 'bone', 'pink', 'spring', 'summer',
|
|
201
|
+
'autumn', 'winter', 'cool', 'Wistia', 'hot',
|
|
202
|
+
'afmhot', 'gist_heat', 'copper', 'RdBu', 'RdYlBu',
|
|
203
|
+
'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic',
|
|
204
|
+
'berlin', 'managua', 'vanimo', 'twilight',
|
|
205
|
+
'twilight_shifted', 'hsv', 'Pastel1', 'Pastel2',
|
|
206
|
+
'Paired', 'Accent', 'okabe_ito', 'Dark2', 'Set1',
|
|
207
|
+
'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c',
|
|
208
|
+
'flag', 'prism', 'ocean', 'gist_earth', 'terrain',
|
|
209
|
+
'gist_stern', 'gnuplot', 'gnuplot2', 'CMRmap',
|
|
210
|
+
'cubehelix', 'brg', 'gist_rainbow', 'rainbow',
|
|
211
|
+
'jet', 'turbo', 'nipy_spectral', 'gist_ncar']
|
|
196
212
|
self.initUI()
|
|
197
213
|
self.background = 'white'
|
|
198
214
|
self.color_blind = False
|
|
199
215
|
self.flicker = False
|
|
216
|
+
self.roi_colors = {}
|
|
217
|
+
self.roi_volumes = {}
|
|
218
|
+
self.roi_visibility = {}
|
|
200
219
|
|
|
201
220
|
def initUI(self):
|
|
202
221
|
# Create the main layout
|
|
@@ -209,10 +228,10 @@ class TrkViewer(QWidget):
|
|
|
209
228
|
btn_xy = QPushButton("Axe 1")
|
|
210
229
|
btn_xz = QPushButton("Axe 2")
|
|
211
230
|
btn_yz = QPushButton("Axe 3")
|
|
212
|
-
btn_iso.clicked.connect(self.view_isometric)
|
|
213
|
-
btn_xy.clicked.connect(self.view_xy)
|
|
214
|
-
btn_xz.clicked.connect(self.view_xz)
|
|
215
|
-
btn_yz.clicked.connect(self.view_yz)
|
|
231
|
+
btn_iso.clicked.connect(lambda: self.set_view("view_isometric"))
|
|
232
|
+
btn_xy.clicked.connect(lambda: self.set_view("view_xy"))
|
|
233
|
+
btn_xz.clicked.connect(lambda: self.set_view("view_xz"))
|
|
234
|
+
btn_yz.clicked.connect(lambda: self.set_view("view_yz"))
|
|
216
235
|
cam_layout.addWidget(camera_label)
|
|
217
236
|
cam_layout.addWidget(btn_iso)
|
|
218
237
|
cam_layout.addWidget(btn_xy)
|
|
@@ -250,7 +269,7 @@ class TrkViewer(QWidget):
|
|
|
250
269
|
self.showPointsCheckbox.stateChanged.connect(self.update_trk_viewer)
|
|
251
270
|
control_layout.addWidget(self.showPointsCheckbox)
|
|
252
271
|
|
|
253
|
-
self.colorMapLabel = QLabel('Color
|
|
272
|
+
self.colorMapLabel = QLabel('Tract Color Mode:')
|
|
254
273
|
control_layout.addWidget(self.colorMapLabel)
|
|
255
274
|
|
|
256
275
|
self.colorMapComboBox = QComboBox()
|
|
@@ -258,13 +277,17 @@ class TrkViewer(QWidget):
|
|
|
258
277
|
self.colorMapComboBox.currentIndexChanged.connect(
|
|
259
278
|
self.update_trk_viewer)
|
|
260
279
|
control_layout.addWidget(self.colorMapComboBox)
|
|
261
|
-
self.colorMapEdit = QLineEdit()
|
|
262
|
-
self.colorMapEdit.textChanged.connect(self.update_trk_viewer)
|
|
263
|
-
self.colorMapEdit.setToolTip(
|
|
264
|
-
'Insert color map name. Name must be in the matplotlib library. Scalar nii.gz must be loaded.')
|
|
265
|
-
control_layout.addWidget(self.colorMapEdit)
|
|
266
280
|
|
|
267
|
-
self.
|
|
281
|
+
self.tractColorMapLabel = QLabel('Tract Colormap (Scalar):')
|
|
282
|
+
control_layout.addWidget(self.tractColorMapLabel)
|
|
283
|
+
|
|
284
|
+
self.tractColorMapComboBox = QComboBox()
|
|
285
|
+
self.tractColorMapComboBox.addItems(self.colormap_list)
|
|
286
|
+
self.tractColorMapComboBox.currentIndexChanged.connect(
|
|
287
|
+
self.update_trk_viewer)
|
|
288
|
+
control_layout.addWidget(self.tractColorMapComboBox)
|
|
289
|
+
|
|
290
|
+
self.volume_label = QLabel('Volume Opacity')
|
|
268
291
|
control_layout.addWidget(self.volume_label)
|
|
269
292
|
|
|
270
293
|
self.nii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
@@ -274,6 +297,15 @@ class TrkViewer(QWidget):
|
|
|
274
297
|
self.nii_opacitySlider.sliderReleased.connect(self.update_nii_viewer)
|
|
275
298
|
control_layout.addWidget(self.nii_opacitySlider)
|
|
276
299
|
|
|
300
|
+
self.niiColorMapLabel = QLabel('Global Volume Colormap:')
|
|
301
|
+
control_layout.addWidget(self.niiColorMapLabel)
|
|
302
|
+
|
|
303
|
+
self.niiColorMapComboBox = QComboBox()
|
|
304
|
+
self.niiColorMapComboBox.addItems(self.colormap_list)
|
|
305
|
+
self.niiColorMapComboBox.currentIndexChanged.connect(
|
|
306
|
+
self.on_nii_colormap_changed)
|
|
307
|
+
control_layout.addWidget(self.niiColorMapComboBox)
|
|
308
|
+
|
|
277
309
|
self.showSlicesCheckbox = QCheckBox('Show Slices')
|
|
278
310
|
self.showSlicesCheckbox.stateChanged.connect(self.update_nii_viewer)
|
|
279
311
|
control_layout.addWidget(self.showSlicesCheckbox)
|
|
@@ -284,7 +316,8 @@ class TrkViewer(QWidget):
|
|
|
284
316
|
self.XSlider.setMinimum(0)
|
|
285
317
|
self.XSlider.setMaximum(100)
|
|
286
318
|
self.XSlider.setValue(100)
|
|
287
|
-
self.XSlider.sliderReleased.connect(
|
|
319
|
+
self.XSlider.sliderReleased.connect(
|
|
320
|
+
lambda: self._update_slice('x', 'nii_x'))
|
|
288
321
|
control_layout.addWidget(self.XSlider)
|
|
289
322
|
self.y_label = QLabel('Y')
|
|
290
323
|
control_layout.addWidget(self.y_label)
|
|
@@ -292,7 +325,8 @@ class TrkViewer(QWidget):
|
|
|
292
325
|
self.YSlider.setMinimum(0)
|
|
293
326
|
self.YSlider.setMaximum(100)
|
|
294
327
|
self.YSlider.setValue(100)
|
|
295
|
-
self.YSlider.sliderReleased.connect(
|
|
328
|
+
self.YSlider.sliderReleased.connect(
|
|
329
|
+
lambda: self._update_slice('y', 'nii_y'))
|
|
296
330
|
control_layout.addWidget(self.YSlider)
|
|
297
331
|
self.z_label = QLabel('Z')
|
|
298
332
|
control_layout.addWidget(self.z_label)
|
|
@@ -300,7 +334,8 @@ class TrkViewer(QWidget):
|
|
|
300
334
|
self.ZSlider.setMinimum(0)
|
|
301
335
|
self.ZSlider.setMaximum(100)
|
|
302
336
|
self.ZSlider.setValue(100)
|
|
303
|
-
self.ZSlider.sliderReleased.connect(
|
|
337
|
+
self.ZSlider.sliderReleased.connect(
|
|
338
|
+
lambda: self._update_slice('z', 'nii_z'))
|
|
304
339
|
control_layout.addWidget(self.ZSlider)
|
|
305
340
|
|
|
306
341
|
# Gifti
|
|
@@ -333,23 +368,22 @@ class TrkViewer(QWidget):
|
|
|
333
368
|
return
|
|
334
369
|
|
|
335
370
|
for trk_path in filePaths:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
self.names.append(self.trk_file)
|
|
371
|
+
print(f"Loaded TRK file: {trk_path}")
|
|
372
|
+
self.actor_types[trk_path] = 'trk'
|
|
339
373
|
|
|
340
374
|
self.update_trk_viewer(reset_camera=True)
|
|
341
|
-
self.
|
|
375
|
+
self.window().refreshActorList()
|
|
342
376
|
|
|
343
377
|
def loadNiftiFile(self):
|
|
344
378
|
options = QFileDialog.Options()
|
|
345
379
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
346
380
|
self, "Open .nii.gz File", "", "Nifti Files (*.nii.gz)", options=options)
|
|
347
381
|
if filePath:
|
|
348
|
-
|
|
349
|
-
print(f"Loaded NIfTI file: {self.nii_file}")
|
|
382
|
+
print(f"Loaded NIfTI file: {filePath}")
|
|
350
383
|
img = nib.load(filePath)
|
|
351
384
|
self.nii_affine = img.affine
|
|
352
385
|
self.nii_data = img.get_fdata()
|
|
386
|
+
|
|
353
387
|
grid = pv.ImageData()
|
|
354
388
|
grid.dimensions = np.array(self.nii_data.shape) + 1
|
|
355
389
|
grid.cell_data['values'] = self.nii_data.flatten(order='F')
|
|
@@ -360,7 +394,12 @@ class TrkViewer(QWidget):
|
|
|
360
394
|
self.ZSlider.setMaximum(self.nii_data.shape[2])
|
|
361
395
|
|
|
362
396
|
self.update_nii_viewer(reset_camera=False)
|
|
363
|
-
self.
|
|
397
|
+
self.window().refreshActorList()
|
|
398
|
+
|
|
399
|
+
# Update 2D view with current colormap
|
|
400
|
+
self.nii_data[self.nii_data == 0] = None
|
|
401
|
+
self.window().ortho_viewer.set_volume(self.nii_data, self.nii_affine)
|
|
402
|
+
self.window().ortho_viewer.set_colormap(self.niiColorMapComboBox.currentText())
|
|
364
403
|
|
|
365
404
|
def loadROIFile(self):
|
|
366
405
|
"""Load one or multiple ROI nifti volumes and render as surfaces."""
|
|
@@ -383,26 +422,34 @@ class TrkViewer(QWidget):
|
|
|
383
422
|
|
|
384
423
|
smooth = mesh.smooth_taubin(n_iter=12)
|
|
385
424
|
|
|
386
|
-
actor_name = f"
|
|
425
|
+
actor_name = f"{roi_path.split('/')[-1]}"
|
|
426
|
+
self.actor_types[actor_name] = "roi"
|
|
427
|
+
self.roi_volumes[actor_name] = roi
|
|
428
|
+
self.roi_visibility[actor_name] = True
|
|
429
|
+
|
|
430
|
+
default_color = np.array([1.0, 1.0, 1.0])
|
|
431
|
+
|
|
432
|
+
self.roi_colors[actor_name] = default_color
|
|
387
433
|
|
|
388
434
|
self.plotter.add_mesh(smooth, name=actor_name, opacity=1,
|
|
389
|
-
color=
|
|
435
|
+
color=default_color, user_matrix=img.affine,
|
|
390
436
|
reset_camera=False, point_size=0,
|
|
391
437
|
render_lines_as_tubes=True)
|
|
392
438
|
|
|
393
|
-
self.
|
|
439
|
+
self.window().refreshActorList()
|
|
440
|
+
|
|
441
|
+
self.window().ortho_viewer.add_roi(actor_name, roi, default_color)
|
|
394
442
|
|
|
395
443
|
def loadGiftiFile(self):
|
|
396
444
|
options = QFileDialog.Options()
|
|
397
445
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
398
446
|
self, "Open .gii File", "", "Gifti Files (*.gii)", options=options)
|
|
399
447
|
if filePath:
|
|
400
|
-
|
|
401
|
-
print(f"Loaded GIfTI file: {self.gii_file}")
|
|
448
|
+
print(f"Loaded GIfTI file: {filePath}")
|
|
402
449
|
self.gii_mesh = gifti_to_pyvista(filePath)
|
|
403
450
|
|
|
404
451
|
self.update_gii_viewer(reset_camera=False)
|
|
405
|
-
self.
|
|
452
|
+
self.window().refreshActorList()
|
|
406
453
|
|
|
407
454
|
def set_background_color(self):
|
|
408
455
|
|
|
@@ -413,29 +460,16 @@ class TrkViewer(QWidget):
|
|
|
413
460
|
|
|
414
461
|
self.plotter.background_color = self.background
|
|
415
462
|
|
|
463
|
+
self.window().ortho_viewer.set_background(self.background)
|
|
464
|
+
|
|
416
465
|
def toggleColorBlind(self):
|
|
417
466
|
|
|
418
467
|
self.color_blind = not self.color_blind
|
|
419
468
|
self.update_trk_viewer()
|
|
420
469
|
|
|
421
|
-
def
|
|
422
|
-
self.plotter
|
|
423
|
-
self.plotter.camera.azimuth += self.flicker*180
|
|
424
|
-
self.flicker = not self.flicker
|
|
425
|
-
|
|
426
|
-
def view_xy(self):
|
|
427
|
-
self.plotter.view_xy()
|
|
428
|
-
self.plotter.camera.azimuth += self.flicker*180
|
|
429
|
-
self.flicker = not self.flicker
|
|
430
|
-
|
|
431
|
-
def view_xz(self):
|
|
432
|
-
self.plotter.view_xz()
|
|
433
|
-
self.plotter.camera.azimuth += self.flicker*180
|
|
434
|
-
self.flicker = not self.flicker
|
|
435
|
-
|
|
436
|
-
def view_yz(self):
|
|
437
|
-
self.plotter.view_yz()
|
|
438
|
-
self.plotter.camera.azimuth += self.flicker*180
|
|
470
|
+
def set_view(self, method):
|
|
471
|
+
getattr(self.plotter, method)()
|
|
472
|
+
self.plotter.camera.azimuth += self.flicker * 180
|
|
439
473
|
self.flicker = not self.flicker
|
|
440
474
|
|
|
441
475
|
def take_screenshot(self):
|
|
@@ -487,104 +521,293 @@ class TrkViewer(QWidget):
|
|
|
487
521
|
self.plotter.render()
|
|
488
522
|
offscreen.close()
|
|
489
523
|
|
|
524
|
+
def on_nii_colormap_changed(self):
|
|
525
|
+
self.update_nii_viewer()
|
|
526
|
+
if hasattr(self.window(), 'ortho_viewer'):
|
|
527
|
+
self.window().ortho_viewer.set_colormap(self.niiColorMapComboBox.currentText())
|
|
528
|
+
|
|
490
529
|
def update_trk_viewer(self, reset_camera: bool = False):
|
|
491
530
|
|
|
492
531
|
opacity = self.opacitySlider.value() / 100.0
|
|
493
532
|
show_points = self.showPointsCheckbox.isChecked()
|
|
494
533
|
|
|
495
|
-
|
|
496
|
-
if
|
|
534
|
+
color_mode = self.colorMapComboBox.currentText()
|
|
535
|
+
if color_mode == 'flesh':
|
|
497
536
|
color_map = 'flesh'
|
|
498
537
|
scalar = None
|
|
499
|
-
elif
|
|
538
|
+
elif color_mode == 'rgb':
|
|
500
539
|
color_map = 'plasma'
|
|
501
540
|
scalar = None
|
|
502
541
|
else:
|
|
503
|
-
color_map = self.
|
|
542
|
+
color_map = self.tractColorMapComboBox.currentText()
|
|
504
543
|
scalar = self.nii_data
|
|
505
544
|
|
|
506
|
-
for file in self.
|
|
507
|
-
|
|
508
|
-
plot_trk(file, opacity=opacity, plotter=self.plotter, scalar=scalar,
|
|
509
|
-
show_points=show_points, color_map=color_map,
|
|
510
|
-
name=file, background=self.background,
|
|
511
|
-
reset_camera=reset_camera, color_blind=self.color_blind)
|
|
512
|
-
|
|
513
|
-
def _update_nii_x(self):
|
|
545
|
+
for file in list(self.actor_types):
|
|
514
546
|
|
|
515
|
-
|
|
547
|
+
if self.actor_types.get(file) == 'trk':
|
|
516
548
|
|
|
517
|
-
|
|
518
|
-
|
|
549
|
+
plot_trk(file, opacity=opacity, plotter=self.plotter, scalar=scalar,
|
|
550
|
+
show_points=show_points, color_map=color_map,
|
|
551
|
+
name=file, background=self.background,
|
|
552
|
+
reset_camera=reset_camera, color_blind=self.color_blind)
|
|
519
553
|
|
|
520
|
-
|
|
521
|
-
self.plotter.add_mesh(slice_x, cmap='grey', name='nii_x',
|
|
522
|
-
show_scalar_bar=False, point_size=0,
|
|
523
|
-
render_lines_as_tubes=True,
|
|
524
|
-
reset_camera=False,
|
|
525
|
-
user_matrix=self.nii_affine)
|
|
526
|
-
else:
|
|
527
|
-
self.plotter.remove_actor('nii_x')
|
|
554
|
+
def _update_slice(self, axis, actor_name):
|
|
528
555
|
|
|
529
|
-
|
|
556
|
+
if self.showSlicesCheckbox.isChecked() and self.grid is not None:
|
|
530
557
|
|
|
531
|
-
|
|
558
|
+
center = (self.XSlider.value(), self.YSlider.value(),
|
|
559
|
+
self.ZSlider.value())
|
|
532
560
|
|
|
533
|
-
|
|
534
|
-
|
|
561
|
+
slice_mesh = self.grid.slice(axis, origin=center)
|
|
562
|
+
cmap_name = self.niiColorMapComboBox.currentText()
|
|
535
563
|
|
|
536
|
-
|
|
537
|
-
self.plotter.add_mesh(slice_y, cmap='grey', name='nii_y',
|
|
564
|
+
self.plotter.add_mesh(slice_mesh, cmap=cmap_name, name=actor_name,
|
|
538
565
|
show_scalar_bar=False, point_size=0,
|
|
539
566
|
render_lines_as_tubes=True,
|
|
540
567
|
reset_camera=False,
|
|
541
568
|
user_matrix=self.nii_affine)
|
|
542
|
-
else:
|
|
543
|
-
self.plotter.remove_actor('nii_y')
|
|
544
|
-
|
|
545
|
-
def _update_nii_z(self):
|
|
546
|
-
|
|
547
|
-
if self.showSlicesCheckbox.isChecked():
|
|
548
569
|
|
|
549
|
-
center = (self.XSlider.value(),
|
|
550
|
-
self.YSlider.value(), self.ZSlider.value())
|
|
551
|
-
|
|
552
|
-
slice_z = self.grid.slice('z', center)
|
|
553
|
-
self.plotter.add_mesh(slice_z, cmap='grey', name='nii_z',
|
|
554
|
-
show_scalar_bar=False, point_size=0,
|
|
555
|
-
render_lines_as_tubes=True,
|
|
556
|
-
reset_camera=False,
|
|
557
|
-
user_matrix=self.nii_affine)
|
|
558
570
|
else:
|
|
559
|
-
self.plotter.remove_actor(
|
|
571
|
+
self.plotter.remove_actor(actor_name)
|
|
560
572
|
|
|
561
573
|
def update_nii_viewer(self, reset_camera: bool = False):
|
|
574
|
+
if self.grid is None:
|
|
575
|
+
return
|
|
562
576
|
|
|
563
|
-
self.
|
|
564
|
-
self.
|
|
565
|
-
self.
|
|
577
|
+
self._update_slice('x', 'nii_x')
|
|
578
|
+
self._update_slice('y', 'nii_y')
|
|
579
|
+
self._update_slice('z', 'nii_z')
|
|
566
580
|
|
|
567
581
|
opacity = self.nii_opacitySlider.value()/1000
|
|
568
|
-
self.
|
|
582
|
+
cmap_name = self.niiColorMapComboBox.currentText()
|
|
583
|
+
|
|
584
|
+
self.plotter.add_volume(self.grid, cmap=cmap_name, opacity=[0, opacity],
|
|
569
585
|
show_scalar_bar=False, name='nii_volume',
|
|
570
586
|
reset_camera=reset_camera,
|
|
571
587
|
user_matrix=self.nii_affine)
|
|
572
588
|
|
|
573
|
-
def update_gii_viewer(self, reset_camera
|
|
589
|
+
def update_gii_viewer(self, reset_camera=False):
|
|
590
|
+
|
|
591
|
+
if not hasattr(self, "gii_mesh"):
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
opacity = self.gii_opacitySlider.value() / 100
|
|
595
|
+
|
|
596
|
+
self.plotter.add_mesh(
|
|
597
|
+
self.gii_mesh,
|
|
598
|
+
color="ghostwhite",
|
|
599
|
+
culling="back",
|
|
600
|
+
smooth_shading=True,
|
|
601
|
+
opacity=opacity,
|
|
602
|
+
name="gii_surface",
|
|
603
|
+
reset_camera=reset_camera,
|
|
604
|
+
point_size=0,
|
|
605
|
+
render_lines_as_tubes=True,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# self.plotter.render()
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
class OrthogonalViewer(QWidget):
|
|
612
|
+
|
|
613
|
+
def __init__(self):
|
|
614
|
+
super().__init__()
|
|
615
|
+
|
|
616
|
+
self.volume = None
|
|
617
|
+
self.affine = None
|
|
618
|
+
self.cmap = "gray"
|
|
619
|
+
|
|
620
|
+
self.rois = {}
|
|
621
|
+
self.roi_visibility = {}
|
|
622
|
+
|
|
623
|
+
self.background = "white"
|
|
624
|
+
|
|
625
|
+
layout = QVBoxLayout(self)
|
|
626
|
+
|
|
627
|
+
toolbar = QHBoxLayout()
|
|
628
|
+
|
|
629
|
+
btn_screenshot = QPushButton("Screenshot")
|
|
630
|
+
btn_screenshot.clicked.connect(self.take_screenshot)
|
|
631
|
+
|
|
632
|
+
toolbar.addStretch()
|
|
633
|
+
toolbar.addWidget(btn_screenshot)
|
|
634
|
+
|
|
635
|
+
layout.addLayout(toolbar)
|
|
636
|
+
|
|
637
|
+
self.fig = Figure(figsize=(12, 4), layout='constrained')
|
|
638
|
+
self.canvas = FigureCanvasQTAgg(self.fig)
|
|
639
|
+
|
|
640
|
+
self.ax_axial = self.fig.add_subplot(131)
|
|
641
|
+
self.ax_coronal = self.fig.add_subplot(132)
|
|
642
|
+
self.ax_sagittal = self.fig.add_subplot(133)
|
|
643
|
+
|
|
644
|
+
layout.addWidget(self.canvas)
|
|
645
|
+
|
|
646
|
+
self.x = 0
|
|
647
|
+
self.y = 0
|
|
648
|
+
self.z = 0
|
|
649
|
+
|
|
650
|
+
self.canvas.mpl_connect("scroll_event", self.on_scroll)
|
|
651
|
+
self.canvas.mpl_connect("button_press_event", self.on_click)
|
|
652
|
+
|
|
653
|
+
def set_volume(self, volume, affine=None):
|
|
654
|
+
|
|
655
|
+
self.volume = volume
|
|
656
|
+
self.affine = affine
|
|
657
|
+
|
|
658
|
+
self.x = volume.shape[0] // 2
|
|
659
|
+
self.y = volume.shape[1] // 2
|
|
660
|
+
self.z = volume.shape[2] // 2
|
|
661
|
+
|
|
662
|
+
self.update_views()
|
|
663
|
+
|
|
664
|
+
def set_colormap(self, cmap_name):
|
|
665
|
+
self.cmap = cmap_name
|
|
666
|
+
self.update_views()
|
|
667
|
+
|
|
668
|
+
def add_roi(self, name, roi, color):
|
|
669
|
+
|
|
670
|
+
self.rois[name] = {"data": roi, "color": color}
|
|
671
|
+
self.roi_visibility[name] = True
|
|
672
|
+
self.update_views()
|
|
673
|
+
|
|
674
|
+
def draw_roi(self, ax, roi_slice, color):
|
|
675
|
+
|
|
676
|
+
if roi_slice.max() == 0:
|
|
677
|
+
return
|
|
678
|
+
|
|
679
|
+
ax.contour(roi_slice, levels=[0.5], colors=[color], linewidths=1.5,
|
|
680
|
+
antialiased=True)
|
|
681
|
+
ax.contourf(roi_slice, levels=[0.5, roi_slice.max()+1], colors=[color],
|
|
682
|
+
alpha=0.5)
|
|
683
|
+
|
|
684
|
+
def update_views(self):
|
|
685
|
+
|
|
686
|
+
if self.volume is None:
|
|
687
|
+
return
|
|
688
|
+
|
|
689
|
+
x = self.x
|
|
690
|
+
y = self.y
|
|
691
|
+
z = self.z
|
|
692
|
+
|
|
693
|
+
axial = self.volume[:, :, z]
|
|
694
|
+
coronal = self.volume[:, y, :]
|
|
695
|
+
sagittal = self.volume[x, :, :]
|
|
696
|
+
|
|
697
|
+
self.ax_axial.clear()
|
|
698
|
+
self.ax_coronal.clear()
|
|
699
|
+
self.ax_sagittal.clear()
|
|
574
700
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
701
|
+
for ax in (self.ax_axial, self.ax_coronal, self.ax_sagittal):
|
|
702
|
+
ax.axis("off")
|
|
703
|
+
|
|
704
|
+
self.ax_axial.imshow(np.rot90(axial), cmap=self.cmap, vmin=0)
|
|
705
|
+
self.ax_coronal.imshow(np.rot90(coronal), cmap=self.cmap, vmin=0)
|
|
706
|
+
self.ax_sagittal.imshow(np.rot90(sagittal), cmap=self.cmap, vmin=0)
|
|
707
|
+
|
|
708
|
+
for name, roi_info in self.rois.items():
|
|
709
|
+
|
|
710
|
+
if not self.roi_visibility[name]:
|
|
711
|
+
continue
|
|
712
|
+
|
|
713
|
+
roi = roi_info["data"]
|
|
714
|
+
color = roi_info["color"]
|
|
715
|
+
|
|
716
|
+
self.draw_roi(self.ax_axial, np.rot90(roi[:, :, z]), color)
|
|
717
|
+
self.draw_roi(self.ax_coronal, np.rot90(roi[:, y, :]), color)
|
|
718
|
+
self.draw_roi(self.ax_sagittal, np.rot90(roi[x, :, :]), color)
|
|
719
|
+
|
|
720
|
+
self.canvas.draw_idle()
|
|
721
|
+
|
|
722
|
+
def take_screenshot(self):
|
|
723
|
+
|
|
724
|
+
options = QFileDialog.Options()
|
|
725
|
+
|
|
726
|
+
file_path, _ = QFileDialog.getSaveFileName(self,
|
|
727
|
+
"Save Orthogonal View", "",
|
|
728
|
+
"PNG Image (*.png);;JPEG Image (*.jpg);;TIFF Image (*.tif);; SVG Image (*.svg)",
|
|
729
|
+
options=options
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
if file_path:
|
|
733
|
+
self.fig.savefig(file_path, dpi=300, transparent=True)
|
|
734
|
+
|
|
735
|
+
def set_background(self, color):
|
|
736
|
+
|
|
737
|
+
self.background = color
|
|
738
|
+
|
|
739
|
+
self.fig.patch.set_facecolor(color)
|
|
740
|
+
|
|
741
|
+
for ax in (self.ax_axial, self.ax_coronal, self.ax_sagittal):
|
|
742
|
+
ax.set_facecolor(color)
|
|
743
|
+
|
|
744
|
+
self.canvas.draw_idle()
|
|
745
|
+
|
|
746
|
+
def on_scroll(self, event):
|
|
747
|
+
|
|
748
|
+
if self.volume is None:
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
step = 1 if event.button == "up" else -1
|
|
752
|
+
|
|
753
|
+
if event.inaxes == self.ax_axial:
|
|
754
|
+
self.z = np.clip(self.z + step, 0, self.volume.shape[2]-1)
|
|
755
|
+
|
|
756
|
+
elif event.inaxes == self.ax_coronal:
|
|
757
|
+
self.y = np.clip(self.y + step, 0, self.volume.shape[1]-1)
|
|
758
|
+
|
|
759
|
+
elif event.inaxes == self.ax_sagittal:
|
|
760
|
+
self.x = np.clip(self.x + step, 0, self.volume.shape[0]-1)
|
|
761
|
+
|
|
762
|
+
self.update_views()
|
|
763
|
+
|
|
764
|
+
def on_click(self, event):
|
|
765
|
+
|
|
766
|
+
if self.volume is None:
|
|
767
|
+
return
|
|
768
|
+
|
|
769
|
+
if event.inaxes is None:
|
|
770
|
+
return
|
|
771
|
+
|
|
772
|
+
if event.xdata is None or event.ydata is None:
|
|
773
|
+
return
|
|
774
|
+
|
|
775
|
+
col = int(round(event.xdata))
|
|
776
|
+
row = int(round(event.ydata))
|
|
777
|
+
|
|
778
|
+
if event.inaxes == self.ax_axial:
|
|
779
|
+
|
|
780
|
+
self.x = np.clip(col, 0, self.volume.shape[0]-1)
|
|
781
|
+
self.y = np.clip(
|
|
782
|
+
self.volume.shape[1]-1-row, 0, self.volume.shape[1]-1)
|
|
783
|
+
|
|
784
|
+
elif event.inaxes == self.ax_coronal:
|
|
785
|
+
|
|
786
|
+
self.x = np.clip(col, 0, self.volume.shape[0]-1)
|
|
787
|
+
self.z = np.clip(
|
|
788
|
+
self.volume.shape[2]-1-row, 0, self.volume.shape[2]-1)
|
|
789
|
+
|
|
790
|
+
elif event.inaxes == self.ax_sagittal:
|
|
791
|
+
|
|
792
|
+
self.y = np.clip(col, 0, self.volume.shape[1]-1)
|
|
793
|
+
self.z = np.clip(
|
|
794
|
+
self.volume.shape[2]-1-row, 0, self.volume.shape[2]-1)
|
|
795
|
+
|
|
796
|
+
self.update_views()
|
|
581
797
|
|
|
582
798
|
|
|
583
799
|
class MainWindow(QMainWindow):
|
|
584
800
|
def __init__(self):
|
|
585
801
|
super().__init__()
|
|
802
|
+
self.tabs = QTabWidget()
|
|
803
|
+
|
|
586
804
|
self.viewer = TrkViewer()
|
|
587
|
-
self.
|
|
805
|
+
self.ortho_viewer = OrthogonalViewer()
|
|
806
|
+
|
|
807
|
+
self.tabs.addTab(self.viewer, "3D Viewer")
|
|
808
|
+
self.tabs.addTab(self.ortho_viewer, "Orthogonal Viewer")
|
|
809
|
+
|
|
810
|
+
self.setCentralWidget(self.tabs)
|
|
588
811
|
self.initActorDock()
|
|
589
812
|
self.initMenuBar()
|
|
590
813
|
self.setWindowTitle("UNVEIL - Tractography Viewer")
|
|
@@ -639,9 +862,14 @@ class MainWindow(QMainWindow):
|
|
|
639
862
|
self.actorDock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea)
|
|
640
863
|
|
|
641
864
|
self.actorTree = QTreeWidget()
|
|
642
|
-
|
|
643
|
-
self.actorTree.
|
|
644
|
-
self.actorTree.
|
|
865
|
+
|
|
866
|
+
self.actorTree.setHeaderLabels(["Visible", "Color", "Actor"])
|
|
867
|
+
self.actorTree.setColumnCount(3)
|
|
868
|
+
self.actorTree.setIndentation(5)
|
|
869
|
+
|
|
870
|
+
self.actorTree.setColumnWidth(0, 45)
|
|
871
|
+
self.actorTree.setColumnWidth(1, 40)
|
|
872
|
+
self.actorTree.setColumnWidth(2, 220)
|
|
645
873
|
|
|
646
874
|
self.actorDock.setWidget(self.actorTree)
|
|
647
875
|
self.addDockWidget(
|
|
@@ -653,15 +881,20 @@ class MainWindow(QMainWindow):
|
|
|
653
881
|
self.viewer.plotter.mesh_added_callback = self.refreshActorList
|
|
654
882
|
self.viewer.plotter.mesh_removed_callback = self.refreshActorList
|
|
655
883
|
|
|
884
|
+
self.actorTree.itemDoubleClicked.connect(self.changeActorColor)
|
|
885
|
+
|
|
886
|
+
# Connect visibility toggles
|
|
887
|
+
self.actorTree.itemChanged.connect(self.onActorVisibilityChanged)
|
|
888
|
+
|
|
656
889
|
def toggleActorDock(self, checked):
|
|
657
890
|
if checked:
|
|
658
|
-
|
|
659
891
|
self.actorDock.show()
|
|
660
892
|
else:
|
|
661
893
|
self.actorDock.hide()
|
|
662
894
|
|
|
663
895
|
def refreshActorList(self):
|
|
664
896
|
"""Rebuild the hierarchical actor list grouped by class."""
|
|
897
|
+
self.actorTree.blockSignals(True)
|
|
665
898
|
self.actorTree.clear()
|
|
666
899
|
|
|
667
900
|
# Groups
|
|
@@ -676,43 +909,91 @@ class MainWindow(QMainWindow):
|
|
|
676
909
|
self.actorTree.addTopLevelItem(group_roi)
|
|
677
910
|
# Loop through PyVista actors
|
|
678
911
|
for name, actor in self.viewer.plotter.actors.items():
|
|
679
|
-
|
|
680
|
-
item
|
|
681
|
-
|
|
912
|
+
|
|
913
|
+
item = QTreeWidgetItem(["", "", name])
|
|
914
|
+
|
|
915
|
+
item.setCheckState(0, Qt.CheckState.Checked if actor.GetVisibility()
|
|
916
|
+
else Qt.CheckState.Unchecked)
|
|
682
917
|
|
|
683
918
|
# Store name for callback
|
|
684
919
|
item.actor_name = name
|
|
685
920
|
|
|
921
|
+
if self.viewer.actor_types.get(name) == 'roi':
|
|
922
|
+
|
|
923
|
+
rgb = self.viewer.roi_colors.get(name, (1, 1, 1))
|
|
924
|
+
|
|
925
|
+
qcolor = QColor(int(rgb[0] * 255), int(rgb[1] * 255),
|
|
926
|
+
int(rgb[2] * 255))
|
|
927
|
+
item.setBackground(1, qcolor)
|
|
928
|
+
|
|
686
929
|
# Insert into the correct group
|
|
687
|
-
if
|
|
930
|
+
if self.viewer.actor_types.get(name) == 'trk':
|
|
688
931
|
group_trk.addChild(item)
|
|
689
932
|
elif name.startswith("nii_"):
|
|
690
933
|
group_nii.addChild(item)
|
|
691
934
|
elif name.startswith("gii_"):
|
|
692
935
|
group_gii.addChild(item)
|
|
693
|
-
elif
|
|
936
|
+
elif self.viewer.actor_types.get(name) == 'roi':
|
|
694
937
|
group_roi.addChild(item)
|
|
695
938
|
else:
|
|
696
939
|
group_trk.addChild(item) # default bucket
|
|
697
940
|
|
|
698
|
-
|
|
699
|
-
self.actorTree.itemChanged.connect(self.onActorVisibilityChanged)
|
|
941
|
+
self.actorTree.blockSignals(False)
|
|
700
942
|
|
|
701
943
|
def onActorVisibilityChanged(self, item, column):
|
|
702
944
|
"""Toggle visibility when user clicks checkbox."""
|
|
703
|
-
if column !=
|
|
945
|
+
if column != 0:
|
|
704
946
|
return
|
|
705
947
|
|
|
706
948
|
actor_name = item.actor_name
|
|
707
|
-
visible = item.checkState(
|
|
949
|
+
visible = item.checkState(0) == Qt.CheckState.Checked
|
|
708
950
|
|
|
709
951
|
try:
|
|
710
952
|
self.viewer.plotter.actors[actor_name].SetVisibility(visible)
|
|
711
953
|
except KeyError:
|
|
712
954
|
pass
|
|
713
955
|
|
|
956
|
+
self.viewer.roi_visibility[actor_name] = visible
|
|
957
|
+
self.ortho_viewer.roi_visibility[actor_name] = visible
|
|
958
|
+
self.ortho_viewer.update_views()
|
|
959
|
+
|
|
714
960
|
self.viewer.plotter.render()
|
|
715
961
|
|
|
962
|
+
def changeActorColor(self, item, column):
|
|
963
|
+
|
|
964
|
+
if column != 1:
|
|
965
|
+
return
|
|
966
|
+
|
|
967
|
+
actor_name = getattr(item, "actor_name", None)
|
|
968
|
+
|
|
969
|
+
if actor_name is None:
|
|
970
|
+
return
|
|
971
|
+
|
|
972
|
+
if self.viewer.actor_types.get(actor_name) != "roi":
|
|
973
|
+
return
|
|
974
|
+
|
|
975
|
+
color = QColorDialog.getColor()
|
|
976
|
+
|
|
977
|
+
if not color.isValid():
|
|
978
|
+
return
|
|
979
|
+
|
|
980
|
+
rgb = (color.red() / 255, color.green() / 255, color.blue() / 255)
|
|
981
|
+
|
|
982
|
+
self.viewer.roi_colors[actor_name] = rgb
|
|
983
|
+
if actor_name in self.ortho_viewer.rois:
|
|
984
|
+
self.ortho_viewer.rois[actor_name]["color"] = rgb
|
|
985
|
+
self.ortho_viewer.update_views()
|
|
986
|
+
|
|
987
|
+
item.setBackground(1, color)
|
|
988
|
+
|
|
989
|
+
try:
|
|
990
|
+
actor = self.viewer.plotter.actors[actor_name]
|
|
991
|
+
actor.GetProperty().SetColor(*rgb)
|
|
992
|
+
self.viewer.plotter.render()
|
|
993
|
+
|
|
994
|
+
except Exception:
|
|
995
|
+
pass
|
|
996
|
+
|
|
716
997
|
|
|
717
998
|
if __name__ == '__main__':
|
|
718
999
|
app = QApplication(sys.argv)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unveil-python
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: Implementation of UNVEIL
|
|
5
|
+
Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: GitHub, https://github.com/DelinteNicolas/UNVEIL
|
|
8
|
+
Keywords: tractography,viewer
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: dipy
|
|
15
|
+
Requires-Dist: pyvista
|
|
16
|
+
Requires-Dist: pyvistaqt
|
|
17
|
+
Requires-Dist: PyQt6
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# UNVEIL
|
|
21
|
+
[](https://pypi.org/project/unveil-python/)
|
|
22
|
+
[](https://pypi.org/project/unveil-python/)
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
UNVEIL is a Python-based visualization tool for interactive exploration of diffusion MRI tractography, anatomical volumes, cortical surfaces, and ROI segmentations. It combines a 3D PyVista viewer with an orthogonal slice viewer.
|
|
27
|
+
|
|
28
|
+
<img width="1297" height="757" alt="image" src="https://github.com/user-attachments/assets/28807e18-17fb-485b-a8ce-2179cdef796b" />
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
The GUI is launched with the following python command in a python console:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
import unveil.__main__
|
|
36
|
+
```
|
|
37
|
+
or with
|
|
38
|
+
```
|
|
39
|
+
python -c "import unveil.__main__"
|
|
40
|
+
```
|
|
41
|
+
from a terminal with a python environment activated.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **3D Tractography Visualization**
|
|
46
|
+
- Load and display `.trk` streamline files.
|
|
47
|
+
- RGB orientation coloring or scalar-based coloring from NIfTI volumes.
|
|
48
|
+
- Adjustable opacity and point/line rendering.
|
|
49
|
+
- **Volume Rendering**
|
|
50
|
+
- Load `.nii.gz` images.
|
|
51
|
+
- Interactive volume rendering and orthogonal slice planes.
|
|
52
|
+
- Adjustable opacity.
|
|
53
|
+
- **ROI Visualization**
|
|
54
|
+
- Load ROI masks from `.nii` / `.nii.gz`.
|
|
55
|
+
- Automatic surface extraction and smoothing.
|
|
56
|
+
- Individual visibility and color control.
|
|
57
|
+
- **Surface Visualization**
|
|
58
|
+
- Load GIFTI (`.gii`) cortical surfaces.
|
|
59
|
+
- Adjustable opacity and smooth shading.
|
|
60
|
+
- **Orthogonal Viewer**
|
|
61
|
+
- Axial, coronal, and sagittal views.
|
|
62
|
+
- Mouse-wheel slice navigation.
|
|
63
|
+
- Click-to-navigate crosshair synchronization.
|
|
64
|
+
- ROI overlay display.
|
|
65
|
+
- Screenshot export.
|
|
66
|
+
- **Scene Management**
|
|
67
|
+
- Actor visibility control.
|
|
68
|
+
- ROI color editing.
|
|
69
|
+
- Background color toggle.
|
|
70
|
+
- Color-blind-friendly tractography coloring.
|
|
71
|
+
- **Export**
|
|
72
|
+
- 3D screenshots.
|
|
73
|
+
- Orthogonal-view screenshots.
|
|
74
|
+
- Animated 360° GIF generation.
|
unveil_python-0.4.0/PKG-INFO
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: unveil-python
|
|
3
|
-
Version: 0.4.0
|
|
4
|
-
Summary: Implementation of UNVEIL
|
|
5
|
-
Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
|
|
6
|
-
License-Expression: GPL-3.0-only
|
|
7
|
-
Project-URL: GitHub, https://github.com/DelinteNicolas/UNVEIL
|
|
8
|
-
Keywords: tractography,viewer
|
|
9
|
-
Classifier: Intended Audience :: Science/Research
|
|
10
|
-
Classifier: Natural Language :: English
|
|
11
|
-
Classifier: Programming Language :: Python
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
License-File: LICENSE
|
|
14
|
-
Requires-Dist: dipy
|
|
15
|
-
Requires-Dist: pyvista
|
|
16
|
-
Requires-Dist: pyvistaqt
|
|
17
|
-
Requires-Dist: PyQt6
|
|
18
|
-
Dynamic: license-file
|
|
19
|
-
|
|
20
|
-
# UNVEIL
|
|
21
|
-
[](https://pypi.org/project/unveil-python/)
|
|
22
|
-
[](https://pypi.org/project/unveil-python/)
|
|
23
|
-

|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
A simple tractography viewer implemented in python.
|
|
27
|
-
|
|
28
|
-
<img width="1297" height="757" alt="image" src="https://github.com/user-attachments/assets/28807e18-17fb-485b-a8ce-2179cdef796b" />
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
The GUI is launched with the following python command in a python console:
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
import unveil.__main__
|
|
35
|
-
```
|
|
36
|
-
or with
|
|
37
|
-
```
|
|
38
|
-
python -c "import unveil.__main__"
|
|
39
|
-
```
|
|
40
|
-
from a terminal with a python environment activated.
|
unveil_python-0.4.0/README.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# UNVEIL
|
|
2
|
-
[](https://pypi.org/project/unveil-python/)
|
|
3
|
-
[](https://pypi.org/project/unveil-python/)
|
|
4
|
-

|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
A simple tractography viewer implemented in python.
|
|
8
|
-
|
|
9
|
-
<img width="1297" height="757" alt="image" src="https://github.com/user-attachments/assets/28807e18-17fb-485b-a8ce-2179cdef796b" />
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
The GUI is launched with the following python command in a python console:
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
import unveil.__main__
|
|
16
|
-
```
|
|
17
|
-
or with
|
|
18
|
-
```
|
|
19
|
-
python -c "import unveil.__main__"
|
|
20
|
-
```
|
|
21
|
-
from a terminal with a python environment activated.
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: unveil-python
|
|
3
|
-
Version: 0.4.0
|
|
4
|
-
Summary: Implementation of UNVEIL
|
|
5
|
-
Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
|
|
6
|
-
License-Expression: GPL-3.0-only
|
|
7
|
-
Project-URL: GitHub, https://github.com/DelinteNicolas/UNVEIL
|
|
8
|
-
Keywords: tractography,viewer
|
|
9
|
-
Classifier: Intended Audience :: Science/Research
|
|
10
|
-
Classifier: Natural Language :: English
|
|
11
|
-
Classifier: Programming Language :: Python
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
License-File: LICENSE
|
|
14
|
-
Requires-Dist: dipy
|
|
15
|
-
Requires-Dist: pyvista
|
|
16
|
-
Requires-Dist: pyvistaqt
|
|
17
|
-
Requires-Dist: PyQt6
|
|
18
|
-
Dynamic: license-file
|
|
19
|
-
|
|
20
|
-
# UNVEIL
|
|
21
|
-
[](https://pypi.org/project/unveil-python/)
|
|
22
|
-
[](https://pypi.org/project/unveil-python/)
|
|
23
|
-

|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
A simple tractography viewer implemented in python.
|
|
27
|
-
|
|
28
|
-
<img width="1297" height="757" alt="image" src="https://github.com/user-attachments/assets/28807e18-17fb-485b-a8ce-2179cdef796b" />
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
The GUI is launched with the following python command in a python console:
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
import unveil.__main__
|
|
35
|
-
```
|
|
36
|
-
or with
|
|
37
|
-
```
|
|
38
|
-
python -c "import unveil.__main__"
|
|
39
|
-
```
|
|
40
|
-
from a terminal with a python environment activated.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|