unveil-python 0.5.1__tar.gz → 0.7.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.1/src/unveil_python.egg-info → unveil_python-0.7.0}/PKG-INFO +2 -1
- {unveil_python-0.5.1 → unveil_python-0.7.0}/pyproject.toml +2 -1
- {unveil_python-0.5.1 → unveil_python-0.7.0}/src/unveil/__init__.py +1 -1
- {unveil_python-0.5.1 → unveil_python-0.7.0}/src/unveil/core.py +506 -67
- {unveil_python-0.5.1 → unveil_python-0.7.0/src/unveil_python.egg-info}/PKG-INFO +2 -1
- {unveil_python-0.5.1 → unveil_python-0.7.0}/src/unveil_python.egg-info/requires.txt +1 -0
- {unveil_python-0.5.1 → unveil_python-0.7.0}/LICENSE +0 -0
- {unveil_python-0.5.1 → unveil_python-0.7.0}/README.md +0 -0
- {unveil_python-0.5.1 → unveil_python-0.7.0}/setup.cfg +0 -0
- {unveil_python-0.5.1 → unveil_python-0.7.0}/src/unveil/__main__.py +0 -0
- {unveil_python-0.5.1 → unveil_python-0.7.0}/src/unveil_python.egg-info/SOURCES.txt +0 -0
- {unveil_python-0.5.1 → unveil_python-0.7.0}/src/unveil_python.egg-info/dependency_links.txt +0 -0
- {unveil_python-0.5.1 → unveil_python-0.7.0}/src/unveil_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unveil-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Implementation of UNVEIL
|
|
5
5
|
Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -15,6 +15,7 @@ Requires-Dist: dipy
|
|
|
15
15
|
Requires-Dist: pyvista
|
|
16
16
|
Requires-Dist: pyvistaqt
|
|
17
17
|
Requires-Dist: PyQt6
|
|
18
|
+
Requires-Dist: binama
|
|
18
19
|
Dynamic: license-file
|
|
19
20
|
|
|
20
21
|
# UNVEIL
|
|
@@ -14,7 +14,8 @@ from PyQt6.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout,
|
|
|
14
14
|
QPushButton, QFileDialog, QCheckBox, QSlider,
|
|
15
15
|
QLabel, QComboBox, QMainWindow, QLineEdit,
|
|
16
16
|
QDockWidget, QTreeWidget, QTreeWidgetItem,
|
|
17
|
-
QColorDialog, QTabWidget
|
|
17
|
+
QColorDialog, QTabWidget, QDoubleSpinBox,
|
|
18
|
+
QGroupBox)
|
|
18
19
|
from PyQt6.QtGui import QAction, QColor
|
|
19
20
|
from dipy.io.streamline import load_tractogram
|
|
20
21
|
from matplotlib import pyplot as plt
|
|
@@ -22,6 +23,7 @@ from matplotlib.colors import LinearSegmentedColormap
|
|
|
22
23
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
|
|
23
24
|
from matplotlib.figure import Figure
|
|
24
25
|
# from unravel.utils import get_streamline_density
|
|
26
|
+
from binama.utils import dilate_atlas_labels
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def gifti_to_pyvista(gii_path):
|
|
@@ -184,6 +186,24 @@ def plot_trk(trk_file, scalar=None, color_map='plasma', opacity: float = 1,
|
|
|
184
186
|
|
|
185
187
|
p.background_color = background
|
|
186
188
|
|
|
189
|
+
def load_gii_data(gii_path):
|
|
190
|
+
"""
|
|
191
|
+
Load all data contained in the GifTi file except vertex coordinates and triangles.
|
|
192
|
+
Returns : dictionnary {"Gii table name" : data}
|
|
193
|
+
"""
|
|
194
|
+
gii_img = nib.load(gii_path)
|
|
195
|
+
vertex_coords = gii_img.get_arrays_from_intent("NIFTI_INTENT_POINTSET")[0].data
|
|
196
|
+
gii_data = {}
|
|
197
|
+
|
|
198
|
+
# Iterate over all tables of the GIfTI
|
|
199
|
+
for darray in gii_img.darrays:
|
|
200
|
+
# Vertex coords and triangles are stored with None as 'Name'
|
|
201
|
+
if darray.meta.get("Name") is not None:
|
|
202
|
+
# Check that the table contains 1 value per vertex
|
|
203
|
+
if darray.data.shape == vertex_coords[:,0].shape:
|
|
204
|
+
gii_data[darray.meta.get("Name")] = darray.data
|
|
205
|
+
|
|
206
|
+
return gii_data
|
|
187
207
|
|
|
188
208
|
class TrkViewer(QWidget):
|
|
189
209
|
def __init__(self):
|
|
@@ -199,7 +219,8 @@ class TrkViewer(QWidget):
|
|
|
199
219
|
'Greys', 'Purples', 'Blues', 'Greens', 'Oranges',
|
|
200
220
|
'Reds', 'bone', 'pink', 'spring', 'summer',
|
|
201
221
|
'autumn', 'winter', 'cool', 'Wistia', 'hot',
|
|
202
|
-
'afmhot', 'gist_heat', 'copper', '
|
|
222
|
+
'afmhot', 'gist_heat', 'copper', 'YlOrRd', 'YlOrBr',
|
|
223
|
+
'RdBu', 'RdYlBu',
|
|
203
224
|
'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic',
|
|
204
225
|
'berlin', 'managua', 'vanimo', 'twilight',
|
|
205
226
|
'twilight_shifted', 'hsv', 'Pastel1', 'Pastel2',
|
|
@@ -253,101 +274,220 @@ class TrkViewer(QWidget):
|
|
|
253
274
|
# Left side: Control panel
|
|
254
275
|
control_widget = QWidget()
|
|
255
276
|
control_layout = QVBoxLayout(control_widget)
|
|
256
|
-
control_widget.setMaximumWidth(
|
|
277
|
+
control_widget.setMaximumWidth(260)
|
|
257
278
|
|
|
258
|
-
|
|
259
|
-
|
|
279
|
+
# Tractography
|
|
280
|
+
trk_group = QGroupBox("TRK / Tractography")
|
|
281
|
+
trk_layout = QVBoxLayout(trk_group)
|
|
260
282
|
|
|
283
|
+
opacity_trk_row = QHBoxLayout()
|
|
284
|
+
opacity_trk_row.setSpacing(5)
|
|
285
|
+
self.opacityLabel = QLabel('Opacity:')
|
|
286
|
+
opacity_trk_row.addWidget(self.opacityLabel)
|
|
261
287
|
self.opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
262
288
|
self.opacitySlider.setMinimum(1)
|
|
263
289
|
self.opacitySlider.setMaximum(100)
|
|
264
290
|
self.opacitySlider.setValue(100)
|
|
265
291
|
self.opacitySlider.sliderReleased.connect(self.update_trk_viewer)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
self.showPointsCheckbox = QCheckBox('Show Points')
|
|
269
|
-
self.showPointsCheckbox.stateChanged.connect(self.update_trk_viewer)
|
|
270
|
-
control_layout.addWidget(self.showPointsCheckbox)
|
|
271
|
-
|
|
272
|
-
self.colorMapLabel = QLabel('Tract Color Mode:')
|
|
273
|
-
control_layout.addWidget(self.colorMapLabel)
|
|
292
|
+
opacity_trk_row.addWidget(self.opacitySlider)
|
|
293
|
+
trk_layout.addLayout(opacity_trk_row)
|
|
274
294
|
|
|
295
|
+
color_mode_trk_row = QHBoxLayout()
|
|
296
|
+
color_mode_trk_row.setSpacing(5)
|
|
297
|
+
self.colorMapLabel = QLabel('Color Mode:')
|
|
298
|
+
color_mode_trk_row.addWidget(self.colorMapLabel)
|
|
275
299
|
self.colorMapComboBox = QComboBox()
|
|
276
300
|
self.colorMapComboBox.addItems(['rgb', 'flesh', 'scalar'])
|
|
277
301
|
self.colorMapComboBox.currentIndexChanged.connect(
|
|
278
302
|
self.update_trk_viewer)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
self.tractColorMapLabel = QLabel('Tract Colormap (Scalar):')
|
|
282
|
-
control_layout.addWidget(self.tractColorMapLabel)
|
|
303
|
+
color_mode_trk_row.addWidget(self.colorMapComboBox)
|
|
304
|
+
trk_layout.addLayout(color_mode_trk_row)
|
|
283
305
|
|
|
306
|
+
colormap_trk_row = QHBoxLayout()
|
|
307
|
+
colormap_trk_row.setSpacing(5)
|
|
308
|
+
self.tractColorMapLabel = QLabel('Colormap (Scalar):')
|
|
309
|
+
colormap_trk_row.addWidget(self.tractColorMapLabel)
|
|
284
310
|
self.tractColorMapComboBox = QComboBox()
|
|
285
311
|
self.tractColorMapComboBox.addItems(self.colormap_list)
|
|
286
312
|
self.tractColorMapComboBox.currentIndexChanged.connect(
|
|
287
313
|
self.update_trk_viewer)
|
|
288
|
-
|
|
314
|
+
colormap_trk_row.addWidget(self.tractColorMapComboBox)
|
|
315
|
+
trk_layout.addLayout(colormap_trk_row)
|
|
316
|
+
|
|
317
|
+
self.showPointsCheckbox = QCheckBox('Show Points')
|
|
318
|
+
self.showPointsCheckbox.stateChanged.connect(self.update_trk_viewer)
|
|
319
|
+
trk_layout.addWidget(self.showPointsCheckbox)
|
|
289
320
|
|
|
290
|
-
|
|
291
|
-
control_layout.addWidget(self.volume_label)
|
|
321
|
+
control_layout.addWidget(trk_group)
|
|
292
322
|
|
|
323
|
+
# Volume
|
|
324
|
+
nii_group = QGroupBox("NIfTI / Volume")
|
|
325
|
+
nii_layout = QVBoxLayout(nii_group)
|
|
326
|
+
|
|
327
|
+
opacity_nii_row = QHBoxLayout()
|
|
328
|
+
opacity_nii_row.setSpacing(5)
|
|
329
|
+
self.volume_label = QLabel('Opacity:')
|
|
330
|
+
opacity_nii_row.addWidget(self.volume_label)
|
|
293
331
|
self.nii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
294
332
|
self.nii_opacitySlider.setMinimum(0)
|
|
295
333
|
self.nii_opacitySlider.setMaximum(1000)
|
|
296
334
|
self.nii_opacitySlider.setValue(45)
|
|
297
335
|
self.nii_opacitySlider.sliderReleased.connect(self.update_nii_viewer)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
self.niiColorMapLabel = QLabel('Global Volume Colormap:')
|
|
301
|
-
control_layout.addWidget(self.niiColorMapLabel)
|
|
336
|
+
opacity_nii_row.addWidget(self.nii_opacitySlider)
|
|
337
|
+
nii_layout.addLayout(opacity_nii_row)
|
|
302
338
|
|
|
339
|
+
colormap_nii_row = QHBoxLayout()
|
|
340
|
+
colormap_nii_row.setSpacing(5)
|
|
341
|
+
self.niiColorMapLabel = QLabel('Colormap:')
|
|
342
|
+
colormap_nii_row.addWidget(self.niiColorMapLabel)
|
|
303
343
|
self.niiColorMapComboBox = QComboBox()
|
|
304
344
|
self.niiColorMapComboBox.addItems(self.colormap_list)
|
|
305
345
|
self.niiColorMapComboBox.currentIndexChanged.connect(
|
|
306
346
|
self.on_nii_colormap_changed)
|
|
307
|
-
|
|
347
|
+
colormap_nii_row.addWidget(self.niiColorMapComboBox)
|
|
348
|
+
nii_layout.addLayout(colormap_nii_row)
|
|
349
|
+
|
|
350
|
+
nii_range_layout = QHBoxLayout()
|
|
351
|
+
|
|
352
|
+
nii_min_layout = QVBoxLayout()
|
|
353
|
+
self.niiMinLabel = QLabel('Min:')
|
|
354
|
+
self.niiMinSpinBox = QDoubleSpinBox()
|
|
355
|
+
self.niiMinSpinBox.setDecimals(2)
|
|
356
|
+
self.niiMinSpinBox.setRange(-1e6, 1e6)
|
|
357
|
+
self.niiMinSpinBox.setSingleStep(0.1)
|
|
358
|
+
self.niiMinSpinBox.setValue(0.0)
|
|
359
|
+
self.niiMinSpinBox.editingFinished.connect(self.update_nii_viewer)
|
|
360
|
+
nii_min_layout.addWidget(self.niiMinLabel)
|
|
361
|
+
nii_min_layout.addWidget(self.niiMinSpinBox)
|
|
362
|
+
|
|
363
|
+
nii_max_layout = QVBoxLayout()
|
|
364
|
+
self.niiMaxLabel = QLabel('Max:')
|
|
365
|
+
self.niiMaxSpinBox = QDoubleSpinBox()
|
|
366
|
+
self.niiMaxSpinBox.setDecimals(2)
|
|
367
|
+
self.niiMaxSpinBox.setRange(-1e6, 1e6)
|
|
368
|
+
self.niiMaxSpinBox.setSingleStep(0.1)
|
|
369
|
+
self.niiMaxSpinBox.setValue(1.0)
|
|
370
|
+
self.niiMaxSpinBox.editingFinished.connect(self.update_nii_viewer)
|
|
371
|
+
nii_max_layout.addWidget(self.niiMaxLabel)
|
|
372
|
+
nii_max_layout.addWidget(self.niiMaxSpinBox)
|
|
373
|
+
|
|
374
|
+
nii_range_layout.addLayout(nii_min_layout)
|
|
375
|
+
nii_range_layout.addLayout(nii_max_layout)
|
|
376
|
+
nii_layout.addLayout(nii_range_layout)
|
|
308
377
|
|
|
309
378
|
self.showSlicesCheckbox = QCheckBox('Show Slices')
|
|
310
379
|
self.showSlicesCheckbox.stateChanged.connect(self.update_nii_viewer)
|
|
311
|
-
|
|
380
|
+
nii_layout.addWidget(self.showSlicesCheckbox)
|
|
312
381
|
|
|
313
|
-
|
|
314
|
-
|
|
382
|
+
x_nii_row = QHBoxLayout()
|
|
383
|
+
x_nii_row.setSpacing(5)
|
|
384
|
+
self.x_label = QLabel('X:')
|
|
385
|
+
x_nii_row.addWidget(self.x_label)
|
|
315
386
|
self.XSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
316
387
|
self.XSlider.setMinimum(0)
|
|
317
388
|
self.XSlider.setMaximum(100)
|
|
318
389
|
self.XSlider.setValue(100)
|
|
319
390
|
self.XSlider.sliderReleased.connect(
|
|
320
391
|
lambda: self._update_slice('x', 'nii_x'))
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
392
|
+
x_nii_row.addWidget(self.XSlider)
|
|
393
|
+
nii_layout.addLayout(x_nii_row)
|
|
394
|
+
y_nii_row = QHBoxLayout()
|
|
395
|
+
y_nii_row.setSpacing(5)
|
|
396
|
+
self.y_label = QLabel('Y:')
|
|
397
|
+
y_nii_row.addWidget(self.y_label)
|
|
324
398
|
self.YSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
325
399
|
self.YSlider.setMinimum(0)
|
|
326
400
|
self.YSlider.setMaximum(100)
|
|
327
401
|
self.YSlider.setValue(100)
|
|
328
402
|
self.YSlider.sliderReleased.connect(
|
|
329
403
|
lambda: self._update_slice('y', 'nii_y'))
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
404
|
+
y_nii_row.addWidget(self.YSlider)
|
|
405
|
+
nii_layout.addLayout(y_nii_row)
|
|
406
|
+
z_nii_row = QHBoxLayout()
|
|
407
|
+
z_nii_row.setSpacing(5)
|
|
408
|
+
self.z_label = QLabel('Z:')
|
|
409
|
+
z_nii_row.addWidget(self.z_label)
|
|
333
410
|
self.ZSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
334
411
|
self.ZSlider.setMinimum(0)
|
|
335
412
|
self.ZSlider.setMaximum(100)
|
|
336
413
|
self.ZSlider.setValue(100)
|
|
337
414
|
self.ZSlider.sliderReleased.connect(
|
|
338
415
|
lambda: self._update_slice('z', 'nii_z'))
|
|
339
|
-
|
|
416
|
+
z_nii_row.addWidget(self.ZSlider)
|
|
417
|
+
nii_layout.addLayout(z_nii_row)
|
|
418
|
+
|
|
419
|
+
control_layout.addWidget(nii_group)
|
|
340
420
|
|
|
341
421
|
# Gifti
|
|
342
|
-
|
|
343
|
-
|
|
422
|
+
gii_group = QGroupBox("GIFTI / Surface")
|
|
423
|
+
gii_layout = QVBoxLayout(gii_group)
|
|
344
424
|
|
|
425
|
+
opacity_gii_row = QHBoxLayout()
|
|
426
|
+
opacity_gii_row.setSpacing(5)
|
|
427
|
+
self.gii_opacityLabel = QLabel('Opacity:')
|
|
428
|
+
opacity_gii_row.addWidget(self.gii_opacityLabel)
|
|
345
429
|
self.gii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
346
430
|
self.gii_opacitySlider.setMinimum(0)
|
|
347
431
|
self.gii_opacitySlider.setMaximum(100)
|
|
348
432
|
self.gii_opacitySlider.setValue(15)
|
|
349
433
|
self.gii_opacitySlider.sliderReleased.connect(self.update_gii_viewer)
|
|
350
|
-
|
|
434
|
+
opacity_gii_row.addWidget(self.gii_opacitySlider)
|
|
435
|
+
gii_layout.addLayout(opacity_gii_row)
|
|
436
|
+
|
|
437
|
+
color_mode_gii_row = QHBoxLayout()
|
|
438
|
+
color_mode_gii_row.setSpacing(5)
|
|
439
|
+
self.giiColorModeLabel = QLabel('Color Mode:')
|
|
440
|
+
color_mode_gii_row.addWidget(self.giiColorModeLabel)
|
|
441
|
+
self.giiColorModeComboBox = QComboBox()
|
|
442
|
+
self.giiColorModeComboBox.addItems(['Solid', 'NIfTI values'])
|
|
443
|
+
self.giiColorModeComboBox.currentIndexChanged.connect(
|
|
444
|
+
lambda index: self.update_gii_viewer(update_color_range=True))
|
|
445
|
+
color_mode_gii_row.addWidget(self.giiColorModeComboBox)
|
|
446
|
+
gii_layout.addLayout(color_mode_gii_row)
|
|
447
|
+
|
|
448
|
+
colormap_gii_row = QHBoxLayout()
|
|
449
|
+
colormap_gii_row.setSpacing(5)
|
|
450
|
+
self.giiColorMapLabel = QLabel('Colormap:')
|
|
451
|
+
colormap_gii_row.addWidget(self.giiColorMapLabel)
|
|
452
|
+
self.giiColorMapComboBox = QComboBox()
|
|
453
|
+
self.giiColorMapComboBox.addItems(self.colormap_list)
|
|
454
|
+
self.giiColorMapComboBox.setCurrentText('viridis')
|
|
455
|
+
self.giiColorMapComboBox.currentIndexChanged.connect(
|
|
456
|
+
self.update_gii_viewer)
|
|
457
|
+
colormap_gii_row.addWidget(self.giiColorMapComboBox)
|
|
458
|
+
gii_layout.addLayout(colormap_gii_row)
|
|
459
|
+
|
|
460
|
+
gii_range_layout = QHBoxLayout()
|
|
461
|
+
|
|
462
|
+
gii_min_layout = QVBoxLayout()
|
|
463
|
+
self.giiMinLabel = QLabel('Min:')
|
|
464
|
+
self.giiMinSpinBox = QDoubleSpinBox()
|
|
465
|
+
self.giiMinSpinBox.setDecimals(2)
|
|
466
|
+
self.giiMinSpinBox.setRange(-1e6, 1e6)
|
|
467
|
+
self.giiMinSpinBox.setSingleStep(0.1)
|
|
468
|
+
self.giiMinSpinBox.setValue(0.0)
|
|
469
|
+
self.giiMinSpinBox.editingFinished.connect(self.update_gii_viewer)
|
|
470
|
+
gii_min_layout.addWidget(self.giiMinLabel)
|
|
471
|
+
gii_min_layout.addWidget(self.giiMinSpinBox)
|
|
472
|
+
|
|
473
|
+
gii_max_layout = QVBoxLayout()
|
|
474
|
+
self.giiMaxLabel = QLabel('Max:')
|
|
475
|
+
self.giiMaxSpinBox = QDoubleSpinBox()
|
|
476
|
+
self.giiMaxSpinBox.setDecimals(2)
|
|
477
|
+
self.giiMaxSpinBox.setRange(-1e6, 1e6)
|
|
478
|
+
self.giiMaxSpinBox.setSingleStep(0.1)
|
|
479
|
+
self.giiMaxSpinBox.setValue(1.0)
|
|
480
|
+
self.giiMaxSpinBox.editingFinished.connect(self.update_gii_viewer)
|
|
481
|
+
gii_max_layout.addWidget(self.giiMaxLabel)
|
|
482
|
+
gii_max_layout.addWidget(self.giiMaxSpinBox)
|
|
483
|
+
|
|
484
|
+
gii_range_layout.addLayout(gii_min_layout)
|
|
485
|
+
gii_range_layout.addLayout(gii_max_layout)
|
|
486
|
+
gii_layout.addLayout(gii_range_layout)
|
|
487
|
+
|
|
488
|
+
control_layout.addWidget(gii_group)
|
|
489
|
+
|
|
490
|
+
self.update_surface_color_controls()
|
|
351
491
|
|
|
352
492
|
# Add the control panel layout to the main layout
|
|
353
493
|
main_layout.addWidget(control_widget)
|
|
@@ -378,27 +518,49 @@ class TrkViewer(QWidget):
|
|
|
378
518
|
options = QFileDialog.Options()
|
|
379
519
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
380
520
|
self, "Open .nii.gz File", "", "Nifti Files (*.nii.gz)", options=options)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
521
|
+
|
|
522
|
+
if not filePath:
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
print(f"Loaded NIfTI file: {filePath}")
|
|
526
|
+
img = nib.load(filePath)
|
|
527
|
+
self.nii_affine = img.affine
|
|
528
|
+
self.nii_data = img.get_fdata()
|
|
386
529
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
530
|
+
self.update_volume_color_range_from_nifti()
|
|
531
|
+
|
|
532
|
+
grid = pv.ImageData()
|
|
533
|
+
grid.dimensions = np.array(self.nii_data.shape) + 1
|
|
534
|
+
grid.cell_data['values'] = self.nii_data.flatten(order='F')
|
|
535
|
+
self.grid = grid
|
|
391
536
|
|
|
392
537
|
self.XSlider.setMaximum(self.nii_data.shape[0])
|
|
393
538
|
self.YSlider.setMaximum(self.nii_data.shape[1])
|
|
394
539
|
self.ZSlider.setMaximum(self.nii_data.shape[2])
|
|
395
540
|
|
|
396
541
|
self.update_nii_viewer(reset_camera=False)
|
|
542
|
+
|
|
543
|
+
# Dilation for surface
|
|
544
|
+
self.nii_data_dilated = dilate_atlas_labels(
|
|
545
|
+
self.nii_data, dilation_width=4)
|
|
546
|
+
self.nii_data_dilated = np.where(self.nii_data_dilated == 0, np.nan,
|
|
547
|
+
self.nii_data_dilated)
|
|
548
|
+
|
|
549
|
+
if hasattr(self, "gii_mesh"):
|
|
550
|
+
# Sample NIfTI values at each surface vertex
|
|
551
|
+
self.nii_surface_values = self.sample_nifti_on_surface(self.gii_mesh)
|
|
552
|
+
self.update_gii_viewer(reset_camera=False)
|
|
553
|
+
else:
|
|
554
|
+
self.nii_surface_values = None
|
|
397
555
|
self.window().refreshActorList()
|
|
398
556
|
|
|
399
557
|
# Update 2D view with current colormap
|
|
400
|
-
self.nii_data
|
|
401
|
-
|
|
558
|
+
ortho_data = self.nii_data.copy()
|
|
559
|
+
ortho_data[ortho_data == 0] = None
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
self.window().ortho_viewer.set_volume(ortho_data, self.nii_affine)
|
|
402
564
|
self.window().ortho_viewer.set_colormap(self.niiColorMapComboBox.currentText())
|
|
403
565
|
|
|
404
566
|
def loadROIFile(self):
|
|
@@ -444,9 +606,31 @@ class TrkViewer(QWidget):
|
|
|
444
606
|
options = QFileDialog.Options()
|
|
445
607
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
446
608
|
self, "Open .gii File", "", "Gifti Files (*.gii)", options=options)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
609
|
+
|
|
610
|
+
if not filePath:
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
print(f"Loaded GIFTI file: {filePath}")
|
|
614
|
+
|
|
615
|
+
self.gii_mesh = gifti_to_pyvista(filePath)
|
|
616
|
+
|
|
617
|
+
self.gii_data = load_gii_data(filePath)
|
|
618
|
+
|
|
619
|
+
self.giiColorModeComboBox.setCurrentIndex(0)
|
|
620
|
+
|
|
621
|
+
for _ in range(self.giiColorModeComboBox.count()-2):
|
|
622
|
+
self.giiColorModeComboBox.removeItem(2)
|
|
623
|
+
|
|
624
|
+
self.giiColorModeComboBox.addItems(list(self.gii_data.keys()))
|
|
625
|
+
|
|
626
|
+
self.actor_types["gii_surface"] = "gii"
|
|
627
|
+
|
|
628
|
+
if hasattr(self, "nii_data"):
|
|
629
|
+
# Sample NIfTI values at each surface vertex
|
|
630
|
+
self.nii_surface_values = self.sample_nifti_on_surface(self.gii_mesh)
|
|
631
|
+
self.update_gii_viewer(reset_camera=False)
|
|
632
|
+
else:
|
|
633
|
+
self.nii_surface_values = None
|
|
450
634
|
|
|
451
635
|
self.update_gii_viewer(reset_camera=False)
|
|
452
636
|
self.window().refreshActorList()
|
|
@@ -581,31 +765,286 @@ class TrkViewer(QWidget):
|
|
|
581
765
|
opacity = self.nii_opacitySlider.value()/1000
|
|
582
766
|
cmap_name = self.niiColorMapComboBox.currentText()
|
|
583
767
|
|
|
768
|
+
vmin = self.niiMinSpinBox.value()
|
|
769
|
+
vmax = self.niiMaxSpinBox.value()
|
|
770
|
+
|
|
771
|
+
# Protect against an invalid range
|
|
772
|
+
if vmax <= vmin:
|
|
773
|
+
vmax = vmin + 1e-6
|
|
774
|
+
|
|
584
775
|
self.plotter.add_volume(self.grid, cmap=cmap_name, opacity=[0, opacity],
|
|
585
776
|
show_scalar_bar=False, name='nii_volume',
|
|
586
|
-
reset_camera=reset_camera,
|
|
777
|
+
reset_camera=reset_camera, clim=[vmin, vmax],
|
|
587
778
|
user_matrix=self.nii_affine)
|
|
588
779
|
|
|
589
|
-
def update_gii_viewer(self, reset_camera=False):
|
|
780
|
+
def update_gii_viewer(self, reset_camera=False, update_color_range = False):
|
|
781
|
+
"""Update the GIFTI surface rendering."""
|
|
590
782
|
|
|
591
783
|
if not hasattr(self, "gii_mesh"):
|
|
592
784
|
return
|
|
593
785
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
self.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
786
|
+
self.update_surface_color_controls()
|
|
787
|
+
|
|
788
|
+
opacity = self.gii_opacitySlider.value() / 100.0
|
|
789
|
+
|
|
790
|
+
color_mode = self.giiColorModeComboBox.currentText()
|
|
791
|
+
|
|
792
|
+
if color_mode == "Solid":
|
|
793
|
+
|
|
794
|
+
self.plotter.add_mesh(self.gii_mesh, color="ghostwhite",
|
|
795
|
+
culling="back", smooth_shading=True,
|
|
796
|
+
opacity=opacity, name="gii_surface",
|
|
797
|
+
reset_camera=reset_camera,
|
|
798
|
+
point_size=0, render_lines_as_tubes=True)
|
|
799
|
+
|
|
800
|
+
else:
|
|
801
|
+
if color_mode == "NIfTI values":
|
|
802
|
+
scalars = self.nii_surface_values
|
|
803
|
+
else:
|
|
804
|
+
scalars = self.gii_data[color_mode]
|
|
805
|
+
|
|
806
|
+
if scalars is None:
|
|
807
|
+
self.plotter.add_mesh(self.gii_mesh, color="ghostwhite",
|
|
808
|
+
culling="back", smooth_shading=True,
|
|
809
|
+
opacity=opacity, name="gii_surface",
|
|
810
|
+
reset_camera=reset_camera,
|
|
811
|
+
point_size=0, render_lines_as_tubes=True)
|
|
812
|
+
return
|
|
813
|
+
|
|
814
|
+
# Copy the mesh so that the original GIFTI mesh is not modified
|
|
815
|
+
surface_mesh = self.gii_mesh.copy(deep=True)
|
|
816
|
+
surface_mesh.point_data[color_mode] = scalars
|
|
817
|
+
|
|
818
|
+
cmap_name = self.giiColorMapComboBox.currentText()
|
|
819
|
+
|
|
820
|
+
if update_color_range:
|
|
821
|
+
self.update_surface_color_range(scalars)
|
|
822
|
+
|
|
823
|
+
vmin = self.giiMinSpinBox.value()
|
|
824
|
+
vmax = self.giiMaxSpinBox.value()
|
|
825
|
+
|
|
826
|
+
# Protect against an invalid range
|
|
827
|
+
if vmax <= vmin:
|
|
828
|
+
vmax = vmin + 1e-6
|
|
829
|
+
|
|
830
|
+
self.plotter.add_mesh(surface_mesh, scalars=color_mode,
|
|
831
|
+
cmap=cmap_name, clim=[vmin, vmax],
|
|
832
|
+
culling="back", smooth_shading=True,
|
|
833
|
+
opacity=opacity, name="gii_surface",
|
|
834
|
+
reset_camera=reset_camera, point_size=0,
|
|
835
|
+
render_lines_as_tubes=True,
|
|
836
|
+
show_scalar_bar=False)
|
|
837
|
+
|
|
838
|
+
self.plotter.render()
|
|
839
|
+
|
|
840
|
+
def update_volume_color_range_from_nifti(self):
|
|
841
|
+
|
|
842
|
+
if self.nii_data is None:
|
|
843
|
+
return
|
|
844
|
+
|
|
845
|
+
data = np.asarray(self.nii_data, dtype=float)
|
|
846
|
+
finite = np.isfinite(data)
|
|
847
|
+
|
|
848
|
+
if not np.any(finite):
|
|
849
|
+
return
|
|
850
|
+
|
|
851
|
+
values = data[finite]
|
|
852
|
+
|
|
853
|
+
vmin = float(np.min(values))
|
|
854
|
+
vmax = float(np.max(values))
|
|
855
|
+
|
|
856
|
+
# Avoid identical limits because PyVista/VTK does not like
|
|
857
|
+
# clim=[x, x].
|
|
858
|
+
if np.isclose(vmin, vmax):
|
|
859
|
+
vmax = vmin + 1.0
|
|
860
|
+
|
|
861
|
+
self.niiMinSpinBox.blockSignals(True)
|
|
862
|
+
self.niiMaxSpinBox.blockSignals(True)
|
|
863
|
+
|
|
864
|
+
self.niiMinSpinBox.setValue(vmin)
|
|
865
|
+
self.niiMaxSpinBox.setValue(vmax)
|
|
866
|
+
|
|
867
|
+
self.niiMinSpinBox.blockSignals(False)
|
|
868
|
+
self.niiMaxSpinBox.blockSignals(False)
|
|
869
|
+
|
|
870
|
+
def update_surface_color_range(self,data):
|
|
871
|
+
|
|
872
|
+
if data is None:
|
|
873
|
+
return
|
|
874
|
+
|
|
875
|
+
data = np.asarray(data, dtype=float)
|
|
876
|
+
finite = np.isfinite(data)
|
|
877
|
+
|
|
878
|
+
if not np.any(finite):
|
|
879
|
+
return
|
|
880
|
+
|
|
881
|
+
values = data[finite]
|
|
882
|
+
|
|
883
|
+
vmin = float(np.min(values))
|
|
884
|
+
vmax = float(np.max(values))
|
|
885
|
+
|
|
886
|
+
# Avoid identical limits because PyVista/VTK does not like
|
|
887
|
+
# clim=[x, x].
|
|
888
|
+
if np.isclose(vmin, vmax):
|
|
889
|
+
vmax = vmin + 1.0
|
|
890
|
+
|
|
891
|
+
self.giiMinSpinBox.blockSignals(True)
|
|
892
|
+
self.giiMaxSpinBox.blockSignals(True)
|
|
893
|
+
|
|
894
|
+
self.giiMinSpinBox.setValue(vmin)
|
|
895
|
+
self.giiMaxSpinBox.setValue(vmax)
|
|
896
|
+
|
|
897
|
+
self.giiMinSpinBox.blockSignals(False)
|
|
898
|
+
self.giiMaxSpinBox.blockSignals(False)
|
|
899
|
+
|
|
900
|
+
def sample_nifti_on_surface(self, mesh):
|
|
901
|
+
"""
|
|
902
|
+
Trilinearly sample the NIfTI volume onto the surface.
|
|
903
|
+
|
|
904
|
+
Vertices outside the NIfTI are assigned NaN.
|
|
905
|
+
"""
|
|
906
|
+
|
|
907
|
+
if self.nii_data is None or not hasattr(self, "nii_affine"):
|
|
908
|
+
return None
|
|
909
|
+
|
|
910
|
+
volume = np.asarray(self.nii_data_dilated, dtype=np.float64)
|
|
911
|
+
|
|
912
|
+
points_world = np.asarray(mesh.points, dtype=np.float64)
|
|
913
|
+
|
|
914
|
+
inv_affine = np.linalg.inv(self.nii_affine)
|
|
915
|
+
|
|
916
|
+
homogeneous = np.column_stack([
|
|
917
|
+
points_world,
|
|
918
|
+
np.ones(len(points_world))
|
|
919
|
+
])
|
|
920
|
+
|
|
921
|
+
points_voxel = (homogeneous @ inv_affine.T)[:, :3]
|
|
922
|
+
|
|
923
|
+
nx, ny, nz = volume.shape[:3]
|
|
924
|
+
|
|
925
|
+
# Identify points that are actually inside the NIfTI
|
|
926
|
+
inside = (
|
|
927
|
+
(points_voxel[:, 0] >= 0) &
|
|
928
|
+
(points_voxel[:, 0] <= nx - 1) &
|
|
929
|
+
(points_voxel[:, 1] >= 0) &
|
|
930
|
+
(points_voxel[:, 1] <= ny - 1) &
|
|
931
|
+
(points_voxel[:, 2] >= 0) &
|
|
932
|
+
(points_voxel[:, 2] <= nz - 1)
|
|
606
933
|
)
|
|
607
934
|
|
|
608
|
-
#
|
|
935
|
+
# Clip only for interpolation
|
|
936
|
+
x = np.clip(points_voxel[:, 0], 0, nx - 1)
|
|
937
|
+
y = np.clip(points_voxel[:, 1], 0, ny - 1)
|
|
938
|
+
z = np.clip(points_voxel[:, 2], 0, nz - 1)
|
|
939
|
+
|
|
940
|
+
x0 = np.floor(x).astype(int)
|
|
941
|
+
y0 = np.floor(y).astype(int)
|
|
942
|
+
z0 = np.floor(z).astype(int)
|
|
943
|
+
|
|
944
|
+
x1 = np.minimum(x0 + 1, nx - 1)
|
|
945
|
+
y1 = np.minimum(y0 + 1, ny - 1)
|
|
946
|
+
z1 = np.minimum(z0 + 1, nz - 1)
|
|
947
|
+
|
|
948
|
+
xd = x - x0
|
|
949
|
+
yd = y - y0
|
|
950
|
+
zd = z - z0
|
|
951
|
+
|
|
952
|
+
c000 = volume[x0, y0, z0]
|
|
953
|
+
c001 = volume[x0, y0, z1]
|
|
954
|
+
c010 = volume[x0, y1, z0]
|
|
955
|
+
c011 = volume[x0, y1, z1]
|
|
956
|
+
|
|
957
|
+
c100 = volume[x1, y0, z0]
|
|
958
|
+
c101 = volume[x1, y0, z1]
|
|
959
|
+
c110 = volume[x1, y1, z0]
|
|
960
|
+
c111 = volume[x1, y1, z1]
|
|
961
|
+
|
|
962
|
+
c00 = c000 * (1 - xd) + c100 * xd
|
|
963
|
+
c01 = c001 * (1 - xd) + c101 * xd
|
|
964
|
+
c10 = c010 * (1 - xd) + c110 * xd
|
|
965
|
+
c11 = c011 * (1 - xd) + c111 * xd
|
|
966
|
+
|
|
967
|
+
c0 = c00 * (1 - yd) + c10 * yd
|
|
968
|
+
c1 = c01 * (1 - yd) + c11 * yd
|
|
969
|
+
|
|
970
|
+
values = c0 * (1 - zd) + c1 * zd
|
|
971
|
+
|
|
972
|
+
# Outside the NIfTI = NaN
|
|
973
|
+
values[~inside] = np.nan
|
|
974
|
+
|
|
975
|
+
return values.astype(np.float32)
|
|
976
|
+
|
|
977
|
+
def sample_nifti_on_surface2(self, mesh):
|
|
978
|
+
"""
|
|
979
|
+
!!! Currently unused
|
|
980
|
+
Sample the currently loaded NIfTI volume at every GIFTI surface vertex.
|
|
981
|
+
|
|
982
|
+
GIFTI vertices are assumed to be in world/mm coordinates.
|
|
983
|
+
The NIfTI affine is used to convert them into voxel coordinates.
|
|
984
|
+
|
|
985
|
+
Nearest-neighbor sampling is used.
|
|
986
|
+
"""
|
|
987
|
+
|
|
988
|
+
if self.nii_data is None:
|
|
989
|
+
return None
|
|
990
|
+
|
|
991
|
+
if not hasattr(self, 'nii_affine'):
|
|
992
|
+
return None
|
|
993
|
+
|
|
994
|
+
vertices_world = np.asarray(mesh.points)
|
|
995
|
+
|
|
996
|
+
# Convert world coordinates -> NIfTI voxel coordinates
|
|
997
|
+
inv_affine = np.linalg.inv(self.nii_affine)
|
|
998
|
+
|
|
999
|
+
homogeneous = np.column_stack([
|
|
1000
|
+
vertices_world,
|
|
1001
|
+
np.ones(len(vertices_world))
|
|
1002
|
+
])
|
|
1003
|
+
|
|
1004
|
+
vertices_voxel = (homogeneous @ inv_affine.T)[:, :3]
|
|
1005
|
+
|
|
1006
|
+
# Nearest-neighbor voxel coordinates
|
|
1007
|
+
vertices_voxel = np.rint(vertices_voxel).astype(int)
|
|
1008
|
+
|
|
1009
|
+
shape = self.nii_data.shape
|
|
1010
|
+
|
|
1011
|
+
# Determine which vertices are inside the NIfTI
|
|
1012
|
+
valid = (
|
|
1013
|
+
(vertices_voxel[:, 0] >= 0) &
|
|
1014
|
+
(vertices_voxel[:, 0] < shape[0]) &
|
|
1015
|
+
(vertices_voxel[:, 1] >= 0) &
|
|
1016
|
+
(vertices_voxel[:, 1] < shape[1]) &
|
|
1017
|
+
(vertices_voxel[:, 2] >= 0) &
|
|
1018
|
+
(vertices_voxel[:, 2] < shape[2])
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
# Initialize outside-of-volume vertices with NaN
|
|
1022
|
+
values = np.full(len(vertices_world), np.nan, dtype=np.float32)
|
|
1023
|
+
|
|
1024
|
+
valid_voxels = vertices_voxel[valid]
|
|
1025
|
+
|
|
1026
|
+
values[valid] = self.nii_data[
|
|
1027
|
+
valid_voxels[:, 0],
|
|
1028
|
+
valid_voxels[:, 1],
|
|
1029
|
+
valid_voxels[:, 2]
|
|
1030
|
+
]
|
|
1031
|
+
|
|
1032
|
+
return values
|
|
1033
|
+
|
|
1034
|
+
def update_surface_color_controls(self):
|
|
1035
|
+
"""Enable surface scalar controls only when using NIfTI values."""
|
|
1036
|
+
|
|
1037
|
+
use_nifti = (self.giiColorModeComboBox.currentText()
|
|
1038
|
+
!= "Solid")
|
|
1039
|
+
|
|
1040
|
+
self.giiColorMapLabel.setEnabled(use_nifti)
|
|
1041
|
+
self.giiColorMapComboBox.setEnabled(use_nifti)
|
|
1042
|
+
|
|
1043
|
+
self.giiMinLabel.setEnabled(use_nifti)
|
|
1044
|
+
self.giiMinSpinBox.setEnabled(use_nifti)
|
|
1045
|
+
|
|
1046
|
+
self.giiMaxLabel.setEnabled(use_nifti)
|
|
1047
|
+
self.giiMaxSpinBox.setEnabled(use_nifti)
|
|
609
1048
|
|
|
610
1049
|
|
|
611
1050
|
class OrthogonalViewer(QWidget):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unveil-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Implementation of UNVEIL
|
|
5
5
|
Author-email: Nicolas Delinte <nicolas.delinte@uclouvain.be>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -15,6 +15,7 @@ Requires-Dist: dipy
|
|
|
15
15
|
Requires-Dist: pyvista
|
|
16
16
|
Requires-Dist: pyvistaqt
|
|
17
17
|
Requires-Dist: PyQt6
|
|
18
|
+
Requires-Dist: binama
|
|
18
19
|
Dynamic: license-file
|
|
19
20
|
|
|
20
21
|
# UNVEIL
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|