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.
- {unveil_python-0.6.0/src/unveil_python.egg-info → unveil_python-0.7.1}/PKG-INFO +1 -1
- {unveil_python-0.6.0 → unveil_python-0.7.1}/pyproject.toml +1 -1
- {unveil_python-0.6.0 → unveil_python-0.7.1}/src/unveil/__init__.py +1 -1
- {unveil_python-0.6.0 → unveil_python-0.7.1}/src/unveil/core.py +83 -42
- unveil_python-0.7.1/src/unveil/utils.py +58 -0
- {unveil_python-0.6.0 → unveil_python-0.7.1/src/unveil_python.egg-info}/PKG-INFO +1 -1
- {unveil_python-0.6.0 → unveil_python-0.7.1}/src/unveil_python.egg-info/SOURCES.txt +1 -0
- {unveil_python-0.6.0 → unveil_python-0.7.1}/LICENSE +0 -0
- {unveil_python-0.6.0 → unveil_python-0.7.1}/README.md +0 -0
- {unveil_python-0.6.0 → unveil_python-0.7.1}/setup.cfg +0 -0
- {unveil_python-0.6.0 → unveil_python-0.7.1}/src/unveil/__main__.py +0 -0
- {unveil_python-0.6.0 → unveil_python-0.7.1}/src/unveil_python.egg-info/dependency_links.txt +0 -0
- {unveil_python-0.6.0 → unveil_python-0.7.1}/src/unveil_python.egg-info/requires.txt +0 -0
- {unveil_python-0.6.0 → unveil_python-0.7.1}/src/unveil_python.egg-info/top_level.txt +0 -0
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
510
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
|
|
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[
|
|
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=
|
|
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
|
|
873
|
+
def update_surface_color_range(self, data):
|
|
833
874
|
|
|
834
|
-
if
|
|
875
|
+
if data is None:
|
|
835
876
|
return
|
|
836
877
|
|
|
837
|
-
data = np.asarray(
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|