unveil-python 0.3.0__tar.gz → 0.5.0__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.0/PKG-INFO +74 -0
- unveil_python-0.5.0/README.md +55 -0
- {unveil_python-0.3.0 → unveil_python-0.5.0}/src/unveil/__init__.py +1 -1
- {unveil_python-0.3.0 → unveil_python-0.5.0}/src/unveil/core.py +374 -122
- unveil_python-0.5.0/src/unveil_python.egg-info/PKG-INFO +74 -0
- unveil_python-0.3.0/PKG-INFO +0 -40
- unveil_python-0.3.0/README.md +0 -21
- unveil_python-0.3.0/src/unveil_python.egg-info/PKG-INFO +0 -40
- {unveil_python-0.3.0 → unveil_python-0.5.0}/LICENSE +0 -0
- {unveil_python-0.3.0 → unveil_python-0.5.0}/pyproject.toml +0 -0
- {unveil_python-0.3.0 → unveil_python-0.5.0}/setup.cfg +0 -0
- {unveil_python-0.3.0 → unveil_python-0.5.0}/src/unveil/__main__.py +0 -0
- {unveil_python-0.3.0 → unveil_python-0.5.0}/src/unveil_python.egg-info/SOURCES.txt +0 -0
- {unveil_python-0.3.0 → unveil_python-0.5.0}/src/unveil_python.egg-info/dependency_links.txt +0 -0
- {unveil_python-0.3.0 → unveil_python-0.5.0}/src/unveil_python.egg-info/requires.txt +0 -0
- {unveil_python-0.3.0 → unveil_python-0.5.0}/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.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
|
+
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,16 +10,18 @@ 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
|
-
# from unravel.viz import plot_trk # We'll use our local version below
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
def gifti_to_pyvista(gii_path):
|
|
@@ -51,9 +53,28 @@ def gifti_to_pyvista(gii_path):
|
|
|
51
53
|
return mesh
|
|
52
54
|
|
|
53
55
|
|
|
56
|
+
def convert_rgb_array_to_new_basis(rgb_array):
|
|
57
|
+
"""
|
|
58
|
+
Converts an array of RGB colors shape [n, 3] into a new basis.
|
|
59
|
+
|
|
60
|
+
rgb_array: numpy array of shape (n, 3) where each row is [R, G, B]
|
|
61
|
+
Returns: numpy array of shape (n, 3) where each row is [newR, newG, newB]
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# Based on the Color-Blind Safe IBM Design Library Color Palette
|
|
65
|
+
C1 = np.array([220, 38, 127])
|
|
66
|
+
C2 = np.array([255, 176, 0])
|
|
67
|
+
C3 = np.array([120, 94, 240])
|
|
68
|
+
|
|
69
|
+
B = np.column_stack([C1, C2, C3])
|
|
70
|
+
new_rgb = rgb_array.dot(B.T)
|
|
71
|
+
|
|
72
|
+
return new_rgb
|
|
73
|
+
|
|
74
|
+
|
|
54
75
|
def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
|
|
55
76
|
show_points: bool = False, background: str = 'black', plotter=None,
|
|
56
|
-
name=None, reset_camera=False):
|
|
77
|
+
name=None, reset_camera=False, color_blind: bool = False):
|
|
57
78
|
'''
|
|
58
79
|
3D render for .trk files.
|
|
59
80
|
|
|
@@ -115,10 +136,12 @@ def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
|
|
|
115
136
|
point = streamlines._data
|
|
116
137
|
next_point = np.roll(point, -1, axis=0)
|
|
117
138
|
vs = next_point-point
|
|
139
|
+
ends = (streamlines._offsets+streamlines._lengths-1)
|
|
140
|
+
if color_blind:
|
|
141
|
+
vs = convert_rgb_array_to_new_basis(np.abs(vs))
|
|
118
142
|
norm = np.linalg.norm(vs, axis=1)
|
|
119
143
|
norm = np.stack((norm,)*3, axis=1, dtype=np.float32)
|
|
120
144
|
norm = np.divide(vs, norm, dtype=np.float64)
|
|
121
|
-
ends = (streamlines._offsets+streamlines._lengths-1)
|
|
122
145
|
norm[ends, :] = norm[ends-1, :]
|
|
123
146
|
scalars = np.abs(norm)
|
|
124
147
|
rgb = True
|
|
@@ -144,41 +167,40 @@ def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
|
|
|
144
167
|
interpolate_before_map=False, render_lines_as_tubes=True,
|
|
145
168
|
line_width=2, point_size=point_size, rgb=rgb,
|
|
146
169
|
cmap=color_map, clim=color_lim, scalars=scalars, name=name,
|
|
147
|
-
reset_camera=reset_camera,
|
|
148
|
-
user_matrix=trk.affine)
|
|
170
|
+
reset_camera=reset_camera, user_matrix=trk.affine)
|
|
149
171
|
|
|
150
172
|
elif color_map == 'flesh':
|
|
151
173
|
p.add_mesh(mesh, opacity=opacity, diffuse=0.4, ambient=ambient,
|
|
152
174
|
interpolate_before_map=False, render_lines_as_tubes=True,
|
|
153
175
|
line_width=2, point_size=point_size, rgb=rgb,
|
|
154
176
|
color=[250, 225, 210], name=name,
|
|
155
|
-
reset_camera=reset_camera,
|
|
156
|
-
user_matrix=trk.affine)
|
|
177
|
+
reset_camera=reset_camera, user_matrix=trk.affine)
|
|
157
178
|
else:
|
|
158
179
|
p.add_mesh(mesh, opacity=opacity, diffuse=diffuse, ambient=ambient,
|
|
159
180
|
interpolate_before_map=False, render_lines_as_tubes=True,
|
|
160
181
|
line_width=2, point_size=point_size, rgb=rgb,
|
|
161
182
|
cmap=color_map, scalars=scalars, name=name,
|
|
162
|
-
reset_camera=reset_camera,
|
|
163
|
-
user_matrix=trk.affine)
|
|
183
|
+
reset_camera=reset_camera, user_matrix=trk.affine)
|
|
164
184
|
|
|
165
185
|
p.background_color = background
|
|
166
|
-
# Do not call p.show() here when using an embedded interactor
|
|
167
186
|
|
|
168
187
|
|
|
169
188
|
class TrkViewer(QWidget):
|
|
170
189
|
def __init__(self):
|
|
171
190
|
super().__init__()
|
|
172
|
-
self.trk_file = None
|
|
173
|
-
self.nii_file = None
|
|
174
191
|
self.nii_data = None
|
|
175
192
|
self.grid = None
|
|
176
|
-
self.
|
|
193
|
+
self.actor_types = {}
|
|
177
194
|
self.x = 0
|
|
178
195
|
self.y = 0
|
|
179
196
|
self.z = 0
|
|
180
197
|
self.initUI()
|
|
181
198
|
self.background = 'white'
|
|
199
|
+
self.color_blind = False
|
|
200
|
+
self.flicker = False
|
|
201
|
+
self.roi_colors = {}
|
|
202
|
+
self.roi_volumes = {}
|
|
203
|
+
self.roi_visibility = {}
|
|
182
204
|
|
|
183
205
|
def initUI(self):
|
|
184
206
|
# Create the main layout
|
|
@@ -191,10 +213,10 @@ class TrkViewer(QWidget):
|
|
|
191
213
|
btn_xy = QPushButton("Axe 1")
|
|
192
214
|
btn_xz = QPushButton("Axe 2")
|
|
193
215
|
btn_yz = QPushButton("Axe 3")
|
|
194
|
-
btn_iso.clicked.connect(self.view_isometric)
|
|
195
|
-
btn_xy.clicked.connect(self.view_xy)
|
|
196
|
-
btn_xz.clicked.connect(self.view_xz)
|
|
197
|
-
btn_yz.clicked.connect(self.view_yz)
|
|
216
|
+
btn_iso.clicked.connect(lambda: self.set_view("view_isometric"))
|
|
217
|
+
btn_xy.clicked.connect(lambda: self.set_view("view_xy"))
|
|
218
|
+
btn_xz.clicked.connect(lambda: self.set_view("view_xz"))
|
|
219
|
+
btn_yz.clicked.connect(lambda: self.set_view("view_yz"))
|
|
198
220
|
cam_layout.addWidget(camera_label)
|
|
199
221
|
cam_layout.addWidget(btn_iso)
|
|
200
222
|
cam_layout.addWidget(btn_xy)
|
|
@@ -266,7 +288,8 @@ class TrkViewer(QWidget):
|
|
|
266
288
|
self.XSlider.setMinimum(0)
|
|
267
289
|
self.XSlider.setMaximum(100)
|
|
268
290
|
self.XSlider.setValue(100)
|
|
269
|
-
self.XSlider.sliderReleased.connect(
|
|
291
|
+
self.XSlider.sliderReleased.connect(
|
|
292
|
+
lambda: self._update_slice('x', 'nii_x'))
|
|
270
293
|
control_layout.addWidget(self.XSlider)
|
|
271
294
|
self.y_label = QLabel('Y')
|
|
272
295
|
control_layout.addWidget(self.y_label)
|
|
@@ -274,7 +297,8 @@ class TrkViewer(QWidget):
|
|
|
274
297
|
self.YSlider.setMinimum(0)
|
|
275
298
|
self.YSlider.setMaximum(100)
|
|
276
299
|
self.YSlider.setValue(100)
|
|
277
|
-
self.YSlider.sliderReleased.connect(
|
|
300
|
+
self.YSlider.sliderReleased.connect(
|
|
301
|
+
lambda: self._update_slice('y', 'nii_y'))
|
|
278
302
|
control_layout.addWidget(self.YSlider)
|
|
279
303
|
self.z_label = QLabel('Z')
|
|
280
304
|
control_layout.addWidget(self.z_label)
|
|
@@ -282,7 +306,8 @@ class TrkViewer(QWidget):
|
|
|
282
306
|
self.ZSlider.setMinimum(0)
|
|
283
307
|
self.ZSlider.setMaximum(100)
|
|
284
308
|
self.ZSlider.setValue(100)
|
|
285
|
-
self.ZSlider.sliderReleased.connect(
|
|
309
|
+
self.ZSlider.sliderReleased.connect(
|
|
310
|
+
lambda: self._update_slice('z', 'nii_z'))
|
|
286
311
|
control_layout.addWidget(self.ZSlider)
|
|
287
312
|
|
|
288
313
|
# Gifti
|
|
@@ -315,23 +340,22 @@ class TrkViewer(QWidget):
|
|
|
315
340
|
return
|
|
316
341
|
|
|
317
342
|
for trk_path in filePaths:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
self.names.append(self.trk_file)
|
|
343
|
+
print(f"Loaded TRK file: {trk_path}")
|
|
344
|
+
self.actor_types[trk_path] = 'trk'
|
|
321
345
|
|
|
322
346
|
self.update_trk_viewer(reset_camera=True)
|
|
323
|
-
self.
|
|
347
|
+
self.window().refreshActorList()
|
|
324
348
|
|
|
325
349
|
def loadNiftiFile(self):
|
|
326
350
|
options = QFileDialog.Options()
|
|
327
351
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
328
352
|
self, "Open .nii.gz File", "", "Nifti Files (*.nii.gz)", options=options)
|
|
329
353
|
if filePath:
|
|
330
|
-
|
|
331
|
-
print(f"Loaded NIfTI file: {self.nii_file}")
|
|
354
|
+
print(f"Loaded NIfTI file: {filePath}")
|
|
332
355
|
img = nib.load(filePath)
|
|
333
356
|
self.nii_affine = img.affine
|
|
334
357
|
self.nii_data = img.get_fdata()
|
|
358
|
+
|
|
335
359
|
grid = pv.ImageData()
|
|
336
360
|
grid.dimensions = np.array(self.nii_data.shape) + 1
|
|
337
361
|
grid.cell_data['values'] = self.nii_data.flatten(order='F')
|
|
@@ -342,7 +366,10 @@ class TrkViewer(QWidget):
|
|
|
342
366
|
self.ZSlider.setMaximum(self.nii_data.shape[2])
|
|
343
367
|
|
|
344
368
|
self.update_nii_viewer(reset_camera=False)
|
|
345
|
-
self.
|
|
369
|
+
self.window().refreshActorList()
|
|
370
|
+
|
|
371
|
+
self.nii_data[self.nii_data == 0] = None
|
|
372
|
+
self.window().ortho_viewer.set_volume(self.nii_data, self.nii_affine)
|
|
346
373
|
|
|
347
374
|
def loadROIFile(self):
|
|
348
375
|
"""Load one or multiple ROI nifti volumes and render as surfaces."""
|
|
@@ -357,7 +384,6 @@ class TrkViewer(QWidget):
|
|
|
357
384
|
for roi_path in filePaths:
|
|
358
385
|
img = nib.load(roi_path)
|
|
359
386
|
roi = img.get_fdata()
|
|
360
|
-
affine = img.affine
|
|
361
387
|
|
|
362
388
|
datapv = pv.wrap(roi)
|
|
363
389
|
datapv.cell_data['labels'] = roi[:-1, :-1, :-1].flatten(order='F')
|
|
@@ -366,32 +392,34 @@ class TrkViewer(QWidget):
|
|
|
366
392
|
|
|
367
393
|
smooth = mesh.smooth_taubin(n_iter=12)
|
|
368
394
|
|
|
369
|
-
actor_name = f"
|
|
395
|
+
actor_name = f"{roi_path.split('/')[-1]}"
|
|
396
|
+
self.actor_types[actor_name] = "roi"
|
|
397
|
+
self.roi_volumes[actor_name] = roi
|
|
398
|
+
self.roi_visibility[actor_name] = True
|
|
399
|
+
|
|
400
|
+
default_color = np.array([1.0, 1.0, 1.0])
|
|
401
|
+
|
|
402
|
+
self.roi_colors[actor_name] = default_color
|
|
403
|
+
|
|
404
|
+
self.plotter.add_mesh(smooth, name=actor_name, opacity=1,
|
|
405
|
+
color=default_color, user_matrix=img.affine,
|
|
406
|
+
reset_camera=False, point_size=0,
|
|
407
|
+
render_lines_as_tubes=True)
|
|
370
408
|
|
|
371
|
-
self.
|
|
372
|
-
smooth,
|
|
373
|
-
name=actor_name,
|
|
374
|
-
opacity=1,
|
|
375
|
-
color="white",
|
|
376
|
-
user_matrix=affine,
|
|
377
|
-
reset_camera=False,
|
|
378
|
-
point_size=0,
|
|
379
|
-
render_lines_as_tubes=True,
|
|
380
|
-
)
|
|
409
|
+
self.window().refreshActorList()
|
|
381
410
|
|
|
382
|
-
self.
|
|
411
|
+
self.window().ortho_viewer.add_roi(actor_name, roi, default_color)
|
|
383
412
|
|
|
384
413
|
def loadGiftiFile(self):
|
|
385
414
|
options = QFileDialog.Options()
|
|
386
415
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
387
416
|
self, "Open .gii File", "", "Gifti Files (*.gii)", options=options)
|
|
388
417
|
if filePath:
|
|
389
|
-
|
|
390
|
-
print(f"Loaded GIfTI file: {self.gii_file}")
|
|
418
|
+
print(f"Loaded GIfTI file: {filePath}")
|
|
391
419
|
self.gii_mesh = gifti_to_pyvista(filePath)
|
|
392
420
|
|
|
393
421
|
self.update_gii_viewer(reset_camera=False)
|
|
394
|
-
self.
|
|
422
|
+
self.window().refreshActorList()
|
|
395
423
|
|
|
396
424
|
def set_background_color(self):
|
|
397
425
|
|
|
@@ -402,25 +430,17 @@ class TrkViewer(QWidget):
|
|
|
402
430
|
|
|
403
431
|
self.plotter.background_color = self.background
|
|
404
432
|
|
|
405
|
-
|
|
406
|
-
# equivalent to BackgroundPlotter 'iso' camera position
|
|
407
|
-
self.plotter.view_isometric()
|
|
408
|
-
self.plotter.render()
|
|
433
|
+
self.window().ortho_viewer.set_background(self.background)
|
|
409
434
|
|
|
410
|
-
def
|
|
411
|
-
# view from +Z axis, XY plane (axial)
|
|
412
|
-
self.plotter.view_xy() # pyvista method
|
|
413
|
-
self.plotter.render()
|
|
435
|
+
def toggleColorBlind(self):
|
|
414
436
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
self.plotter.view_xz()
|
|
418
|
-
self.plotter.render()
|
|
437
|
+
self.color_blind = not self.color_blind
|
|
438
|
+
self.update_trk_viewer()
|
|
419
439
|
|
|
420
|
-
def
|
|
421
|
-
|
|
422
|
-
self.plotter.
|
|
423
|
-
self.
|
|
440
|
+
def set_view(self, method):
|
|
441
|
+
getattr(self.plotter, method)()
|
|
442
|
+
self.plotter.camera.azimuth += self.flicker * 180
|
|
443
|
+
self.flicker = not self.flicker
|
|
424
444
|
|
|
425
445
|
def take_screenshot(self):
|
|
426
446
|
"""Take a screenshot of the current 3D view."""
|
|
@@ -451,7 +471,7 @@ class TrkViewer(QWidget):
|
|
|
451
471
|
|
|
452
472
|
# Copy all meshes and volumes from the main plotter to the offscreen one
|
|
453
473
|
for actor_name, actor in self.plotter.actors.items():
|
|
454
|
-
offscreen.add_actor(actor
|
|
474
|
+
offscreen.add_actor(actor)
|
|
455
475
|
|
|
456
476
|
# Match background color and camera
|
|
457
477
|
offscreen.background_color = self.plotter.background_color
|
|
@@ -487,66 +507,38 @@ class TrkViewer(QWidget):
|
|
|
487
507
|
color_map = self.colorMapEdit.text()
|
|
488
508
|
scalar = self.nii_data
|
|
489
509
|
|
|
490
|
-
for file in self.
|
|
510
|
+
for file in list(self.actor_types):
|
|
491
511
|
|
|
492
|
-
|
|
493
|
-
show_points=show_points, color_map=color_map,
|
|
494
|
-
name=file, background=self.background,
|
|
495
|
-
reset_camera=reset_camera)
|
|
512
|
+
if self.actor_types.get(file) == 'trk':
|
|
496
513
|
|
|
497
|
-
|
|
514
|
+
plot_trk(file, opacity=opacity, plotter=self.plotter, scalar=scalar,
|
|
515
|
+
show_points=show_points, color_map=color_map,
|
|
516
|
+
name=file, background=self.background,
|
|
517
|
+
reset_camera=reset_camera, color_blind=self.color_blind)
|
|
498
518
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
center = (self.XSlider.value(),
|
|
502
|
-
self.YSlider.value(), self.ZSlider.value())
|
|
503
|
-
|
|
504
|
-
slice_x = self.grid.slice('x', center)
|
|
505
|
-
self.plotter.add_mesh(slice_x, cmap='grey', name='nii_x',
|
|
506
|
-
show_scalar_bar=False, point_size=0,
|
|
507
|
-
render_lines_as_tubes=True,
|
|
508
|
-
reset_camera=False,
|
|
509
|
-
user_matrix=self.nii_affine)
|
|
510
|
-
else:
|
|
511
|
-
self.plotter.remove_actor('nii_x')
|
|
512
|
-
|
|
513
|
-
def _update_nii_y(self):
|
|
519
|
+
def _update_slice(self, axis, actor_name):
|
|
514
520
|
|
|
515
521
|
if self.showSlicesCheckbox.isChecked():
|
|
516
522
|
|
|
517
|
-
center = (self.XSlider.value(),
|
|
518
|
-
self.
|
|
519
|
-
|
|
520
|
-
slice_y = self.grid.slice('y', center)
|
|
521
|
-
self.plotter.add_mesh(slice_y, cmap='grey', name='nii_y',
|
|
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_y')
|
|
523
|
+
center = (self.XSlider.value(), self.YSlider.value(),
|
|
524
|
+
self.ZSlider.value())
|
|
528
525
|
|
|
529
|
-
|
|
526
|
+
slice_mesh = self.grid.slice(axis, origin=center)
|
|
530
527
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
center = (self.XSlider.value(),
|
|
534
|
-
self.YSlider.value(), self.ZSlider.value())
|
|
535
|
-
|
|
536
|
-
slice_z = self.grid.slice('z', center)
|
|
537
|
-
self.plotter.add_mesh(slice_z, cmap='grey', name='nii_z',
|
|
528
|
+
self.plotter.add_mesh(slice_mesh, cmap='grey', name=actor_name,
|
|
538
529
|
show_scalar_bar=False, point_size=0,
|
|
539
530
|
render_lines_as_tubes=True,
|
|
540
531
|
reset_camera=False,
|
|
541
532
|
user_matrix=self.nii_affine)
|
|
533
|
+
|
|
542
534
|
else:
|
|
543
|
-
self.plotter.remove_actor(
|
|
535
|
+
self.plotter.remove_actor(actor_name)
|
|
544
536
|
|
|
545
537
|
def update_nii_viewer(self, reset_camera: bool = False):
|
|
546
538
|
|
|
547
|
-
self.
|
|
548
|
-
self.
|
|
549
|
-
self.
|
|
539
|
+
self._update_slice('x', 'nii_x')
|
|
540
|
+
self._update_slice('y', 'nii_y')
|
|
541
|
+
self._update_slice('z', 'nii_z')
|
|
550
542
|
|
|
551
543
|
opacity = self.nii_opacitySlider.value()/1000
|
|
552
544
|
self.plotter.add_volume(self.grid, cmap='gray', opacity=[0, opacity],
|
|
@@ -564,11 +556,209 @@ class TrkViewer(QWidget):
|
|
|
564
556
|
render_lines_as_tubes=True,)
|
|
565
557
|
|
|
566
558
|
|
|
559
|
+
class OrthogonalViewer(QWidget):
|
|
560
|
+
|
|
561
|
+
def __init__(self):
|
|
562
|
+
super().__init__()
|
|
563
|
+
|
|
564
|
+
self.volume = None
|
|
565
|
+
self.affine = None
|
|
566
|
+
|
|
567
|
+
self.rois = {}
|
|
568
|
+
self.roi_visibility = {}
|
|
569
|
+
|
|
570
|
+
self.background = "white"
|
|
571
|
+
|
|
572
|
+
layout = QVBoxLayout(self)
|
|
573
|
+
|
|
574
|
+
toolbar = QHBoxLayout()
|
|
575
|
+
|
|
576
|
+
btn_screenshot = QPushButton("Screenshot")
|
|
577
|
+
btn_screenshot.clicked.connect(self.take_screenshot)
|
|
578
|
+
|
|
579
|
+
toolbar.addStretch()
|
|
580
|
+
toolbar.addWidget(btn_screenshot)
|
|
581
|
+
|
|
582
|
+
layout.addLayout(toolbar)
|
|
583
|
+
|
|
584
|
+
self.fig = Figure(figsize=(12, 4), layout='constrained')
|
|
585
|
+
self.canvas = FigureCanvasQTAgg(self.fig)
|
|
586
|
+
|
|
587
|
+
self.ax_axial = self.fig.add_subplot(131)
|
|
588
|
+
self.ax_coronal = self.fig.add_subplot(132)
|
|
589
|
+
self.ax_sagittal = self.fig.add_subplot(133)
|
|
590
|
+
|
|
591
|
+
layout.addWidget(self.canvas)
|
|
592
|
+
|
|
593
|
+
self.x = 0
|
|
594
|
+
self.y = 0
|
|
595
|
+
self.z = 0
|
|
596
|
+
|
|
597
|
+
self.canvas.mpl_connect("scroll_event", self.on_scroll)
|
|
598
|
+
self.canvas.mpl_connect("button_press_event", self.on_click)
|
|
599
|
+
|
|
600
|
+
def set_volume(self, volume, affine=None):
|
|
601
|
+
|
|
602
|
+
self.volume = volume
|
|
603
|
+
self.affine = affine
|
|
604
|
+
|
|
605
|
+
self.x = volume.shape[0] // 2
|
|
606
|
+
self.y = volume.shape[1] // 2
|
|
607
|
+
self.z = volume.shape[2] // 2
|
|
608
|
+
|
|
609
|
+
self.update_views()
|
|
610
|
+
|
|
611
|
+
def add_roi(self, name, roi, color):
|
|
612
|
+
|
|
613
|
+
self.rois[name] = {"data": roi, "color": color}
|
|
614
|
+
self.roi_visibility[name] = True
|
|
615
|
+
self.update_views()
|
|
616
|
+
|
|
617
|
+
def draw_roi(self, ax, roi_slice, color):
|
|
618
|
+
|
|
619
|
+
if roi_slice.max() == 0:
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
ax.contour(roi_slice, levels=[0.5], colors=[color], linewidths=1.5,
|
|
623
|
+
antialiased=True)
|
|
624
|
+
ax.contourf(roi_slice, levels=[0.5, roi_slice.max()+1], colors=[color],
|
|
625
|
+
alpha=0.5)
|
|
626
|
+
|
|
627
|
+
def update_views(self):
|
|
628
|
+
|
|
629
|
+
if self.volume is None:
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
x = self.x
|
|
633
|
+
y = self.y
|
|
634
|
+
z = self.z
|
|
635
|
+
|
|
636
|
+
axial = self.volume[:, :, z]
|
|
637
|
+
coronal = self.volume[:, y, :]
|
|
638
|
+
sagittal = self.volume[x, :, :]
|
|
639
|
+
|
|
640
|
+
self.ax_axial.clear()
|
|
641
|
+
self.ax_coronal.clear()
|
|
642
|
+
self.ax_sagittal.clear()
|
|
643
|
+
|
|
644
|
+
for ax in (self.ax_axial, self.ax_coronal, self.ax_sagittal):
|
|
645
|
+
ax.axis("off")
|
|
646
|
+
|
|
647
|
+
self.ax_axial.imshow(np.rot90(axial), cmap="gray", vmin=0)
|
|
648
|
+
self.ax_coronal.imshow(np.rot90(coronal), cmap="gray", vmin=0)
|
|
649
|
+
self.ax_sagittal.imshow(np.rot90(sagittal), cmap="gray", vmin=0)
|
|
650
|
+
|
|
651
|
+
for name, roi_info in self.rois.items():
|
|
652
|
+
|
|
653
|
+
if not self.roi_visibility[name]:
|
|
654
|
+
continue
|
|
655
|
+
|
|
656
|
+
roi = roi_info["data"]
|
|
657
|
+
color = roi_info["color"]
|
|
658
|
+
|
|
659
|
+
self.draw_roi(self.ax_axial, np.rot90(roi[:, :, z]), color)
|
|
660
|
+
self.draw_roi(self.ax_coronal, np.rot90(roi[:, y, :]), color)
|
|
661
|
+
self.draw_roi(self.ax_sagittal, np.rot90(roi[x, :, :]), color)
|
|
662
|
+
|
|
663
|
+
self.canvas.draw_idle()
|
|
664
|
+
|
|
665
|
+
# self.window().viewer.XSlider.setValue(self.x)
|
|
666
|
+
# self.window().viewer.YSlider.setValue(self.y)
|
|
667
|
+
# self.window().viewer.ZSlider.setValue(self.z)
|
|
668
|
+
|
|
669
|
+
# self.window().viewer._update_slice('x', 'nii_x')
|
|
670
|
+
# self.window().viewer._update_slice('y', 'nii_y')
|
|
671
|
+
# self.window().viewer._update_slice('z', 'nii_z')
|
|
672
|
+
|
|
673
|
+
def take_screenshot(self):
|
|
674
|
+
|
|
675
|
+
options = QFileDialog.Options()
|
|
676
|
+
|
|
677
|
+
file_path, _ = QFileDialog.getSaveFileName(self,
|
|
678
|
+
"Save Orthogonal View", "",
|
|
679
|
+
"PNG Image (*.png);;JPEG Image (*.jpg);;TIFF Image (*.tif);; SVG Image (*.svg)",
|
|
680
|
+
options=options
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
if file_path:
|
|
684
|
+
self.fig.savefig(file_path, dpi=300, transparent=True)
|
|
685
|
+
|
|
686
|
+
def set_background(self, color):
|
|
687
|
+
|
|
688
|
+
self.background = color
|
|
689
|
+
|
|
690
|
+
self.fig.patch.set_facecolor(color)
|
|
691
|
+
|
|
692
|
+
for ax in (self.ax_axial, self.ax_coronal, self.ax_sagittal):
|
|
693
|
+
ax.set_facecolor(color)
|
|
694
|
+
|
|
695
|
+
self.canvas.draw_idle()
|
|
696
|
+
|
|
697
|
+
def on_scroll(self, event):
|
|
698
|
+
|
|
699
|
+
if self.volume is None:
|
|
700
|
+
return
|
|
701
|
+
|
|
702
|
+
step = 1 if event.button == "up" else -1
|
|
703
|
+
|
|
704
|
+
if event.inaxes == self.ax_axial:
|
|
705
|
+
self.z = np.clip(self.z + step, 0, self.volume.shape[2]-1)
|
|
706
|
+
|
|
707
|
+
elif event.inaxes == self.ax_coronal:
|
|
708
|
+
self.y = np.clip(self.y + step, 0, self.volume.shape[1]-1)
|
|
709
|
+
|
|
710
|
+
elif event.inaxes == self.ax_sagittal:
|
|
711
|
+
self.x = np.clip(self.x + step, 0, self.volume.shape[0]-1)
|
|
712
|
+
|
|
713
|
+
self.update_views()
|
|
714
|
+
|
|
715
|
+
def on_click(self, event):
|
|
716
|
+
|
|
717
|
+
if self.volume is None:
|
|
718
|
+
return
|
|
719
|
+
|
|
720
|
+
if event.inaxes is None:
|
|
721
|
+
return
|
|
722
|
+
|
|
723
|
+
if event.xdata is None or event.ydata is None:
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
col = int(round(event.xdata))
|
|
727
|
+
row = int(round(event.ydata))
|
|
728
|
+
|
|
729
|
+
if event.inaxes == self.ax_axial:
|
|
730
|
+
|
|
731
|
+
self.x = np.clip(col, 0, self.volume.shape[0]-1)
|
|
732
|
+
self.y = np.clip(
|
|
733
|
+
self.volume.shape[1]-1-row, 0, self.volume.shape[1]-1)
|
|
734
|
+
|
|
735
|
+
elif event.inaxes == self.ax_coronal:
|
|
736
|
+
|
|
737
|
+
self.x = np.clip(col, 0, self.volume.shape[0]-1)
|
|
738
|
+
self.z = np.clip(
|
|
739
|
+
self.volume.shape[2]-1-row, 0, self.volume.shape[2]-1)
|
|
740
|
+
|
|
741
|
+
elif event.inaxes == self.ax_sagittal:
|
|
742
|
+
|
|
743
|
+
self.y = np.clip(col, 0, self.volume.shape[1]-1)
|
|
744
|
+
self.z = np.clip(
|
|
745
|
+
self.volume.shape[2]-1-row, 0, self.volume.shape[2]-1)
|
|
746
|
+
|
|
747
|
+
self.update_views()
|
|
748
|
+
|
|
749
|
+
|
|
567
750
|
class MainWindow(QMainWindow):
|
|
568
751
|
def __init__(self):
|
|
569
752
|
super().__init__()
|
|
753
|
+
self.tabs = QTabWidget()
|
|
754
|
+
|
|
570
755
|
self.viewer = TrkViewer()
|
|
571
|
-
self.
|
|
756
|
+
self.ortho_viewer = OrthogonalViewer()
|
|
757
|
+
|
|
758
|
+
self.tabs.addTab(self.viewer, "3D Viewer")
|
|
759
|
+
self.tabs.addTab(self.ortho_viewer, "Orthogonal Viewer")
|
|
760
|
+
|
|
761
|
+
self.setCentralWidget(self.tabs)
|
|
572
762
|
self.initActorDock()
|
|
573
763
|
self.initMenuBar()
|
|
574
764
|
self.setWindowTitle("UNVEIL - Tractography Viewer")
|
|
@@ -609,19 +799,28 @@ class MainWindow(QMainWindow):
|
|
|
609
799
|
setBackColorAction = QAction('Background Color', self)
|
|
610
800
|
setBackColorAction.triggered.connect(self.viewer.set_background_color)
|
|
611
801
|
viewMenu.addAction(setBackColorAction)
|
|
612
|
-
toggleActorListAction = QAction(
|
|
613
|
-
|
|
802
|
+
toggleActorListAction = QAction("Toggle Actor List", self,
|
|
803
|
+
checkable=True)
|
|
614
804
|
toggleActorListAction.triggered.connect(self.toggleActorDock)
|
|
615
805
|
viewMenu.addAction(toggleActorListAction)
|
|
806
|
+
toggleColorBlindAction = QAction("Use color-blind colors", self,
|
|
807
|
+
checkable=True)
|
|
808
|
+
toggleColorBlindAction.triggered.connect(self.viewer.toggleColorBlind)
|
|
809
|
+
viewMenu.addAction(toggleColorBlindAction)
|
|
616
810
|
|
|
617
811
|
def initActorDock(self):
|
|
618
812
|
self.actorDock = QDockWidget("Scene Actors", self)
|
|
619
813
|
self.actorDock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea)
|
|
620
814
|
|
|
621
815
|
self.actorTree = QTreeWidget()
|
|
622
|
-
|
|
623
|
-
self.actorTree.
|
|
624
|
-
self.actorTree.
|
|
816
|
+
|
|
817
|
+
self.actorTree.setHeaderLabels(["Visible", "Color", "Actor"])
|
|
818
|
+
self.actorTree.setColumnCount(3)
|
|
819
|
+
self.actorTree.setIndentation(5)
|
|
820
|
+
|
|
821
|
+
self.actorTree.setColumnWidth(0, 45)
|
|
822
|
+
self.actorTree.setColumnWidth(1, 40)
|
|
823
|
+
self.actorTree.setColumnWidth(2, 220)
|
|
625
824
|
|
|
626
825
|
self.actorDock.setWidget(self.actorTree)
|
|
627
826
|
self.addDockWidget(
|
|
@@ -633,15 +832,20 @@ class MainWindow(QMainWindow):
|
|
|
633
832
|
self.viewer.plotter.mesh_added_callback = self.refreshActorList
|
|
634
833
|
self.viewer.plotter.mesh_removed_callback = self.refreshActorList
|
|
635
834
|
|
|
835
|
+
self.actorTree.itemDoubleClicked.connect(self.changeActorColor)
|
|
836
|
+
|
|
837
|
+
# Connect visibility toggles
|
|
838
|
+
self.actorTree.itemChanged.connect(self.onActorVisibilityChanged)
|
|
839
|
+
|
|
636
840
|
def toggleActorDock(self, checked):
|
|
637
841
|
if checked:
|
|
638
|
-
|
|
639
842
|
self.actorDock.show()
|
|
640
843
|
else:
|
|
641
844
|
self.actorDock.hide()
|
|
642
845
|
|
|
643
846
|
def refreshActorList(self):
|
|
644
847
|
"""Rebuild the hierarchical actor list grouped by class."""
|
|
848
|
+
self.actorTree.blockSignals(True)
|
|
645
849
|
self.actorTree.clear()
|
|
646
850
|
|
|
647
851
|
# Groups
|
|
@@ -656,43 +860,91 @@ class MainWindow(QMainWindow):
|
|
|
656
860
|
self.actorTree.addTopLevelItem(group_roi)
|
|
657
861
|
# Loop through PyVista actors
|
|
658
862
|
for name, actor in self.viewer.plotter.actors.items():
|
|
659
|
-
|
|
660
|
-
item
|
|
661
|
-
|
|
863
|
+
|
|
864
|
+
item = QTreeWidgetItem(["", "", name])
|
|
865
|
+
|
|
866
|
+
item.setCheckState(0, Qt.CheckState.Checked if actor.GetVisibility()
|
|
867
|
+
else Qt.CheckState.Unchecked)
|
|
662
868
|
|
|
663
869
|
# Store name for callback
|
|
664
870
|
item.actor_name = name
|
|
665
871
|
|
|
872
|
+
if self.viewer.actor_types.get(name) == 'roi':
|
|
873
|
+
|
|
874
|
+
rgb = self.viewer.roi_colors.get(name, (1, 1, 1))
|
|
875
|
+
|
|
876
|
+
qcolor = QColor(int(rgb[0] * 255), int(rgb[1] * 255),
|
|
877
|
+
int(rgb[2] * 255))
|
|
878
|
+
item.setBackground(1, qcolor)
|
|
879
|
+
|
|
666
880
|
# Insert into the correct group
|
|
667
|
-
if
|
|
881
|
+
if self.viewer.actor_types.get(name) == 'trk':
|
|
668
882
|
group_trk.addChild(item)
|
|
669
883
|
elif name.startswith("nii_"):
|
|
670
884
|
group_nii.addChild(item)
|
|
671
885
|
elif name.startswith("gii_"):
|
|
672
886
|
group_gii.addChild(item)
|
|
673
|
-
elif
|
|
887
|
+
elif self.viewer.actor_types.get(name) == 'roi':
|
|
674
888
|
group_roi.addChild(item)
|
|
675
889
|
else:
|
|
676
890
|
group_trk.addChild(item) # default bucket
|
|
677
891
|
|
|
678
|
-
|
|
679
|
-
self.actorTree.itemChanged.connect(self.onActorVisibilityChanged)
|
|
892
|
+
self.actorTree.blockSignals(False)
|
|
680
893
|
|
|
681
894
|
def onActorVisibilityChanged(self, item, column):
|
|
682
895
|
"""Toggle visibility when user clicks checkbox."""
|
|
683
|
-
if column !=
|
|
896
|
+
if column != 0:
|
|
684
897
|
return
|
|
685
898
|
|
|
686
899
|
actor_name = item.actor_name
|
|
687
|
-
visible = item.checkState(
|
|
900
|
+
visible = item.checkState(0) == Qt.CheckState.Checked
|
|
688
901
|
|
|
689
902
|
try:
|
|
690
903
|
self.viewer.plotter.actors[actor_name].SetVisibility(visible)
|
|
691
904
|
except KeyError:
|
|
692
905
|
pass
|
|
693
906
|
|
|
907
|
+
self.viewer.roi_visibility[actor_name] = visible
|
|
908
|
+
self.ortho_viewer.roi_visibility[actor_name] = visible
|
|
909
|
+
self.ortho_viewer.update_views()
|
|
910
|
+
|
|
694
911
|
self.viewer.plotter.render()
|
|
695
912
|
|
|
913
|
+
def changeActorColor(self, item, column):
|
|
914
|
+
|
|
915
|
+
if column != 1:
|
|
916
|
+
return
|
|
917
|
+
|
|
918
|
+
actor_name = getattr(item, "actor_name", None)
|
|
919
|
+
|
|
920
|
+
if actor_name is None:
|
|
921
|
+
return
|
|
922
|
+
|
|
923
|
+
if self.viewer.actor_types.get(actor_name) != "roi":
|
|
924
|
+
return
|
|
925
|
+
|
|
926
|
+
color = QColorDialog.getColor()
|
|
927
|
+
|
|
928
|
+
if not color.isValid():
|
|
929
|
+
return
|
|
930
|
+
|
|
931
|
+
rgb = (color.red() / 255, color.green() / 255, color.blue() / 255)
|
|
932
|
+
|
|
933
|
+
self.viewer.roi_colors[actor_name] = rgb
|
|
934
|
+
if actor_name in self.ortho_viewer.rois:
|
|
935
|
+
self.ortho_viewer.rois[actor_name]["color"] = rgb
|
|
936
|
+
self.ortho_viewer.update_views()
|
|
937
|
+
|
|
938
|
+
item.setBackground(1, color)
|
|
939
|
+
|
|
940
|
+
try:
|
|
941
|
+
actor = self.viewer.plotter.actors[actor_name]
|
|
942
|
+
actor.GetProperty().SetColor(*rgb)
|
|
943
|
+
self.viewer.plotter.render()
|
|
944
|
+
|
|
945
|
+
except Exception:
|
|
946
|
+
pass
|
|
947
|
+
|
|
696
948
|
|
|
697
949
|
if __name__ == '__main__':
|
|
698
950
|
app = QApplication(sys.argv)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unveil-python
|
|
3
|
+
Version: 0.5.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
|
+
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.3.0/PKG-INFO
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: unveil-python
|
|
3
|
-
Version: 0.3.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
|
-

|
|
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.3.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
|
-

|
|
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.3.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
|
-

|
|
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
|