unveil-python 0.6.0__tar.gz → 0.7.1__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unveil-python
3
- Version: 0.6.0
3
+ Version: 0.7.1
4
4
  Summary: Implementation of UNVEIL
5
5
  Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
6
6
  License-Expression: GPL-3.0-only
@@ -18,7 +18,7 @@ classifiers = ['Intended Audience :: Science/Research',
18
18
  dependencies = ['dipy',
19
19
  'pyvista',
20
20
  'pyvistaqt',
21
- 'PyQt6'
21
+ 'PyQt6',
22
22
  ]
23
23
  dynamic = ["version"]
24
24
 
@@ -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.6.0"
4
+ __version__ = "0.7.1"
5
5
  __author__ = 'Nicolas Delinte'
@@ -12,7 +12,7 @@ from pyvistaqt import QtInteractor
12
12
  from PyQt6.QtCore import Qt
13
13
  from PyQt6.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout,
14
14
  QPushButton, QFileDialog, QCheckBox, QSlider,
15
- QLabel, QComboBox, QMainWindow, QLineEdit,
15
+ QLabel, QComboBox, QMainWindow,
16
16
  QDockWidget, QTreeWidget, QTreeWidgetItem,
17
17
  QColorDialog, QTabWidget, QDoubleSpinBox,
18
18
  QGroupBox)
@@ -23,7 +23,7 @@ from matplotlib.colors import LinearSegmentedColormap
23
23
  from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
24
24
  from matplotlib.figure import Figure
25
25
  # from unravel.utils import get_streamline_density
26
- from binama.utils import dilate_atlas_labels
26
+ from unveil.utils import dilate_atlas_labels
27
27
 
28
28
 
29
29
  def gifti_to_pyvista(gii_path):
@@ -187,6 +187,27 @@ def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
187
187
  p.background_color = background
188
188
 
189
189
 
190
+ def load_gii_data(gii_path):
191
+ """
192
+ Load all data contained in the GifTi file except vertex coordinates and triangles.
193
+ Returns : dictionnary {"Gii table name" : data}
194
+ """
195
+ gii_img = nib.load(gii_path)
196
+ vertex_coords = gii_img.get_arrays_from_intent(
197
+ "NIFTI_INTENT_POINTSET")[0].data
198
+ gii_data = {}
199
+
200
+ # Iterate over all tables of the GIfTI
201
+ for darray in gii_img.darrays:
202
+ # Vertex coords and triangles are stored with None as 'Name'
203
+ if darray.meta.get("Name") is not None:
204
+ # Check that the table contains 1 value per vertex
205
+ if darray.data.shape == vertex_coords[:, 0].shape:
206
+ gii_data[darray.meta.get("Name")] = darray.data
207
+
208
+ return gii_data
209
+
210
+
190
211
  class TrkViewer(QWidget):
191
212
  def __init__(self):
192
213
  super().__init__()
@@ -423,7 +444,7 @@ class TrkViewer(QWidget):
423
444
  self.giiColorModeComboBox = QComboBox()
424
445
  self.giiColorModeComboBox.addItems(['Solid', 'NIfTI values'])
425
446
  self.giiColorModeComboBox.currentIndexChanged.connect(
426
- self.update_gii_viewer)
447
+ lambda index: self.update_gii_viewer(update_color_range=True))
427
448
  color_mode_gii_row.addWidget(self.giiColorModeComboBox)
428
449
  gii_layout.addLayout(color_mode_gii_row)
429
450
 
@@ -500,39 +521,47 @@ class TrkViewer(QWidget):
500
521
  options = QFileDialog.Options()
501
522
  filePath, _ = QFileDialog.getOpenFileName(
502
523
  self, "Open .nii.gz File", "", "Nifti Files (*.nii.gz)", options=options)
503
- if filePath:
504
- print(f"Loaded NIfTI file: {filePath}")
505
- img = nib.load(filePath)
506
- self.nii_affine = img.affine
507
- self.nii_data = img.get_fdata()
508
524
 
509
- self.update_volume_color_range_from_nifti()
510
- self.update_surface_color_range_from_nifti()
525
+ if not filePath:
526
+ return
527
+
528
+ print(f"Loaded NIfTI file: {filePath}")
529
+ img = nib.load(filePath)
530
+ self.nii_affine = img.affine
531
+ self.nii_data = img.get_fdata()
532
+
533
+ self.update_volume_color_range_from_nifti()
511
534
 
512
- grid = pv.ImageData()
513
- grid.dimensions = np.array(self.nii_data.shape) + 1
514
- grid.cell_data['values'] = self.nii_data.flatten(order='F')
515
- self.grid = grid
535
+ grid = pv.ImageData()
536
+ grid.dimensions = np.array(self.nii_data.shape) + 1
537
+ grid.cell_data['values'] = self.nii_data.flatten(order='F')
538
+ self.grid = grid
516
539
 
517
540
  self.XSlider.setMaximum(self.nii_data.shape[0])
518
541
  self.YSlider.setMaximum(self.nii_data.shape[1])
519
542
  self.ZSlider.setMaximum(self.nii_data.shape[2])
520
543
 
521
544
  self.update_nii_viewer(reset_camera=False)
545
+
546
+ # Dilation for surface
547
+ self.nii_data_dilated = dilate_atlas_labels(
548
+ self.nii_data, dilation_width=4)
549
+ self.nii_data_dilated = np.where(self.nii_data_dilated == 0, np.nan,
550
+ self.nii_data_dilated)
551
+
522
552
  if hasattr(self, "gii_mesh"):
553
+ # Sample NIfTI values at each surface vertex
554
+ self.nii_surface_values = self.sample_nifti_on_surface(
555
+ self.gii_mesh)
523
556
  self.update_gii_viewer(reset_camera=False)
557
+ else:
558
+ self.nii_surface_values = None
524
559
  self.window().refreshActorList()
525
560
 
526
561
  # Update 2D view with current colormap
527
562
  ortho_data = self.nii_data.copy()
528
563
  ortho_data[ortho_data == 0] = None
529
564
 
530
- # Dilation for surface
531
- self.nii_data_dilated = dilate_atlas_labels(
532
- self.nii_data, dilation_width=4)
533
- self.nii_data_dilated = np.where(self.nii_data_dilated == 0, np.nan,
534
- self.nii_data_dilated)
535
-
536
565
  self.window().ortho_viewer.set_volume(ortho_data, self.nii_affine)
537
566
  self.window().ortho_viewer.set_colormap(self.niiColorMapComboBox.currentText())
538
567
 
@@ -577,14 +606,8 @@ class TrkViewer(QWidget):
577
606
 
578
607
  def loadGiftiFile(self):
579
608
  options = QFileDialog.Options()
580
-
581
609
  filePath, _ = QFileDialog.getOpenFileName(
582
- self,
583
- "Open .gii File",
584
- "",
585
- "Gifti Files (*.gii)",
586
- options=options
587
- )
610
+ self, "Open .gii File", "", "Gifti Files (*.gii)", options=options)
588
611
 
589
612
  if not filePath:
590
613
  return
@@ -593,8 +616,25 @@ class TrkViewer(QWidget):
593
616
 
594
617
  self.gii_mesh = gifti_to_pyvista(filePath)
595
618
 
619
+ self.gii_data = load_gii_data(filePath)
620
+
621
+ self.giiColorModeComboBox.setCurrentIndex(0)
622
+
623
+ for _ in range(self.giiColorModeComboBox.count()-2):
624
+ self.giiColorModeComboBox.removeItem(2)
625
+
626
+ self.giiColorModeComboBox.addItems(list(self.gii_data.keys()))
627
+
596
628
  self.actor_types["gii_surface"] = "gii"
597
629
 
630
+ if hasattr(self, "nii_data"):
631
+ # Sample NIfTI values at each surface vertex
632
+ self.nii_surface_values = self.sample_nifti_on_surface(
633
+ self.gii_mesh)
634
+ self.update_gii_viewer(reset_camera=False)
635
+ else:
636
+ self.nii_surface_values = None
637
+
598
638
  self.update_gii_viewer(reset_camera=False)
599
639
  self.window().refreshActorList()
600
640
 
@@ -740,7 +780,7 @@ class TrkViewer(QWidget):
740
780
  reset_camera=reset_camera, clim=[vmin, vmax],
741
781
  user_matrix=self.nii_affine)
742
782
 
743
- def update_gii_viewer(self, reset_camera=False):
783
+ def update_gii_viewer(self, reset_camera=False, update_color_range=False):
744
784
  """Update the GIFTI surface rendering."""
745
785
 
746
786
  if not hasattr(self, "gii_mesh"):
@@ -760,9 +800,13 @@ class TrkViewer(QWidget):
760
800
  reset_camera=reset_camera,
761
801
  point_size=0, render_lines_as_tubes=True)
762
802
 
763
- elif color_mode == "NIfTI values":
803
+ else:
804
+ if color_mode == "NIfTI values":
805
+ scalars = self.nii_surface_values
806
+ else:
807
+ scalars = self.gii_data[color_mode]
764
808
 
765
- if self.nii_data is None:
809
+ if scalars is None:
766
810
  self.plotter.add_mesh(self.gii_mesh, color="ghostwhite",
767
811
  culling="back", smooth_shading=True,
768
812
  opacity=opacity, name="gii_surface",
@@ -770,18 +814,15 @@ class TrkViewer(QWidget):
770
814
  point_size=0, render_lines_as_tubes=True)
771
815
  return
772
816
 
773
- # Sample NIfTI values at each surface vertex
774
- scalars = self.sample_nifti_on_surface(self.gii_mesh)
775
-
776
- if scalars is None:
777
- return
778
-
779
817
  # Copy the mesh so that the original GIFTI mesh is not modified
780
818
  surface_mesh = self.gii_mesh.copy(deep=True)
781
- surface_mesh.point_data["NIfTI values"] = scalars
819
+ surface_mesh.point_data[color_mode] = scalars
782
820
 
783
821
  cmap_name = self.giiColorMapComboBox.currentText()
784
822
 
823
+ if update_color_range:
824
+ self.update_surface_color_range(scalars)
825
+
785
826
  vmin = self.giiMinSpinBox.value()
786
827
  vmax = self.giiMaxSpinBox.value()
787
828
 
@@ -789,7 +830,7 @@ class TrkViewer(QWidget):
789
830
  if vmax <= vmin:
790
831
  vmax = vmin + 1e-6
791
832
 
792
- self.plotter.add_mesh(surface_mesh, scalars="NIfTI values",
833
+ self.plotter.add_mesh(surface_mesh, scalars=color_mode,
793
834
  cmap=cmap_name, clim=[vmin, vmax],
794
835
  culling="back", smooth_shading=True,
795
836
  opacity=opacity, name="gii_surface",
@@ -829,12 +870,12 @@ class TrkViewer(QWidget):
829
870
  self.niiMinSpinBox.blockSignals(False)
830
871
  self.niiMaxSpinBox.blockSignals(False)
831
872
 
832
- def update_surface_color_range_from_nifti(self):
873
+ def update_surface_color_range(self, data):
833
874
 
834
- if self.nii_data is None:
875
+ if data is None:
835
876
  return
836
877
 
837
- data = np.asarray(self.nii_data, dtype=float)
878
+ data = np.asarray(data, dtype=float)
838
879
  finite = np.isfinite(data)
839
880
 
840
881
  if not np.any(finite):
@@ -997,7 +1038,7 @@ class TrkViewer(QWidget):
997
1038
  """Enable surface scalar controls only when using NIfTI values."""
998
1039
 
999
1040
  use_nifti = (self.giiColorModeComboBox.currentText()
1000
- == "NIfTI values")
1041
+ != "Solid")
1001
1042
 
1002
1043
  self.giiColorMapLabel.setEnabled(use_nifti)
1003
1044
  self.giiColorMapComboBox.setEnabled(use_nifti)
@@ -0,0 +1,58 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Mon Oct 26 10:03:52 2026
4
+
5
+ @author: N. Delinte
6
+ """
7
+
8
+ import numpy as np
9
+ from scipy.ndimage import distance_transform_edt
10
+
11
+
12
+ def dilate_atlas_labels(atlas, brain_mask=None, dilation_width: float = 1,
13
+ min_label: int = 0):
14
+ """
15
+ Dilates cortical atlas labels to include nearby unlabeled voxels (0),
16
+ constrained by a brain mask.
17
+ Copied from 'pilab-binama' package.
18
+
19
+ Parameters
20
+ ----------
21
+ atlas : np.ndarray (3D, int)
22
+ Cortical atlas with integer labels (0 = unlabeled).
23
+ brain_mask : np.ndarray (3D, bool or int), optional
24
+ Binary mask of the brain (same shape as atlas). The default is one
25
+ dilation_width : float, optional
26
+ Maximum dilation distance in voxels. The default is None.
27
+ min_label : int, optional
28
+ Labels below this values are not dilated
29
+
30
+ Returns
31
+ -------
32
+ dilated_atlas : np.ndarray (3D, int)
33
+ Atlas with dilated labels.
34
+ """
35
+
36
+ # Mask unlabeled voxels inside the brain
37
+ unlabeled = np.where(atlas <= min_label, 1, 0)
38
+ if brain_mask is not None:
39
+ unlabeled *= brain_mask.astype('int32')
40
+
41
+ # Compute distance transform from labeled voxels
42
+ # Also retrieve indices of nearest labeled voxel for each position
43
+ distances, nearest_idx = distance_transform_edt(unlabeled,
44
+ return_indices=True)
45
+
46
+ # Copy atlas to output
47
+ dilated_atlas = atlas.copy()
48
+
49
+ # For unlabeled voxels within dilation_width, assign nearest label
50
+ within_dilation = unlabeled & (distances <= dilation_width)
51
+
52
+ # Map nearest labeled voxel indices back to atlas labels
53
+ coords = np.argwhere(within_dilation)
54
+ for x, y, z in coords:
55
+ nx, ny, nz = nearest_idx[:, x, y, z]
56
+ dilated_atlas[x, y, z] = atlas[nx, ny, nz]
57
+
58
+ return dilated_atlas
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unveil-python
3
- Version: 0.6.0
3
+ Version: 0.7.1
4
4
  Summary: Implementation of UNVEIL
5
5
  Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
6
6
  License-Expression: GPL-3.0-only
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/unveil/__init__.py
5
5
  src/unveil/__main__.py
6
6
  src/unveil/core.py
7
+ src/unveil/utils.py
7
8
  src/unveil_python.egg-info/PKG-INFO
8
9
  src/unveil_python.egg-info/SOURCES.txt
9
10
  src/unveil_python.egg-info/dependency_links.txt
File without changes
File without changes
File without changes