unveil-python 0.0.2__tar.gz → 0.1.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.1.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.1.0"
5
5
  __author__ = 'Nicolas Delinte'
@@ -4,7 +4,6 @@ Created on Tue Mar 4 13:03:28 2025
4
4
 
5
5
  @author: nicol
6
6
  """
7
-
8
7
  import sys
9
8
  import numpy as np
10
9
  import nibabel as nib
@@ -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,38 @@ 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
+
218
+ global_layout.addLayout(cam_layout)
219
+
220
+ main_layout = QHBoxLayout()
221
+ global_layout.addLayout(main_layout)
152
222
 
153
223
  # 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)
224
+ control_widget = QWidget()
225
+ control_layout = QVBoxLayout(control_widget)
226
+ control_widget.setMaximumWidth(250)
158
227
 
159
228
  self.opacityLabel = QLabel('Opacity:')
160
229
  control_layout.addWidget(self.opacityLabel)
@@ -188,6 +257,13 @@ class TrkViewer(QWidget):
188
257
  self.showVolumeCheckbox.stateChanged.connect(self.update_nii_viewer)
189
258
  control_layout.addWidget(self.showVolumeCheckbox)
190
259
 
260
+ self.nii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
261
+ self.nii_opacitySlider.setMinimum(0)
262
+ self.nii_opacitySlider.setMaximum(1000)
263
+ self.nii_opacitySlider.setValue(45)
264
+ self.nii_opacitySlider.sliderReleased.connect(self.update_nii_viewer)
265
+ control_layout.addWidget(self.nii_opacitySlider)
266
+
191
267
  self.showSlicesCheckbox = QCheckBox('Show Slices')
192
268
  self.showSlicesCheckbox.stateChanged.connect(self.update_nii_viewer)
193
269
  control_layout.addWidget(self.showSlicesCheckbox)
@@ -220,18 +296,26 @@ class TrkViewer(QWidget):
220
296
  self.ZSlider.sliderReleased.connect(self._update_nii_z)
221
297
  control_layout.addWidget(self.ZSlider)
222
298
 
223
- # self.viewButton = QPushButton('View 3D Plot')
224
- # self.viewButton.clicked.connect(self.viewPlot)
225
- # control_layout.addWidget(self.viewButton)
299
+ # Gifti
300
+ self.showSurfaceCheckbox = QCheckBox('Show Surface')
301
+ self.showSurfaceCheckbox.stateChanged.connect(self.update_gii_viewer)
302
+ control_layout.addWidget(self.showSurfaceCheckbox)
303
+
304
+ self.gii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
305
+ self.gii_opacitySlider.setMinimum(0)
306
+ self.gii_opacitySlider.setMaximum(100)
307
+ self.gii_opacitySlider.setValue(15)
308
+ self.gii_opacitySlider.sliderReleased.connect(self.update_gii_viewer)
309
+ control_layout.addWidget(self.gii_opacitySlider)
226
310
 
227
311
  # Add the control panel layout to the main layout
228
- main_layout.addLayout(control_layout)
312
+ main_layout.addWidget(control_widget)
229
313
 
230
314
  # Right side: PyVista viewer embedded in the GUI using QtInteractor
231
315
  self.plotter = QtInteractor(self)
232
316
  main_layout.addWidget(self.plotter.interactor)
233
317
 
234
- self.setLayout(main_layout)
318
+ self.setLayout(global_layout)
235
319
  self.setWindowTitle("Tractography Viewer")
236
320
 
237
321
  def loadTrkFile(self):
@@ -244,7 +328,7 @@ class TrkViewer(QWidget):
244
328
  self.names.append(self.trk_file)
245
329
  self.selected_name = self.trk_file
246
330
 
247
- self.update_trk_viewer()
331
+ self.update_trk_viewer(reset_camera=True)
248
332
 
249
333
  def loadNiftiFile(self):
250
334
  options = QFileDialog.Options()
@@ -253,9 +337,13 @@ class TrkViewer(QWidget):
253
337
  if filePath:
254
338
  self.nii_file = filePath
255
339
  print(f"Loaded NIfTI file: {self.nii_file}")
256
- self.nii_data = nib.load(filePath).get_fdata()
340
+ img = nib.load(filePath)
341
+ self.nii_data = img.get_fdata()
257
342
  grid = pv.ImageData()
258
343
  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]
259
347
  grid.cell_data['values'] = self.nii_data.flatten(order='F')
260
348
  self.grid = grid
261
349
 
@@ -263,7 +351,18 @@ class TrkViewer(QWidget):
263
351
  self.YSlider.setMaximum(self.nii_data.shape[1])
264
352
  self.ZSlider.setMaximum(self.nii_data.shape[2])
265
353
 
266
- self.update_nii_viewer()
354
+ self.update_nii_viewer(reset_camera=True)
355
+
356
+ def loadGiftiFile(self):
357
+ options = QFileDialog.Options()
358
+ filePath, _ = QFileDialog.getOpenFileName(
359
+ self, "Open .gii File", "", "Gifti Files (*.gii)", options=options)
360
+ if filePath:
361
+ self.gii_file = filePath
362
+ print(f"Loaded GIfTI file: {self.gii_file}")
363
+ self.gii_mesh = gifti_to_pyvista(filePath)
364
+
365
+ self.update_gii_viewer(reset_camera=True)
267
366
 
268
367
  def set_background_color(self):
269
368
 
@@ -274,7 +373,40 @@ class TrkViewer(QWidget):
274
373
 
275
374
  self.plotter.background_color = self.background
276
375
 
277
- def update_trk_viewer(self):
376
+ def view_isometric(self):
377
+ # equivalent to BackgroundPlotter 'iso' camera position
378
+ self.plotter.view_isometric()
379
+ self.plotter.render()
380
+
381
+ def view_xy(self):
382
+ # view from +Z axis, XY plane (axial)
383
+ self.plotter.view_xy() # pyvista method
384
+ self.plotter.render()
385
+
386
+ def view_xz(self):
387
+ # view from +Y axis, XZ plane (sagittal)
388
+ self.plotter.view_xz()
389
+ self.plotter.render()
390
+
391
+ def view_yz(self):
392
+ # view from +X axis, YZ plane (coronal)
393
+ self.plotter.view_yz()
394
+ self.plotter.render()
395
+
396
+ def take_screenshot(self):
397
+ """Take a screenshot of the current 3D view."""
398
+ options = QFileDialog.Options()
399
+ file_path, _ = QFileDialog.getSaveFileName(
400
+ self,
401
+ "Save Screenshot",
402
+ "",
403
+ "PNG Image (*.png);;JPEG Image (*.jpg);;TIFF Image (*.tif)",
404
+ options=options
405
+ )
406
+ if file_path:
407
+ self.plotter.screenshot(file_path, transparent_background=True)
408
+
409
+ def update_trk_viewer(self, reset_camera=False):
278
410
 
279
411
  opacity = self.opacitySlider.value() / 100.0
280
412
  show_points = self.showPointsCheckbox.isChecked()
@@ -294,7 +426,8 @@ class TrkViewer(QWidget):
294
426
 
295
427
  plot_trk(file, opacity=opacity, plotter=self.plotter, scalar=scalar,
296
428
  show_points=show_points, color_map=color_map,
297
- name=file, background=self.background)
429
+ name=file, background=self.background,
430
+ reset_camera=reset_camera)
298
431
 
299
432
  def _update_nii_x(self):
300
433
 
@@ -307,7 +440,8 @@ class TrkViewer(QWidget):
307
440
  slice_x = self.grid.slice('x', center)
308
441
  self.plotter.add_mesh(slice_x, cmap='grey', name='nii_x',
309
442
  show_scalar_bar=False, point_size=0,
310
- render_lines_as_tubes=True)
443
+ render_lines_as_tubes=True,
444
+ reset_camera=False)
311
445
  else:
312
446
  self.plotter.remove_actor('nii_x')
313
447
 
@@ -322,7 +456,8 @@ class TrkViewer(QWidget):
322
456
  slice_y = self.grid.slice('y', center)
323
457
  self.plotter.add_mesh(slice_y, cmap='grey', name='nii_y',
324
458
  show_scalar_bar=False, point_size=0,
325
- render_lines_as_tubes=True)
459
+ render_lines_as_tubes=True,
460
+ reset_camera=False)
326
461
  else:
327
462
  self.plotter.remove_actor('nii_y')
328
463
 
@@ -334,21 +469,18 @@ class TrkViewer(QWidget):
334
469
  self.YSlider.value(), self.ZSlider.value())
335
470
 
336
471
  if self.showZCheckbox.isChecked():
337
- z_val = self.ZSlider.value()
338
472
  slice_z = self.grid.slice('z', center)
339
473
  self.plotter.add_mesh(slice_z, cmap='grey', name='nii_z',
340
474
  show_scalar_bar=False, point_size=0,
341
- render_lines_as_tubes=True)
475
+ render_lines_as_tubes=True,
476
+ reset_camera=False)
342
477
  else:
343
478
  self.plotter.remove_actor('nii_z')
344
479
 
345
- def update_nii_viewer(self):
480
+ def update_nii_viewer(self, reset_camera=False):
346
481
 
347
482
  if self.showSlicesCheckbox.isChecked():
348
483
 
349
- center = (self.XSlider.value(),
350
- self.YSlider.value(), self.ZSlider.value())
351
-
352
484
  self._update_nii_x()
353
485
  self._update_nii_y()
354
486
  self._update_nii_z()
@@ -359,34 +491,25 @@ class TrkViewer(QWidget):
359
491
  self.plotter.remove_actor('nii_z')
360
492
 
361
493
  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')
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)
365
498
  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)
499
+ self.plotter.remove_actor('nii_volume', reset_camera=reset_camera)
377
500
 
378
- # self.plotter.add_mesh_slice_orthogonal(self.grid, cmap='gray',
379
- # show_scalar_bar=False,
380
- # tubing=False)
501
+ def update_gii_viewer(self, reset_camera=False):
381
502
 
382
- self.update_nii_viewer()
503
+ if self.showSurfaceCheckbox.isChecked():
383
504
 
384
- if self.trk_file is not None:
385
-
386
- self.update_trk_viewer()
387
-
388
- self.plotter.reset_camera()
389
- self.plotter.render()
505
+ opacity = self.gii_opacitySlider.value()/100
506
+ self.plotter.add_mesh(self.gii_mesh, color="ghostwhite",
507
+ culling='front', smooth_shading=True,
508
+ opacity=opacity, name='gii_surface',
509
+ reset_camera=reset_camera)
510
+ else:
511
+ # self.plotter.remove_actor('gii_surface', reset_camera=reset_camera)
512
+ self.plotter.actors['gii_surface'].visibility = False
390
513
 
391
514
 
392
515
  class MainWindow(QMainWindow):
@@ -413,6 +536,11 @@ class MainWindow(QMainWindow):
413
536
  loadNiftiAction.triggered.connect(self.viewer.loadNiftiFile)
414
537
  fileMenu.addAction(loadNiftiAction)
415
538
 
539
+ # Action to load .gii file
540
+ loadGiftiAction = QAction('Load .gii File', self)
541
+ loadGiftiAction.triggered.connect(self.viewer.loadGiftiFile)
542
+ fileMenu.addAction(loadGiftiAction)
543
+
416
544
  # Optional: Exit action
417
545
  exitAction = QAction('Exit', self)
418
546
  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.1.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