unveil-python 0.2.0__tar.gz → 0.3.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.3.0}/PKG-INFO +8 -2
- {unveil_python-0.2.0 → unveil_python-0.3.0}/README.md +7 -1
- {unveil_python-0.2.0 → unveil_python-0.3.0}/src/unveil/__init__.py +1 -1
- {unveil_python-0.2.0 → unveil_python-0.3.0}/src/unveil/core.py +201 -100
- {unveil_python-0.2.0 → unveil_python-0.3.0/src/unveil_python.egg-info}/PKG-INFO +8 -2
- {unveil_python-0.2.0 → unveil_python-0.3.0}/LICENSE +0 -0
- {unveil_python-0.2.0 → unveil_python-0.3.0}/pyproject.toml +0 -0
- {unveil_python-0.2.0 → unveil_python-0.3.0}/setup.cfg +0 -0
- {unveil_python-0.2.0 → unveil_python-0.3.0}/src/unveil/__main__.py +0 -0
- {unveil_python-0.2.0 → unveil_python-0.3.0}/src/unveil_python.egg-info/SOURCES.txt +0 -0
- {unveil_python-0.2.0 → unveil_python-0.3.0}/src/unveil_python.egg-info/dependency_links.txt +0 -0
- {unveil_python-0.2.0 → unveil_python-0.3.0}/src/unveil_python.egg-info/requires.txt +0 -0
- {unveil_python-0.2.0 → unveil_python-0.3.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.3.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
|
|
@@ -27,8 +27,14 @@ A simple tractography viewer implemented in python.
|
|
|
27
27
|
|
|
28
28
|

|
|
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.
|
|
@@ -8,8 +8,14 @@ A simple tractography viewer implemented in python.
|
|
|
8
8
|
|
|
9
9
|

|
|
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,7 +13,8 @@ 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
|
|
@@ -27,17 +28,6 @@ def gifti_to_pyvista(gii_path):
|
|
|
27
28
|
using the inverse of the given NIfTI affine.
|
|
28
29
|
"""
|
|
29
30
|
|
|
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
31
|
# Load GIFTI file
|
|
42
32
|
img = nib.load(gii_path)
|
|
43
33
|
coords_list = img.get_arrays_from_intent('NIFTI_INTENT_POINTSET')
|
|
@@ -51,15 +41,12 @@ def gifti_to_pyvista(gii_path):
|
|
|
51
41
|
coords_world = coords_list[0].data.astype(np.float32)
|
|
52
42
|
faces = face_list[0].data.astype(np.int32)
|
|
53
43
|
|
|
54
|
-
# Transform to voxel space
|
|
55
|
-
coords_voxel = nib.affines.apply_affine(inv_affine, coords_world)
|
|
56
|
-
|
|
57
44
|
# Build PyVista face array (VTK expects [3, i1, i2, i3, 3, ...])
|
|
58
45
|
faces_pv = np.hstack(
|
|
59
46
|
(np.full((faces.shape[0], 1), 3, dtype=np.int32), faces)).astype(np.int32)
|
|
60
47
|
|
|
61
48
|
# Create PolyData mesh in voxel space
|
|
62
|
-
mesh = pv.PolyData(
|
|
49
|
+
mesh = pv.PolyData(coords_world, faces_pv)
|
|
63
50
|
|
|
64
51
|
return mesh
|
|
65
52
|
|
|
@@ -157,20 +144,23 @@ def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
|
|
|
157
144
|
interpolate_before_map=False, render_lines_as_tubes=True,
|
|
158
145
|
line_width=2, point_size=point_size, rgb=rgb,
|
|
159
146
|
cmap=color_map, clim=color_lim, scalars=scalars, name=name,
|
|
160
|
-
reset_camera=reset_camera
|
|
147
|
+
reset_camera=reset_camera,
|
|
148
|
+
user_matrix=trk.affine)
|
|
161
149
|
|
|
162
150
|
elif color_map == 'flesh':
|
|
163
151
|
p.add_mesh(mesh, opacity=opacity, diffuse=0.4, ambient=ambient,
|
|
164
152
|
interpolate_before_map=False, render_lines_as_tubes=True,
|
|
165
153
|
line_width=2, point_size=point_size, rgb=rgb,
|
|
166
154
|
color=[250, 225, 210], name=name,
|
|
167
|
-
reset_camera=reset_camera
|
|
155
|
+
reset_camera=reset_camera,
|
|
156
|
+
user_matrix=trk.affine)
|
|
168
157
|
else:
|
|
169
158
|
p.add_mesh(mesh, opacity=opacity, diffuse=diffuse, ambient=ambient,
|
|
170
159
|
interpolate_before_map=False, render_lines_as_tubes=True,
|
|
171
160
|
line_width=2, point_size=point_size, rgb=rgb,
|
|
172
161
|
cmap=color_map, scalars=scalars, name=name,
|
|
173
|
-
reset_camera=reset_camera
|
|
162
|
+
reset_camera=reset_camera,
|
|
163
|
+
user_matrix=trk.affine)
|
|
174
164
|
|
|
175
165
|
p.background_color = background
|
|
176
166
|
# Do not call p.show() here when using an embedded interactor
|
|
@@ -184,7 +174,6 @@ class TrkViewer(QWidget):
|
|
|
184
174
|
self.nii_data = None
|
|
185
175
|
self.grid = None
|
|
186
176
|
self.names = []
|
|
187
|
-
self.selected_name = None
|
|
188
177
|
self.x = 0
|
|
189
178
|
self.y = 0
|
|
190
179
|
self.z = 0
|
|
@@ -257,9 +246,8 @@ class TrkViewer(QWidget):
|
|
|
257
246
|
'Insert color map name. Name must be in the matplotlib library. Scalar nii.gz must be loaded.')
|
|
258
247
|
control_layout.addWidget(self.colorMapEdit)
|
|
259
248
|
|
|
260
|
-
self.
|
|
261
|
-
|
|
262
|
-
control_layout.addWidget(self.showVolumeCheckbox)
|
|
249
|
+
self.volume_label = QLabel('Volume')
|
|
250
|
+
control_layout.addWidget(self.volume_label)
|
|
263
251
|
|
|
264
252
|
self.nii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
265
253
|
self.nii_opacitySlider.setMinimum(0)
|
|
@@ -272,27 +260,24 @@ class TrkViewer(QWidget):
|
|
|
272
260
|
self.showSlicesCheckbox.stateChanged.connect(self.update_nii_viewer)
|
|
273
261
|
control_layout.addWidget(self.showSlicesCheckbox)
|
|
274
262
|
|
|
275
|
-
self.
|
|
276
|
-
|
|
277
|
-
control_layout.addWidget(self.showXCheckbox)
|
|
263
|
+
self.x_label = QLabel('X')
|
|
264
|
+
control_layout.addWidget(self.x_label)
|
|
278
265
|
self.XSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
279
266
|
self.XSlider.setMinimum(0)
|
|
280
267
|
self.XSlider.setMaximum(100)
|
|
281
268
|
self.XSlider.setValue(100)
|
|
282
269
|
self.XSlider.sliderReleased.connect(self._update_nii_x)
|
|
283
270
|
control_layout.addWidget(self.XSlider)
|
|
284
|
-
self.
|
|
285
|
-
|
|
286
|
-
control_layout.addWidget(self.showYCheckbox)
|
|
271
|
+
self.y_label = QLabel('Y')
|
|
272
|
+
control_layout.addWidget(self.y_label)
|
|
287
273
|
self.YSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
288
274
|
self.YSlider.setMinimum(0)
|
|
289
275
|
self.YSlider.setMaximum(100)
|
|
290
276
|
self.YSlider.setValue(100)
|
|
291
277
|
self.YSlider.sliderReleased.connect(self._update_nii_y)
|
|
292
278
|
control_layout.addWidget(self.YSlider)
|
|
293
|
-
self.
|
|
294
|
-
|
|
295
|
-
control_layout.addWidget(self.showZCheckbox)
|
|
279
|
+
self.z_label = QLabel('Z')
|
|
280
|
+
control_layout.addWidget(self.z_label)
|
|
296
281
|
self.ZSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
297
282
|
self.ZSlider.setMinimum(0)
|
|
298
283
|
self.ZSlider.setMaximum(100)
|
|
@@ -301,9 +286,8 @@ class TrkViewer(QWidget):
|
|
|
301
286
|
control_layout.addWidget(self.ZSlider)
|
|
302
287
|
|
|
303
288
|
# Gifti
|
|
304
|
-
self.
|
|
305
|
-
|
|
306
|
-
control_layout.addWidget(self.showSurfaceCheckbox)
|
|
289
|
+
self.surface_label = QLabel('Surface')
|
|
290
|
+
control_layout.addWidget(self.surface_label)
|
|
307
291
|
|
|
308
292
|
self.gii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
309
293
|
self.gii_opacitySlider.setMinimum(0)
|
|
@@ -324,15 +308,19 @@ class TrkViewer(QWidget):
|
|
|
324
308
|
|
|
325
309
|
def loadTrkFile(self):
|
|
326
310
|
options = QFileDialog.Options()
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
311
|
+
filePaths, _ = QFileDialog.getOpenFileNames(self, "Open TRK Files", "",
|
|
312
|
+
"Tractography Files (*.trk)",
|
|
313
|
+
options=options)
|
|
314
|
+
if not filePaths:
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
for trk_path in filePaths:
|
|
318
|
+
self.trk_file = trk_path
|
|
331
319
|
print(f"Loaded TRK file: {self.trk_file}")
|
|
332
320
|
self.names.append(self.trk_file)
|
|
333
|
-
self.selected_name = self.trk_file
|
|
334
321
|
|
|
335
322
|
self.update_trk_viewer(reset_camera=True)
|
|
323
|
+
self.parent().refreshActorList()
|
|
336
324
|
|
|
337
325
|
def loadNiftiFile(self):
|
|
338
326
|
options = QFileDialog.Options()
|
|
@@ -342,12 +330,10 @@ class TrkViewer(QWidget):
|
|
|
342
330
|
self.nii_file = filePath
|
|
343
331
|
print(f"Loaded NIfTI file: {self.nii_file}")
|
|
344
332
|
img = nib.load(filePath)
|
|
333
|
+
self.nii_affine = img.affine
|
|
345
334
|
self.nii_data = img.get_fdata()
|
|
346
335
|
grid = pv.ImageData()
|
|
347
336
|
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
337
|
grid.cell_data['values'] = self.nii_data.flatten(order='F')
|
|
352
338
|
self.grid = grid
|
|
353
339
|
|
|
@@ -355,7 +341,45 @@ class TrkViewer(QWidget):
|
|
|
355
341
|
self.YSlider.setMaximum(self.nii_data.shape[1])
|
|
356
342
|
self.ZSlider.setMaximum(self.nii_data.shape[2])
|
|
357
343
|
|
|
358
|
-
self.update_nii_viewer(reset_camera=
|
|
344
|
+
self.update_nii_viewer(reset_camera=False)
|
|
345
|
+
self.parent().refreshActorList()
|
|
346
|
+
|
|
347
|
+
def loadROIFile(self):
|
|
348
|
+
"""Load one or multiple ROI nifti volumes and render as surfaces."""
|
|
349
|
+
options = QFileDialog.Options()
|
|
350
|
+
filePaths, _ = QFileDialog.getOpenFileNames(self, "Open ROI Files", "",
|
|
351
|
+
"Nifti Files (*.nii *.nii.gz)",
|
|
352
|
+
options=options)
|
|
353
|
+
|
|
354
|
+
if not filePaths:
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
for roi_path in filePaths:
|
|
358
|
+
img = nib.load(roi_path)
|
|
359
|
+
roi = img.get_fdata()
|
|
360
|
+
affine = img.affine
|
|
361
|
+
|
|
362
|
+
datapv = pv.wrap(roi)
|
|
363
|
+
datapv.cell_data['labels'] = roi[:-1, :-1, :-1].flatten(order='F')
|
|
364
|
+
vol = datapv.threshold(value=1, scalars='labels')
|
|
365
|
+
mesh = vol.extract_surface()
|
|
366
|
+
|
|
367
|
+
smooth = mesh.smooth_taubin(n_iter=12)
|
|
368
|
+
|
|
369
|
+
actor_name = f"roi_{roi_path.split('/')[-1]}"
|
|
370
|
+
|
|
371
|
+
self.plotter.add_mesh(
|
|
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
|
+
)
|
|
381
|
+
|
|
382
|
+
self.parent().refreshActorList()
|
|
359
383
|
|
|
360
384
|
def loadGiftiFile(self):
|
|
361
385
|
options = QFileDialog.Options()
|
|
@@ -366,7 +390,8 @@ class TrkViewer(QWidget):
|
|
|
366
390
|
print(f"Loaded GIfTI file: {self.gii_file}")
|
|
367
391
|
self.gii_mesh = gifti_to_pyvista(filePath)
|
|
368
392
|
|
|
369
|
-
self.update_gii_viewer(reset_camera=
|
|
393
|
+
self.update_gii_viewer(reset_camera=False)
|
|
394
|
+
self.parent().refreshActorList()
|
|
370
395
|
|
|
371
396
|
def set_background_color(self):
|
|
372
397
|
|
|
@@ -446,7 +471,7 @@ class TrkViewer(QWidget):
|
|
|
446
471
|
self.plotter.render()
|
|
447
472
|
offscreen.close()
|
|
448
473
|
|
|
449
|
-
def update_trk_viewer(self, reset_camera=False):
|
|
474
|
+
def update_trk_viewer(self, reset_camera: bool = False):
|
|
450
475
|
|
|
451
476
|
opacity = self.opacitySlider.value() / 100.0
|
|
452
477
|
show_points = self.showPointsCheckbox.isChecked()
|
|
@@ -476,14 +501,14 @@ class TrkViewer(QWidget):
|
|
|
476
501
|
center = (self.XSlider.value(),
|
|
477
502
|
self.YSlider.value(), self.ZSlider.value())
|
|
478
503
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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')
|
|
487
512
|
|
|
488
513
|
def _update_nii_y(self):
|
|
489
514
|
|
|
@@ -492,14 +517,14 @@ class TrkViewer(QWidget):
|
|
|
492
517
|
center = (self.XSlider.value(),
|
|
493
518
|
self.YSlider.value(), self.ZSlider.value())
|
|
494
519
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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')
|
|
503
528
|
|
|
504
529
|
def _update_nii_z(self):
|
|
505
530
|
|
|
@@ -508,48 +533,35 @@ class TrkViewer(QWidget):
|
|
|
508
533
|
center = (self.XSlider.value(),
|
|
509
534
|
self.YSlider.value(), self.ZSlider.value())
|
|
510
535
|
|
|
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
|
-
|
|
536
|
+
slice_z = self.grid.slice('z', center)
|
|
537
|
+
self.plotter.add_mesh(slice_z, cmap='grey', name='nii_z',
|
|
538
|
+
show_scalar_bar=False, point_size=0,
|
|
539
|
+
render_lines_as_tubes=True,
|
|
540
|
+
reset_camera=False,
|
|
541
|
+
user_matrix=self.nii_affine)
|
|
528
542
|
else:
|
|
529
|
-
self.plotter.remove_actor('nii_x')
|
|
530
|
-
self.plotter.remove_actor('nii_y')
|
|
531
543
|
self.plotter.remove_actor('nii_z')
|
|
532
544
|
|
|
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)
|
|
545
|
+
def update_nii_viewer(self, reset_camera: bool = False):
|
|
540
546
|
|
|
541
|
-
|
|
547
|
+
self._update_nii_x()
|
|
548
|
+
self._update_nii_y()
|
|
549
|
+
self._update_nii_z()
|
|
542
550
|
|
|
543
|
-
|
|
551
|
+
opacity = self.nii_opacitySlider.value()/1000
|
|
552
|
+
self.plotter.add_volume(self.grid, cmap='gray', opacity=[0, opacity],
|
|
553
|
+
show_scalar_bar=False, name='nii_volume',
|
|
554
|
+
reset_camera=reset_camera,
|
|
555
|
+
user_matrix=self.nii_affine)
|
|
544
556
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
557
|
+
def update_gii_viewer(self, reset_camera: bool = False):
|
|
558
|
+
|
|
559
|
+
opacity = self.gii_opacitySlider.value()/100
|
|
560
|
+
self.plotter.add_mesh(self.gii_mesh, color="ghostwhite",
|
|
561
|
+
culling='back', smooth_shading=True,
|
|
562
|
+
opacity=opacity, name='gii_surface',
|
|
563
|
+
reset_camera=reset_camera, point_size=0,
|
|
564
|
+
render_lines_as_tubes=True,)
|
|
553
565
|
|
|
554
566
|
|
|
555
567
|
class MainWindow(QMainWindow):
|
|
@@ -557,6 +569,7 @@ class MainWindow(QMainWindow):
|
|
|
557
569
|
super().__init__()
|
|
558
570
|
self.viewer = TrkViewer()
|
|
559
571
|
self.setCentralWidget(self.viewer)
|
|
572
|
+
self.initActorDock()
|
|
560
573
|
self.initMenuBar()
|
|
561
574
|
self.setWindowTitle("UNVEIL - Tractography Viewer")
|
|
562
575
|
|
|
@@ -581,16 +594,104 @@ class MainWindow(QMainWindow):
|
|
|
581
594
|
loadGiftiAction.triggered.connect(self.viewer.loadGiftiFile)
|
|
582
595
|
fileMenu.addAction(loadGiftiAction)
|
|
583
596
|
|
|
597
|
+
# Load ROIs
|
|
598
|
+
loadROIAction = QAction('Load ROIs (.nii/.nii.gz)', self)
|
|
599
|
+
loadROIAction.triggered.connect(self.viewer.loadROIFile)
|
|
600
|
+
fileMenu.addAction(loadROIAction)
|
|
601
|
+
|
|
584
602
|
# Optional: Exit action
|
|
585
603
|
exitAction = QAction('Exit', self)
|
|
586
604
|
exitAction.triggered.connect(self.close)
|
|
587
605
|
fileMenu.addAction(exitAction)
|
|
588
606
|
|
|
589
607
|
# View menu
|
|
590
|
-
|
|
608
|
+
viewMenu = menubar.addMenu('View')
|
|
591
609
|
setBackColorAction = QAction('Background Color', self)
|
|
592
610
|
setBackColorAction.triggered.connect(self.viewer.set_background_color)
|
|
593
|
-
|
|
611
|
+
viewMenu.addAction(setBackColorAction)
|
|
612
|
+
toggleActorListAction = QAction(
|
|
613
|
+
"Toggle Actor List", self, checkable=True)
|
|
614
|
+
toggleActorListAction.triggered.connect(self.toggleActorDock)
|
|
615
|
+
viewMenu.addAction(toggleActorListAction)
|
|
616
|
+
|
|
617
|
+
def initActorDock(self):
|
|
618
|
+
self.actorDock = QDockWidget("Scene Actors", self)
|
|
619
|
+
self.actorDock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea)
|
|
620
|
+
|
|
621
|
+
self.actorTree = QTreeWidget()
|
|
622
|
+
self.actorTree.setHeaderLabels(["Actor", "Visible"])
|
|
623
|
+
self.actorTree.setColumnCount(2)
|
|
624
|
+
self.actorTree.setColumnWidth(0, 200)
|
|
625
|
+
|
|
626
|
+
self.actorDock.setWidget(self.actorTree)
|
|
627
|
+
self.addDockWidget(
|
|
628
|
+
Qt.DockWidgetArea.RightDockWidgetArea, self.actorDock)
|
|
629
|
+
|
|
630
|
+
self.actorDock.hide() # start hidden
|
|
631
|
+
|
|
632
|
+
# Refresh the list whenever actors change
|
|
633
|
+
self.viewer.plotter.mesh_added_callback = self.refreshActorList
|
|
634
|
+
self.viewer.plotter.mesh_removed_callback = self.refreshActorList
|
|
635
|
+
|
|
636
|
+
def toggleActorDock(self, checked):
|
|
637
|
+
if checked:
|
|
638
|
+
|
|
639
|
+
self.actorDock.show()
|
|
640
|
+
else:
|
|
641
|
+
self.actorDock.hide()
|
|
642
|
+
|
|
643
|
+
def refreshActorList(self):
|
|
644
|
+
"""Rebuild the hierarchical actor list grouped by class."""
|
|
645
|
+
self.actorTree.clear()
|
|
646
|
+
|
|
647
|
+
# Groups
|
|
648
|
+
group_trk = QTreeWidgetItem(["TRK", ""])
|
|
649
|
+
group_nii = QTreeWidgetItem(["NIfTI Volume", ""])
|
|
650
|
+
group_roi = QTreeWidgetItem(["ROI Surfaces", ""])
|
|
651
|
+
group_gii = QTreeWidgetItem(["GIFTI", ""])
|
|
652
|
+
|
|
653
|
+
self.actorTree.addTopLevelItem(group_trk)
|
|
654
|
+
self.actorTree.addTopLevelItem(group_nii)
|
|
655
|
+
self.actorTree.addTopLevelItem(group_gii)
|
|
656
|
+
self.actorTree.addTopLevelItem(group_roi)
|
|
657
|
+
# Loop through PyVista actors
|
|
658
|
+
for name, actor in self.viewer.plotter.actors.items():
|
|
659
|
+
item = QTreeWidgetItem([name, ""])
|
|
660
|
+
item.setCheckState(
|
|
661
|
+
1, Qt.CheckState.Checked if actor.GetVisibility() else Qt.CheckState.Unchecked)
|
|
662
|
+
|
|
663
|
+
# Store name for callback
|
|
664
|
+
item.actor_name = name
|
|
665
|
+
|
|
666
|
+
# Insert into the correct group
|
|
667
|
+
if name.endswith(".trk"):
|
|
668
|
+
group_trk.addChild(item)
|
|
669
|
+
elif name.startswith("nii_"):
|
|
670
|
+
group_nii.addChild(item)
|
|
671
|
+
elif name.startswith("gii_"):
|
|
672
|
+
group_gii.addChild(item)
|
|
673
|
+
elif name.startswith("roi_"):
|
|
674
|
+
group_roi.addChild(item)
|
|
675
|
+
else:
|
|
676
|
+
group_trk.addChild(item) # default bucket
|
|
677
|
+
|
|
678
|
+
# Connect visibility toggles
|
|
679
|
+
self.actorTree.itemChanged.connect(self.onActorVisibilityChanged)
|
|
680
|
+
|
|
681
|
+
def onActorVisibilityChanged(self, item, column):
|
|
682
|
+
"""Toggle visibility when user clicks checkbox."""
|
|
683
|
+
if column != 1:
|
|
684
|
+
return
|
|
685
|
+
|
|
686
|
+
actor_name = item.actor_name
|
|
687
|
+
visible = item.checkState(1) == Qt.CheckState.Checked
|
|
688
|
+
|
|
689
|
+
try:
|
|
690
|
+
self.viewer.plotter.actors[actor_name].SetVisibility(visible)
|
|
691
|
+
except KeyError:
|
|
692
|
+
pass
|
|
693
|
+
|
|
694
|
+
self.viewer.plotter.render()
|
|
594
695
|
|
|
595
696
|
|
|
596
697
|
if __name__ == '__main__':
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unveil-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -27,8 +27,14 @@ A simple tractography viewer implemented in python.
|
|
|
27
27
|
|
|
28
28
|

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