unveil-python 0.1.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.1.0/src/unveil_python.egg-info → unveil_python-0.3.0}/PKG-INFO +8 -2
- {unveil_python-0.1.0 → unveil_python-0.3.0}/README.md +7 -1
- {unveil_python-0.1.0 → unveil_python-0.3.0}/src/unveil/__init__.py +1 -1
- {unveil_python-0.1.0 → unveil_python-0.3.0}/src/unveil/core.py +251 -110
- {unveil_python-0.1.0 → unveil_python-0.3.0/src/unveil_python.egg-info}/PKG-INFO +8 -2
- {unveil_python-0.1.0 → unveil_python-0.3.0}/LICENSE +0 -0
- {unveil_python-0.1.0 → unveil_python-0.3.0}/pyproject.toml +0 -0
- {unveil_python-0.1.0 → unveil_python-0.3.0}/setup.cfg +0 -0
- {unveil_python-0.1.0 → unveil_python-0.3.0}/src/unveil/__main__.py +0 -0
- {unveil_python-0.1.0 → unveil_python-0.3.0}/src/unveil_python.egg-info/SOURCES.txt +0 -0
- {unveil_python-0.1.0 → unveil_python-0.3.0}/src/unveil_python.egg-info/dependency_links.txt +0 -0
- {unveil_python-0.1.0 → unveil_python-0.3.0}/src/unveil_python.egg-info/requires.txt +0 -0
- {unveil_python-0.1.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.
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Created on Tue Mar 4 13:03:28 2025
|
|
4
4
|
|
|
5
|
-
@author:
|
|
5
|
+
@author: DELINTE Nicolas
|
|
6
6
|
"""
|
|
7
7
|
import sys
|
|
8
8
|
import numpy as np
|
|
9
9
|
import nibabel as nib
|
|
10
10
|
import pyvista as pv
|
|
11
|
-
from pyvistaqt import QtInteractor
|
|
11
|
+
from pyvistaqt import QtInteractor
|
|
12
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
|
|
@@ -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
|
|
@@ -214,6 +203,10 @@ class TrkViewer(QWidget):
|
|
|
214
203
|
btn_screenshot = QPushButton("Screenshot")
|
|
215
204
|
btn_screenshot.clicked.connect(self.take_screenshot)
|
|
216
205
|
cam_layout.addWidget(btn_screenshot)
|
|
206
|
+
btn_gif = QPushButton("Create GIF")
|
|
207
|
+
btn_gif.setToolTip("This takes ~2min to save after animation end.")
|
|
208
|
+
btn_gif.clicked.connect(self.create_gif)
|
|
209
|
+
cam_layout.addWidget(btn_gif)
|
|
217
210
|
|
|
218
211
|
global_layout.addLayout(cam_layout)
|
|
219
212
|
|
|
@@ -253,9 +246,8 @@ class TrkViewer(QWidget):
|
|
|
253
246
|
'Insert color map name. Name must be in the matplotlib library. Scalar nii.gz must be loaded.')
|
|
254
247
|
control_layout.addWidget(self.colorMapEdit)
|
|
255
248
|
|
|
256
|
-
self.
|
|
257
|
-
|
|
258
|
-
control_layout.addWidget(self.showVolumeCheckbox)
|
|
249
|
+
self.volume_label = QLabel('Volume')
|
|
250
|
+
control_layout.addWidget(self.volume_label)
|
|
259
251
|
|
|
260
252
|
self.nii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
261
253
|
self.nii_opacitySlider.setMinimum(0)
|
|
@@ -268,27 +260,24 @@ class TrkViewer(QWidget):
|
|
|
268
260
|
self.showSlicesCheckbox.stateChanged.connect(self.update_nii_viewer)
|
|
269
261
|
control_layout.addWidget(self.showSlicesCheckbox)
|
|
270
262
|
|
|
271
|
-
self.
|
|
272
|
-
|
|
273
|
-
control_layout.addWidget(self.showXCheckbox)
|
|
263
|
+
self.x_label = QLabel('X')
|
|
264
|
+
control_layout.addWidget(self.x_label)
|
|
274
265
|
self.XSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
275
266
|
self.XSlider.setMinimum(0)
|
|
276
267
|
self.XSlider.setMaximum(100)
|
|
277
268
|
self.XSlider.setValue(100)
|
|
278
269
|
self.XSlider.sliderReleased.connect(self._update_nii_x)
|
|
279
270
|
control_layout.addWidget(self.XSlider)
|
|
280
|
-
self.
|
|
281
|
-
|
|
282
|
-
control_layout.addWidget(self.showYCheckbox)
|
|
271
|
+
self.y_label = QLabel('Y')
|
|
272
|
+
control_layout.addWidget(self.y_label)
|
|
283
273
|
self.YSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
284
274
|
self.YSlider.setMinimum(0)
|
|
285
275
|
self.YSlider.setMaximum(100)
|
|
286
276
|
self.YSlider.setValue(100)
|
|
287
277
|
self.YSlider.sliderReleased.connect(self._update_nii_y)
|
|
288
278
|
control_layout.addWidget(self.YSlider)
|
|
289
|
-
self.
|
|
290
|
-
|
|
291
|
-
control_layout.addWidget(self.showZCheckbox)
|
|
279
|
+
self.z_label = QLabel('Z')
|
|
280
|
+
control_layout.addWidget(self.z_label)
|
|
292
281
|
self.ZSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
293
282
|
self.ZSlider.setMinimum(0)
|
|
294
283
|
self.ZSlider.setMaximum(100)
|
|
@@ -297,9 +286,8 @@ class TrkViewer(QWidget):
|
|
|
297
286
|
control_layout.addWidget(self.ZSlider)
|
|
298
287
|
|
|
299
288
|
# Gifti
|
|
300
|
-
self.
|
|
301
|
-
|
|
302
|
-
control_layout.addWidget(self.showSurfaceCheckbox)
|
|
289
|
+
self.surface_label = QLabel('Surface')
|
|
290
|
+
control_layout.addWidget(self.surface_label)
|
|
303
291
|
|
|
304
292
|
self.gii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
305
293
|
self.gii_opacitySlider.setMinimum(0)
|
|
@@ -320,15 +308,19 @@ class TrkViewer(QWidget):
|
|
|
320
308
|
|
|
321
309
|
def loadTrkFile(self):
|
|
322
310
|
options = QFileDialog.Options()
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
327
319
|
print(f"Loaded TRK file: {self.trk_file}")
|
|
328
320
|
self.names.append(self.trk_file)
|
|
329
|
-
self.selected_name = self.trk_file
|
|
330
321
|
|
|
331
322
|
self.update_trk_viewer(reset_camera=True)
|
|
323
|
+
self.parent().refreshActorList()
|
|
332
324
|
|
|
333
325
|
def loadNiftiFile(self):
|
|
334
326
|
options = QFileDialog.Options()
|
|
@@ -338,12 +330,10 @@ class TrkViewer(QWidget):
|
|
|
338
330
|
self.nii_file = filePath
|
|
339
331
|
print(f"Loaded NIfTI file: {self.nii_file}")
|
|
340
332
|
img = nib.load(filePath)
|
|
333
|
+
self.nii_affine = img.affine
|
|
341
334
|
self.nii_data = img.get_fdata()
|
|
342
335
|
grid = pv.ImageData()
|
|
343
336
|
grid.dimensions = np.array(self.nii_data.shape) + 1
|
|
344
|
-
# World coordinates
|
|
345
|
-
# grid.origin = img.affine[:3, 3]
|
|
346
|
-
# grid.spacing = np.diag(img.affine)[:3]
|
|
347
337
|
grid.cell_data['values'] = self.nii_data.flatten(order='F')
|
|
348
338
|
self.grid = grid
|
|
349
339
|
|
|
@@ -351,7 +341,45 @@ class TrkViewer(QWidget):
|
|
|
351
341
|
self.YSlider.setMaximum(self.nii_data.shape[1])
|
|
352
342
|
self.ZSlider.setMaximum(self.nii_data.shape[2])
|
|
353
343
|
|
|
354
|
-
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()
|
|
355
383
|
|
|
356
384
|
def loadGiftiFile(self):
|
|
357
385
|
options = QFileDialog.Options()
|
|
@@ -362,7 +390,8 @@ class TrkViewer(QWidget):
|
|
|
362
390
|
print(f"Loaded GIfTI file: {self.gii_file}")
|
|
363
391
|
self.gii_mesh = gifti_to_pyvista(filePath)
|
|
364
392
|
|
|
365
|
-
self.update_gii_viewer(reset_camera=
|
|
393
|
+
self.update_gii_viewer(reset_camera=False)
|
|
394
|
+
self.parent().refreshActorList()
|
|
366
395
|
|
|
367
396
|
def set_background_color(self):
|
|
368
397
|
|
|
@@ -396,17 +425,53 @@ class TrkViewer(QWidget):
|
|
|
396
425
|
def take_screenshot(self):
|
|
397
426
|
"""Take a screenshot of the current 3D view."""
|
|
398
427
|
options = QFileDialog.Options()
|
|
399
|
-
file_path, _ = QFileDialog.getSaveFileName(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
"",
|
|
403
|
-
"PNG Image (*.png);;JPEG Image (*.jpg);;TIFF Image (*.tif)",
|
|
404
|
-
options=options
|
|
405
|
-
)
|
|
428
|
+
file_path, _ = QFileDialog.getSaveFileName(self, "Save Screenshot", "",
|
|
429
|
+
"PNG Image (*.png);;JPEG Image (*.jpg);;TIFF Image (*.tif)",
|
|
430
|
+
options=options)
|
|
406
431
|
if file_path:
|
|
407
432
|
self.plotter.screenshot(file_path, transparent_background=True)
|
|
408
433
|
|
|
409
|
-
def
|
|
434
|
+
def create_gif(self):
|
|
435
|
+
"""Create a 360° rotation GIF of the current 3D view."""
|
|
436
|
+
options = QFileDialog.Options()
|
|
437
|
+
file_path, _ = QFileDialog.getSaveFileName(self, "Save GIF", "",
|
|
438
|
+
"GIF Files (*.gif)",
|
|
439
|
+
options=options)
|
|
440
|
+
|
|
441
|
+
if not file_path:
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
# Ensure the file has a .gif extension
|
|
445
|
+
if not file_path.lower().endswith(".gif"):
|
|
446
|
+
file_path += ".gif"
|
|
447
|
+
|
|
448
|
+
# Use an offscreen plotter so the main one remains visible
|
|
449
|
+
offscreen = pv.Plotter(
|
|
450
|
+
off_screen=True, window_size=self.plotter.window_size)
|
|
451
|
+
|
|
452
|
+
# Copy all meshes and volumes from the main plotter to the offscreen one
|
|
453
|
+
for actor_name, actor in self.plotter.actors.items():
|
|
454
|
+
offscreen.add_actor(actor.copy())
|
|
455
|
+
|
|
456
|
+
# Match background color and camera
|
|
457
|
+
offscreen.background_color = self.plotter.background_color
|
|
458
|
+
offscreen.camera_position = self.plotter.camera_position
|
|
459
|
+
offscreen.camera.SetPosition(self.plotter.camera.GetPosition())
|
|
460
|
+
offscreen.camera.SetFocalPoint(self.plotter.camera.GetFocalPoint())
|
|
461
|
+
offscreen.camera.SetViewUp(self.plotter.camera.GetViewUp())
|
|
462
|
+
|
|
463
|
+
# Create the 360° rotation GIF
|
|
464
|
+
offscreen.open_gif(file_path, fps=20)
|
|
465
|
+
n_frames = 360
|
|
466
|
+
for i in range(n_frames):
|
|
467
|
+
offscreen.camera.Azimuth(360 / n_frames)
|
|
468
|
+
offscreen.render()
|
|
469
|
+
offscreen.write_frame()
|
|
470
|
+
self.plotter.camera.azimuth += 360 / n_frames
|
|
471
|
+
self.plotter.render()
|
|
472
|
+
offscreen.close()
|
|
473
|
+
|
|
474
|
+
def update_trk_viewer(self, reset_camera: bool = False):
|
|
410
475
|
|
|
411
476
|
opacity = self.opacitySlider.value() / 100.0
|
|
412
477
|
show_points = self.showPointsCheckbox.isChecked()
|
|
@@ -436,14 +501,14 @@ class TrkViewer(QWidget):
|
|
|
436
501
|
center = (self.XSlider.value(),
|
|
437
502
|
self.YSlider.value(), self.ZSlider.value())
|
|
438
503
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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')
|
|
447
512
|
|
|
448
513
|
def _update_nii_y(self):
|
|
449
514
|
|
|
@@ -452,14 +517,14 @@ class TrkViewer(QWidget):
|
|
|
452
517
|
center = (self.XSlider.value(),
|
|
453
518
|
self.YSlider.value(), self.ZSlider.value())
|
|
454
519
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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')
|
|
463
528
|
|
|
464
529
|
def _update_nii_z(self):
|
|
465
530
|
|
|
@@ -468,48 +533,35 @@ class TrkViewer(QWidget):
|
|
|
468
533
|
center = (self.XSlider.value(),
|
|
469
534
|
self.YSlider.value(), self.ZSlider.value())
|
|
470
535
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
else:
|
|
478
|
-
self.plotter.remove_actor('nii_z')
|
|
479
|
-
|
|
480
|
-
def update_nii_viewer(self, reset_camera=False):
|
|
481
|
-
|
|
482
|
-
if self.showSlicesCheckbox.isChecked():
|
|
483
|
-
|
|
484
|
-
self._update_nii_x()
|
|
485
|
-
self._update_nii_y()
|
|
486
|
-
self._update_nii_z()
|
|
487
|
-
|
|
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)
|
|
488
542
|
else:
|
|
489
|
-
self.plotter.remove_actor('nii_x')
|
|
490
|
-
self.plotter.remove_actor('nii_y')
|
|
491
543
|
self.plotter.remove_actor('nii_z')
|
|
492
544
|
|
|
493
|
-
|
|
494
|
-
opacity = self.nii_opacitySlider.value()/1000
|
|
495
|
-
self.plotter.add_volume(self.grid, cmap='gray', opacity=[0, opacity],
|
|
496
|
-
show_scalar_bar=False, name='nii_volume',
|
|
497
|
-
reset_camera=reset_camera)
|
|
498
|
-
else:
|
|
499
|
-
self.plotter.remove_actor('nii_volume', reset_camera=reset_camera)
|
|
545
|
+
def update_nii_viewer(self, reset_camera: bool = False):
|
|
500
546
|
|
|
501
|
-
|
|
547
|
+
self._update_nii_x()
|
|
548
|
+
self._update_nii_y()
|
|
549
|
+
self._update_nii_z()
|
|
502
550
|
|
|
503
|
-
|
|
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)
|
|
504
556
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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,)
|
|
513
565
|
|
|
514
566
|
|
|
515
567
|
class MainWindow(QMainWindow):
|
|
@@ -517,8 +569,9 @@ class MainWindow(QMainWindow):
|
|
|
517
569
|
super().__init__()
|
|
518
570
|
self.viewer = TrkViewer()
|
|
519
571
|
self.setCentralWidget(self.viewer)
|
|
572
|
+
self.initActorDock()
|
|
520
573
|
self.initMenuBar()
|
|
521
|
-
self.setWindowTitle("Tractography Viewer
|
|
574
|
+
self.setWindowTitle("UNVEIL - Tractography Viewer")
|
|
522
575
|
|
|
523
576
|
def initMenuBar(self):
|
|
524
577
|
menubar = self.menuBar()
|
|
@@ -541,16 +594,104 @@ class MainWindow(QMainWindow):
|
|
|
541
594
|
loadGiftiAction.triggered.connect(self.viewer.loadGiftiFile)
|
|
542
595
|
fileMenu.addAction(loadGiftiAction)
|
|
543
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
|
+
|
|
544
602
|
# Optional: Exit action
|
|
545
603
|
exitAction = QAction('Exit', self)
|
|
546
604
|
exitAction.triggered.connect(self.close)
|
|
547
605
|
fileMenu.addAction(exitAction)
|
|
548
606
|
|
|
549
607
|
# View menu
|
|
550
|
-
|
|
608
|
+
viewMenu = menubar.addMenu('View')
|
|
551
609
|
setBackColorAction = QAction('Background Color', self)
|
|
552
610
|
setBackColorAction.triggered.connect(self.viewer.set_background_color)
|
|
553
|
-
|
|
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()
|
|
554
695
|
|
|
555
696
|
|
|
556
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
|