unveil-python 0.5.0__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.0/src/unveil_python.egg-info → unveil_python-0.6.0}/PKG-INFO +1 -1
- {unveil_python-0.5.0 → unveil_python-0.6.0}/src/unveil/__init__.py +1 -1
- {unveil_python-0.5.0 → unveil_python-0.6.0}/src/unveil/core.py +516 -66
- {unveil_python-0.5.0 → unveil_python-0.6.0/src/unveil_python.egg-info}/PKG-INFO +1 -1
- {unveil_python-0.5.0 → unveil_python-0.6.0}/LICENSE +0 -0
- {unveil_python-0.5.0 → unveil_python-0.6.0}/README.md +0 -0
- {unveil_python-0.5.0 → unveil_python-0.6.0}/pyproject.toml +0 -0
- {unveil_python-0.5.0 → unveil_python-0.6.0}/setup.cfg +0 -0
- {unveil_python-0.5.0 → unveil_python-0.6.0}/src/unveil/__main__.py +0 -0
- {unveil_python-0.5.0 → unveil_python-0.6.0}/src/unveil_python.egg-info/SOURCES.txt +0 -0
- {unveil_python-0.5.0 → unveil_python-0.6.0}/src/unveil_python.egg-info/dependency_links.txt +0 -0
- {unveil_python-0.5.0 → unveil_python-0.6.0}/src/unveil_python.egg-info/requires.txt +0 -0
- {unveil_python-0.5.0 → 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):
|
|
@@ -194,6 +196,22 @@ class TrkViewer(QWidget):
|
|
|
194
196
|
self.x = 0
|
|
195
197
|
self.y = 0
|
|
196
198
|
self.z = 0
|
|
199
|
+
self.colormap_list = ['gray',
|
|
200
|
+
'viridis', 'plasma', 'inferno', 'magma', 'cividis',
|
|
201
|
+
'Greys', 'Purples', 'Blues', 'Greens', 'Oranges',
|
|
202
|
+
'Reds', 'bone', 'pink', 'spring', 'summer',
|
|
203
|
+
'autumn', 'winter', 'cool', 'Wistia', 'hot',
|
|
204
|
+
'afmhot', 'gist_heat', 'copper', 'YlOrRd', 'YlOrBr',
|
|
205
|
+
'RdBu', 'RdYlBu',
|
|
206
|
+
'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic',
|
|
207
|
+
'berlin', 'managua', 'vanimo', 'twilight',
|
|
208
|
+
'twilight_shifted', 'hsv', 'Pastel1', 'Pastel2',
|
|
209
|
+
'Paired', 'Accent', 'okabe_ito', 'Dark2', 'Set1',
|
|
210
|
+
'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c',
|
|
211
|
+
'flag', 'prism', 'ocean', 'gist_earth', 'terrain',
|
|
212
|
+
'gist_stern', 'gnuplot', 'gnuplot2', 'CMRmap',
|
|
213
|
+
'cubehelix', 'brg', 'gist_rainbow', 'rainbow',
|
|
214
|
+
'jet', 'turbo', 'nipy_spectral', 'gist_ncar']
|
|
197
215
|
self.initUI()
|
|
198
216
|
self.background = 'white'
|
|
199
217
|
self.color_blind = False
|
|
@@ -238,88 +256,220 @@ class TrkViewer(QWidget):
|
|
|
238
256
|
# Left side: Control panel
|
|
239
257
|
control_widget = QWidget()
|
|
240
258
|
control_layout = QVBoxLayout(control_widget)
|
|
241
|
-
control_widget.setMaximumWidth(
|
|
259
|
+
control_widget.setMaximumWidth(260)
|
|
242
260
|
|
|
243
|
-
|
|
244
|
-
|
|
261
|
+
# Tractography
|
|
262
|
+
trk_group = QGroupBox("TRK / Tractography")
|
|
263
|
+
trk_layout = QVBoxLayout(trk_group)
|
|
245
264
|
|
|
265
|
+
opacity_trk_row = QHBoxLayout()
|
|
266
|
+
opacity_trk_row.setSpacing(5)
|
|
267
|
+
self.opacityLabel = QLabel('Opacity:')
|
|
268
|
+
opacity_trk_row.addWidget(self.opacityLabel)
|
|
246
269
|
self.opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
247
270
|
self.opacitySlider.setMinimum(1)
|
|
248
271
|
self.opacitySlider.setMaximum(100)
|
|
249
272
|
self.opacitySlider.setValue(100)
|
|
250
273
|
self.opacitySlider.sliderReleased.connect(self.update_trk_viewer)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
self.showPointsCheckbox = QCheckBox('Show Points')
|
|
254
|
-
self.showPointsCheckbox.stateChanged.connect(self.update_trk_viewer)
|
|
255
|
-
control_layout.addWidget(self.showPointsCheckbox)
|
|
256
|
-
|
|
257
|
-
self.colorMapLabel = QLabel('Color Map:')
|
|
258
|
-
control_layout.addWidget(self.colorMapLabel)
|
|
274
|
+
opacity_trk_row.addWidget(self.opacitySlider)
|
|
275
|
+
trk_layout.addLayout(opacity_trk_row)
|
|
259
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)
|
|
260
281
|
self.colorMapComboBox = QComboBox()
|
|
261
282
|
self.colorMapComboBox.addItems(['rgb', 'flesh', 'scalar'])
|
|
262
283
|
self.colorMapComboBox.currentIndexChanged.connect(
|
|
263
284
|
self.update_trk_viewer)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
285
|
+
color_mode_trk_row.addWidget(self.colorMapComboBox)
|
|
286
|
+
trk_layout.addLayout(color_mode_trk_row)
|
|
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)
|
|
292
|
+
self.tractColorMapComboBox = QComboBox()
|
|
293
|
+
self.tractColorMapComboBox.addItems(self.colormap_list)
|
|
294
|
+
self.tractColorMapComboBox.currentIndexChanged.connect(
|
|
295
|
+
self.update_trk_viewer)
|
|
296
|
+
colormap_trk_row.addWidget(self.tractColorMapComboBox)
|
|
297
|
+
trk_layout.addLayout(colormap_trk_row)
|
|
270
298
|
|
|
271
|
-
self.
|
|
272
|
-
|
|
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)
|
|
273
304
|
|
|
305
|
+
# Volume
|
|
306
|
+
nii_group = QGroupBox("NIfTI / Volume")
|
|
307
|
+
nii_layout = QVBoxLayout(nii_group)
|
|
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)
|
|
274
313
|
self.nii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
275
314
|
self.nii_opacitySlider.setMinimum(0)
|
|
276
315
|
self.nii_opacitySlider.setMaximum(1000)
|
|
277
316
|
self.nii_opacitySlider.setValue(45)
|
|
278
317
|
self.nii_opacitySlider.sliderReleased.connect(self.update_nii_viewer)
|
|
279
|
-
|
|
318
|
+
opacity_nii_row.addWidget(self.nii_opacitySlider)
|
|
319
|
+
nii_layout.addLayout(opacity_nii_row)
|
|
320
|
+
|
|
321
|
+
colormap_nii_row = QHBoxLayout()
|
|
322
|
+
colormap_nii_row.setSpacing(5)
|
|
323
|
+
self.niiColorMapLabel = QLabel('Colormap:')
|
|
324
|
+
colormap_nii_row.addWidget(self.niiColorMapLabel)
|
|
325
|
+
self.niiColorMapComboBox = QComboBox()
|
|
326
|
+
self.niiColorMapComboBox.addItems(self.colormap_list)
|
|
327
|
+
self.niiColorMapComboBox.currentIndexChanged.connect(
|
|
328
|
+
self.on_nii_colormap_changed)
|
|
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)
|
|
280
359
|
|
|
281
360
|
self.showSlicesCheckbox = QCheckBox('Show Slices')
|
|
282
361
|
self.showSlicesCheckbox.stateChanged.connect(self.update_nii_viewer)
|
|
283
|
-
|
|
362
|
+
nii_layout.addWidget(self.showSlicesCheckbox)
|
|
284
363
|
|
|
285
|
-
|
|
286
|
-
|
|
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)
|
|
287
368
|
self.XSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
288
369
|
self.XSlider.setMinimum(0)
|
|
289
370
|
self.XSlider.setMaximum(100)
|
|
290
371
|
self.XSlider.setValue(100)
|
|
291
372
|
self.XSlider.sliderReleased.connect(
|
|
292
373
|
lambda: self._update_slice('x', 'nii_x'))
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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)
|
|
296
380
|
self.YSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
297
381
|
self.YSlider.setMinimum(0)
|
|
298
382
|
self.YSlider.setMaximum(100)
|
|
299
383
|
self.YSlider.setValue(100)
|
|
300
384
|
self.YSlider.sliderReleased.connect(
|
|
301
385
|
lambda: self._update_slice('y', 'nii_y'))
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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)
|
|
305
392
|
self.ZSlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
306
393
|
self.ZSlider.setMinimum(0)
|
|
307
394
|
self.ZSlider.setMaximum(100)
|
|
308
395
|
self.ZSlider.setValue(100)
|
|
309
396
|
self.ZSlider.sliderReleased.connect(
|
|
310
397
|
lambda: self._update_slice('z', 'nii_z'))
|
|
311
|
-
|
|
398
|
+
z_nii_row.addWidget(self.ZSlider)
|
|
399
|
+
nii_layout.addLayout(z_nii_row)
|
|
400
|
+
|
|
401
|
+
control_layout.addWidget(nii_group)
|
|
312
402
|
|
|
313
403
|
# Gifti
|
|
314
|
-
|
|
315
|
-
|
|
404
|
+
gii_group = QGroupBox("GIFTI / Surface")
|
|
405
|
+
gii_layout = QVBoxLayout(gii_group)
|
|
316
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)
|
|
317
411
|
self.gii_opacitySlider = QSlider(Qt.Orientation.Horizontal, self)
|
|
318
412
|
self.gii_opacitySlider.setMinimum(0)
|
|
319
413
|
self.gii_opacitySlider.setMaximum(100)
|
|
320
414
|
self.gii_opacitySlider.setValue(15)
|
|
321
415
|
self.gii_opacitySlider.sliderReleased.connect(self.update_gii_viewer)
|
|
322
|
-
|
|
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()
|
|
323
473
|
|
|
324
474
|
# Add the control panel layout to the main layout
|
|
325
475
|
main_layout.addWidget(control_widget)
|
|
@@ -356,6 +506,9 @@ class TrkViewer(QWidget):
|
|
|
356
506
|
self.nii_affine = img.affine
|
|
357
507
|
self.nii_data = img.get_fdata()
|
|
358
508
|
|
|
509
|
+
self.update_volume_color_range_from_nifti()
|
|
510
|
+
self.update_surface_color_range_from_nifti()
|
|
511
|
+
|
|
359
512
|
grid = pv.ImageData()
|
|
360
513
|
grid.dimensions = np.array(self.nii_data.shape) + 1
|
|
361
514
|
grid.cell_data['values'] = self.nii_data.flatten(order='F')
|
|
@@ -366,10 +519,22 @@ class TrkViewer(QWidget):
|
|
|
366
519
|
self.ZSlider.setMaximum(self.nii_data.shape[2])
|
|
367
520
|
|
|
368
521
|
self.update_nii_viewer(reset_camera=False)
|
|
522
|
+
if hasattr(self, "gii_mesh"):
|
|
523
|
+
self.update_gii_viewer(reset_camera=False)
|
|
369
524
|
self.window().refreshActorList()
|
|
370
525
|
|
|
371
|
-
|
|
372
|
-
self.
|
|
526
|
+
# Update 2D view with current colormap
|
|
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)
|
|
537
|
+
self.window().ortho_viewer.set_colormap(self.niiColorMapComboBox.currentText())
|
|
373
538
|
|
|
374
539
|
def loadROIFile(self):
|
|
375
540
|
"""Load one or multiple ROI nifti volumes and render as surfaces."""
|
|
@@ -412,11 +577,23 @@ class TrkViewer(QWidget):
|
|
|
412
577
|
|
|
413
578
|
def loadGiftiFile(self):
|
|
414
579
|
options = QFileDialog.Options()
|
|
580
|
+
|
|
415
581
|
filePath, _ = QFileDialog.getOpenFileName(
|
|
416
|
-
self,
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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"
|
|
420
597
|
|
|
421
598
|
self.update_gii_viewer(reset_camera=False)
|
|
422
599
|
self.window().refreshActorList()
|
|
@@ -491,20 +668,25 @@ class TrkViewer(QWidget):
|
|
|
491
668
|
self.plotter.render()
|
|
492
669
|
offscreen.close()
|
|
493
670
|
|
|
671
|
+
def on_nii_colormap_changed(self):
|
|
672
|
+
self.update_nii_viewer()
|
|
673
|
+
if hasattr(self.window(), 'ortho_viewer'):
|
|
674
|
+
self.window().ortho_viewer.set_colormap(self.niiColorMapComboBox.currentText())
|
|
675
|
+
|
|
494
676
|
def update_trk_viewer(self, reset_camera: bool = False):
|
|
495
677
|
|
|
496
678
|
opacity = self.opacitySlider.value() / 100.0
|
|
497
679
|
show_points = self.showPointsCheckbox.isChecked()
|
|
498
680
|
|
|
499
|
-
|
|
500
|
-
if
|
|
681
|
+
color_mode = self.colorMapComboBox.currentText()
|
|
682
|
+
if color_mode == 'flesh':
|
|
501
683
|
color_map = 'flesh'
|
|
502
684
|
scalar = None
|
|
503
|
-
elif
|
|
685
|
+
elif color_mode == 'rgb':
|
|
504
686
|
color_map = 'plasma'
|
|
505
687
|
scalar = None
|
|
506
688
|
else:
|
|
507
|
-
color_map = self.
|
|
689
|
+
color_map = self.tractColorMapComboBox.currentText()
|
|
508
690
|
scalar = self.nii_data
|
|
509
691
|
|
|
510
692
|
for file in list(self.actor_types):
|
|
@@ -518,14 +700,15 @@ class TrkViewer(QWidget):
|
|
|
518
700
|
|
|
519
701
|
def _update_slice(self, axis, actor_name):
|
|
520
702
|
|
|
521
|
-
if self.showSlicesCheckbox.isChecked():
|
|
703
|
+
if self.showSlicesCheckbox.isChecked() and self.grid is not None:
|
|
522
704
|
|
|
523
705
|
center = (self.XSlider.value(), self.YSlider.value(),
|
|
524
706
|
self.ZSlider.value())
|
|
525
707
|
|
|
526
708
|
slice_mesh = self.grid.slice(axis, origin=center)
|
|
709
|
+
cmap_name = self.niiColorMapComboBox.currentText()
|
|
527
710
|
|
|
528
|
-
self.plotter.add_mesh(slice_mesh, cmap=
|
|
711
|
+
self.plotter.add_mesh(slice_mesh, cmap=cmap_name, name=actor_name,
|
|
529
712
|
show_scalar_bar=False, point_size=0,
|
|
530
713
|
render_lines_as_tubes=True,
|
|
531
714
|
reset_camera=False,
|
|
@@ -535,25 +718,295 @@ class TrkViewer(QWidget):
|
|
|
535
718
|
self.plotter.remove_actor(actor_name)
|
|
536
719
|
|
|
537
720
|
def update_nii_viewer(self, reset_camera: bool = False):
|
|
721
|
+
if self.grid is None:
|
|
722
|
+
return
|
|
538
723
|
|
|
539
724
|
self._update_slice('x', 'nii_x')
|
|
540
725
|
self._update_slice('y', 'nii_y')
|
|
541
726
|
self._update_slice('z', 'nii_z')
|
|
542
727
|
|
|
543
728
|
opacity = self.nii_opacitySlider.value()/1000
|
|
544
|
-
self.
|
|
729
|
+
cmap_name = self.niiColorMapComboBox.currentText()
|
|
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
|
+
|
|
738
|
+
self.plotter.add_volume(self.grid, cmap=cmap_name, opacity=[0, opacity],
|
|
545
739
|
show_scalar_bar=False, name='nii_volume',
|
|
546
|
-
reset_camera=reset_camera,
|
|
740
|
+
reset_camera=reset_camera, clim=[vmin, vmax],
|
|
547
741
|
user_matrix=self.nii_affine)
|
|
548
742
|
|
|
549
|
-
def update_gii_viewer(self, reset_camera
|
|
743
|
+
def update_gii_viewer(self, reset_camera=False):
|
|
744
|
+
"""Update the GIFTI surface rendering."""
|
|
745
|
+
|
|
746
|
+
if not hasattr(self, "gii_mesh"):
|
|
747
|
+
return
|
|
748
|
+
|
|
749
|
+
self.update_surface_color_controls()
|
|
550
750
|
|
|
551
|
-
opacity = self.gii_opacitySlider.value()/100
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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)
|
|
895
|
+
)
|
|
896
|
+
|
|
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)
|
|
557
1010
|
|
|
558
1011
|
|
|
559
1012
|
class OrthogonalViewer(QWidget):
|
|
@@ -563,6 +1016,7 @@ class OrthogonalViewer(QWidget):
|
|
|
563
1016
|
|
|
564
1017
|
self.volume = None
|
|
565
1018
|
self.affine = None
|
|
1019
|
+
self.cmap = "gray"
|
|
566
1020
|
|
|
567
1021
|
self.rois = {}
|
|
568
1022
|
self.roi_visibility = {}
|
|
@@ -608,6 +1062,10 @@ class OrthogonalViewer(QWidget):
|
|
|
608
1062
|
|
|
609
1063
|
self.update_views()
|
|
610
1064
|
|
|
1065
|
+
def set_colormap(self, cmap_name):
|
|
1066
|
+
self.cmap = cmap_name
|
|
1067
|
+
self.update_views()
|
|
1068
|
+
|
|
611
1069
|
def add_roi(self, name, roi, color):
|
|
612
1070
|
|
|
613
1071
|
self.rois[name] = {"data": roi, "color": color}
|
|
@@ -644,9 +1102,9 @@ class OrthogonalViewer(QWidget):
|
|
|
644
1102
|
for ax in (self.ax_axial, self.ax_coronal, self.ax_sagittal):
|
|
645
1103
|
ax.axis("off")
|
|
646
1104
|
|
|
647
|
-
self.ax_axial.imshow(np.rot90(axial), cmap=
|
|
648
|
-
self.ax_coronal.imshow(np.rot90(coronal), cmap=
|
|
649
|
-
self.ax_sagittal.imshow(np.rot90(sagittal), cmap=
|
|
1105
|
+
self.ax_axial.imshow(np.rot90(axial), cmap=self.cmap, vmin=0)
|
|
1106
|
+
self.ax_coronal.imshow(np.rot90(coronal), cmap=self.cmap, vmin=0)
|
|
1107
|
+
self.ax_sagittal.imshow(np.rot90(sagittal), cmap=self.cmap, vmin=0)
|
|
650
1108
|
|
|
651
1109
|
for name, roi_info in self.rois.items():
|
|
652
1110
|
|
|
@@ -662,14 +1120,6 @@ class OrthogonalViewer(QWidget):
|
|
|
662
1120
|
|
|
663
1121
|
self.canvas.draw_idle()
|
|
664
1122
|
|
|
665
|
-
# self.window().viewer.XSlider.setValue(self.x)
|
|
666
|
-
# self.window().viewer.YSlider.setValue(self.y)
|
|
667
|
-
# self.window().viewer.ZSlider.setValue(self.z)
|
|
668
|
-
|
|
669
|
-
# self.window().viewer._update_slice('x', 'nii_x')
|
|
670
|
-
# self.window().viewer._update_slice('y', 'nii_y')
|
|
671
|
-
# self.window().viewer._update_slice('z', 'nii_z')
|
|
672
|
-
|
|
673
1123
|
def take_screenshot(self):
|
|
674
1124
|
|
|
675
1125
|
options = QFileDialog.Options()
|
|
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
|