unveil-python 0.2.0__tar.gz → 0.4.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.2.0/src/unveil_python.egg-info → unveil_python-0.4.0}/PKG-INFO +9 -3
- {unveil_python-0.2.0 → unveil_python-0.4.0}/README.md +8 -2
- {unveil_python-0.2.0 → unveil_python-0.4.0}/src/unveil/__init__.py +1 -1
- {unveil_python-0.2.0 → unveil_python-0.4.0}/src/unveil/core.py +236 -115
- {unveil_python-0.2.0 → unveil_python-0.4.0/src/unveil_python.egg-info}/PKG-INFO +9 -3
- {unveil_python-0.2.0 → unveil_python-0.4.0}/LICENSE +0 -0
- {unveil_python-0.2.0 → unveil_python-0.4.0}/pyproject.toml +0 -0
- {unveil_python-0.2.0 → unveil_python-0.4.0}/setup.cfg +0 -0
- {unveil_python-0.2.0 → unveil_python-0.4.0}/src/unveil/__main__.py +0 -0
- {unveil_python-0.2.0 → unveil_python-0.4.0}/src/unveil_python.egg-info/SOURCES.txt +0 -0
- {unveil_python-0.2.0 → unveil_python-0.4.0}/src/unveil_python.egg-info/dependency_links.txt +0 -0
- {unveil_python-0.2.0 → unveil_python-0.4.0}/src/unveil_python.egg-info/requires.txt +0 -0
- {unveil_python-0.2.0 → unveil_python-0.4.0}/src/unveil_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unveil-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Implementation of UNVEIL
|
|
5
5
|
Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -25,10 +25,16 @@ Dynamic: license-file
|
|
|
25
25
|
|
|
26
26
|
A simple tractography viewer implemented in python.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
<img width="1297" height="757" alt="image" src="https://github.com/user-attachments/assets/28807e18-17fb-485b-a8ce-2179cdef796b" />
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
The GUI is launched with the following python command in a python console:
|
|
31
32
|
|
|
32
33
|
```
|
|
33
34
|
import unveil.__main__
|
|
34
35
|
```
|
|
36
|
+
or with
|
|
37
|
+
```
|
|
38
|
+
python -c "import unveil.__main__"
|
|
39
|
+
```
|
|
40
|
+
from a terminal with a python environment activated.
|
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
A simple tractography viewer implemented in python.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<img width="1297" height="757" alt="image" src="https://github.com/user-attachments/assets/28807e18-17fb-485b-a8ce-2179cdef796b" />
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
The GUI is launched with the following python command in a python console:
|
|
12
13
|
|
|
13
14
|
```
|
|
14
15
|
import unveil.__main__
|
|
15
16
|
```
|
|
17
|
+
or with
|
|
18
|
+
```
|
|
19
|
+
python -c "import unveil.__main__"
|
|
20
|
+
```
|
|
21
|
+
from a terminal with a python environment activated.
|
|
@@ -13,12 +13,12 @@ from PyQt6.QtCore import Qt
|
|
|
13
13
|
from PyQt6.QtGui import QAction
|
|
14
14
|
from PyQt6.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout,
|
|
15
15
|
QPushButton, QFileDialog, QCheckBox, QSlider,
|
|
16
|
-
QLabel, QComboBox, QMainWindow, QLineEdit
|
|
16
|
+
QLabel, QComboBox, QMainWindow, QLineEdit,
|
|
17
|
+
QDockWidget, QTreeWidget, QTreeWidgetItem)
|
|
17
18
|
from dipy.io.streamline import load_tractogram
|
|
18
19
|
from matplotlib import pyplot as plt
|
|
19
20
|
from matplotlib.colors import LinearSegmentedColormap
|
|
20
21
|
# from unravel.utils import get_streamline_density
|
|
21
|
-
# from unravel.viz import plot_trk # We'll use our local version below
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def gifti_to_pyvista(gii_path):
|
|
@@ -27,17 +27,6 @@ def gifti_to_pyvista(gii_path):
|
|
|
27
27
|
using the inverse of the given NIfTI affine.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
# Affine from MNI152 1mm template (voxel→world)
|
|
31
|
-
affine = np.array([
|
|
32
|
-
[-1.0, 0.0, 0.0, 90.0],
|
|
33
|
-
[0.0, 1.0, 0.0, -126.0],
|
|
34
|
-
[0.0, 0.0, 1.0, -72.0],
|
|
35
|
-
[0.0, 0.0, 0.0, 1.0]
|
|
36
|
-
])
|
|
37
|
-
|
|
38
|
-
# Compute inverse to go from world (mm) → voxel coordinates
|
|
39
|
-
inv_affine = np.linalg.inv(affine)
|
|
40
|
-
|
|
41
30
|
# Load GIFTI file
|
|
42
31
|
img = nib.load(gii_path)
|
|
43
32
|
coords_list = img.get_arrays_from_intent('NIFTI_INTENT_POINTSET')
|
|
@@ -51,22 +40,38 @@ def gifti_to_pyvista(gii_path):
|
|
|
51
40
|
coords_world = coords_list[0].data.astype(np.float32)
|
|
52
41
|
faces = face_list[0].data.astype(np.int32)
|
|
53
42
|
|
|
54
|
-
# Transform to voxel space
|
|
55
|
-
coords_voxel = nib.affines.apply_affine(inv_affine, coords_world)
|
|
56
|
-
|
|
57
43
|
# Build PyVista face array (VTK expects [3, i1, i2, i3, 3, ...])
|
|
58
44
|
faces_pv = np.hstack(
|
|
59
45
|
(np.full((faces.shape[0], 1), 3, dtype=np.int32), faces)).astype(np.int32)
|
|
60
46
|
|
|
61
47
|
# Create PolyData mesh in voxel space
|
|
62
|
-
mesh = pv.PolyData(
|
|
48
|
+
mesh = pv.PolyData(coords_world, faces_pv)
|
|
63
49
|
|
|
64
50
|
return mesh
|
|
65
51
|
|
|
66
52
|
|
|
53
|
+
def convert_rgb_array_to_new_basis(rgb_array):
|
|
54
|
+
"""
|
|
55
|
+
Converts an array of RGB colors shape [n, 3] into a new basis.
|
|
56
|
+
|
|
57
|
+
rgb_array: numpy array of shape (n, 3) where each row is [R, G, B]
|
|
58
|
+
Returns: numpy array of shape (n, 3) where each row is [newR, newG, newB]
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# Based on the Color-Blind Safe IBM Design Library Color Palette
|
|
62
|
+
C1 = np.array([220, 38, 127])
|
|
63
|
+
C2 = np.array([255, 176, 0])
|
|
64
|
+
C3 = np.array([120, 94, 240])
|
|
65
|
+
|
|
66
|
+
B = np.column_stack([C1, C2, C3])
|
|
67
|
+
new_rgb = rgb_array.dot(B.T)
|
|
68
|
+
|
|
69
|
+
return new_rgb
|
|
70
|
+
|
|
71
|
+
|
|
67
72
|
def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
|
|
68
73
|
show_points: bool = False, background: str = 'black', plotter=None,
|
|
69
|
-
name=None, reset_camera=False):
|
|
74
|
+
name=None, reset_camera=False, color_blind: bool = False):
|
|
70
75
|
'''
|
|
71
76
|
3D render for .trk files.
|
|
72
77
|
|
|
@@ -128,10 +133,12 @@ def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
|
|
|
128
133
|
point = streamlines._data
|
|
129
134
|
next_point = np.roll(point, -1, axis=0)
|
|
130
135
|
vs = next_point-point
|
|
136
|
+
ends = (streamlines._offsets+streamlines._lengths-1)
|
|
137
|
+
if color_blind:
|
|
138
|
+
vs = convert_rgb_array_to_new_basis(np.abs(vs))
|
|
131
139
|
norm = np.linalg.norm(vs, axis=1)
|
|
132
140
|
norm = np.stack((norm,)*3, axis=1, dtype=np.float32)
|
|
133
141
|
norm = np.divide(vs, norm, dtype=np.float64)
|
|
134
|
-
ends = (streamlines._offsets+streamlines._lengths-1)
|
|
135
142
|
norm[ends, :] = norm[ends-1, :]
|
|
136
143
|
scalars = np.abs(norm)
|
|
137
144
|
rgb = True
|
|
@@ -157,23 +164,22 @@ def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
|
|
|
157
164
|
interpolate_before_map=False, render_lines_as_tubes=True,
|
|
158
165
|
line_width=2, point_size=point_size, rgb=rgb,
|
|
159
166
|
cmap=color_map, clim=color_lim, scalars=scalars, name=name,
|
|
160
|
-
reset_camera=reset_camera)
|
|
167
|
+
reset_camera=reset_camera, user_matrix=trk.affine)
|
|
161
168
|
|
|
162
169
|
elif color_map == 'flesh':
|
|
163
170
|
p.add_mesh(mesh, opacity=opacity, diffuse=0.4, ambient=ambient,
|
|
164
171
|
interpolate_before_map=False, render_lines_as_tubes=True,
|
|
165
172
|
line_width=2, point_size=point_size, rgb=rgb,
|
|
166
173
|
color=[250, 225, 210], name=name,
|
|
167
|
-
reset_camera=reset_camera)
|
|
174
|
+
reset_camera=reset_camera, user_matrix=trk.affine)
|
|
168
175
|
else:
|
|
169
176
|
p.add_mesh(mesh, opacity=opacity, diffuse=diffuse, ambient=ambient,
|
|
170
177
|
interpolate_before_map=False, render_lines_as_tubes=True,
|
|
171
178
|
line_width=2, point_size=point_size, rgb=rgb,
|
|
172
179
|
cmap=color_map, scalars=scalars, name=name,
|
|
173
|
-
reset_camera=reset_camera)
|
|
180
|
+
reset_camera=reset_camera, user_matrix=trk.affine)
|
|
174
181
|
|
|
175
182
|
p.background_color = background
|
|
176
|
-
# Do not call p.show() here when using an embedded interactor
|
|
177
183
|
|
|
178
184
|
|
|
179
185
|
class TrkViewer(QWidget):
|
|
@@ -184,12 +190,13 @@ class TrkViewer(QWidget):
|
|
|
184
190
|
self.nii_data = None
|
|
185
191
|
self.grid = None
|
|
186
192
|
self.names = []
|
|
187
|
-
self.selected_name = None
|
|
188
193
|
self.x = 0
|
|
189
194
|
self.y = 0
|
|
190
195
|
self.z = 0
|
|
191
196
|
self.initUI()
|
|
192
197
|
self.background = 'white'
|
|
198
|
+
self.color_blind = False
|
|
199
|
+
self.flicker = False
|
|
193
200
|
|
|
194
201
|
def initUI(self):
|
|
195
202
|
# Create the main layout
|
|
@@ -257,9 +264,8 @@ class TrkViewer(QWidget):
|
|
|
257
264
|
'Insert color map name. Name must be in the matplotlib library. Scalar nii.gz must be loaded.')
|
|
258
265
|
control_layout.addWidget(self.colorMapEdit)
|
|
259
266
|
|
|
260
|
-
self.
|
|
261
|
-
|
|
262
|
-
control_layout.addWidget(self.showVolumeCheckbox)
|
|
267
|
+
self.volume_label = QLabel('Volume')
|
|
268
|
+
control_layout.addWidget(self.volume_label)
|
|
263
269
|
|
|
264
270
|
self.nii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
265
271
|
self.nii_opacitySlider.setMinimum(0)
|
|
@@ -272,27 +278,24 @@ class TrkViewer(QWidget):
|
|
|
272
278
|
self.showSlicesCheckbox.stateChanged.connect(self.update_nii_viewer)
|
|
273
279
|
control_layout.addWidget(self.showSlicesCheckbox)
|
|
274
280
|
|
|
275
|
-
self.
|
|
276
|
-
|
|
277
|
-
control_layout.addWidget(self.showXCheckbox)
|
|
281
|
+
self.x_label = QLabel('X')
|
|
282
|
+
control_layout.addWidget(self.x_label)
|
|
278
283
|
self.XSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
279
284
|
self.XSlider.setMinimum(0)
|
|
280
285
|
self.XSlider.setMaximum(100)
|
|
281
286
|
self.XSlider.setValue(100)
|
|
282
287
|
self.XSlider.sliderReleased.connect(self._update_nii_x)
|
|
283
288
|
control_layout.addWidget(self.XSlider)
|
|
284
|
-
self.
|
|
285
|
-
|
|
286
|
-
control_layout.addWidget(self.showYCheckbox)
|
|
289
|
+
self.y_label = QLabel('Y')
|
|
290
|
+
control_layout.addWidget(self.y_label)
|
|
287
291
|
self.YSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
288
292
|
self.YSlider.setMinimum(0)
|
|
289
293
|
self.YSlider.setMaximum(100)
|
|
290
294
|
self.YSlider.setValue(100)
|
|
291
295
|
self.YSlider.sliderReleased.connect(self._update_nii_y)
|
|
292
296
|
control_layout.addWidget(self.YSlider)
|
|
293
|
-
self.
|
|
294
|
-
|
|
295
|
-
control_layout.addWidget(self.showZCheckbox)
|
|
297
|
+
self.z_label = QLabel('Z')
|
|
298
|
+
control_layout.addWidget(self.z_label)
|
|
296
299
|
self.ZSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
297
300
|
self.ZSlider.setMinimum(0)
|
|
298
301
|
self.ZSlider.setMaximum(100)
|
|
@@ -301,9 +304,8 @@ class TrkViewer(QWidget):
|
|
|
301
304
|
control_layout.addWidget(self.ZSlider)
|
|
302
305
|
|
|
303
306
|
# Gifti
|
|
304
|
-
self.
|
|
305
|
-
|
|
306
|
-
control_layout.addWidget(self.showSurfaceCheckbox)
|
|
307
|
+
self.surface_label = QLabel('Surface')
|
|
308
|
+
control_layout.addWidget(self.surface_label)
|
|
307
309
|
|
|
308
310
|
self.gii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
309
311
|
self.gii_opacitySlider.setMinimum(0)
|
|
@@ -324,15 +326,19 @@ class TrkViewer(QWidget):
|
|
|
324
326
|
|
|
325
327
|
def loadTrkFile(self):
|
|
326
328
|
options = QFileDialog.Options()
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
filePaths, _ = QFileDialog.getOpenFileNames(self, "Open TRK Files", "",
|
|
330
|
+
"Tractography Files (*.trk)",
|
|
331
|
+
options=options)
|
|
332
|
+
if not filePaths:
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
for trk_path in filePaths:
|
|
336
|
+
self.trk_file = trk_path
|
|
331
337
|
print(f"Loaded TRK file: {self.trk_file}")
|
|
332
338
|
self.names.append(self.trk_file)
|
|
333
|
-
self.selected_name = self.trk_file
|
|
334
339
|
|
|
335
340
|
self.update_trk_viewer(reset_camera=True)
|
|
341
|
+
self.parent().refreshActorList()
|
|
336
342
|
|
|
337
343
|
def loadNiftiFile(self):
|
|
338
344
|
options = QFileDialog.Options()
|
|
@@ -342,12 +348,10 @@ class TrkViewer(QWidget):
|
|
|
342
348
|
self.nii_file = filePath
|
|
343
349
|
print(f"Loaded NIfTI file: {self.nii_file}")
|
|
344
350
|
img = nib.load(filePath)
|
|
351
|
+
self.nii_affine = img.affine
|
|
345
352
|
self.nii_data = img.get_fdata()
|
|
346
353
|
grid = pv.ImageData()
|
|
347
354
|
grid.dimensions = np.array(self.nii_data.shape) + 1
|
|
348
|
-
# World coordinates
|
|
349
|
-
# grid.origin = img.affine[:3, 3]
|
|
350
|
-
# grid.spacing = np.diag(img.affine)[:3]
|
|
351
355
|
grid.cell_data['values'] = self.nii_data.flatten(order='F')
|
|
352
356
|
self.grid = grid
|
|
353
357
|
|
|
@@ -355,7 +359,38 @@ class TrkViewer(QWidget):
|
|
|
355
359
|
self.YSlider.setMaximum(self.nii_data.shape[1])
|
|
356
360
|
self.ZSlider.setMaximum(self.nii_data.shape[2])
|
|
357
361
|
|
|
358
|
-
self.update_nii_viewer(reset_camera=
|
|
362
|
+
self.update_nii_viewer(reset_camera=False)
|
|
363
|
+
self.parent().refreshActorList()
|
|
364
|
+
|
|
365
|
+
def loadROIFile(self):
|
|
366
|
+
"""Load one or multiple ROI nifti volumes and render as surfaces."""
|
|
367
|
+
options = QFileDialog.Options()
|
|
368
|
+
filePaths, _ = QFileDialog.getOpenFileNames(self, "Open ROI Files", "",
|
|
369
|
+
"Nifti Files (*.nii *.nii.gz)",
|
|
370
|
+
options=options)
|
|
371
|
+
|
|
372
|
+
if not filePaths:
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
for roi_path in filePaths:
|
|
376
|
+
img = nib.load(roi_path)
|
|
377
|
+
roi = img.get_fdata()
|
|
378
|
+
|
|
379
|
+
datapv = pv.wrap(roi)
|
|
380
|
+
datapv.cell_data['labels'] = roi[:-1, :-1, :-1].flatten(order='F')
|
|
381
|
+
vol = datapv.threshold(value=1, scalars='labels')
|
|
382
|
+
mesh = vol.extract_surface()
|
|
383
|
+
|
|
384
|
+
smooth = mesh.smooth_taubin(n_iter=12)
|
|
385
|
+
|
|
386
|
+
actor_name = f"roi_{roi_path.split('/')[-1]}"
|
|
387
|
+
|
|
388
|
+
self.plotter.add_mesh(smooth, name=actor_name, opacity=1,
|
|
389
|
+
color="white", user_matrix=img.affine,
|
|
390
|
+
reset_camera=False, point_size=0,
|
|
391
|
+
render_lines_as_tubes=True)
|
|
392
|
+
|
|
393
|
+
self.parent().refreshActorList()
|
|
359
394
|
|
|
360
395
|
def loadGiftiFile(self):
|
|
361
396
|
options = QFileDialog.Options()
|
|
@@ -366,7 +401,8 @@ class TrkViewer(QWidget):
|
|
|
366
401
|
print(f"Loaded GIfTI file: {self.gii_file}")
|
|
367
402
|
self.gii_mesh = gifti_to_pyvista(filePath)
|
|
368
403
|
|
|
369
|
-
self.update_gii_viewer(reset_camera=
|
|
404
|
+
self.update_gii_viewer(reset_camera=False)
|
|
405
|
+
self.parent().refreshActorList()
|
|
370
406
|
|
|
371
407
|
def set_background_color(self):
|
|
372
408
|
|
|
@@ -377,25 +413,30 @@ class TrkViewer(QWidget):
|
|
|
377
413
|
|
|
378
414
|
self.plotter.background_color = self.background
|
|
379
415
|
|
|
416
|
+
def toggleColorBlind(self):
|
|
417
|
+
|
|
418
|
+
self.color_blind = not self.color_blind
|
|
419
|
+
self.update_trk_viewer()
|
|
420
|
+
|
|
380
421
|
def view_isometric(self):
|
|
381
|
-
# equivalent to BackgroundPlotter 'iso' camera position
|
|
382
422
|
self.plotter.view_isometric()
|
|
383
|
-
self.plotter.
|
|
423
|
+
self.plotter.camera.azimuth += self.flicker*180
|
|
424
|
+
self.flicker = not self.flicker
|
|
384
425
|
|
|
385
426
|
def view_xy(self):
|
|
386
|
-
|
|
387
|
-
self.plotter.
|
|
388
|
-
self.
|
|
427
|
+
self.plotter.view_xy()
|
|
428
|
+
self.plotter.camera.azimuth += self.flicker*180
|
|
429
|
+
self.flicker = not self.flicker
|
|
389
430
|
|
|
390
431
|
def view_xz(self):
|
|
391
|
-
# view from +Y axis, XZ plane (sagittal)
|
|
392
432
|
self.plotter.view_xz()
|
|
393
|
-
self.plotter.
|
|
433
|
+
self.plotter.camera.azimuth += self.flicker*180
|
|
434
|
+
self.flicker = not self.flicker
|
|
394
435
|
|
|
395
436
|
def view_yz(self):
|
|
396
|
-
# view from +X axis, YZ plane (coronal)
|
|
397
437
|
self.plotter.view_yz()
|
|
398
|
-
self.plotter.
|
|
438
|
+
self.plotter.camera.azimuth += self.flicker*180
|
|
439
|
+
self.flicker = not self.flicker
|
|
399
440
|
|
|
400
441
|
def take_screenshot(self):
|
|
401
442
|
"""Take a screenshot of the current 3D view."""
|
|
@@ -426,7 +467,7 @@ class TrkViewer(QWidget):
|
|
|
426
467
|
|
|
427
468
|
# Copy all meshes and volumes from the main plotter to the offscreen one
|
|
428
469
|
for actor_name, actor in self.plotter.actors.items():
|
|
429
|
-
offscreen.add_actor(actor
|
|
470
|
+
offscreen.add_actor(actor)
|
|
430
471
|
|
|
431
472
|
# Match background color and camera
|
|
432
473
|
offscreen.background_color = self.plotter.background_color
|
|
@@ -446,7 +487,7 @@ class TrkViewer(QWidget):
|
|
|
446
487
|
self.plotter.render()
|
|
447
488
|
offscreen.close()
|
|
448
489
|
|
|
449
|
-
def update_trk_viewer(self, reset_camera=False):
|
|
490
|
+
def update_trk_viewer(self, reset_camera: bool = False):
|
|
450
491
|
|
|
451
492
|
opacity = self.opacitySlider.value() / 100.0
|
|
452
493
|
show_points = self.showPointsCheckbox.isChecked()
|
|
@@ -467,7 +508,7 @@ class TrkViewer(QWidget):
|
|
|
467
508
|
plot_trk(file, opacity=opacity, plotter=self.plotter, scalar=scalar,
|
|
468
509
|
show_points=show_points, color_map=color_map,
|
|
469
510
|
name=file, background=self.background,
|
|
470
|
-
reset_camera=reset_camera)
|
|
511
|
+
reset_camera=reset_camera, color_blind=self.color_blind)
|
|
471
512
|
|
|
472
513
|
def _update_nii_x(self):
|
|
473
514
|
|
|
@@ -476,14 +517,14 @@ class TrkViewer(QWidget):
|
|
|
476
517
|
center = (self.XSlider.value(),
|
|
477
518
|
self.YSlider.value(), self.ZSlider.value())
|
|
478
519
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
520
|
+
slice_x = self.grid.slice('x', center)
|
|
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')
|
|
487
528
|
|
|
488
529
|
def _update_nii_y(self):
|
|
489
530
|
|
|
@@ -492,14 +533,14 @@ class TrkViewer(QWidget):
|
|
|
492
533
|
center = (self.XSlider.value(),
|
|
493
534
|
self.YSlider.value(), self.ZSlider.value())
|
|
494
535
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
536
|
+
slice_y = self.grid.slice('y', center)
|
|
537
|
+
self.plotter.add_mesh(slice_y, cmap='grey', name='nii_y',
|
|
538
|
+
show_scalar_bar=False, point_size=0,
|
|
539
|
+
render_lines_as_tubes=True,
|
|
540
|
+
reset_camera=False,
|
|
541
|
+
user_matrix=self.nii_affine)
|
|
542
|
+
else:
|
|
543
|
+
self.plotter.remove_actor('nii_y')
|
|
503
544
|
|
|
504
545
|
def _update_nii_z(self):
|
|
505
546
|
|
|
@@ -508,48 +549,35 @@ class TrkViewer(QWidget):
|
|
|
508
549
|
center = (self.XSlider.value(),
|
|
509
550
|
self.YSlider.value(), self.ZSlider.value())
|
|
510
551
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
else:
|
|
518
|
-
self.plotter.remove_actor('nii_z')
|
|
519
|
-
|
|
520
|
-
def update_nii_viewer(self, reset_camera=False):
|
|
521
|
-
|
|
522
|
-
if self.showSlicesCheckbox.isChecked():
|
|
523
|
-
|
|
524
|
-
self._update_nii_x()
|
|
525
|
-
self._update_nii_y()
|
|
526
|
-
self._update_nii_z()
|
|
527
|
-
|
|
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)
|
|
528
558
|
else:
|
|
529
|
-
self.plotter.remove_actor('nii_x')
|
|
530
|
-
self.plotter.remove_actor('nii_y')
|
|
531
559
|
self.plotter.remove_actor('nii_z')
|
|
532
560
|
|
|
533
|
-
|
|
534
|
-
opacity = self.nii_opacitySlider.value()/1000
|
|
535
|
-
self.plotter.add_volume(self.grid, cmap='gray', opacity=[0, opacity],
|
|
536
|
-
show_scalar_bar=False, name='nii_volume',
|
|
537
|
-
reset_camera=reset_camera)
|
|
538
|
-
else:
|
|
539
|
-
self.plotter.remove_actor('nii_volume', reset_camera=reset_camera)
|
|
561
|
+
def update_nii_viewer(self, reset_camera: bool = False):
|
|
540
562
|
|
|
541
|
-
|
|
563
|
+
self._update_nii_x()
|
|
564
|
+
self._update_nii_y()
|
|
565
|
+
self._update_nii_z()
|
|
542
566
|
|
|
543
|
-
|
|
567
|
+
opacity = self.nii_opacitySlider.value()/1000
|
|
568
|
+
self.plotter.add_volume(self.grid, cmap='gray', opacity=[0, opacity],
|
|
569
|
+
show_scalar_bar=False, name='nii_volume',
|
|
570
|
+
reset_camera=reset_camera,
|
|
571
|
+
user_matrix=self.nii_affine)
|
|
544
572
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
573
|
+
def update_gii_viewer(self, reset_camera: bool = False):
|
|
574
|
+
|
|
575
|
+
opacity = self.gii_opacitySlider.value()/100
|
|
576
|
+
self.plotter.add_mesh(self.gii_mesh, color="ghostwhite",
|
|
577
|
+
culling='back', smooth_shading=True,
|
|
578
|
+
opacity=opacity, name='gii_surface',
|
|
579
|
+
reset_camera=reset_camera, point_size=0,
|
|
580
|
+
render_lines_as_tubes=True,)
|
|
553
581
|
|
|
554
582
|
|
|
555
583
|
class MainWindow(QMainWindow):
|
|
@@ -557,6 +585,7 @@ class MainWindow(QMainWindow):
|
|
|
557
585
|
super().__init__()
|
|
558
586
|
self.viewer = TrkViewer()
|
|
559
587
|
self.setCentralWidget(self.viewer)
|
|
588
|
+
self.initActorDock()
|
|
560
589
|
self.initMenuBar()
|
|
561
590
|
self.setWindowTitle("UNVEIL - Tractography Viewer")
|
|
562
591
|
|
|
@@ -581,16 +610,108 @@ class MainWindow(QMainWindow):
|
|
|
581
610
|
loadGiftiAction.triggered.connect(self.viewer.loadGiftiFile)
|
|
582
611
|
fileMenu.addAction(loadGiftiAction)
|
|
583
612
|
|
|
613
|
+
# Load ROIs
|
|
614
|
+
loadROIAction = QAction('Load ROIs (.nii/.nii.gz)', self)
|
|
615
|
+
loadROIAction.triggered.connect(self.viewer.loadROIFile)
|
|
616
|
+
fileMenu.addAction(loadROIAction)
|
|
617
|
+
|
|
584
618
|
# Optional: Exit action
|
|
585
619
|
exitAction = QAction('Exit', self)
|
|
586
620
|
exitAction.triggered.connect(self.close)
|
|
587
621
|
fileMenu.addAction(exitAction)
|
|
588
622
|
|
|
589
623
|
# View menu
|
|
590
|
-
|
|
624
|
+
viewMenu = menubar.addMenu('View')
|
|
591
625
|
setBackColorAction = QAction('Background Color', self)
|
|
592
626
|
setBackColorAction.triggered.connect(self.viewer.set_background_color)
|
|
593
|
-
|
|
627
|
+
viewMenu.addAction(setBackColorAction)
|
|
628
|
+
toggleActorListAction = QAction("Toggle Actor List", self,
|
|
629
|
+
checkable=True)
|
|
630
|
+
toggleActorListAction.triggered.connect(self.toggleActorDock)
|
|
631
|
+
viewMenu.addAction(toggleActorListAction)
|
|
632
|
+
toggleColorBlindAction = QAction("Use color-blind colors", self,
|
|
633
|
+
checkable=True)
|
|
634
|
+
toggleColorBlindAction.triggered.connect(self.viewer.toggleColorBlind)
|
|
635
|
+
viewMenu.addAction(toggleColorBlindAction)
|
|
636
|
+
|
|
637
|
+
def initActorDock(self):
|
|
638
|
+
self.actorDock = QDockWidget("Scene Actors", self)
|
|
639
|
+
self.actorDock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea)
|
|
640
|
+
|
|
641
|
+
self.actorTree = QTreeWidget()
|
|
642
|
+
self.actorTree.setHeaderLabels(["Actor", "Visible"])
|
|
643
|
+
self.actorTree.setColumnCount(2)
|
|
644
|
+
self.actorTree.setColumnWidth(0, 200)
|
|
645
|
+
|
|
646
|
+
self.actorDock.setWidget(self.actorTree)
|
|
647
|
+
self.addDockWidget(
|
|
648
|
+
Qt.DockWidgetArea.RightDockWidgetArea, self.actorDock)
|
|
649
|
+
|
|
650
|
+
self.actorDock.hide() # start hidden
|
|
651
|
+
|
|
652
|
+
# Refresh the list whenever actors change
|
|
653
|
+
self.viewer.plotter.mesh_added_callback = self.refreshActorList
|
|
654
|
+
self.viewer.plotter.mesh_removed_callback = self.refreshActorList
|
|
655
|
+
|
|
656
|
+
def toggleActorDock(self, checked):
|
|
657
|
+
if checked:
|
|
658
|
+
|
|
659
|
+
self.actorDock.show()
|
|
660
|
+
else:
|
|
661
|
+
self.actorDock.hide()
|
|
662
|
+
|
|
663
|
+
def refreshActorList(self):
|
|
664
|
+
"""Rebuild the hierarchical actor list grouped by class."""
|
|
665
|
+
self.actorTree.clear()
|
|
666
|
+
|
|
667
|
+
# Groups
|
|
668
|
+
group_trk = QTreeWidgetItem(["TRK", ""])
|
|
669
|
+
group_nii = QTreeWidgetItem(["NIfTI Volume", ""])
|
|
670
|
+
group_roi = QTreeWidgetItem(["ROI Surfaces", ""])
|
|
671
|
+
group_gii = QTreeWidgetItem(["GIFTI", ""])
|
|
672
|
+
|
|
673
|
+
self.actorTree.addTopLevelItem(group_trk)
|
|
674
|
+
self.actorTree.addTopLevelItem(group_nii)
|
|
675
|
+
self.actorTree.addTopLevelItem(group_gii)
|
|
676
|
+
self.actorTree.addTopLevelItem(group_roi)
|
|
677
|
+
# Loop through PyVista actors
|
|
678
|
+
for name, actor in self.viewer.plotter.actors.items():
|
|
679
|
+
item = QTreeWidgetItem([name, ""])
|
|
680
|
+
item.setCheckState(
|
|
681
|
+
1, Qt.CheckState.Checked if actor.GetVisibility() else Qt.CheckState.Unchecked)
|
|
682
|
+
|
|
683
|
+
# Store name for callback
|
|
684
|
+
item.actor_name = name
|
|
685
|
+
|
|
686
|
+
# Insert into the correct group
|
|
687
|
+
if name.endswith(".trk"):
|
|
688
|
+
group_trk.addChild(item)
|
|
689
|
+
elif name.startswith("nii_"):
|
|
690
|
+
group_nii.addChild(item)
|
|
691
|
+
elif name.startswith("gii_"):
|
|
692
|
+
group_gii.addChild(item)
|
|
693
|
+
elif name.startswith("roi_"):
|
|
694
|
+
group_roi.addChild(item)
|
|
695
|
+
else:
|
|
696
|
+
group_trk.addChild(item) # default bucket
|
|
697
|
+
|
|
698
|
+
# Connect visibility toggles
|
|
699
|
+
self.actorTree.itemChanged.connect(self.onActorVisibilityChanged)
|
|
700
|
+
|
|
701
|
+
def onActorVisibilityChanged(self, item, column):
|
|
702
|
+
"""Toggle visibility when user clicks checkbox."""
|
|
703
|
+
if column != 1:
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
actor_name = item.actor_name
|
|
707
|
+
visible = item.checkState(1) == Qt.CheckState.Checked
|
|
708
|
+
|
|
709
|
+
try:
|
|
710
|
+
self.viewer.plotter.actors[actor_name].SetVisibility(visible)
|
|
711
|
+
except KeyError:
|
|
712
|
+
pass
|
|
713
|
+
|
|
714
|
+
self.viewer.plotter.render()
|
|
594
715
|
|
|
595
716
|
|
|
596
717
|
if __name__ == '__main__':
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unveil-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Implementation of UNVEIL
|
|
5
5
|
Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -25,10 +25,16 @@ Dynamic: license-file
|
|
|
25
25
|
|
|
26
26
|
A simple tractography viewer implemented in python.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
<img width="1297" height="757" alt="image" src="https://github.com/user-attachments/assets/28807e18-17fb-485b-a8ce-2179cdef796b" />
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
The GUI is launched with the following python command in a python console:
|
|
31
32
|
|
|
32
33
|
```
|
|
33
34
|
import unveil.__main__
|
|
34
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
|