unveil-python 0.4.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.4.0 → unveil_python-0.5.0}/src/unveil/__init__.py +1 -1
- {unveil_python-0.4.0 → unveil_python-0.5.0}/src/unveil/core.py +333 -101
- unveil_python-0.5.0/src/unveil_python.egg-info/PKG-INFO +74 -0
- unveil_python-0.4.0/PKG-INFO +0 -40
- unveil_python-0.4.0/README.md +0 -21
- unveil_python-0.4.0/src/unveil_python.egg-info/PKG-INFO +0 -40
- {unveil_python-0.4.0 → unveil_python-0.5.0}/LICENSE +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.0}/pyproject.toml +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.0}/setup.cfg +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.0}/src/unveil/__main__.py +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.0}/src/unveil_python.egg-info/SOURCES.txt +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.0}/src/unveil_python.egg-info/dependency_links.txt +0 -0
- {unveil_python-0.4.0 → unveil_python-0.5.0}/src/unveil_python.egg-info/requires.txt +0 -0
- {unveil_python-0.4.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,14 +10,17 @@ import nibabel as nib
|
|
|
10
10
|
import pyvista as pv
|
|
11
11
|
from pyvistaqt import QtInteractor
|
|
12
12
|
from PyQt6.QtCore import Qt
|
|
13
|
-
from PyQt6.QtGui import QAction
|
|
14
13
|
from PyQt6.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout,
|
|
15
14
|
QPushButton, QFileDialog, QCheckBox, QSlider,
|
|
16
15
|
QLabel, QComboBox, QMainWindow, QLineEdit,
|
|
17
|
-
QDockWidget, QTreeWidget, QTreeWidgetItem
|
|
16
|
+
QDockWidget, QTreeWidget, QTreeWidgetItem,
|
|
17
|
+
QColorDialog, QTabWidget)
|
|
18
|
+
from PyQt6.QtGui import QAction, QColor
|
|
18
19
|
from dipy.io.streamline import load_tractogram
|
|
19
20
|
from matplotlib import pyplot as plt
|
|
20
21
|
from matplotlib.colors import LinearSegmentedColormap
|
|
22
|
+
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
|
|
23
|
+
from matplotlib.figure import Figure
|
|
21
24
|
# from unravel.utils import get_streamline_density
|
|
22
25
|
|
|
23
26
|
|
|
@@ -185,11 +188,9 @@ def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
|
|
|
185
188
|
class TrkViewer(QWidget):
|
|
186
189
|
def __init__(self):
|
|
187
190
|
super().__init__()
|
|
188
|
-
self.trk_file = None
|
|
189
|
-
self.nii_file = None
|
|
190
191
|
self.nii_data = None
|
|
191
192
|
self.grid = None
|
|
192
|
-
self.
|
|
193
|
+
self.actor_types = {}
|
|
193
194
|
self.x = 0
|
|
194
195
|
self.y = 0
|
|
195
196
|
self.z = 0
|
|
@@ -197,6 +198,9 @@ class TrkViewer(QWidget):
|
|
|
197
198
|
self.background = 'white'
|
|
198
199
|
self.color_blind = False
|
|
199
200
|
self.flicker = False
|
|
201
|
+
self.roi_colors = {}
|
|
202
|
+
self.roi_volumes = {}
|
|
203
|
+
self.roi_visibility = {}
|
|
200
204
|
|
|
201
205
|
def initUI(self):
|
|
202
206
|
# Create the main layout
|
|
@@ -209,10 +213,10 @@ class TrkViewer(QWidget):
|
|
|
209
213
|
btn_xy = QPushButton("Axe 1")
|
|
210
214
|
btn_xz = QPushButton("Axe 2")
|
|
211
215
|
btn_yz = QPushButton("Axe 3")
|
|
212
|
-
btn_iso.clicked.connect(self.view_isometric)
|
|
213
|
-
btn_xy.clicked.connect(self.view_xy)
|
|
214
|
-
btn_xz.clicked.connect(self.view_xz)
|
|
215
|
-
btn_yz.clicked.connect(self.view_yz)
|
|
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"))
|
|
216
220
|
cam_layout.addWidget(camera_label)
|
|
217
221
|
cam_layout.addWidget(btn_iso)
|
|
218
222
|
cam_layout.addWidget(btn_xy)
|
|
@@ -284,7 +288,8 @@ class TrkViewer(QWidget):
|
|
|
284
288
|
self.XSlider.setMinimum(0)
|
|
285
289
|
self.XSlider.setMaximum(100)
|
|
286
290
|
self.XSlider.setValue(100)
|
|
287
|
-
self.XSlider.sliderReleased.connect(
|
|
291
|
+
self.XSlider.sliderReleased.connect(
|
|
292
|
+
lambda: self._update_slice('x', 'nii_x'))
|
|
288
293
|
control_layout.addWidget(self.XSlider)
|
|
289
294
|
self.y_label = QLabel('Y')
|
|
290
295
|
control_layout.addWidget(self.y_label)
|
|
@@ -292,7 +297,8 @@ class TrkViewer(QWidget):
|
|
|
292
297
|
self.YSlider.setMinimum(0)
|
|
293
298
|
self.YSlider.setMaximum(100)
|
|
294
299
|
self.YSlider.setValue(100)
|
|
295
|
-
self.YSlider.sliderReleased.connect(
|
|
300
|
+
self.YSlider.sliderReleased.connect(
|
|
301
|
+
lambda: self._update_slice('y', 'nii_y'))
|
|
296
302
|
control_layout.addWidget(self.YSlider)
|
|
297
303
|
self.z_label = QLabel('Z')
|
|
298
304
|
control_layout.addWidget(self.z_label)
|
|
@@ -300,7 +306,8 @@ class TrkViewer(QWidget):
|
|
|
300
306
|
self.ZSlider.setMinimum(0)
|
|
301
307
|
self.ZSlider.setMaximum(100)
|
|
302
308
|
self.ZSlider.setValue(100)
|
|
303
|
-
self.ZSlider.sliderReleased.connect(
|
|
309
|
+
self.ZSlider.sliderReleased.connect(
|
|
310
|
+
lambda: self._update_slice('z', 'nii_z'))
|
|
304
311
|
control_layout.addWidget(self.ZSlider)
|
|
305
312
|
|
|
306
313
|
# Gifti
|
|
@@ -333,23 +340,22 @@ class TrkViewer(QWidget):
|
|
|
333
340
|
return
|
|
334
341
|
|
|
335
342
|
for trk_path in filePaths:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
self.names.append(self.trk_file)
|
|
343
|
+
print(f"Loaded TRK file: {trk_path}")
|
|
344
|
+
self.actor_types[trk_path] = 'trk'
|
|
339
345
|
|
|
340
346
|
self.update_trk_viewer(reset_camera=True)
|
|
341
|
-
self.
|
|
347
|
+
self.window().refreshActorList()
|
|
342
348
|
|
|
343
349
|
def loadNiftiFile(self):
|
|
344
350
|
options = QFileDialog.Options()
|
|
345
351
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
346
352
|
self, "Open .nii.gz File", "", "Nifti Files (*.nii.gz)", options=options)
|
|
347
353
|
if filePath:
|
|
348
|
-
|
|
349
|
-
print(f"Loaded NIfTI file: {self.nii_file}")
|
|
354
|
+
print(f"Loaded NIfTI file: {filePath}")
|
|
350
355
|
img = nib.load(filePath)
|
|
351
356
|
self.nii_affine = img.affine
|
|
352
357
|
self.nii_data = img.get_fdata()
|
|
358
|
+
|
|
353
359
|
grid = pv.ImageData()
|
|
354
360
|
grid.dimensions = np.array(self.nii_data.shape) + 1
|
|
355
361
|
grid.cell_data['values'] = self.nii_data.flatten(order='F')
|
|
@@ -360,7 +366,10 @@ class TrkViewer(QWidget):
|
|
|
360
366
|
self.ZSlider.setMaximum(self.nii_data.shape[2])
|
|
361
367
|
|
|
362
368
|
self.update_nii_viewer(reset_camera=False)
|
|
363
|
-
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)
|
|
364
373
|
|
|
365
374
|
def loadROIFile(self):
|
|
366
375
|
"""Load one or multiple ROI nifti volumes and render as surfaces."""
|
|
@@ -383,26 +392,34 @@ class TrkViewer(QWidget):
|
|
|
383
392
|
|
|
384
393
|
smooth = mesh.smooth_taubin(n_iter=12)
|
|
385
394
|
|
|
386
|
-
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
|
|
387
403
|
|
|
388
404
|
self.plotter.add_mesh(smooth, name=actor_name, opacity=1,
|
|
389
|
-
color=
|
|
405
|
+
color=default_color, user_matrix=img.affine,
|
|
390
406
|
reset_camera=False, point_size=0,
|
|
391
407
|
render_lines_as_tubes=True)
|
|
392
408
|
|
|
393
|
-
self.
|
|
409
|
+
self.window().refreshActorList()
|
|
410
|
+
|
|
411
|
+
self.window().ortho_viewer.add_roi(actor_name, roi, default_color)
|
|
394
412
|
|
|
395
413
|
def loadGiftiFile(self):
|
|
396
414
|
options = QFileDialog.Options()
|
|
397
415
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
398
416
|
self, "Open .gii File", "", "Gifti Files (*.gii)", options=options)
|
|
399
417
|
if filePath:
|
|
400
|
-
|
|
401
|
-
print(f"Loaded GIfTI file: {self.gii_file}")
|
|
418
|
+
print(f"Loaded GIfTI file: {filePath}")
|
|
402
419
|
self.gii_mesh = gifti_to_pyvista(filePath)
|
|
403
420
|
|
|
404
421
|
self.update_gii_viewer(reset_camera=False)
|
|
405
|
-
self.
|
|
422
|
+
self.window().refreshActorList()
|
|
406
423
|
|
|
407
424
|
def set_background_color(self):
|
|
408
425
|
|
|
@@ -413,29 +430,16 @@ class TrkViewer(QWidget):
|
|
|
413
430
|
|
|
414
431
|
self.plotter.background_color = self.background
|
|
415
432
|
|
|
433
|
+
self.window().ortho_viewer.set_background(self.background)
|
|
434
|
+
|
|
416
435
|
def toggleColorBlind(self):
|
|
417
436
|
|
|
418
437
|
self.color_blind = not self.color_blind
|
|
419
438
|
self.update_trk_viewer()
|
|
420
439
|
|
|
421
|
-
def
|
|
422
|
-
self.plotter
|
|
423
|
-
self.plotter.camera.azimuth += self.flicker*180
|
|
424
|
-
self.flicker = not self.flicker
|
|
425
|
-
|
|
426
|
-
def view_xy(self):
|
|
427
|
-
self.plotter.view_xy()
|
|
428
|
-
self.plotter.camera.azimuth += self.flicker*180
|
|
429
|
-
self.flicker = not self.flicker
|
|
430
|
-
|
|
431
|
-
def view_xz(self):
|
|
432
|
-
self.plotter.view_xz()
|
|
433
|
-
self.plotter.camera.azimuth += self.flicker*180
|
|
434
|
-
self.flicker = not self.flicker
|
|
435
|
-
|
|
436
|
-
def view_yz(self):
|
|
437
|
-
self.plotter.view_yz()
|
|
438
|
-
self.plotter.camera.azimuth += self.flicker*180
|
|
440
|
+
def set_view(self, method):
|
|
441
|
+
getattr(self.plotter, method)()
|
|
442
|
+
self.plotter.camera.azimuth += self.flicker * 180
|
|
439
443
|
self.flicker = not self.flicker
|
|
440
444
|
|
|
441
445
|
def take_screenshot(self):
|
|
@@ -503,66 +507,38 @@ class TrkViewer(QWidget):
|
|
|
503
507
|
color_map = self.colorMapEdit.text()
|
|
504
508
|
scalar = self.nii_data
|
|
505
509
|
|
|
506
|
-
for file in self.
|
|
507
|
-
|
|
508
|
-
plot_trk(file, opacity=opacity, plotter=self.plotter, scalar=scalar,
|
|
509
|
-
show_points=show_points, color_map=color_map,
|
|
510
|
-
name=file, background=self.background,
|
|
511
|
-
reset_camera=reset_camera, color_blind=self.color_blind)
|
|
510
|
+
for file in list(self.actor_types):
|
|
512
511
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if self.showSlicesCheckbox.isChecked():
|
|
512
|
+
if self.actor_types.get(file) == 'trk':
|
|
516
513
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
self.plotter.add_mesh(slice_x, cmap='grey', name='nii_x',
|
|
522
|
-
show_scalar_bar=False, point_size=0,
|
|
523
|
-
render_lines_as_tubes=True,
|
|
524
|
-
reset_camera=False,
|
|
525
|
-
user_matrix=self.nii_affine)
|
|
526
|
-
else:
|
|
527
|
-
self.plotter.remove_actor('nii_x')
|
|
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)
|
|
528
518
|
|
|
529
|
-
def
|
|
519
|
+
def _update_slice(self, axis, actor_name):
|
|
530
520
|
|
|
531
521
|
if self.showSlicesCheckbox.isChecked():
|
|
532
522
|
|
|
533
|
-
center = (self.XSlider.value(),
|
|
534
|
-
self.
|
|
535
|
-
|
|
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')
|
|
544
|
-
|
|
545
|
-
def _update_nii_z(self):
|
|
523
|
+
center = (self.XSlider.value(), self.YSlider.value(),
|
|
524
|
+
self.ZSlider.value())
|
|
546
525
|
|
|
547
|
-
|
|
526
|
+
slice_mesh = self.grid.slice(axis, origin=center)
|
|
548
527
|
|
|
549
|
-
|
|
550
|
-
self.YSlider.value(), self.ZSlider.value())
|
|
551
|
-
|
|
552
|
-
slice_z = self.grid.slice('z', center)
|
|
553
|
-
self.plotter.add_mesh(slice_z, cmap='grey', name='nii_z',
|
|
528
|
+
self.plotter.add_mesh(slice_mesh, cmap='grey', name=actor_name,
|
|
554
529
|
show_scalar_bar=False, point_size=0,
|
|
555
530
|
render_lines_as_tubes=True,
|
|
556
531
|
reset_camera=False,
|
|
557
532
|
user_matrix=self.nii_affine)
|
|
533
|
+
|
|
558
534
|
else:
|
|
559
|
-
self.plotter.remove_actor(
|
|
535
|
+
self.plotter.remove_actor(actor_name)
|
|
560
536
|
|
|
561
537
|
def update_nii_viewer(self, reset_camera: bool = False):
|
|
562
538
|
|
|
563
|
-
self.
|
|
564
|
-
self.
|
|
565
|
-
self.
|
|
539
|
+
self._update_slice('x', 'nii_x')
|
|
540
|
+
self._update_slice('y', 'nii_y')
|
|
541
|
+
self._update_slice('z', 'nii_z')
|
|
566
542
|
|
|
567
543
|
opacity = self.nii_opacitySlider.value()/1000
|
|
568
544
|
self.plotter.add_volume(self.grid, cmap='gray', opacity=[0, opacity],
|
|
@@ -580,11 +556,209 @@ class TrkViewer(QWidget):
|
|
|
580
556
|
render_lines_as_tubes=True,)
|
|
581
557
|
|
|
582
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
|
+
|
|
583
750
|
class MainWindow(QMainWindow):
|
|
584
751
|
def __init__(self):
|
|
585
752
|
super().__init__()
|
|
753
|
+
self.tabs = QTabWidget()
|
|
754
|
+
|
|
586
755
|
self.viewer = TrkViewer()
|
|
587
|
-
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)
|
|
588
762
|
self.initActorDock()
|
|
589
763
|
self.initMenuBar()
|
|
590
764
|
self.setWindowTitle("UNVEIL - Tractography Viewer")
|
|
@@ -639,9 +813,14 @@ class MainWindow(QMainWindow):
|
|
|
639
813
|
self.actorDock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea)
|
|
640
814
|
|
|
641
815
|
self.actorTree = QTreeWidget()
|
|
642
|
-
|
|
643
|
-
self.actorTree.
|
|
644
|
-
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)
|
|
645
824
|
|
|
646
825
|
self.actorDock.setWidget(self.actorTree)
|
|
647
826
|
self.addDockWidget(
|
|
@@ -653,15 +832,20 @@ class MainWindow(QMainWindow):
|
|
|
653
832
|
self.viewer.plotter.mesh_added_callback = self.refreshActorList
|
|
654
833
|
self.viewer.plotter.mesh_removed_callback = self.refreshActorList
|
|
655
834
|
|
|
835
|
+
self.actorTree.itemDoubleClicked.connect(self.changeActorColor)
|
|
836
|
+
|
|
837
|
+
# Connect visibility toggles
|
|
838
|
+
self.actorTree.itemChanged.connect(self.onActorVisibilityChanged)
|
|
839
|
+
|
|
656
840
|
def toggleActorDock(self, checked):
|
|
657
841
|
if checked:
|
|
658
|
-
|
|
659
842
|
self.actorDock.show()
|
|
660
843
|
else:
|
|
661
844
|
self.actorDock.hide()
|
|
662
845
|
|
|
663
846
|
def refreshActorList(self):
|
|
664
847
|
"""Rebuild the hierarchical actor list grouped by class."""
|
|
848
|
+
self.actorTree.blockSignals(True)
|
|
665
849
|
self.actorTree.clear()
|
|
666
850
|
|
|
667
851
|
# Groups
|
|
@@ -676,43 +860,91 @@ class MainWindow(QMainWindow):
|
|
|
676
860
|
self.actorTree.addTopLevelItem(group_roi)
|
|
677
861
|
# Loop through PyVista actors
|
|
678
862
|
for name, actor in self.viewer.plotter.actors.items():
|
|
679
|
-
|
|
680
|
-
item
|
|
681
|
-
|
|
863
|
+
|
|
864
|
+
item = QTreeWidgetItem(["", "", name])
|
|
865
|
+
|
|
866
|
+
item.setCheckState(0, Qt.CheckState.Checked if actor.GetVisibility()
|
|
867
|
+
else Qt.CheckState.Unchecked)
|
|
682
868
|
|
|
683
869
|
# Store name for callback
|
|
684
870
|
item.actor_name = name
|
|
685
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
|
+
|
|
686
880
|
# Insert into the correct group
|
|
687
|
-
if
|
|
881
|
+
if self.viewer.actor_types.get(name) == 'trk':
|
|
688
882
|
group_trk.addChild(item)
|
|
689
883
|
elif name.startswith("nii_"):
|
|
690
884
|
group_nii.addChild(item)
|
|
691
885
|
elif name.startswith("gii_"):
|
|
692
886
|
group_gii.addChild(item)
|
|
693
|
-
elif
|
|
887
|
+
elif self.viewer.actor_types.get(name) == 'roi':
|
|
694
888
|
group_roi.addChild(item)
|
|
695
889
|
else:
|
|
696
890
|
group_trk.addChild(item) # default bucket
|
|
697
891
|
|
|
698
|
-
|
|
699
|
-
self.actorTree.itemChanged.connect(self.onActorVisibilityChanged)
|
|
892
|
+
self.actorTree.blockSignals(False)
|
|
700
893
|
|
|
701
894
|
def onActorVisibilityChanged(self, item, column):
|
|
702
895
|
"""Toggle visibility when user clicks checkbox."""
|
|
703
|
-
if column !=
|
|
896
|
+
if column != 0:
|
|
704
897
|
return
|
|
705
898
|
|
|
706
899
|
actor_name = item.actor_name
|
|
707
|
-
visible = item.checkState(
|
|
900
|
+
visible = item.checkState(0) == Qt.CheckState.Checked
|
|
708
901
|
|
|
709
902
|
try:
|
|
710
903
|
self.viewer.plotter.actors[actor_name].SetVisibility(visible)
|
|
711
904
|
except KeyError:
|
|
712
905
|
pass
|
|
713
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
|
+
|
|
714
911
|
self.viewer.plotter.render()
|
|
715
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
|
+
|
|
716
948
|
|
|
717
949
|
if __name__ == '__main__':
|
|
718
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.4.0/PKG-INFO
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: unveil-python
|
|
3
|
-
Version: 0.4.0
|
|
4
|
-
Summary: Implementation of UNVEIL
|
|
5
|
-
Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
|
|
6
|
-
License-Expression: GPL-3.0-only
|
|
7
|
-
Project-URL: GitHub, https://github.com/DelinteNicolas/UNVEIL
|
|
8
|
-
Keywords: tractography,viewer
|
|
9
|
-
Classifier: Intended Audience :: Science/Research
|
|
10
|
-
Classifier: Natural Language :: English
|
|
11
|
-
Classifier: Programming Language :: Python
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
License-File: LICENSE
|
|
14
|
-
Requires-Dist: dipy
|
|
15
|
-
Requires-Dist: pyvista
|
|
16
|
-
Requires-Dist: pyvistaqt
|
|
17
|
-
Requires-Dist: PyQt6
|
|
18
|
-
Dynamic: license-file
|
|
19
|
-
|
|
20
|
-
# UNVEIL
|
|
21
|
-
[](https://pypi.org/project/unveil-python/)
|
|
22
|
-
[](https://pypi.org/project/unveil-python/)
|
|
23
|
-

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

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

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