ndslice 0.5.2__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.
- {ndslice-0.5.2 → ndslice-0.6.0}/PKG-INFO +3 -1
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice/__main__.py +1 -1
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice/ndslice.py +198 -35
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice/selectors.py +35 -7
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice.egg-info/PKG-INFO +3 -1
- {ndslice-0.5.2 → ndslice-0.6.0}/pyproject.toml +3 -1
- {ndslice-0.5.2 → ndslice-0.6.0}/LICENSE +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/README.md +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice/__init__.py +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice/file_interpreters.py +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice/imageview2d.py +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice/video_export.py +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice.egg-info/SOURCES.txt +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice.egg-info/dependency_links.txt +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice.egg-info/entry_points.txt +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice.egg-info/requires.txt +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/ndslice.egg-info/top_level.txt +0 -0
- {ndslice-0.5.2 → ndslice-0.6.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndslice
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Interactive N-dimensional numpy array viewer with FFT support
|
|
5
5
|
Author-email: Henric Rydén <henric.ryden@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
23
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
22
24
|
Requires-Python: >=3.8
|
|
23
25
|
Description-Content-Type: text/markdown
|
|
@@ -45,7 +45,7 @@ For files with multiple datasets (HDF5, NPZ, MAT), a GUI selector will automatic
|
|
|
45
45
|
# Single-dataset formats is handled by file_interpreters.load_file
|
|
46
46
|
if suffix in ['.npy', '.rec', '.cfl', '.dcm', '.nii', '.nii.gz', '.txt']:
|
|
47
47
|
data = load_file(filepath)
|
|
48
|
-
ndslice(data=data, title=filepath.name, block=False)
|
|
48
|
+
ndslice(data=data, title=filepath.name, block=False, filepath=filepath)
|
|
49
49
|
continue
|
|
50
50
|
|
|
51
51
|
# Multi-dataset formats - use selectors
|
|
@@ -29,13 +29,13 @@ class Domain(Enum):
|
|
|
29
29
|
class NDSliceWindow(QtWidgets.QMainWindow):
|
|
30
30
|
# Styling constants — use pt (point) units so font sizes are DPI-independent
|
|
31
31
|
DIMENSION_LABEL_STYLE = "QLabel { font-size: 9pt; padding: 1px; margin: 2px; }"
|
|
32
|
-
FLIP_ICON_STYLE = "QLabel { font-size: 15pt; padding: 0px; margin: 0px; }"
|
|
33
|
-
BUTTON_STYLE = "QPushButton { font-size: 9pt; padding: 2px; margin: 2px; }"
|
|
34
|
-
SPINBOX_STYLE = "QSpinBox { font-size: 9pt; }"
|
|
32
|
+
FLIP_ICON_STYLE = "QLabel { font-size: 15pt; padding: 0px; margin: 0px; color: palette(text); }"
|
|
33
|
+
BUTTON_STYLE = "QPushButton { font-size: 9pt; padding: 2px; margin: 2px; } QPushButton:disabled { color: palette(mid); }"
|
|
34
|
+
SPINBOX_STYLE = "QSpinBox { font-size: 9pt; } QSpinBox:disabled { color: palette(mid); }"
|
|
35
35
|
RADIO_BUTTON_STYLE = "QRadioButton { font-size: 9pt; }"
|
|
36
36
|
GROUPBOX_BASE_STYLE = "QGroupBox { font-size: 9pt; font-weight: bold; border: 1px solid palette(mid); border-radius: 3px; margin-top: 1.4ex; padding-top: 3pt; } QGroupBox::title { subcontrol-origin: margin; left: 8px; padding: 0 3px; }"
|
|
37
37
|
|
|
38
|
-
def __init__(self, data, complex_dim=None):
|
|
38
|
+
def __init__(self, data, complex_dim=None, filepath=None, dataset_path=None, selector_class_name=None):
|
|
39
39
|
super(NDSliceWindow, self).__init__()
|
|
40
40
|
self.resize(800,800)
|
|
41
41
|
|
|
@@ -45,6 +45,9 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
45
45
|
self.channel = None
|
|
46
46
|
self.scale = None
|
|
47
47
|
self._force_autolevel = False
|
|
48
|
+
self._filepath = filepath
|
|
49
|
+
self._dataset_path = dataset_path
|
|
50
|
+
self._selector_class_name = selector_class_name
|
|
48
51
|
|
|
49
52
|
self.axis_flipped = [False] * data.ndim # Track flip state so that one can toggle dims and come back to the same flip state
|
|
50
53
|
|
|
@@ -121,14 +124,7 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
121
124
|
self.channel_button_group.addButton(self.widgets['buttons']['channel']['imag'])
|
|
122
125
|
self.channel_button_group.addButton(self.widgets['buttons']['channel']['abs'])
|
|
123
126
|
self.channel_button_group.addButton(self.widgets['buttons']['channel']['angle'])
|
|
124
|
-
|
|
125
|
-
# Set default checked radio button
|
|
126
|
-
if self.widgets['buttons']['channel']['abs'].isEnabled():
|
|
127
|
-
self.widgets['buttons']['channel']['abs'].setChecked(True)
|
|
128
|
-
#self.channel = 'abs'
|
|
129
|
-
else:
|
|
130
|
-
self.widgets['buttons']['channel']['real'].setChecked(True)
|
|
131
|
-
#self.channel = 'real'
|
|
127
|
+
self._update_channel_controls()
|
|
132
128
|
|
|
133
129
|
self.scale_button_group = QtWidgets.QButtonGroup()
|
|
134
130
|
#self.scale_button_group.addButton(self.widgets['buttons']['processing']['log'])
|
|
@@ -445,7 +441,16 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
445
441
|
|
|
446
442
|
# Add tab widget to the main layout
|
|
447
443
|
self.layouts['topDown'].addWidget(self.tab_widget)
|
|
448
|
-
|
|
444
|
+
|
|
445
|
+
# Reload / file-changed button (⟳ by default, ⚠️ when file changes on disk)
|
|
446
|
+
self._reload_btn = QtWidgets.QPushButton("⟳")
|
|
447
|
+
self._reload_btn.setStyleSheet("QPushButton { font-size: 18pt; padding: 1px 2px; margin: 0px; border: none; background: transparent; }")
|
|
448
|
+
self._reload_btn.setToolTip("Reload file")
|
|
449
|
+
self._reload_btn.setFlat(True)
|
|
450
|
+
self._reload_btn.setFixedSize(28, 20)
|
|
451
|
+
self._reload_btn.clicked.connect(self._reload_file)
|
|
452
|
+
self._reload_btn.setVisible(filepath is not None)
|
|
453
|
+
self.layouts['topUp'].addWidget(self._reload_btn)
|
|
449
454
|
self.layouts['topUp'].addWidget(self.widgets['labels']['pixelValue'])
|
|
450
455
|
self.layouts['topUp'].addWidget(self.widgets['labels']['arrayInfo'])
|
|
451
456
|
|
|
@@ -498,6 +503,13 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
498
503
|
self.update()
|
|
499
504
|
self.show()
|
|
500
505
|
|
|
506
|
+
# Set up file watcher if a filepath was provided (QFileSystemWatcher uses
|
|
507
|
+
# OS-native events: inotify on Linux, FSEvents on macOS, ReadDirectoryChanges on Windows)
|
|
508
|
+
self._file_watcher = None
|
|
509
|
+
if filepath is not None:
|
|
510
|
+
self._file_watcher = Qt.QtCore.QFileSystemWatcher([str(filepath)])
|
|
511
|
+
self._file_watcher.fileChanged.connect(self._on_file_changed)
|
|
512
|
+
|
|
501
513
|
|
|
502
514
|
|
|
503
515
|
|
|
@@ -541,10 +553,12 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
541
553
|
def _apply_fft(self, dim):
|
|
542
554
|
"""Apply forward FFT along specified dimension"""
|
|
543
555
|
self.data = np.fft.ifftshift(np.fft.ifft(np.fft.fftshift(self.data), axis=dim, norm='ortho'))
|
|
556
|
+
self._update_channel_controls()
|
|
544
557
|
|
|
545
558
|
def _apply_ifft(self, dim):
|
|
546
559
|
"""Apply inverse FFT along specified dimension"""
|
|
547
560
|
self.data = np.fft.ifftshift(np.fft.fft(np.fft.fftshift(self.data), axis=dim, norm='ortho'))
|
|
561
|
+
self._update_channel_controls()
|
|
548
562
|
|
|
549
563
|
def flipAxisClicked(self, event, dim):
|
|
550
564
|
"""Handle click on flip axis icon"""
|
|
@@ -635,6 +649,30 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
635
649
|
indicator.setText('')
|
|
636
650
|
indicator.setToolTip('')
|
|
637
651
|
indicator.setCursor(QtGui.QCursor(Qt.QtCore.Qt.CursorShape.ArrowCursor))
|
|
652
|
+
|
|
653
|
+
def _update_channel_controls(self):
|
|
654
|
+
"""Keep channel options in sync with the current array dtype."""
|
|
655
|
+
is_complex = np.iscomplexobj(self.data)
|
|
656
|
+
channel_buttons = self.widgets['buttons']['channel']
|
|
657
|
+
enabled_channels = {
|
|
658
|
+
'real': True,
|
|
659
|
+
'abs': True,
|
|
660
|
+
'imag': is_complex,
|
|
661
|
+
'angle': is_complex,
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
checked_channel = next(
|
|
665
|
+
(name for name, button in channel_buttons.items() if button.isChecked()),
|
|
666
|
+
None,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
for name, button in channel_buttons.items():
|
|
670
|
+
button.setEnabled(enabled_channels[name])
|
|
671
|
+
|
|
672
|
+
if checked_channel not in enabled_channels or not enabled_channels[checked_channel]:
|
|
673
|
+
checked_channel = 'abs' if enabled_channels['abs'] else 'real'
|
|
674
|
+
|
|
675
|
+
channel_buttons[checked_channel].setChecked(True)
|
|
638
676
|
|
|
639
677
|
def complexOrRealClicked(self, event, dim):
|
|
640
678
|
if self.can_combine_as_complex[dim] and not self.combined_as_complex[dim]:
|
|
@@ -683,12 +721,7 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
683
721
|
self.line_plot_dimension = new_dim
|
|
684
722
|
break
|
|
685
723
|
|
|
686
|
-
|
|
687
|
-
for button in self.widgets['buttons']['channel'].values():
|
|
688
|
-
button.setEnabled(True)
|
|
689
|
-
|
|
690
|
-
if not self.widgets['buttons']['channel']['abs'].isChecked():
|
|
691
|
-
self.widgets['buttons']['channel']['abs'].setChecked(True)
|
|
724
|
+
self._update_channel_controls()
|
|
692
725
|
|
|
693
726
|
# Update UI
|
|
694
727
|
self.update_complex_indicators()
|
|
@@ -717,12 +750,7 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
717
750
|
# Update max value for the spinbox for this dimension
|
|
718
751
|
self.widgets['spins']['slice_indices'][dim].setMaximum(self.data.shape[dim] - 1)
|
|
719
752
|
|
|
720
|
-
|
|
721
|
-
for button in self.widgets['buttons']['channel'].values():
|
|
722
|
-
button.setEnabled(False)
|
|
723
|
-
|
|
724
|
-
# Switch to 'real' channel
|
|
725
|
-
self.widgets['buttons']['channel']['real'].setChecked(True)
|
|
753
|
+
self._update_channel_controls()
|
|
726
754
|
|
|
727
755
|
# Update UI
|
|
728
756
|
self.update_complex_indicators()
|
|
@@ -1185,11 +1213,11 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
1185
1213
|
event.accept()
|
|
1186
1214
|
return
|
|
1187
1215
|
elif key == Qt.QtCore.Qt.Key.Key_5:
|
|
1188
|
-
self.setColormap('
|
|
1216
|
+
self.setColormap('cividis')
|
|
1189
1217
|
event.accept()
|
|
1190
1218
|
return
|
|
1191
1219
|
elif key == Qt.QtCore.Qt.Key.Key_6:
|
|
1192
|
-
self.setColormap('
|
|
1220
|
+
self.setColormap('CET-CBL1')
|
|
1193
1221
|
event.accept()
|
|
1194
1222
|
return
|
|
1195
1223
|
elif key == Qt.QtCore.Qt.Key.Key_7:
|
|
@@ -1200,8 +1228,7 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
1200
1228
|
self.setColormap('d3-warm')
|
|
1201
1229
|
event.accept()
|
|
1202
1230
|
return
|
|
1203
|
-
|
|
1204
|
-
|
|
1231
|
+
|
|
1205
1232
|
# Pass event to parent if not handled
|
|
1206
1233
|
super().keyPressEvent(event)
|
|
1207
1234
|
|
|
@@ -1462,23 +1489,159 @@ class NDSliceWindow(QtWidgets.QMainWindow):
|
|
|
1462
1489
|
progress_dialog = VideoExportDialog(self)
|
|
1463
1490
|
progress_dialog.start_export(worker, self.data.shape[export_dim])
|
|
1464
1491
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1492
|
+
def _on_file_changed(self, path):
|
|
1493
|
+
"""Called by QFileSystemWatcher when the source file changes on disk."""
|
|
1494
|
+
self._reload_btn.setText("⚠️")
|
|
1495
|
+
self._reload_btn.setToolTip("File changed — click to reload")
|
|
1496
|
+
# Re-add the path: handles atomic replacement where the original inode disappears
|
|
1497
|
+
if self._file_watcher:
|
|
1498
|
+
self._file_watcher.addPath(path)
|
|
1499
|
+
|
|
1500
|
+
def _reload_file(self):
|
|
1501
|
+
"""Reload data from the source file, preserving slice positions where possible."""
|
|
1502
|
+
if self._filepath is None:
|
|
1503
|
+
return
|
|
1504
|
+
try:
|
|
1505
|
+
new_data = None
|
|
1506
|
+
new_dataset_path = self._dataset_path
|
|
1507
|
+
|
|
1508
|
+
if self._selector_class_name is None:
|
|
1509
|
+
from .file_interpreters import load_file
|
|
1510
|
+
new_data = load_file(self._filepath)
|
|
1511
|
+
else:
|
|
1512
|
+
from .selectors import H5DatasetSelector, NpzDatasetSelector, MatDatasetSelector
|
|
1513
|
+
selector_map = {
|
|
1514
|
+
'H5DatasetSelector': H5DatasetSelector,
|
|
1515
|
+
'NpzDatasetSelector': NpzDatasetSelector,
|
|
1516
|
+
'MatDatasetSelector': MatDatasetSelector,
|
|
1517
|
+
}
|
|
1518
|
+
selector_cls = selector_map.get(self._selector_class_name)
|
|
1519
|
+
if selector_cls is None:
|
|
1520
|
+
return
|
|
1521
|
+
selector = selector_cls(self._filepath)
|
|
1522
|
+
compatible_keys = {d[0] for d in selector.compatible_datasets}
|
|
1523
|
+
if self._dataset_path is not None and self._dataset_path in compatible_keys:
|
|
1524
|
+
new_data = selector.load_data(self._dataset_path)
|
|
1525
|
+
selector.close()
|
|
1526
|
+
elif selector.requires_gui():
|
|
1527
|
+
selected = selector.show()
|
|
1528
|
+
if selected is None:
|
|
1529
|
+
selector.close()
|
|
1530
|
+
return # User cancelled — keep ⚠️ visible
|
|
1531
|
+
new_data = selector.load_data(selected)
|
|
1532
|
+
new_dataset_path = selected
|
|
1533
|
+
selector.close()
|
|
1534
|
+
else:
|
|
1535
|
+
result = selector.get_single_data()
|
|
1536
|
+
selector.close()
|
|
1537
|
+
if result is None:
|
|
1538
|
+
return
|
|
1539
|
+
new_dataset_path, new_data = result
|
|
1540
|
+
|
|
1541
|
+
if new_data is None:
|
|
1542
|
+
return
|
|
1543
|
+
|
|
1544
|
+
self._dataset_path = new_dataset_path
|
|
1545
|
+
self._reset_data(new_data)
|
|
1546
|
+
self._reload_btn.setText("⟳")
|
|
1547
|
+
self._reload_btn.setToolTip("Reload file")
|
|
1548
|
+
if self._file_watcher and self._filepath:
|
|
1549
|
+
self._file_watcher.addPath(str(self._filepath))
|
|
1550
|
+
except Exception as e:
|
|
1551
|
+
QtWidgets.QMessageBox.warning(self, "Reload Error", f"Failed to reload:\n{e}")
|
|
1552
|
+
|
|
1553
|
+
def _reset_data(self, new_data):
|
|
1554
|
+
"""Replace the displayed data, clamping slice positions to the new shape."""
|
|
1555
|
+
old_ndim = self.data.ndim
|
|
1556
|
+
new_ndim = new_data.ndim
|
|
1557
|
+
|
|
1558
|
+
if new_ndim != old_ndim:
|
|
1559
|
+
# Per-dimension widgets were built for old_ndim and cannot be rebuilt in-place.
|
|
1560
|
+
# Open a fresh window with the new data and close this one.
|
|
1561
|
+
win = NDSliceWindow(new_data,
|
|
1562
|
+
filepath=self._filepath,
|
|
1563
|
+
dataset_path=self._dataset_path,
|
|
1564
|
+
selector_class_name=self._selector_class_name)
|
|
1565
|
+
win.setWindowTitle(self.windowTitle())
|
|
1566
|
+
win.show()
|
|
1567
|
+
self.close()
|
|
1568
|
+
return
|
|
1569
|
+
|
|
1570
|
+
self.data = new_data
|
|
1571
|
+
self.singleton = [e == 1 for e in new_data.shape]
|
|
1572
|
+
|
|
1573
|
+
if np.iscomplexobj(new_data):
|
|
1574
|
+
self.can_combine_as_complex = [False] * new_ndim
|
|
1575
|
+
else:
|
|
1576
|
+
self.can_combine_as_complex = [new_data.shape[i] == 2 for i in range(new_ndim)]
|
|
1577
|
+
self.combined_as_complex = [False] * new_ndim
|
|
1578
|
+
|
|
1579
|
+
# Reset FFT domain state and dim label styling
|
|
1580
|
+
self.domain = [Domain.NATIVE for _ in range(new_ndim)]
|
|
1581
|
+
for i, label in enumerate(self.widgets['labels']['dims']):
|
|
1582
|
+
label.setStyleSheet(self.DIMENSION_LABEL_STYLE)
|
|
1583
|
+
label.setText(f'[{new_data.shape[i]}]')
|
|
1584
|
+
|
|
1585
|
+
# Update spinbox maximums (auto-clamps current value)
|
|
1586
|
+
for i in range(new_ndim):
|
|
1587
|
+
self.widgets['spins']['slice_indices'][i].setMaximum(new_data.shape[i] - 1)
|
|
1588
|
+
|
|
1589
|
+
# Clamp selected_indices: keep existing choices where still valid, fill from valid dims
|
|
1590
|
+
valid_dims = [i for i in range(new_ndim) if not self.singleton[i]]
|
|
1591
|
+
clamped = [i for i in self.selected_indices if i < new_ndim and not self.singleton[i]]
|
|
1592
|
+
for d in valid_dims:
|
|
1593
|
+
if len(clamped) >= 2:
|
|
1594
|
+
break
|
|
1595
|
+
if d not in clamped:
|
|
1596
|
+
clamped.append(d)
|
|
1597
|
+
if not clamped and valid_dims:
|
|
1598
|
+
clamped = [valid_dims[0]]
|
|
1599
|
+
self.selected_indices = clamped[:2]
|
|
1600
|
+
|
|
1601
|
+
# Clamp line_plot_dimension
|
|
1602
|
+
if self.line_plot_dimension >= new_ndim or self.singleton[self.line_plot_dimension]:
|
|
1603
|
+
for d in range(new_ndim):
|
|
1604
|
+
if not self.singleton[d]:
|
|
1605
|
+
self.line_plot_dimension = d
|
|
1606
|
+
break
|
|
1607
|
+
|
|
1608
|
+
self.axis_flipped = [False] * new_ndim
|
|
1609
|
+
|
|
1610
|
+
self._update_channel_controls()
|
|
1611
|
+
self.update_complex_indicators()
|
|
1612
|
+
if len(self.selected_indices) >= 1:
|
|
1613
|
+
self.changedIndex(True, 0, self.selected_indices[0], update=False)
|
|
1614
|
+
if len(self.selected_indices) >= 2:
|
|
1615
|
+
self.changedIndex(True, 1, self.selected_indices[1], update=False)
|
|
1616
|
+
self.update_dimension_controls()
|
|
1617
|
+
self._force_autolevel = True
|
|
1618
|
+
self.update()
|
|
1619
|
+
|
|
1620
|
+
|
|
1621
|
+
def _run_window(data, title, complex_dim=None, filepath=None, dataset_path=None, selector_class_name=None):
|
|
1467
1622
|
"""Opens a window in a separate process which is blocked on exec()"""
|
|
1468
1623
|
app = pg.mkQApp() # sets QT_ENABLE_HIGHDPI_SCALING + PassThrough rounding before creating QApp
|
|
1469
|
-
|
|
1624
|
+
app.setStyle('Fusion')
|
|
1625
|
+
win = NDSliceWindow(data, complex_dim=complex_dim, filepath=filepath,
|
|
1626
|
+
dataset_path=dataset_path, selector_class_name=selector_class_name)
|
|
1470
1627
|
win.setWindowTitle(title)
|
|
1471
1628
|
win.show()
|
|
1472
1629
|
app.exec()
|
|
1473
1630
|
|
|
1474
|
-
def ndslice(data, title='', block=False, complex_dim=None):
|
|
1631
|
+
def ndslice(data, title='', block=False, complex_dim=None, filepath=None, dataset_path=None, selector_class_name=None):
|
|
1475
1632
|
if not isinstance(data, np.ndarray):
|
|
1476
1633
|
raise TypeError("data must be a numpy array")
|
|
1477
1634
|
if data.ndim < 1:
|
|
1478
1635
|
raise ValueError("data must have at least 1 dimension")
|
|
1479
1636
|
|
|
1480
1637
|
if block:
|
|
1481
|
-
_run_window(data, title, complex_dim
|
|
1638
|
+
_run_window(data, title, complex_dim, filepath=filepath,
|
|
1639
|
+
dataset_path=dataset_path, selector_class_name=selector_class_name)
|
|
1482
1640
|
else:
|
|
1483
|
-
|
|
1641
|
+
import multiprocessing.process as _mp_process
|
|
1642
|
+
p = mp.Process(target=_run_window, args=(data, title, complex_dim),
|
|
1643
|
+
kwargs={'filepath': filepath, 'dataset_path': dataset_path,
|
|
1644
|
+
'selector_class_name': selector_class_name})
|
|
1645
|
+
p.start()
|
|
1646
|
+
_mp_process._children.discard(p)
|
|
1484
1647
|
|
|
@@ -34,10 +34,14 @@ def _show_selector(filepath, selector_class_name, interpret_as_complex):
|
|
|
34
34
|
# Show dialog
|
|
35
35
|
if selected_path := selector.show():
|
|
36
36
|
data = selector.load_data(selected_path)
|
|
37
|
+
selector.close()
|
|
37
38
|
ndslice(data=data,
|
|
38
39
|
title=f"{filepath.name} - {selected_path}",
|
|
39
40
|
block=True,
|
|
40
|
-
complex_dim=interpret_as_complex
|
|
41
|
+
complex_dim=interpret_as_complex,
|
|
42
|
+
filepath=filepath,
|
|
43
|
+
dataset_path=selected_path,
|
|
44
|
+
selector_class_name=selector_class_name)
|
|
41
45
|
|
|
42
46
|
|
|
43
47
|
class DatasetSelector:
|
|
@@ -68,7 +72,11 @@ class DatasetSelector:
|
|
|
68
72
|
def load_data(self, path):
|
|
69
73
|
"""Load data for a given dataset path/name. Returns numpy array."""
|
|
70
74
|
raise NotImplementedError("Subclasses must implement load_data()")
|
|
71
|
-
|
|
75
|
+
|
|
76
|
+
def close(self):
|
|
77
|
+
"""Close any open file handles. Subclasses override if needed."""
|
|
78
|
+
pass
|
|
79
|
+
|
|
72
80
|
def _build_tree(self):
|
|
73
81
|
"""Build the tree widget. Must be implemented by subclasses."""
|
|
74
82
|
raise NotImplementedError("Subclasses must implement _build_tree()")
|
|
@@ -94,10 +102,14 @@ class DatasetSelector:
|
|
|
94
102
|
result = self.get_single_data()
|
|
95
103
|
if result:
|
|
96
104
|
name, data = result
|
|
105
|
+
self.close()
|
|
97
106
|
ndslice(data=data,
|
|
98
107
|
title=f"{self.filepath.name} - {name}",
|
|
99
108
|
block=block,
|
|
100
|
-
complex_dim=self.interpret_as_complex
|
|
109
|
+
complex_dim=self.interpret_as_complex,
|
|
110
|
+
filepath=self.filepath,
|
|
111
|
+
dataset_path=name,
|
|
112
|
+
selector_class_name=self.__class__.__name__)
|
|
101
113
|
return True
|
|
102
114
|
else:
|
|
103
115
|
return False
|
|
@@ -110,16 +122,24 @@ class DatasetSelector:
|
|
|
110
122
|
|
|
111
123
|
if selected_path := self.show():
|
|
112
124
|
data = self.load_data(selected_path)
|
|
125
|
+
self.close()
|
|
113
126
|
ndslice(data=data,
|
|
114
127
|
title=f"{self.filepath.name} - {selected_path}",
|
|
115
128
|
block=True,
|
|
116
|
-
complex_dim=self.interpret_as_complex
|
|
129
|
+
complex_dim=self.interpret_as_complex,
|
|
130
|
+
filepath=self.filepath,
|
|
131
|
+
dataset_path=selected_path,
|
|
132
|
+
selector_class_name=self.__class__.__name__)
|
|
117
133
|
return True
|
|
118
134
|
return False
|
|
119
135
|
else:
|
|
120
136
|
import multiprocessing as mp
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
import multiprocessing.process as _mp_process
|
|
138
|
+
p = mp.Process(target=_show_selector,
|
|
139
|
+
args=(self.filepath, self.__class__.__name__, self.interpret_as_complex))
|
|
140
|
+
p.start()
|
|
141
|
+
_mp_process._children.discard(p)
|
|
142
|
+
self.close()
|
|
123
143
|
return True
|
|
124
144
|
|
|
125
145
|
def _create_dialog(self):
|
|
@@ -249,6 +269,10 @@ class H5DatasetSelector(DatasetSelector):
|
|
|
249
269
|
if hasattr(self, 'h5_file') and self.h5_file:
|
|
250
270
|
self.h5_file.close()
|
|
251
271
|
|
|
272
|
+
def close(self):
|
|
273
|
+
if hasattr(self, 'h5_file') and self.h5_file:
|
|
274
|
+
self.h5_file.close()
|
|
275
|
+
|
|
252
276
|
def _detect_complex_interpretation(self):
|
|
253
277
|
"""
|
|
254
278
|
If there's a single compatible dataset with 'complex' in name and a size-2 dimension,
|
|
@@ -406,7 +430,11 @@ class NpzDatasetSelector(DatasetSelector):
|
|
|
406
430
|
"""Close the NPZ file when selector is destroyed."""
|
|
407
431
|
if hasattr(self, 'npz_file'):
|
|
408
432
|
self.npz_file.close()
|
|
409
|
-
|
|
433
|
+
|
|
434
|
+
def close(self):
|
|
435
|
+
if hasattr(self, 'npz_file'):
|
|
436
|
+
self.npz_file.close()
|
|
437
|
+
|
|
410
438
|
def _find_compatible_datasets(self):
|
|
411
439
|
"""Find all compatible datasets in the NPZ file."""
|
|
412
440
|
compatible = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndslice
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Interactive N-dimensional numpy array viewer with FFT support
|
|
5
5
|
Author-email: Henric Rydén <henric.ryden@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
23
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
22
24
|
Requires-Python: >=3.8
|
|
23
25
|
Description-Content-Type: text/markdown
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ndslice"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Interactive N-dimensional numpy array viewer with FFT support"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -22,6 +22,8 @@ classifiers = [
|
|
|
22
22
|
"Programming Language :: Python :: 3.10",
|
|
23
23
|
"Programming Language :: Python :: 3.11",
|
|
24
24
|
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
25
27
|
"Topic :: Scientific/Engineering :: Visualization",
|
|
26
28
|
]
|
|
27
29
|
keywords = ["visualization", "numpy", "fft", "image-viewer", "data-visualization"]
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|