unveil-python 0.0.2__tar.gz → 0.2.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.
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unveil-python
3
- Version: 0.0.2
3
+ Version: 0.2.0
4
4
  Summary: Implementation of UNVEIL
5
5
  Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
6
+ License-Expression: GPL-3.0-only
6
7
  Project-URL: GitHub, https://github.com/DelinteNicolas/UNVEIL
7
8
  Keywords: tractography,viewer
8
9
  Classifier: Intended Audience :: Science/Research
@@ -12,10 +13,22 @@ Description-Content-Type: text/markdown
12
13
  License-File: LICENSE
13
14
  Requires-Dist: dipy
14
15
  Requires-Dist: pyvista
16
+ Requires-Dist: pyvistaqt
15
17
  Requires-Dist: PyQt6
16
18
  Dynamic: license-file
17
19
 
18
20
  # UNVEIL
21
+ [![PyPI](https://img.shields.io/pypi/v/unveil-python?label=pypi%20package)](https://pypi.org/project/unveil-python/)
22
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/unveil-python)](https://pypi.org/project/unveil-python/)
23
+ ![GitHub repo size](https://img.shields.io/github/repo-size/DelinteNicolas/unveil)
24
+
25
+
19
26
  A simple tractography viewer implemented in python.
20
27
 
21
28
  ![image](https://github.com/user-attachments/assets/4fbefa7a-3fc2-446a-8835-2844266b14fa)
29
+
30
+ The GUI is launched with the following python command:
31
+
32
+ ```
33
+ import unveil.__main__
34
+ ```
@@ -0,0 +1,15 @@
1
+ # UNVEIL
2
+ [![PyPI](https://img.shields.io/pypi/v/unveil-python?label=pypi%20package)](https://pypi.org/project/unveil-python/)
3
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/unveil-python)](https://pypi.org/project/unveil-python/)
4
+ ![GitHub repo size](https://img.shields.io/github/repo-size/DelinteNicolas/unveil)
5
+
6
+
7
+ A simple tractography viewer implemented in python.
8
+
9
+ ![image](https://github.com/user-attachments/assets/4fbefa7a-3fc2-446a-8835-2844266b14fa)
10
+
11
+ The GUI is launched with the following python command:
12
+
13
+ ```
14
+ import unveil.__main__
15
+ ```
@@ -9,6 +9,7 @@ authors = [
9
9
  ]
10
10
  readme="README.md"
11
11
  description = "Implementation of UNVEIL"
12
+ license = "GPL-3.0-only"
12
13
  keywords = ["tractography", "viewer"]
13
14
  classifiers = ['Intended Audience :: Science/Research',
14
15
  'Natural Language :: English',
@@ -16,6 +17,7 @@ classifiers = ['Intended Audience :: Science/Research',
16
17
  ]
17
18
  dependencies = ['dipy',
18
19
  'pyvista',
20
+ 'pyvistaqt',
19
21
  'PyQt6'
20
22
  ]
21
23
  dynamic = ["version"]
@@ -27,4 +29,4 @@ version = {attr = "unveil.__version__"}
27
29
  "GitHub" = "https://github.com/DelinteNicolas/UNVEIL"
28
30
 
29
31
  [tool.setuptools.packages.find]
30
- where = ["src"]
32
+ where = ["src"]
@@ -1,5 +1,5 @@
1
1
  """
2
2
  This package contains an UNVEIL, a lightweight tractography viewer implemented in python."""
3
3
 
4
- __version__ = "0.0.2"
4
+ __version__ = "0.2.0"
5
5
  __author__ = 'Nicolas Delinte'
@@ -2,14 +2,13 @@
2
2
  """
3
3
  Created on Tue Mar 4 13:03:28 2025
4
4
 
5
- @author: nicol
5
+ @author: DELINTE Nicolas
6
6
  """
7
-
8
7
  import sys
9
8
  import numpy as np
10
9
  import nibabel as nib
11
10
  import pyvista as pv
12
- from pyvistaqt import QtInteractor # For embedding PyVista in PyQt6
11
+ from pyvistaqt import QtInteractor
13
12
  from PyQt6.QtCore import Qt
14
13
  from PyQt6.QtGui import QAction
15
14
  from PyQt6.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout,
@@ -22,9 +21,52 @@ from matplotlib.colors import LinearSegmentedColormap
22
21
  # from unravel.viz import plot_trk # We'll use our local version below
23
22
 
24
23
 
24
+ def gifti_to_pyvista(gii_path):
25
+ """
26
+ Load a GIFTI surface and transform coordinates into voxel space
27
+ using the inverse of the given NIfTI affine.
28
+ """
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
+ # Load GIFTI file
42
+ img = nib.load(gii_path)
43
+ coords_list = img.get_arrays_from_intent('NIFTI_INTENT_POINTSET')
44
+ face_list = img.get_arrays_from_intent('NIFTI_INTENT_TRIANGLE')
45
+
46
+ if len(coords_list) == 0 or len(face_list) == 0:
47
+ raise RuntimeError(
48
+ "Could not find POINTSET or TRIANGLE arrays in GIFTI file.")
49
+
50
+ # Extract data
51
+ coords_world = coords_list[0].data.astype(np.float32)
52
+ faces = face_list[0].data.astype(np.int32)
53
+
54
+ # Transform to voxel space
55
+ coords_voxel = nib.affines.apply_affine(inv_affine, coords_world)
56
+
57
+ # Build PyVista face array (VTK expects [3, i1, i2, i3, 3, ...])
58
+ faces_pv = np.hstack(
59
+ (np.full((faces.shape[0], 1), 3, dtype=np.int32), faces)).astype(np.int32)
60
+
61
+ # Create PolyData mesh in voxel space
62
+ mesh = pv.PolyData(coords_voxel, faces_pv)
63
+
64
+ return mesh
65
+
66
+
25
67
  def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
26
68
  show_points: bool = False, background: str = 'black', plotter=None,
27
- name=None):
69
+ name=None, reset_camera=False):
28
70
  '''
29
71
  3D render for .trk files.
30
72
 
@@ -114,18 +156,21 @@ def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
114
156
  p.add_mesh(mesh, ambient=ambient, opacity=opacity, diffuse=diffuse,
115
157
  interpolate_before_map=False, render_lines_as_tubes=True,
116
158
  line_width=2, point_size=point_size, rgb=rgb,
117
- cmap=color_map, clim=color_lim, scalars=scalars, name=name)
159
+ cmap=color_map, clim=color_lim, scalars=scalars, name=name,
160
+ reset_camera=reset_camera)
118
161
 
119
162
  elif color_map == 'flesh':
120
163
  p.add_mesh(mesh, opacity=opacity, diffuse=0.4, ambient=ambient,
121
164
  interpolate_before_map=False, render_lines_as_tubes=True,
122
165
  line_width=2, point_size=point_size, rgb=rgb,
123
- color=[250, 225, 210], name=name)
166
+ color=[250, 225, 210], name=name,
167
+ reset_camera=reset_camera)
124
168
  else:
125
169
  p.add_mesh(mesh, opacity=opacity, diffuse=diffuse, ambient=ambient,
126
170
  interpolate_before_map=False, render_lines_as_tubes=True,
127
171
  line_width=2, point_size=point_size, rgb=rgb,
128
- cmap=color_map, scalars=scalars, name=name)
172
+ cmap=color_map, scalars=scalars, name=name,
173
+ reset_camera=reset_camera)
129
174
 
130
175
  p.background_color = background
131
176
  # Do not call p.show() here when using an embedded interactor
@@ -147,14 +192,42 @@ class TrkViewer(QWidget):
147
192
  self.background = 'white'
148
193
 
149
194
  def initUI(self):
150
- # Create the main horizontal layout
151
- main_layout = QHBoxLayout(self)
195
+ # Create the main layout
196
+ global_layout = QVBoxLayout(self)
197
+
198
+ # Camera toolbar (buttons for orientation)
199
+ camera_label = QLabel('Camera: ')
200
+ cam_layout = QHBoxLayout()
201
+ btn_iso = QPushButton("Isometric")
202
+ btn_xy = QPushButton("Axe 1")
203
+ btn_xz = QPushButton("Axe 2")
204
+ btn_yz = QPushButton("Axe 3")
205
+ btn_iso.clicked.connect(self.view_isometric)
206
+ btn_xy.clicked.connect(self.view_xy)
207
+ btn_xz.clicked.connect(self.view_xz)
208
+ btn_yz.clicked.connect(self.view_yz)
209
+ cam_layout.addWidget(camera_label)
210
+ cam_layout.addWidget(btn_iso)
211
+ cam_layout.addWidget(btn_xy)
212
+ cam_layout.addWidget(btn_xz)
213
+ cam_layout.addWidget(btn_yz)
214
+ btn_screenshot = QPushButton("Screenshot")
215
+ btn_screenshot.clicked.connect(self.take_screenshot)
216
+ cam_layout.addWidget(btn_screenshot)
217
+ btn_gif = QPushButton("Create GIF")
218
+ btn_gif.setToolTip("This takes ~2min to save after animation end.")
219
+ btn_gif.clicked.connect(self.create_gif)
220
+ cam_layout.addWidget(btn_gif)
221
+
222
+ global_layout.addLayout(cam_layout)
223
+
224
+ main_layout = QHBoxLayout()
225
+ global_layout.addLayout(main_layout)
152
226
 
153
227
  # Left side: Control panel
154
- control_layout = QVBoxLayout()
155
- # self.loadButton = QPushButton('Load .trk File')
156
- # self.loadButton.clicked.connect(self.loadTrkFile)
157
- # control_layout.addWidget(self.loadButton)
228
+ control_widget = QWidget()
229
+ control_layout = QVBoxLayout(control_widget)
230
+ control_widget.setMaximumWidth(250)
158
231
 
159
232
  self.opacityLabel = QLabel('Opacity:')
160
233
  control_layout.addWidget(self.opacityLabel)
@@ -188,6 +261,13 @@ class TrkViewer(QWidget):
188
261
  self.showVolumeCheckbox.stateChanged.connect(self.update_nii_viewer)
189
262
  control_layout.addWidget(self.showVolumeCheckbox)
190
263
 
264
+ self.nii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
265
+ self.nii_opacitySlider.setMinimum(0)
266
+ self.nii_opacitySlider.setMaximum(1000)
267
+ self.nii_opacitySlider.setValue(45)
268
+ self.nii_opacitySlider.sliderReleased.connect(self.update_nii_viewer)
269
+ control_layout.addWidget(self.nii_opacitySlider)
270
+
191
271
  self.showSlicesCheckbox = QCheckBox('Show Slices')
192
272
  self.showSlicesCheckbox.stateChanged.connect(self.update_nii_viewer)
193
273
  control_layout.addWidget(self.showSlicesCheckbox)
@@ -220,18 +300,26 @@ class TrkViewer(QWidget):
220
300
  self.ZSlider.sliderReleased.connect(self._update_nii_z)
221
301
  control_layout.addWidget(self.ZSlider)
222
302
 
223
- # self.viewButton = QPushButton('View 3D Plot')
224
- # self.viewButton.clicked.connect(self.viewPlot)
225
- # control_layout.addWidget(self.viewButton)
303
+ # Gifti
304
+ self.showSurfaceCheckbox = QCheckBox('Show Surface')
305
+ self.showSurfaceCheckbox.stateChanged.connect(self.update_gii_viewer)
306
+ control_layout.addWidget(self.showSurfaceCheckbox)
307
+
308
+ self.gii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
309
+ self.gii_opacitySlider.setMinimum(0)
310
+ self.gii_opacitySlider.setMaximum(100)
311
+ self.gii_opacitySlider.setValue(15)
312
+ self.gii_opacitySlider.sliderReleased.connect(self.update_gii_viewer)
313
+ control_layout.addWidget(self.gii_opacitySlider)
226
314
 
227
315
  # Add the control panel layout to the main layout
228
- main_layout.addLayout(control_layout)
316
+ main_layout.addWidget(control_widget)
229
317
 
230
318
  # Right side: PyVista viewer embedded in the GUI using QtInteractor
231
319
  self.plotter = QtInteractor(self)
232
320
  main_layout.addWidget(self.plotter.interactor)
233
321
 
234
- self.setLayout(main_layout)
322
+ self.setLayout(global_layout)
235
323
  self.setWindowTitle("Tractography Viewer")
236
324
 
237
325
  def loadTrkFile(self):
@@ -244,7 +332,7 @@ class TrkViewer(QWidget):
244
332
  self.names.append(self.trk_file)
245
333
  self.selected_name = self.trk_file
246
334
 
247
- self.update_trk_viewer()
335
+ self.update_trk_viewer(reset_camera=True)
248
336
 
249
337
  def loadNiftiFile(self):
250
338
  options = QFileDialog.Options()
@@ -253,9 +341,13 @@ class TrkViewer(QWidget):
253
341
  if filePath:
254
342
  self.nii_file = filePath
255
343
  print(f"Loaded NIfTI file: {self.nii_file}")
256
- self.nii_data = nib.load(filePath).get_fdata()
344
+ img = nib.load(filePath)
345
+ self.nii_data = img.get_fdata()
257
346
  grid = pv.ImageData()
258
347
  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]
259
351
  grid.cell_data['values'] = self.nii_data.flatten(order='F')
260
352
  self.grid = grid
261
353
 
@@ -263,7 +355,18 @@ class TrkViewer(QWidget):
263
355
  self.YSlider.setMaximum(self.nii_data.shape[1])
264
356
  self.ZSlider.setMaximum(self.nii_data.shape[2])
265
357
 
266
- self.update_nii_viewer()
358
+ self.update_nii_viewer(reset_camera=True)
359
+
360
+ def loadGiftiFile(self):
361
+ options = QFileDialog.Options()
362
+ filePath, _ = QFileDialog.getOpenFileName(
363
+ self, "Open .gii File", "", "Gifti Files (*.gii)", options=options)
364
+ if filePath:
365
+ self.gii_file = filePath
366
+ print(f"Loaded GIfTI file: {self.gii_file}")
367
+ self.gii_mesh = gifti_to_pyvista(filePath)
368
+
369
+ self.update_gii_viewer(reset_camera=True)
267
370
 
268
371
  def set_background_color(self):
269
372
 
@@ -274,7 +377,76 @@ class TrkViewer(QWidget):
274
377
 
275
378
  self.plotter.background_color = self.background
276
379
 
277
- def update_trk_viewer(self):
380
+ def view_isometric(self):
381
+ # equivalent to BackgroundPlotter 'iso' camera position
382
+ self.plotter.view_isometric()
383
+ self.plotter.render()
384
+
385
+ def view_xy(self):
386
+ # view from +Z axis, XY plane (axial)
387
+ self.plotter.view_xy() # pyvista method
388
+ self.plotter.render()
389
+
390
+ def view_xz(self):
391
+ # view from +Y axis, XZ plane (sagittal)
392
+ self.plotter.view_xz()
393
+ self.plotter.render()
394
+
395
+ def view_yz(self):
396
+ # view from +X axis, YZ plane (coronal)
397
+ self.plotter.view_yz()
398
+ self.plotter.render()
399
+
400
+ def take_screenshot(self):
401
+ """Take a screenshot of the current 3D view."""
402
+ options = QFileDialog.Options()
403
+ file_path, _ = QFileDialog.getSaveFileName(self, "Save Screenshot", "",
404
+ "PNG Image (*.png);;JPEG Image (*.jpg);;TIFF Image (*.tif)",
405
+ options=options)
406
+ if file_path:
407
+ self.plotter.screenshot(file_path, transparent_background=True)
408
+
409
+ def create_gif(self):
410
+ """Create a 360° rotation GIF of the current 3D view."""
411
+ options = QFileDialog.Options()
412
+ file_path, _ = QFileDialog.getSaveFileName(self, "Save GIF", "",
413
+ "GIF Files (*.gif)",
414
+ options=options)
415
+
416
+ if not file_path:
417
+ return
418
+
419
+ # Ensure the file has a .gif extension
420
+ if not file_path.lower().endswith(".gif"):
421
+ file_path += ".gif"
422
+
423
+ # Use an offscreen plotter so the main one remains visible
424
+ offscreen = pv.Plotter(
425
+ off_screen=True, window_size=self.plotter.window_size)
426
+
427
+ # Copy all meshes and volumes from the main plotter to the offscreen one
428
+ for actor_name, actor in self.plotter.actors.items():
429
+ offscreen.add_actor(actor.copy())
430
+
431
+ # Match background color and camera
432
+ offscreen.background_color = self.plotter.background_color
433
+ offscreen.camera_position = self.plotter.camera_position
434
+ offscreen.camera.SetPosition(self.plotter.camera.GetPosition())
435
+ offscreen.camera.SetFocalPoint(self.plotter.camera.GetFocalPoint())
436
+ offscreen.camera.SetViewUp(self.plotter.camera.GetViewUp())
437
+
438
+ # Create the 360° rotation GIF
439
+ offscreen.open_gif(file_path, fps=20)
440
+ n_frames = 360
441
+ for i in range(n_frames):
442
+ offscreen.camera.Azimuth(360 / n_frames)
443
+ offscreen.render()
444
+ offscreen.write_frame()
445
+ self.plotter.camera.azimuth += 360 / n_frames
446
+ self.plotter.render()
447
+ offscreen.close()
448
+
449
+ def update_trk_viewer(self, reset_camera=False):
278
450
 
279
451
  opacity = self.opacitySlider.value() / 100.0
280
452
  show_points = self.showPointsCheckbox.isChecked()
@@ -294,7 +466,8 @@ class TrkViewer(QWidget):
294
466
 
295
467
  plot_trk(file, opacity=opacity, plotter=self.plotter, scalar=scalar,
296
468
  show_points=show_points, color_map=color_map,
297
- name=file, background=self.background)
469
+ name=file, background=self.background,
470
+ reset_camera=reset_camera)
298
471
 
299
472
  def _update_nii_x(self):
300
473
 
@@ -307,7 +480,8 @@ class TrkViewer(QWidget):
307
480
  slice_x = self.grid.slice('x', center)
308
481
  self.plotter.add_mesh(slice_x, cmap='grey', name='nii_x',
309
482
  show_scalar_bar=False, point_size=0,
310
- render_lines_as_tubes=True)
483
+ render_lines_as_tubes=True,
484
+ reset_camera=False)
311
485
  else:
312
486
  self.plotter.remove_actor('nii_x')
313
487
 
@@ -322,7 +496,8 @@ class TrkViewer(QWidget):
322
496
  slice_y = self.grid.slice('y', center)
323
497
  self.plotter.add_mesh(slice_y, cmap='grey', name='nii_y',
324
498
  show_scalar_bar=False, point_size=0,
325
- render_lines_as_tubes=True)
499
+ render_lines_as_tubes=True,
500
+ reset_camera=False)
326
501
  else:
327
502
  self.plotter.remove_actor('nii_y')
328
503
 
@@ -334,21 +509,18 @@ class TrkViewer(QWidget):
334
509
  self.YSlider.value(), self.ZSlider.value())
335
510
 
336
511
  if self.showZCheckbox.isChecked():
337
- z_val = self.ZSlider.value()
338
512
  slice_z = self.grid.slice('z', center)
339
513
  self.plotter.add_mesh(slice_z, cmap='grey', name='nii_z',
340
514
  show_scalar_bar=False, point_size=0,
341
- render_lines_as_tubes=True)
515
+ render_lines_as_tubes=True,
516
+ reset_camera=False)
342
517
  else:
343
518
  self.plotter.remove_actor('nii_z')
344
519
 
345
- def update_nii_viewer(self):
520
+ def update_nii_viewer(self, reset_camera=False):
346
521
 
347
522
  if self.showSlicesCheckbox.isChecked():
348
523
 
349
- center = (self.XSlider.value(),
350
- self.YSlider.value(), self.ZSlider.value())
351
-
352
524
  self._update_nii_x()
353
525
  self._update_nii_y()
354
526
  self._update_nii_z()
@@ -359,34 +531,25 @@ class TrkViewer(QWidget):
359
531
  self.plotter.remove_actor('nii_z')
360
532
 
361
533
  if self.showVolumeCheckbox.isChecked():
362
-
363
- self.plotter.add_volume(self.grid, cmap='gray', opacity=[0.0, 0.045],
364
- show_scalar_bar=False, name='nii_volume')
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)
365
538
  else:
366
- self.plotter.remove_actor('nii_volume')
367
-
368
- def viewPlot(self):
369
-
370
- # Clear previous meshes if necessary
371
- self.plotter.clear()
372
-
373
- if self.nii_data is not None and self.grid is not None:
374
- # Add a slice of the NIfTI volume to the scene
375
- # self.plotter.add_mesh_slice(self.grid, cmap='gray',
376
- # show_scalar_bar=False)
539
+ self.plotter.remove_actor('nii_volume', reset_camera=reset_camera)
377
540
 
378
- # self.plotter.add_mesh_slice_orthogonal(self.grid, cmap='gray',
379
- # show_scalar_bar=False,
380
- # tubing=False)
541
+ def update_gii_viewer(self, reset_camera=False):
381
542
 
382
- self.update_nii_viewer()
543
+ if self.showSurfaceCheckbox.isChecked():
383
544
 
384
- if self.trk_file is not None:
385
-
386
- self.update_trk_viewer()
387
-
388
- self.plotter.reset_camera()
389
- self.plotter.render()
545
+ opacity = self.gii_opacitySlider.value()/100
546
+ self.plotter.add_mesh(self.gii_mesh, color="ghostwhite",
547
+ culling='front', smooth_shading=True,
548
+ opacity=opacity, name='gii_surface',
549
+ reset_camera=reset_camera)
550
+ else:
551
+ self.plotter.remove_actor('gii_surface', reset_camera=reset_camera)
552
+ # self.plotter.actors['gii_surface'].visibility = False
390
553
 
391
554
 
392
555
  class MainWindow(QMainWindow):
@@ -395,7 +558,7 @@ class MainWindow(QMainWindow):
395
558
  self.viewer = TrkViewer()
396
559
  self.setCentralWidget(self.viewer)
397
560
  self.initMenuBar()
398
- self.setWindowTitle("Tractography Viewer with Menubar")
561
+ self.setWindowTitle("UNVEIL - Tractography Viewer")
399
562
 
400
563
  def initMenuBar(self):
401
564
  menubar = self.menuBar()
@@ -413,6 +576,11 @@ class MainWindow(QMainWindow):
413
576
  loadNiftiAction.triggered.connect(self.viewer.loadNiftiFile)
414
577
  fileMenu.addAction(loadNiftiAction)
415
578
 
579
+ # Action to load .gii file
580
+ loadGiftiAction = QAction('Load .gii File', self)
581
+ loadGiftiAction.triggered.connect(self.viewer.loadGiftiFile)
582
+ fileMenu.addAction(loadGiftiAction)
583
+
416
584
  # Optional: Exit action
417
585
  exitAction = QAction('Exit', self)
418
586
  exitAction.triggered.connect(self.close)
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unveil-python
3
- Version: 0.0.2
3
+ Version: 0.2.0
4
4
  Summary: Implementation of UNVEIL
5
5
  Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
6
+ License-Expression: GPL-3.0-only
6
7
  Project-URL: GitHub, https://github.com/DelinteNicolas/UNVEIL
7
8
  Keywords: tractography,viewer
8
9
  Classifier: Intended Audience :: Science/Research
@@ -12,10 +13,22 @@ Description-Content-Type: text/markdown
12
13
  License-File: LICENSE
13
14
  Requires-Dist: dipy
14
15
  Requires-Dist: pyvista
16
+ Requires-Dist: pyvistaqt
15
17
  Requires-Dist: PyQt6
16
18
  Dynamic: license-file
17
19
 
18
20
  # UNVEIL
21
+ [![PyPI](https://img.shields.io/pypi/v/unveil-python?label=pypi%20package)](https://pypi.org/project/unveil-python/)
22
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/unveil-python)](https://pypi.org/project/unveil-python/)
23
+ ![GitHub repo size](https://img.shields.io/github/repo-size/DelinteNicolas/unveil)
24
+
25
+
19
26
  A simple tractography viewer implemented in python.
20
27
 
21
28
  ![image](https://github.com/user-attachments/assets/4fbefa7a-3fc2-446a-8835-2844266b14fa)
29
+
30
+ The GUI is launched with the following python command:
31
+
32
+ ```
33
+ import unveil.__main__
34
+ ```
@@ -1,3 +1,4 @@
1
1
  dipy
2
2
  pyvista
3
+ pyvistaqt
3
4
  PyQt6
@@ -1,4 +0,0 @@
1
- # UNVEIL
2
- A simple tractography viewer implemented in python.
3
-
4
- ![image](https://github.com/user-attachments/assets/4fbefa7a-3fc2-446a-8835-2844266b14fa)
File without changes
File without changes