unveil-python 0.5.1__tar.gz → 0.6.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.6.0}/PKG-INFO +1 -1
- {unveil_python-0.5.1 → unveil_python-0.6.0}/src/unveil/__init__.py +1 -1
- {unveil_python-0.5.1 → unveil_python-0.6.0}/src/unveil/core.py +459 -58
- {unveil_python-0.5.1 → unveil_python-0.6.0/src/unveil_python.egg-info}/PKG-INFO +1 -1
- {unveil_python-0.5.1 → unveil_python-0.6.0}/LICENSE +0 -0
- {unveil_python-0.5.1 → unveil_python-0.6.0}/README.md +0 -0
- {unveil_python-0.5.1 → unveil_python-0.6.0}/pyproject.toml +0 -0
- {unveil_python-0.5.1 → unveil_python-0.6.0}/setup.cfg +0 -0
- {unveil_python-0.5.1 → unveil_python-0.6.0}/src/unveil/__main__.py +0 -0
- {unveil_python-0.5.1 → unveil_python-0.6.0}/src/unveil_python.egg-info/SOURCES.txt +0 -0
- {unveil_python-0.5.1 → unveil_python-0.6.0}/src/unveil_python.egg-info/dependency_links.txt +0 -0
- {unveil_python-0.5.1 → unveil_python-0.6.0}/src/unveil_python.egg-info/requires.txt +0 -0
- {unveil_python-0.5.1 → unveil_python-0.6.0}/src/unveil_python.egg-info/top_level.txt +0 -0
|
@@ -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):
|
|
@@ -199,7 +201,8 @@ class TrkViewer(QWidget):
|
|
|
199
201
|
'Greys', 'Purples', 'Blues', 'Greens', 'Oranges',
|
|
200
202
|
'Reds', 'bone', 'pink', 'spring', 'summer',
|
|
201
203
|
'autumn', 'winter', 'cool', 'Wistia', 'hot',
|
|
202
|
-
'afmhot', 'gist_heat', 'copper', '
|
|
204
|
+
'afmhot', 'gist_heat', 'copper', 'YlOrRd', 'YlOrBr',
|
|
205
|
+
'RdBu', 'RdYlBu',
|
|
203
206
|
'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic',
|
|
204
207
|
'berlin', 'managua', 'vanimo', 'twilight',
|
|
205
208
|
'twilight_shifted', 'hsv', 'Pastel1', 'Pastel2',
|
|
@@ -253,101 +256,220 @@ class TrkViewer(QWidget):
|
|
|
253
256
|
# Left side: Control panel
|
|
254
257
|
control_widget = QWidget()
|
|
255
258
|
control_layout = QVBoxLayout(control_widget)
|
|
256
|
-
control_widget.setMaximumWidth(
|
|
259
|
+
control_widget.setMaximumWidth(260)
|
|
257
260
|
|
|
258
|
-
|
|
259
|
-
|
|
261
|
+
# Tractography
|
|
262
|
+
trk_group = QGroupBox("TRK / Tractography")
|
|
263
|
+
trk_layout = QVBoxLayout(trk_group)
|
|
260
264
|
|
|
265
|
+
opacity_trk_row = QHBoxLayout()
|
|
266
|
+
opacity_trk_row.setSpacing(5)
|
|
267
|
+
self.opacityLabel = QLabel('Opacity:')
|
|
268
|
+
opacity_trk_row.addWidget(self.opacityLabel)
|
|
261
269
|
self.opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
262
270
|
self.opacitySlider.setMinimum(1)
|
|
263
271
|
self.opacitySlider.setMaximum(100)
|
|
264
272
|
self.opacitySlider.setValue(100)
|
|
265
273
|
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)
|
|
274
|
+
opacity_trk_row.addWidget(self.opacitySlider)
|
|
275
|
+
trk_layout.addLayout(opacity_trk_row)
|
|
274
276
|
|
|
277
|
+
color_mode_trk_row = QHBoxLayout()
|
|
278
|
+
color_mode_trk_row.setSpacing(5)
|
|
279
|
+
self.colorMapLabel = QLabel('Color Mode:')
|
|
280
|
+
color_mode_trk_row.addWidget(self.colorMapLabel)
|
|
275
281
|
self.colorMapComboBox = QComboBox()
|
|
276
282
|
self.colorMapComboBox.addItems(['rgb', 'flesh', 'scalar'])
|
|
277
283
|
self.colorMapComboBox.currentIndexChanged.connect(
|
|
278
284
|
self.update_trk_viewer)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
self.tractColorMapLabel = QLabel('Tract Colormap (Scalar):')
|
|
282
|
-
control_layout.addWidget(self.tractColorMapLabel)
|
|
285
|
+
color_mode_trk_row.addWidget(self.colorMapComboBox)
|
|
286
|
+
trk_layout.addLayout(color_mode_trk_row)
|
|
283
287
|
|
|
288
|
+
colormap_trk_row = QHBoxLayout()
|
|
289
|
+
colormap_trk_row.setSpacing(5)
|
|
290
|
+
self.tractColorMapLabel = QLabel('Colormap (Scalar):')
|
|
291
|
+
colormap_trk_row.addWidget(self.tractColorMapLabel)
|
|
284
292
|
self.tractColorMapComboBox = QComboBox()
|
|
285
293
|
self.tractColorMapComboBox.addItems(self.colormap_list)
|
|
286
294
|
self.tractColorMapComboBox.currentIndexChanged.connect(
|
|
287
295
|
self.update_trk_viewer)
|
|
288
|
-
|
|
296
|
+
colormap_trk_row.addWidget(self.tractColorMapComboBox)
|
|
297
|
+
trk_layout.addLayout(colormap_trk_row)
|
|
289
298
|
|
|
290
|
-
self.
|
|
291
|
-
|
|
299
|
+
self.showPointsCheckbox = QCheckBox('Show Points')
|
|
300
|
+
self.showPointsCheckbox.stateChanged.connect(self.update_trk_viewer)
|
|
301
|
+
trk_layout.addWidget(self.showPointsCheckbox)
|
|
302
|
+
|
|
303
|
+
control_layout.addWidget(trk_group)
|
|
304
|
+
|
|
305
|
+
# Volume
|
|
306
|
+
nii_group = QGroupBox("NIfTI / Volume")
|
|
307
|
+
nii_layout = QVBoxLayout(nii_group)
|
|
292
308
|
|
|
309
|
+
opacity_nii_row = QHBoxLayout()
|
|
310
|
+
opacity_nii_row.setSpacing(5)
|
|
311
|
+
self.volume_label = QLabel('Opacity:')
|
|
312
|
+
opacity_nii_row.addWidget(self.volume_label)
|
|
293
313
|
self.nii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
294
314
|
self.nii_opacitySlider.setMinimum(0)
|
|
295
315
|
self.nii_opacitySlider.setMaximum(1000)
|
|
296
316
|
self.nii_opacitySlider.setValue(45)
|
|
297
317
|
self.nii_opacitySlider.sliderReleased.connect(self.update_nii_viewer)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
self.niiColorMapLabel = QLabel('Global Volume Colormap:')
|
|
301
|
-
control_layout.addWidget(self.niiColorMapLabel)
|
|
318
|
+
opacity_nii_row.addWidget(self.nii_opacitySlider)
|
|
319
|
+
nii_layout.addLayout(opacity_nii_row)
|
|
302
320
|
|
|
321
|
+
colormap_nii_row = QHBoxLayout()
|
|
322
|
+
colormap_nii_row.setSpacing(5)
|
|
323
|
+
self.niiColorMapLabel = QLabel('Colormap:')
|
|
324
|
+
colormap_nii_row.addWidget(self.niiColorMapLabel)
|
|
303
325
|
self.niiColorMapComboBox = QComboBox()
|
|
304
326
|
self.niiColorMapComboBox.addItems(self.colormap_list)
|
|
305
327
|
self.niiColorMapComboBox.currentIndexChanged.connect(
|
|
306
328
|
self.on_nii_colormap_changed)
|
|
307
|
-
|
|
329
|
+
colormap_nii_row.addWidget(self.niiColorMapComboBox)
|
|
330
|
+
nii_layout.addLayout(colormap_nii_row)
|
|
331
|
+
|
|
332
|
+
nii_range_layout = QHBoxLayout()
|
|
333
|
+
|
|
334
|
+
nii_min_layout = QVBoxLayout()
|
|
335
|
+
self.niiMinLabel = QLabel('Min:')
|
|
336
|
+
self.niiMinSpinBox = QDoubleSpinBox()
|
|
337
|
+
self.niiMinSpinBox.setDecimals(2)
|
|
338
|
+
self.niiMinSpinBox.setRange(-1e6, 1e6)
|
|
339
|
+
self.niiMinSpinBox.setSingleStep(0.1)
|
|
340
|
+
self.niiMinSpinBox.setValue(0.0)
|
|
341
|
+
self.niiMinSpinBox.editingFinished.connect(self.update_nii_viewer)
|
|
342
|
+
nii_min_layout.addWidget(self.niiMinLabel)
|
|
343
|
+
nii_min_layout.addWidget(self.niiMinSpinBox)
|
|
344
|
+
|
|
345
|
+
nii_max_layout = QVBoxLayout()
|
|
346
|
+
self.niiMaxLabel = QLabel('Max:')
|
|
347
|
+
self.niiMaxSpinBox = QDoubleSpinBox()
|
|
348
|
+
self.niiMaxSpinBox.setDecimals(2)
|
|
349
|
+
self.niiMaxSpinBox.setRange(-1e6, 1e6)
|
|
350
|
+
self.niiMaxSpinBox.setSingleStep(0.1)
|
|
351
|
+
self.niiMaxSpinBox.setValue(1.0)
|
|
352
|
+
self.niiMaxSpinBox.editingFinished.connect(self.update_nii_viewer)
|
|
353
|
+
nii_max_layout.addWidget(self.niiMaxLabel)
|
|
354
|
+
nii_max_layout.addWidget(self.niiMaxSpinBox)
|
|
355
|
+
|
|
356
|
+
nii_range_layout.addLayout(nii_min_layout)
|
|
357
|
+
nii_range_layout.addLayout(nii_max_layout)
|
|
358
|
+
nii_layout.addLayout(nii_range_layout)
|
|
308
359
|
|
|
309
360
|
self.showSlicesCheckbox = QCheckBox('Show Slices')
|
|
310
361
|
self.showSlicesCheckbox.stateChanged.connect(self.update_nii_viewer)
|
|
311
|
-
|
|
362
|
+
nii_layout.addWidget(self.showSlicesCheckbox)
|
|
312
363
|
|
|
313
|
-
|
|
314
|
-
|
|
364
|
+
x_nii_row = QHBoxLayout()
|
|
365
|
+
x_nii_row.setSpacing(5)
|
|
366
|
+
self.x_label = QLabel('X:')
|
|
367
|
+
x_nii_row.addWidget(self.x_label)
|
|
315
368
|
self.XSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
316
369
|
self.XSlider.setMinimum(0)
|
|
317
370
|
self.XSlider.setMaximum(100)
|
|
318
371
|
self.XSlider.setValue(100)
|
|
319
372
|
self.XSlider.sliderReleased.connect(
|
|
320
373
|
lambda: self._update_slice('x', 'nii_x'))
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
374
|
+
x_nii_row.addWidget(self.XSlider)
|
|
375
|
+
nii_layout.addLayout(x_nii_row)
|
|
376
|
+
y_nii_row = QHBoxLayout()
|
|
377
|
+
y_nii_row.setSpacing(5)
|
|
378
|
+
self.y_label = QLabel('Y:')
|
|
379
|
+
y_nii_row.addWidget(self.y_label)
|
|
324
380
|
self.YSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
325
381
|
self.YSlider.setMinimum(0)
|
|
326
382
|
self.YSlider.setMaximum(100)
|
|
327
383
|
self.YSlider.setValue(100)
|
|
328
384
|
self.YSlider.sliderReleased.connect(
|
|
329
385
|
lambda: self._update_slice('y', 'nii_y'))
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
386
|
+
y_nii_row.addWidget(self.YSlider)
|
|
387
|
+
nii_layout.addLayout(y_nii_row)
|
|
388
|
+
z_nii_row = QHBoxLayout()
|
|
389
|
+
z_nii_row.setSpacing(5)
|
|
390
|
+
self.z_label = QLabel('Z:')
|
|
391
|
+
z_nii_row.addWidget(self.z_label)
|
|
333
392
|
self.ZSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
334
393
|
self.ZSlider.setMinimum(0)
|
|
335
394
|
self.ZSlider.setMaximum(100)
|
|
336
395
|
self.ZSlider.setValue(100)
|
|
337
396
|
self.ZSlider.sliderReleased.connect(
|
|
338
397
|
lambda: self._update_slice('z', 'nii_z'))
|
|
339
|
-
|
|
398
|
+
z_nii_row.addWidget(self.ZSlider)
|
|
399
|
+
nii_layout.addLayout(z_nii_row)
|
|
400
|
+
|
|
401
|
+
control_layout.addWidget(nii_group)
|
|
340
402
|
|
|
341
403
|
# Gifti
|
|
342
|
-
|
|
343
|
-
|
|
404
|
+
gii_group = QGroupBox("GIFTI / Surface")
|
|
405
|
+
gii_layout = QVBoxLayout(gii_group)
|
|
344
406
|
|
|
407
|
+
opacity_gii_row = QHBoxLayout()
|
|
408
|
+
opacity_gii_row.setSpacing(5)
|
|
409
|
+
self.gii_opacityLabel = QLabel('Opacity:')
|
|
410
|
+
opacity_gii_row.addWidget(self.gii_opacityLabel)
|
|
345
411
|
self.gii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
346
412
|
self.gii_opacitySlider.setMinimum(0)
|
|
347
413
|
self.gii_opacitySlider.setMaximum(100)
|
|
348
414
|
self.gii_opacitySlider.setValue(15)
|
|
349
415
|
self.gii_opacitySlider.sliderReleased.connect(self.update_gii_viewer)
|
|
350
|
-
|
|
416
|
+
opacity_gii_row.addWidget(self.gii_opacitySlider)
|
|
417
|
+
gii_layout.addLayout(opacity_gii_row)
|
|
418
|
+
|
|
419
|
+
color_mode_gii_row = QHBoxLayout()
|
|
420
|
+
color_mode_gii_row.setSpacing(5)
|
|
421
|
+
self.giiColorModeLabel = QLabel('Color Mode:')
|
|
422
|
+
color_mode_gii_row.addWidget(self.giiColorModeLabel)
|
|
423
|
+
self.giiColorModeComboBox = QComboBox()
|
|
424
|
+
self.giiColorModeComboBox.addItems(['Solid', 'NIfTI values'])
|
|
425
|
+
self.giiColorModeComboBox.currentIndexChanged.connect(
|
|
426
|
+
self.update_gii_viewer)
|
|
427
|
+
color_mode_gii_row.addWidget(self.giiColorModeComboBox)
|
|
428
|
+
gii_layout.addLayout(color_mode_gii_row)
|
|
429
|
+
|
|
430
|
+
colormap_gii_row = QHBoxLayout()
|
|
431
|
+
colormap_gii_row.setSpacing(5)
|
|
432
|
+
self.giiColorMapLabel = QLabel('Colormap:')
|
|
433
|
+
colormap_gii_row.addWidget(self.giiColorMapLabel)
|
|
434
|
+
self.giiColorMapComboBox = QComboBox()
|
|
435
|
+
self.giiColorMapComboBox.addItems(self.colormap_list)
|
|
436
|
+
self.giiColorMapComboBox.setCurrentText('viridis')
|
|
437
|
+
self.giiColorMapComboBox.currentIndexChanged.connect(
|
|
438
|
+
self.update_gii_viewer)
|
|
439
|
+
colormap_gii_row.addWidget(self.giiColorMapComboBox)
|
|
440
|
+
gii_layout.addLayout(colormap_gii_row)
|
|
441
|
+
|
|
442
|
+
gii_range_layout = QHBoxLayout()
|
|
443
|
+
|
|
444
|
+
gii_min_layout = QVBoxLayout()
|
|
445
|
+
self.giiMinLabel = QLabel('Min:')
|
|
446
|
+
self.giiMinSpinBox = QDoubleSpinBox()
|
|
447
|
+
self.giiMinSpinBox.setDecimals(2)
|
|
448
|
+
self.giiMinSpinBox.setRange(-1e6, 1e6)
|
|
449
|
+
self.giiMinSpinBox.setSingleStep(0.1)
|
|
450
|
+
self.giiMinSpinBox.setValue(0.0)
|
|
451
|
+
self.giiMinSpinBox.editingFinished.connect(self.update_gii_viewer)
|
|
452
|
+
gii_min_layout.addWidget(self.giiMinLabel)
|
|
453
|
+
gii_min_layout.addWidget(self.giiMinSpinBox)
|
|
454
|
+
|
|
455
|
+
gii_max_layout = QVBoxLayout()
|
|
456
|
+
self.giiMaxLabel = QLabel('Max:')
|
|
457
|
+
self.giiMaxSpinBox = QDoubleSpinBox()
|
|
458
|
+
self.giiMaxSpinBox.setDecimals(2)
|
|
459
|
+
self.giiMaxSpinBox.setRange(-1e6, 1e6)
|
|
460
|
+
self.giiMaxSpinBox.setSingleStep(0.1)
|
|
461
|
+
self.giiMaxSpinBox.setValue(1.0)
|
|
462
|
+
self.giiMaxSpinBox.editingFinished.connect(self.update_gii_viewer)
|
|
463
|
+
gii_max_layout.addWidget(self.giiMaxLabel)
|
|
464
|
+
gii_max_layout.addWidget(self.giiMaxSpinBox)
|
|
465
|
+
|
|
466
|
+
gii_range_layout.addLayout(gii_min_layout)
|
|
467
|
+
gii_range_layout.addLayout(gii_max_layout)
|
|
468
|
+
gii_layout.addLayout(gii_range_layout)
|
|
469
|
+
|
|
470
|
+
control_layout.addWidget(gii_group)
|
|
471
|
+
|
|
472
|
+
self.update_surface_color_controls()
|
|
351
473
|
|
|
352
474
|
# Add the control panel layout to the main layout
|
|
353
475
|
main_layout.addWidget(control_widget)
|
|
@@ -384,6 +506,9 @@ class TrkViewer(QWidget):
|
|
|
384
506
|
self.nii_affine = img.affine
|
|
385
507
|
self.nii_data = img.get_fdata()
|
|
386
508
|
|
|
509
|
+
self.update_volume_color_range_from_nifti()
|
|
510
|
+
self.update_surface_color_range_from_nifti()
|
|
511
|
+
|
|
387
512
|
grid = pv.ImageData()
|
|
388
513
|
grid.dimensions = np.array(self.nii_data.shape) + 1
|
|
389
514
|
grid.cell_data['values'] = self.nii_data.flatten(order='F')
|
|
@@ -394,11 +519,21 @@ class TrkViewer(QWidget):
|
|
|
394
519
|
self.ZSlider.setMaximum(self.nii_data.shape[2])
|
|
395
520
|
|
|
396
521
|
self.update_nii_viewer(reset_camera=False)
|
|
522
|
+
if hasattr(self, "gii_mesh"):
|
|
523
|
+
self.update_gii_viewer(reset_camera=False)
|
|
397
524
|
self.window().refreshActorList()
|
|
398
525
|
|
|
399
526
|
# Update 2D view with current colormap
|
|
400
|
-
self.nii_data
|
|
401
|
-
|
|
527
|
+
ortho_data = self.nii_data.copy()
|
|
528
|
+
ortho_data[ortho_data == 0] = None
|
|
529
|
+
|
|
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
|
+
self.window().ortho_viewer.set_volume(ortho_data, self.nii_affine)
|
|
402
537
|
self.window().ortho_viewer.set_colormap(self.niiColorMapComboBox.currentText())
|
|
403
538
|
|
|
404
539
|
def loadROIFile(self):
|
|
@@ -442,11 +577,23 @@ class TrkViewer(QWidget):
|
|
|
442
577
|
|
|
443
578
|
def loadGiftiFile(self):
|
|
444
579
|
options = QFileDialog.Options()
|
|
580
|
+
|
|
445
581
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
446
|
-
self,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
582
|
+
self,
|
|
583
|
+
"Open .gii File",
|
|
584
|
+
"",
|
|
585
|
+
"Gifti Files (*.gii)",
|
|
586
|
+
options=options
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
if not filePath:
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
print(f"Loaded GIFTI file: {filePath}")
|
|
593
|
+
|
|
594
|
+
self.gii_mesh = gifti_to_pyvista(filePath)
|
|
595
|
+
|
|
596
|
+
self.actor_types["gii_surface"] = "gii"
|
|
450
597
|
|
|
451
598
|
self.update_gii_viewer(reset_camera=False)
|
|
452
599
|
self.window().refreshActorList()
|
|
@@ -581,31 +728,285 @@ class TrkViewer(QWidget):
|
|
|
581
728
|
opacity = self.nii_opacitySlider.value()/1000
|
|
582
729
|
cmap_name = self.niiColorMapComboBox.currentText()
|
|
583
730
|
|
|
731
|
+
vmin = self.niiMinSpinBox.value()
|
|
732
|
+
vmax = self.niiMaxSpinBox.value()
|
|
733
|
+
|
|
734
|
+
# Protect against an invalid range
|
|
735
|
+
if vmax <= vmin:
|
|
736
|
+
vmax = vmin + 1e-6
|
|
737
|
+
|
|
584
738
|
self.plotter.add_volume(self.grid, cmap=cmap_name, opacity=[0, opacity],
|
|
585
739
|
show_scalar_bar=False, name='nii_volume',
|
|
586
|
-
reset_camera=reset_camera,
|
|
740
|
+
reset_camera=reset_camera, clim=[vmin, vmax],
|
|
587
741
|
user_matrix=self.nii_affine)
|
|
588
742
|
|
|
589
743
|
def update_gii_viewer(self, reset_camera=False):
|
|
744
|
+
"""Update the GIFTI surface rendering."""
|
|
590
745
|
|
|
591
746
|
if not hasattr(self, "gii_mesh"):
|
|
592
747
|
return
|
|
593
748
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
self.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
749
|
+
self.update_surface_color_controls()
|
|
750
|
+
|
|
751
|
+
opacity = self.gii_opacitySlider.value() / 100.0
|
|
752
|
+
|
|
753
|
+
color_mode = self.giiColorModeComboBox.currentText()
|
|
754
|
+
|
|
755
|
+
if color_mode == "Solid":
|
|
756
|
+
|
|
757
|
+
self.plotter.add_mesh(self.gii_mesh, color="ghostwhite",
|
|
758
|
+
culling="back", smooth_shading=True,
|
|
759
|
+
opacity=opacity, name="gii_surface",
|
|
760
|
+
reset_camera=reset_camera,
|
|
761
|
+
point_size=0, render_lines_as_tubes=True)
|
|
762
|
+
|
|
763
|
+
elif color_mode == "NIfTI values":
|
|
764
|
+
|
|
765
|
+
if self.nii_data is None:
|
|
766
|
+
self.plotter.add_mesh(self.gii_mesh, color="ghostwhite",
|
|
767
|
+
culling="back", smooth_shading=True,
|
|
768
|
+
opacity=opacity, name="gii_surface",
|
|
769
|
+
reset_camera=reset_camera,
|
|
770
|
+
point_size=0, render_lines_as_tubes=True)
|
|
771
|
+
return
|
|
772
|
+
|
|
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
|
+
# Copy the mesh so that the original GIFTI mesh is not modified
|
|
780
|
+
surface_mesh = self.gii_mesh.copy(deep=True)
|
|
781
|
+
surface_mesh.point_data["NIfTI values"] = scalars
|
|
782
|
+
|
|
783
|
+
cmap_name = self.giiColorMapComboBox.currentText()
|
|
784
|
+
|
|
785
|
+
vmin = self.giiMinSpinBox.value()
|
|
786
|
+
vmax = self.giiMaxSpinBox.value()
|
|
787
|
+
|
|
788
|
+
# Protect against an invalid range
|
|
789
|
+
if vmax <= vmin:
|
|
790
|
+
vmax = vmin + 1e-6
|
|
791
|
+
|
|
792
|
+
self.plotter.add_mesh(surface_mesh, scalars="NIfTI values",
|
|
793
|
+
cmap=cmap_name, clim=[vmin, vmax],
|
|
794
|
+
culling="back", smooth_shading=True,
|
|
795
|
+
opacity=opacity, name="gii_surface",
|
|
796
|
+
reset_camera=reset_camera, point_size=0,
|
|
797
|
+
render_lines_as_tubes=True,
|
|
798
|
+
show_scalar_bar=False)
|
|
799
|
+
|
|
800
|
+
self.plotter.render()
|
|
801
|
+
|
|
802
|
+
def update_volume_color_range_from_nifti(self):
|
|
803
|
+
|
|
804
|
+
if self.nii_data is None:
|
|
805
|
+
return
|
|
806
|
+
|
|
807
|
+
data = np.asarray(self.nii_data, dtype=float)
|
|
808
|
+
finite = np.isfinite(data)
|
|
809
|
+
|
|
810
|
+
if not np.any(finite):
|
|
811
|
+
return
|
|
812
|
+
|
|
813
|
+
values = data[finite]
|
|
814
|
+
|
|
815
|
+
vmin = float(np.min(values))
|
|
816
|
+
vmax = float(np.max(values))
|
|
817
|
+
|
|
818
|
+
# Avoid identical limits because PyVista/VTK does not like
|
|
819
|
+
# clim=[x, x].
|
|
820
|
+
if np.isclose(vmin, vmax):
|
|
821
|
+
vmax = vmin + 1.0
|
|
822
|
+
|
|
823
|
+
self.niiMinSpinBox.blockSignals(True)
|
|
824
|
+
self.niiMaxSpinBox.blockSignals(True)
|
|
825
|
+
|
|
826
|
+
self.niiMinSpinBox.setValue(vmin)
|
|
827
|
+
self.niiMaxSpinBox.setValue(vmax)
|
|
828
|
+
|
|
829
|
+
self.niiMinSpinBox.blockSignals(False)
|
|
830
|
+
self.niiMaxSpinBox.blockSignals(False)
|
|
831
|
+
|
|
832
|
+
def update_surface_color_range_from_nifti(self):
|
|
833
|
+
|
|
834
|
+
if self.nii_data is None:
|
|
835
|
+
return
|
|
836
|
+
|
|
837
|
+
data = np.asarray(self.nii_data, dtype=float)
|
|
838
|
+
finite = np.isfinite(data)
|
|
839
|
+
|
|
840
|
+
if not np.any(finite):
|
|
841
|
+
return
|
|
842
|
+
|
|
843
|
+
values = data[finite]
|
|
844
|
+
|
|
845
|
+
vmin = float(np.min(values))
|
|
846
|
+
vmax = float(np.max(values))
|
|
847
|
+
|
|
848
|
+
# Avoid identical limits because PyVista/VTK does not like
|
|
849
|
+
# clim=[x, x].
|
|
850
|
+
if np.isclose(vmin, vmax):
|
|
851
|
+
vmax = vmin + 1.0
|
|
852
|
+
|
|
853
|
+
self.giiMinSpinBox.blockSignals(True)
|
|
854
|
+
self.giiMaxSpinBox.blockSignals(True)
|
|
855
|
+
|
|
856
|
+
self.giiMinSpinBox.setValue(vmin)
|
|
857
|
+
self.giiMaxSpinBox.setValue(vmax)
|
|
858
|
+
|
|
859
|
+
self.giiMinSpinBox.blockSignals(False)
|
|
860
|
+
self.giiMaxSpinBox.blockSignals(False)
|
|
861
|
+
|
|
862
|
+
def sample_nifti_on_surface(self, mesh):
|
|
863
|
+
"""
|
|
864
|
+
Trilinearly sample the NIfTI volume onto the surface.
|
|
865
|
+
|
|
866
|
+
Vertices outside the NIfTI are assigned NaN.
|
|
867
|
+
"""
|
|
868
|
+
|
|
869
|
+
if self.nii_data is None or not hasattr(self, "nii_affine"):
|
|
870
|
+
return None
|
|
871
|
+
|
|
872
|
+
volume = np.asarray(self.nii_data_dilated, dtype=np.float64)
|
|
873
|
+
|
|
874
|
+
points_world = np.asarray(mesh.points, dtype=np.float64)
|
|
875
|
+
|
|
876
|
+
inv_affine = np.linalg.inv(self.nii_affine)
|
|
877
|
+
|
|
878
|
+
homogeneous = np.column_stack([
|
|
879
|
+
points_world,
|
|
880
|
+
np.ones(len(points_world))
|
|
881
|
+
])
|
|
882
|
+
|
|
883
|
+
points_voxel = (homogeneous @ inv_affine.T)[:, :3]
|
|
884
|
+
|
|
885
|
+
nx, ny, nz = volume.shape[:3]
|
|
886
|
+
|
|
887
|
+
# Identify points that are actually inside the NIfTI
|
|
888
|
+
inside = (
|
|
889
|
+
(points_voxel[:, 0] >= 0) &
|
|
890
|
+
(points_voxel[:, 0] <= nx - 1) &
|
|
891
|
+
(points_voxel[:, 1] >= 0) &
|
|
892
|
+
(points_voxel[:, 1] <= ny - 1) &
|
|
893
|
+
(points_voxel[:, 2] >= 0) &
|
|
894
|
+
(points_voxel[:, 2] <= nz - 1)
|
|
606
895
|
)
|
|
607
896
|
|
|
608
|
-
#
|
|
897
|
+
# Clip only for interpolation
|
|
898
|
+
x = np.clip(points_voxel[:, 0], 0, nx - 1)
|
|
899
|
+
y = np.clip(points_voxel[:, 1], 0, ny - 1)
|
|
900
|
+
z = np.clip(points_voxel[:, 2], 0, nz - 1)
|
|
901
|
+
|
|
902
|
+
x0 = np.floor(x).astype(int)
|
|
903
|
+
y0 = np.floor(y).astype(int)
|
|
904
|
+
z0 = np.floor(z).astype(int)
|
|
905
|
+
|
|
906
|
+
x1 = np.minimum(x0 + 1, nx - 1)
|
|
907
|
+
y1 = np.minimum(y0 + 1, ny - 1)
|
|
908
|
+
z1 = np.minimum(z0 + 1, nz - 1)
|
|
909
|
+
|
|
910
|
+
xd = x - x0
|
|
911
|
+
yd = y - y0
|
|
912
|
+
zd = z - z0
|
|
913
|
+
|
|
914
|
+
c000 = volume[x0, y0, z0]
|
|
915
|
+
c001 = volume[x0, y0, z1]
|
|
916
|
+
c010 = volume[x0, y1, z0]
|
|
917
|
+
c011 = volume[x0, y1, z1]
|
|
918
|
+
|
|
919
|
+
c100 = volume[x1, y0, z0]
|
|
920
|
+
c101 = volume[x1, y0, z1]
|
|
921
|
+
c110 = volume[x1, y1, z0]
|
|
922
|
+
c111 = volume[x1, y1, z1]
|
|
923
|
+
|
|
924
|
+
c00 = c000 * (1 - xd) + c100 * xd
|
|
925
|
+
c01 = c001 * (1 - xd) + c101 * xd
|
|
926
|
+
c10 = c010 * (1 - xd) + c110 * xd
|
|
927
|
+
c11 = c011 * (1 - xd) + c111 * xd
|
|
928
|
+
|
|
929
|
+
c0 = c00 * (1 - yd) + c10 * yd
|
|
930
|
+
c1 = c01 * (1 - yd) + c11 * yd
|
|
931
|
+
|
|
932
|
+
values = c0 * (1 - zd) + c1 * zd
|
|
933
|
+
|
|
934
|
+
# Outside the NIfTI = NaN
|
|
935
|
+
values[~inside] = np.nan
|
|
936
|
+
|
|
937
|
+
return values.astype(np.float32)
|
|
938
|
+
|
|
939
|
+
def sample_nifti_on_surface2(self, mesh):
|
|
940
|
+
"""
|
|
941
|
+
!!! Currently unused
|
|
942
|
+
Sample the currently loaded NIfTI volume at every GIFTI surface vertex.
|
|
943
|
+
|
|
944
|
+
GIFTI vertices are assumed to be in world/mm coordinates.
|
|
945
|
+
The NIfTI affine is used to convert them into voxel coordinates.
|
|
946
|
+
|
|
947
|
+
Nearest-neighbor sampling is used.
|
|
948
|
+
"""
|
|
949
|
+
|
|
950
|
+
if self.nii_data is None:
|
|
951
|
+
return None
|
|
952
|
+
|
|
953
|
+
if not hasattr(self, 'nii_affine'):
|
|
954
|
+
return None
|
|
955
|
+
|
|
956
|
+
vertices_world = np.asarray(mesh.points)
|
|
957
|
+
|
|
958
|
+
# Convert world coordinates -> NIfTI voxel coordinates
|
|
959
|
+
inv_affine = np.linalg.inv(self.nii_affine)
|
|
960
|
+
|
|
961
|
+
homogeneous = np.column_stack([
|
|
962
|
+
vertices_world,
|
|
963
|
+
np.ones(len(vertices_world))
|
|
964
|
+
])
|
|
965
|
+
|
|
966
|
+
vertices_voxel = (homogeneous @ inv_affine.T)[:, :3]
|
|
967
|
+
|
|
968
|
+
# Nearest-neighbor voxel coordinates
|
|
969
|
+
vertices_voxel = np.rint(vertices_voxel).astype(int)
|
|
970
|
+
|
|
971
|
+
shape = self.nii_data.shape
|
|
972
|
+
|
|
973
|
+
# Determine which vertices are inside the NIfTI
|
|
974
|
+
valid = (
|
|
975
|
+
(vertices_voxel[:, 0] >= 0) &
|
|
976
|
+
(vertices_voxel[:, 0] < shape[0]) &
|
|
977
|
+
(vertices_voxel[:, 1] >= 0) &
|
|
978
|
+
(vertices_voxel[:, 1] < shape[1]) &
|
|
979
|
+
(vertices_voxel[:, 2] >= 0) &
|
|
980
|
+
(vertices_voxel[:, 2] < shape[2])
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
# Initialize outside-of-volume vertices with NaN
|
|
984
|
+
values = np.full(len(vertices_world), np.nan, dtype=np.float32)
|
|
985
|
+
|
|
986
|
+
valid_voxels = vertices_voxel[valid]
|
|
987
|
+
|
|
988
|
+
values[valid] = self.nii_data[
|
|
989
|
+
valid_voxels[:, 0],
|
|
990
|
+
valid_voxels[:, 1],
|
|
991
|
+
valid_voxels[:, 2]
|
|
992
|
+
]
|
|
993
|
+
|
|
994
|
+
return values
|
|
995
|
+
|
|
996
|
+
def update_surface_color_controls(self):
|
|
997
|
+
"""Enable surface scalar controls only when using NIfTI values."""
|
|
998
|
+
|
|
999
|
+
use_nifti = (self.giiColorModeComboBox.currentText()
|
|
1000
|
+
== "NIfTI values")
|
|
1001
|
+
|
|
1002
|
+
self.giiColorMapLabel.setEnabled(use_nifti)
|
|
1003
|
+
self.giiColorMapComboBox.setEnabled(use_nifti)
|
|
1004
|
+
|
|
1005
|
+
self.giiMinLabel.setEnabled(use_nifti)
|
|
1006
|
+
self.giiMinSpinBox.setEnabled(use_nifti)
|
|
1007
|
+
|
|
1008
|
+
self.giiMaxLabel.setEnabled(use_nifti)
|
|
1009
|
+
self.giiMaxSpinBox.setEnabled(use_nifti)
|
|
609
1010
|
|
|
610
1011
|
|
|
611
1012
|
class OrthogonalViewer(QWidget):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|