ndslice 0.1.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.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 [Your Name]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ndslice-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: ndslice
3
+ Version: 0.1.0
4
+ Summary: Interactive N-dimensional numpy array viewer with FFT support
5
+ Author-email: Henric Rydén <henric.ryden@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/henricryden/ndslice
8
+ Project-URL: Documentation, https://github.com/henricryden/ndslice#readme
9
+ Project-URL: Repository, https://github.com/henricryden/ndslice
10
+ Project-URL: Bug Tracker, https://github.com/henricryden/ndslice/issues
11
+ Keywords: visualization,numpy,fft,image-viewer,data-visualization
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Scientific/Engineering :: Visualization
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: numpy>=1.20.0
27
+ Requires-Dist: pyqtgraph>=0.12.0
28
+ Requires-Dist: PyQt5>=5.15.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: build>=0.10.0; extra == "dev"
31
+ Requires-Dist: twine>=4.0.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # ndslice
35
+
36
+ Interactive N-dimensional array viewer with FFT support for NumPy arrays.
37
+
38
+ ## Features
39
+
40
+ - **N-dimensional slicing**: View any 2D slice of your N-dimensional data
41
+ - **Multiple view modes**: Image view and line plot modes
42
+ - **FFT/IFFT support**: Apply Fourier transforms along any dimension with a click
43
+ - **Complex data support**: View real, imaginary, magnitude, or phase components
44
+ - **Scale transformations**: Linear and symmetric logarithmic scaling
45
+ - **Interactive controls**: Mouse hover for pixel values, dynamic zooming, and panning
46
+
47
+ ## Installation
48
+
49
+ ### From PyPI
50
+
51
+ ```bash
52
+ pip install ndslice
53
+ ```
54
+
55
+ ### From source
56
+
57
+ ```bash
58
+ git clone https://github.com/henricryden/ndslice.git
59
+ cd ndslice
60
+ pip install -e .
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ ### Basic Usage
66
+
67
+ The `ndslice()` function opens an interactive window to explore your N-dimensional arrays:
68
+
69
+ ```python
70
+ from ndslice import ndslice
71
+ import numpy as np
72
+
73
+ # View a 4D array
74
+ data_4d = np.random.randn(10, 20, 30, 40)
75
+ ndslice(data_4d)
76
+
77
+ # View complex FFT data
78
+ fft_data = np.fft.fftn(data_4d)
79
+ ndslice(fft_data, title='FFT Data')
80
+ ```
81
+
82
+ ### Interactive Features
83
+
84
+ - **Dimension Selection**: Click Y/X buttons to choose which dimensions to display
85
+ - **Slicing**: Use spinboxes to select the slice index for other dimensions
86
+ - **FFT Transforms**:
87
+ - Left-click dimension labels to apply FFT
88
+ - Right-click to apply inverse FFT
89
+ - Click again to return to native domain
90
+ - **Channel Selection** (for complex data): Real, Imaginary, Magnitude, or Phase
91
+ - **Scale Options**: Linear or Symmetric Log scaling
92
+ - **Display Modes**: Square pixels, square FOV, or fit to window
93
+ - **View Modes**: Switch between 2D image view and 1D line plot
94
+ - **Complex Data**: View real, imaginary, magnitude, or phase components
95
+
96
+ ## Requirements
97
+
98
+ - Python >= 3.8
99
+ - NumPy >= 1.20.0
100
+ - PyQtGraph >= 0.12.0
101
+ - PyQt5 >= 5.15.0
102
+
103
+ ## License
104
+
105
+ MIT License - see LICENSE file for details.
106
+
107
+ ## Contributing
108
+
109
+ Contributions are welcome! Please feel free to submit a Pull Request.
110
+
111
+ ## Acknowledgments
112
+
113
+ Built with [PyQtGraph](https://www.pyqtgraph.org/) for high-performance visualization.
@@ -0,0 +1,80 @@
1
+ # ndslice
2
+
3
+ Interactive N-dimensional array viewer with FFT support for NumPy arrays.
4
+
5
+ ## Features
6
+
7
+ - **N-dimensional slicing**: View any 2D slice of your N-dimensional data
8
+ - **Multiple view modes**: Image view and line plot modes
9
+ - **FFT/IFFT support**: Apply Fourier transforms along any dimension with a click
10
+ - **Complex data support**: View real, imaginary, magnitude, or phase components
11
+ - **Scale transformations**: Linear and symmetric logarithmic scaling
12
+ - **Interactive controls**: Mouse hover for pixel values, dynamic zooming, and panning
13
+
14
+ ## Installation
15
+
16
+ ### From PyPI
17
+
18
+ ```bash
19
+ pip install ndslice
20
+ ```
21
+
22
+ ### From source
23
+
24
+ ```bash
25
+ git clone https://github.com/henricryden/ndslice.git
26
+ cd ndslice
27
+ pip install -e .
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Basic Usage
33
+
34
+ The `ndslice()` function opens an interactive window to explore your N-dimensional arrays:
35
+
36
+ ```python
37
+ from ndslice import ndslice
38
+ import numpy as np
39
+
40
+ # View a 4D array
41
+ data_4d = np.random.randn(10, 20, 30, 40)
42
+ ndslice(data_4d)
43
+
44
+ # View complex FFT data
45
+ fft_data = np.fft.fftn(data_4d)
46
+ ndslice(fft_data, title='FFT Data')
47
+ ```
48
+
49
+ ### Interactive Features
50
+
51
+ - **Dimension Selection**: Click Y/X buttons to choose which dimensions to display
52
+ - **Slicing**: Use spinboxes to select the slice index for other dimensions
53
+ - **FFT Transforms**:
54
+ - Left-click dimension labels to apply FFT
55
+ - Right-click to apply inverse FFT
56
+ - Click again to return to native domain
57
+ - **Channel Selection** (for complex data): Real, Imaginary, Magnitude, or Phase
58
+ - **Scale Options**: Linear or Symmetric Log scaling
59
+ - **Display Modes**: Square pixels, square FOV, or fit to window
60
+ - **View Modes**: Switch between 2D image view and 1D line plot
61
+ - **Complex Data**: View real, imaginary, magnitude, or phase components
62
+
63
+ ## Requirements
64
+
65
+ - Python >= 3.8
66
+ - NumPy >= 1.20.0
67
+ - PyQtGraph >= 0.12.0
68
+ - PyQt5 >= 5.15.0
69
+
70
+ ## License
71
+
72
+ MIT License - see LICENSE file for details.
73
+
74
+ ## Contributing
75
+
76
+ Contributions are welcome! Please feel free to submit a Pull Request.
77
+
78
+ ## Acknowledgments
79
+
80
+ Built with [PyQtGraph](https://www.pyqtgraph.org/) for high-performance visualization.
@@ -0,0 +1,9 @@
1
+ """
2
+ ndslice - Interactive N-dimensional array viewer with FFT support
3
+ """
4
+
5
+ from .ndslice import ndslice, NDSliceWindow, Domain
6
+ from .imageview2d import ImageView2D
7
+
8
+ __version__ = "0.1.0"
9
+ __all__ = ["ndslice", "NDSliceWindow", "ImageView2D", "Domain"]
@@ -0,0 +1,266 @@
1
+ import numpy as np
2
+ from pyqtgraph.Qt import QtGui, QtWidgets
3
+ import pyqtgraph as pg
4
+ from pyqtgraph.graphicsItems.ImageItem import ImageItem
5
+ from pyqtgraph.graphicsItems.ViewBox import ViewBox
6
+
7
+
8
+ class ImageView2D(QtWidgets.QWidget):
9
+ """
10
+ Simplified widget for displaying 2D image data.
11
+
12
+ Features:
13
+ - 2D image display via ImageItem
14
+ - Zoom/pan via ViewBox
15
+ - Histogram with level controls
16
+ - Auto-ranging and level adjustment
17
+ """
18
+
19
+ def __init__(self, parent=None, view=None, imageItem=None):
20
+ """
21
+ Parameters
22
+ ----------
23
+ parent : QWidget
24
+ Parent widget
25
+ view : ViewBox
26
+ If specified, this ViewBox will be used for display
27
+ imageItem : ImageItem
28
+ If specified, this ImageItem will be used for display
29
+ """
30
+ super().__init__(parent)
31
+
32
+ self.image = None
33
+ self.imageDisp = None
34
+ self.levelMin = None
35
+ self.levelMax = None
36
+ self.displayMode = 'square_pixels' # Default to square pixels
37
+
38
+ # Create the UI layout
39
+ self.setupUI()
40
+
41
+ # Create view if not provided
42
+ if view is None:
43
+ self.view = ViewBox()
44
+ else:
45
+ self.view = view
46
+ self.graphicsView.setCentralItem(self.view)
47
+ self.view.setAspectLocked(True)
48
+ self.view.invertY()
49
+
50
+ # Create image item if not provided
51
+ if imageItem is None:
52
+ self.imageItem = ImageItem()
53
+ else:
54
+ self.imageItem = imageItem
55
+ self.view.addItem(self.imageItem)
56
+
57
+ # Setup histogram
58
+ self.histogram.setImageItem(self.imageItem)
59
+ self.histogram.setLevelMode('mono') # Force mono mode for scalar values
60
+
61
+ # Initialize levels
62
+ self.levelMin = 0.0
63
+ self.levelMax = 1.0
64
+
65
+ def setupUI(self):
66
+ """Create the user interface"""
67
+ # Main layout
68
+ self.layout = QtWidgets.QHBoxLayout(self)
69
+ self.layout.setContentsMargins(0, 0, 0, 0)
70
+ self.layout.setSpacing(0)
71
+
72
+ # Graphics view for image display
73
+ self.graphicsView = pg.GraphicsView()
74
+ self.layout.addWidget(self.graphicsView, 1) # Give it most of the space
75
+
76
+ # Histogram widget
77
+ self.histogram = pg.HistogramLUTWidget()
78
+ self.layout.addWidget(self.histogram)
79
+
80
+ def setImage(self, img, autoRange=True, autoLevels=True, levels=None,
81
+ pos=None, scale=None, transform=None, autoHistogramRange=True):
82
+ """
83
+ Set the image to be displayed.
84
+
85
+ Parameters
86
+ ----------
87
+ img : np.ndarray
88
+ 2D image data to display
89
+ autoRange : bool
90
+ Whether to auto-scale the view to fit the image
91
+ autoLevels : bool
92
+ Whether to auto-adjust the histogram levels
93
+ levels : tuple
94
+ (min, max) levels for the histogram
95
+ pos : tuple
96
+ Position offset for the image
97
+ scale : tuple
98
+ Scale factors for the image
99
+ transform : QTransform
100
+ Transform to apply to the image
101
+ autoHistogramRange : bool
102
+ Whether to auto-scale the histogram range
103
+ """
104
+ if not isinstance(img, np.ndarray):
105
+ raise TypeError("Image must be a numpy array")
106
+
107
+ if img.ndim != 2:
108
+ raise ValueError("ImageView2D only supports 2D images")
109
+
110
+ self.image = img
111
+ self.imageDisp = None
112
+
113
+ # Update the image display
114
+ self.updateImage(autoHistogramRange=autoHistogramRange)
115
+
116
+ # Set levels
117
+ if levels is None and autoLevels:
118
+ self.autoLevels()
119
+ elif levels is not None:
120
+ if isinstance(levels, (list, tuple)) and len(levels) == 2:
121
+ self.setLevels(levels[0], levels[1])
122
+ else:
123
+ self.setLevels(*levels)
124
+
125
+ # Set transform
126
+ if transform is None:
127
+ if pos is not None or scale is not None:
128
+ if pos is None:
129
+ pos = (0, 0)
130
+ if scale is None:
131
+ scale = (1, 1)
132
+ transform = QtGui.QTransform()
133
+ transform.translate(pos[0], pos[1])
134
+ transform.scale(scale[0], scale[1])
135
+
136
+ if transform is not None:
137
+ self.imageItem.setTransform(transform)
138
+
139
+ # Update aspect ratio based on display mode
140
+ self._updateAspectRatio()
141
+
142
+ # Auto range the view
143
+ if autoRange:
144
+ self.autoRange()
145
+
146
+ def updateImage(self, autoHistogramRange=True):
147
+ """Update the displayed image"""
148
+ if self.image is None:
149
+ return
150
+
151
+ # For 2D images, we can display directly
152
+ self.imageDisp = self.image
153
+
154
+ # Calculate min/max levels from the image data for histogram
155
+ self._updateImageLevels()
156
+
157
+ # Set the image data
158
+ self.imageItem.setImage(self.imageDisp, autoLevels=False)
159
+
160
+ # Update histogram range if requested
161
+ if autoHistogramRange:
162
+ self.histogram.setHistogramRange(self.levelMin, self.levelMax)
163
+
164
+ def autoRange(self):
165
+ """Auto scale and pan the view to fit the image"""
166
+ if self.imageDisp is not None:
167
+ self.view.autoRange()
168
+
169
+ def _updateImageLevels(self):
170
+ """Update the min/max levels from the current image data"""
171
+ if self.imageDisp is not None:
172
+ # Use the same approach as the original ImageView
173
+ finite_data = self.imageDisp[np.isfinite(self.imageDisp)]
174
+ if len(finite_data) > 0:
175
+ self.levelMin = float(np.min(finite_data))
176
+ self.levelMax = float(np.max(finite_data))
177
+ else:
178
+ self.levelMin = 0.0
179
+ self.levelMax = 1.0
180
+
181
+ def autoLevels(self):
182
+ """Automatically set the histogram levels based on image data"""
183
+ if self.imageDisp is not None:
184
+ self._updateImageLevels()
185
+ self.setLevels(self.levelMin, self.levelMax)
186
+
187
+ def setLevels(self, min_level, max_level):
188
+ """Set the histogram levels"""
189
+ self.histogram.setLevels(min_level, max_level)
190
+
191
+ def getLevels(self):
192
+ """Get the current histogram levels"""
193
+ return self.histogram.getLevels()
194
+
195
+ def setHistogramRange(self, min_val, max_val):
196
+ """Set the range of the histogram"""
197
+ self.histogram.setHistogramRange(min_val, max_val)
198
+
199
+ def getProcessedImage(self):
200
+ """Get the processed image data"""
201
+ return self.imageDisp
202
+
203
+ def getView(self):
204
+ """Get the ViewBox containing the image"""
205
+ return self.view
206
+
207
+ def getImageItem(self):
208
+ """Get the ImageItem"""
209
+ return self.imageItem
210
+
211
+ def getHistogramWidget(self):
212
+ """Get the histogram widget"""
213
+ return self.histogram
214
+
215
+ def clear(self):
216
+ """Clear the displayed image"""
217
+ self.image = None
218
+ self.imageDisp = None
219
+ self.imageItem.clear()
220
+
221
+ def setColorMap(self, colormap):
222
+ """Set the color map for the histogram"""
223
+ self.histogram.gradient.setColorMap(colormap)
224
+
225
+ def setDisplayMode(self, mode):
226
+ """Set the display mode.
227
+
228
+ Modes:
229
+ - 'square_pixels': force square pixel display (aspect ratio 1.0)
230
+ - 'square_fov' : lock aspect ratio to image width/height (field of view square)
231
+ - 'fit' : allow non-uniform scaling so the entire image fits viewport
232
+ """
233
+ if mode not in ('square_pixels', 'square_fov', 'fit'):
234
+ raise ValueError(f"Unknown display mode: {mode}")
235
+ self.displayMode = mode
236
+ self._updateAspectRatio()
237
+
238
+ def _updateAspectRatio(self):
239
+ """Update the aspect ratio based on display mode"""
240
+ if self.image is None:
241
+ return
242
+
243
+ if self.displayMode == 'square_pixels':
244
+ # Square pixels: maintain 1:1 aspect ratio
245
+ self.view.setAspectLocked(True, ratio=1.0)
246
+ elif self.displayMode == 'square_fov':
247
+ # Square FOV: adjust aspect ratio based on image dimensions
248
+ height, width = self.image.shape
249
+ aspect_ratio = width / height
250
+ self.view.setAspectLocked(True, ratio=aspect_ratio)
251
+ elif self.displayMode == 'fit':
252
+ # Fit: allow free aspect so the whole image fits inside the view box
253
+ self.view.setAspectLocked(False)
254
+ # Ensure view box ranges cover the image exactly
255
+ self.view.autoRange()
256
+
257
+ # Trigger a refresh of the view
258
+ if hasattr(self, 'imageItem') and self.imageItem is not None:
259
+ self.view.autoRange()
260
+
261
+ # --- Qt Events -----------------------------------------------------
262
+ def resizeEvent(self, event):
263
+ """On resize, if in 'fit' mode keep the image fully visible."""
264
+ super().resizeEvent(event)
265
+ if self.displayMode == 'fit' and self.image is not None:
266
+ self.view.autoRange()