mss-imageproc 0.0.1__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.
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: mss-imageproc
3
+ Version: 0.0.1
4
+ Summary: Image processing utilities for MSS Designer Models
5
+ Author-email: "Sunip K. Mukherjee" <sunipkmukherjee@gmail.com>
6
+ License: MIT
7
+ Keywords: image-processing,remote-sensing,mosaic
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: astropy>=7.1
14
+ Requires-Dist: dacite>=1.8.1
15
+ Requires-Dist: natsort>=8.4.0
16
+ Requires-Dist: numpy>=1.26
17
+ Requires-Dist: scikit-image>=0.22
18
+ Requires-Dist: serde-dataclass>=0.0.4
19
+ Requires-Dist: tomlkit>=0.12.5
20
+ Requires-Dist: xarray>=2024.1.0
21
+ Requires-Dist: netcdf4>=1.6.3
22
+ Requires-Dist: astropy-xarray==0.1.0
23
+
24
+ # mss-imageproc
25
+ This repository contains the Python library `mss-imageproc`, which provides tools for processing images
26
+ from instruments modeled using the Multi-Slit Spectrograph (MSS) Designer. The library includes functions
27
+ applying image transformations to map detector images onto the focal plane of the instrument, as well as
28
+ remove diffraction slit curvature from the images. This library is designed to be used stand-alone, using
29
+ the instrument configuration files generated by the MSS Designer. This library is also used internally by
30
+ the MSS Designer.
31
+
32
+ # Installation
33
+ You can install the library using pip:
34
+ ```bash
35
+ pip install mss-imageproc
36
+ ```
37
+
38
+ # Usage
39
+ The library is used in the following way:
40
+ ```python
41
+ # Import the library
42
+ from mss_imageproc import MosaicImageStraightener
43
+
44
+ # Create a straightener object using the instrument configuration file
45
+ straightener = MosaicImageStraightener.from_instrument_config('path/to/instrument_curve_maps.bin')
46
+ # Load an image and map it onto the mosaic grid
47
+ image_array = ... # Load your image as a 2D NumPy array
48
+ mapped_image = straightener.load_image(image_array)
49
+ # Straighten the image by removing slit curvature
50
+ straightened_images = straightener.straighten_image(mapped_image)
51
+ # The result is a dictionary of straightened images, one for each window. You can access them like this:
52
+ for window_name, straightened_image in straightened_images.items():
53
+ print(f"Straightened image for window {window_name}:")
54
+ straightened_image.show() # Show the straightened image
55
+ ```
@@ -0,0 +1,32 @@
1
+ # mss-imageproc
2
+ This repository contains the Python library `mss-imageproc`, which provides tools for processing images
3
+ from instruments modeled using the Multi-Slit Spectrograph (MSS) Designer. The library includes functions
4
+ applying image transformations to map detector images onto the focal plane of the instrument, as well as
5
+ remove diffraction slit curvature from the images. This library is designed to be used stand-alone, using
6
+ the instrument configuration files generated by the MSS Designer. This library is also used internally by
7
+ the MSS Designer.
8
+
9
+ # Installation
10
+ You can install the library using pip:
11
+ ```bash
12
+ pip install mss-imageproc
13
+ ```
14
+
15
+ # Usage
16
+ The library is used in the following way:
17
+ ```python
18
+ # Import the library
19
+ from mss_imageproc import MosaicImageStraightener
20
+
21
+ # Create a straightener object using the instrument configuration file
22
+ straightener = MosaicImageStraightener.from_instrument_config('path/to/instrument_curve_maps.bin')
23
+ # Load an image and map it onto the mosaic grid
24
+ image_array = ... # Load your image as a 2D NumPy array
25
+ mapped_image = straightener.load_image(image_array)
26
+ # Straighten the image by removing slit curvature
27
+ straightened_images = straightener.straighten_image(mapped_image)
28
+ # The result is a dictionary of straightened images, one for each window. You can access them like this:
29
+ for window_name, straightened_image in straightened_images.items():
30
+ print(f"Straightened image for window {window_name}:")
31
+ straightened_image.show() # Show the straightened image
32
+ ```
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mss-imageproc"
7
+ version = "0.0.1"
8
+ description = "Image processing utilities for MSS Designer Models"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ authors = [{ name = "Sunip K. Mukherjee", email = "sunipkmukherjee@gmail.com" }]
12
+ keywords = ["image-processing", "remote-sensing", "mosaic"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ dependencies = [
19
+ "astropy>=7.1",
20
+ "dacite>=1.8.1",
21
+ "natsort>=8.4.0",
22
+ "numpy>=1.26",
23
+ "scikit-image>=0.22",
24
+ "serde-dataclass>=0.0.4",
25
+ "tomlkit>=0.12.5",
26
+ "xarray>=2024.1.0",
27
+ "netcdf4>=1.6.3",
28
+ "astropy-xarray==0.1.0",
29
+ ]
30
+ license = { text = "MIT" }
31
+ [tool.setuptools]
32
+ package-dir = { "" = "src" }
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version(__package__ or 'mss_imageproc')
4
+
5
+ from .straighten_image import (
6
+ MosaicImageMapper,
7
+ MosaicImageStraightener,
8
+ ScaleType,
9
+ TranslationType,
10
+ PixelSizeType,
11
+ TransformationMatrix
12
+ )
13
+
14
+ __all__ = [
15
+ "MosaicImageMapper",
16
+ "MosaicImageStraightener",
17
+ "ScaleType",
18
+ "TranslationType",
19
+ "PixelSizeType",
20
+ "TransformationMatrix",
21
+ "__version__",
22
+ ]
@@ -0,0 +1,522 @@
1
+ # %% Imports
2
+ from __future__ import annotations
3
+ from json import JSONEncoder
4
+ from pathlib import Path
5
+ import tarfile
6
+ from tempfile import TemporaryDirectory
7
+ from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple, Union, overload
8
+ from dacite import Config
9
+ from tomlkit import register_encoder
10
+ from tomlkit.items import Item as TomlItem, item as tomlitem
11
+ from xarray import Dataset, DataArray, concat, MergeError, load_dataset
12
+ from astropy.units import Quantity
13
+ import astropy.units as u
14
+ from skimage.transform import warp, AffineTransform
15
+ from natsort import natsorted
16
+ from dataclasses import dataclass, field
17
+ from numpy import arange, asarray, fromstring, interp, meshgrid, sqrt, stack, nan, ndarray, where
18
+ from numpy.typing import NDArray
19
+ from serde_dataclass import json_config, toml_config, JsonDataclass, TomlDataclass
20
+ import astropy_xarray as _
21
+ import sys
22
+
23
+ # %% Type Aliases
24
+ if sys.version_info >= (3, 11):
25
+ type MaybeQuantity = str | Quantity
26
+ else:
27
+ from typing import TypeAlias, Union
28
+ MaybeQuantity: TypeAlias = Union[str, Quantity]
29
+
30
+
31
+ def to_quantity(value: MaybeQuantity) -> Quantity:
32
+ if isinstance(value, str):
33
+ return Quantity(value)
34
+ elif isinstance(value, Quantity):
35
+ return value
36
+ else:
37
+ raise ValueError('Invalid quantity specification.')
38
+
39
+
40
+ def optional_quantity(value: Optional[MaybeQuantity]) -> Optional[Quantity]:
41
+ if value is None:
42
+ return None
43
+ return to_quantity(value)
44
+
45
+ # %% Serde Helpers
46
+
47
+
48
+ @register_encoder
49
+ def qty_ndarray_encoder(obj: Any, /, _parent=None, _sort_keys=False) -> TomlItem:
50
+ if isinstance(obj, Quantity):
51
+ return tomlitem(f'{obj}')
52
+ elif isinstance(obj, ndarray):
53
+ return tomlitem(obj.tolist())
54
+ else:
55
+ raise TypeError(
56
+ f'Object of type {type(obj)} is not JSON serializable.')
57
+
58
+
59
+ class QuantityEncoder(JSONEncoder):
60
+ def default(self, o: Any) -> Any:
61
+ if isinstance(o, Quantity):
62
+ return f'{o}'
63
+ elif isinstance(o, ndarray):
64
+ return o.tolist()
65
+ else:
66
+ return super().default(o)
67
+
68
+
69
+ class QuantityDecoder:
70
+ @staticmethod
71
+ def decode_qty(value: str) -> Quantity:
72
+ value = value.strip()
73
+ if value.startswith('['):
74
+ arr_str, unit = value.rsplit(']', 1)
75
+ arr_str = arr_str.strip('[]')
76
+ arr = fromstring(arr_str, sep=' ', dtype=float)
77
+ return Quantity(arr, unit.strip())
78
+ return Quantity(value)
79
+
80
+ @staticmethod
81
+ def decode_ndarray(value: List[Any]) -> NDArray:
82
+ return asarray(value, dtype=float)
83
+
84
+ @property
85
+ def config(self) -> Config:
86
+ return Config(
87
+ type_hooks={
88
+ Quantity: self.decode_qty,
89
+ ndarray: self.decode_ndarray
90
+ },
91
+ )
92
+
93
+
94
+ QUANTITY_DECODER = QuantityDecoder().config
95
+
96
+ # %% Definitions
97
+
98
+ ScaleType = Union[float, Tuple[float, float]]
99
+ TranslationType = Tuple[float, float]
100
+ PixelSizeType = Tuple[float, float]
101
+ PaddingMode = Literal['constant', 'edge', 'symmetric', 'reflect', 'wrap']
102
+
103
+
104
+ @dataclass
105
+ @json_config(ser=QuantityEncoder, de=QUANTITY_DECODER)
106
+ @toml_config(de=QUANTITY_DECODER)
107
+ class TransformationMatrix(JsonDataclass, TomlDataclass):
108
+ """Reusable affine transform state and composition helper."""
109
+
110
+ matrix: ndarray = field(
111
+ default_factory=lambda: asarray([
112
+ [1.0, 0.0, 0.0],
113
+ [0.0, 1.0, 0.0],
114
+ [0.0, 0.0, 1.0],
115
+ ], dtype=float),
116
+ metadata={
117
+ 'description': '3x3 affine transformation matrix in homogeneous coordinates.',
118
+ 'typecheck': lambda x, _: isinstance(x, (list, ndarray)) and asarray(x).shape == (3, 3),
119
+ }
120
+ )
121
+
122
+ def __post_init__(self) -> None:
123
+ self.matrix = asarray(self.matrix, dtype=float)
124
+ if self.matrix.shape != (3, 3):
125
+ raise ValueError('matrix must have shape (3, 3)')
126
+
127
+ @classmethod
128
+ def from_matrix(
129
+ cls,
130
+ matrix: NDArray,
131
+ ) -> TransformationMatrix:
132
+ return cls(matrix=asarray(matrix, dtype=float))
133
+
134
+ def append(self, affine: AffineTransform) -> None:
135
+ self.matrix = affine.params @ self.matrix
136
+
137
+ def reset(self) -> None:
138
+ self.matrix = asarray([
139
+ [1.0, 0.0, 0.0],
140
+ [0.0, 1.0, 0.0],
141
+ [0.0, 0.0, 1.0],
142
+ ], dtype=float)
143
+
144
+ def affine(self) -> AffineTransform:
145
+ return AffineTransform(matrix=self.matrix.copy())
146
+
147
+ def effective_scale(self) -> Tuple[float, float]:
148
+ a = float(self.matrix[0, 0])
149
+ b = float(self.matrix[0, 1])
150
+ d = float(self.matrix[1, 0])
151
+ e = float(self.matrix[1, 1])
152
+ sx = float(sqrt(a * a + d * d))
153
+ sy = float(sqrt(b * b + e * e))
154
+ return abs(sx), abs(sy)
155
+
156
+
157
+ @dataclass
158
+ @json_config(ser=QuantityEncoder, de=QUANTITY_DECODER)
159
+ @toml_config(de=QUANTITY_DECODER)
160
+ class MosaicImageMapper(JsonDataclass):
161
+ """Map an image onto mosaic coordinates using a provided affine matrix.
162
+
163
+ This helper is intentionally lightweight compared with ``MosaicImageTransform``:
164
+ it requires only source image axes, mosaic bounds, and a transformation matrix.
165
+ """
166
+
167
+ source_x: ndarray
168
+ source_y: ndarray
169
+ target_x: ndarray
170
+ target_y: ndarray
171
+ pixel_size: PixelSizeType
172
+ bounds_x: Tuple[float, float]
173
+ bounds_y: Tuple[float, float]
174
+ transform: TransformationMatrix = field(
175
+ default_factory=TransformationMatrix)
176
+ _source_x0: float = field(init=False)
177
+ _source_y0: float = field(init=False)
178
+ _inv_dx: float = field(init=False)
179
+ _inv_dy: float = field(init=False)
180
+ _use_linear_x: bool = field(init=False)
181
+ _use_linear_y: bool = field(init=False)
182
+
183
+ def __post_init__(self) -> None:
184
+ self.source_x = asarray(self.source_x, dtype=float)
185
+ self.source_y = asarray(self.source_y, dtype=float)
186
+ self.target_x = asarray(self.target_x, dtype=float)
187
+ self.target_y = asarray(self.target_y, dtype=float)
188
+ if self.source_x.ndim != 1 or self.source_y.ndim != 1:
189
+ raise ValueError('source_x and source_y must be 1D arrays')
190
+ if self.target_x.ndim != 1 or self.target_y.ndim != 1:
191
+ raise ValueError('target_x and target_y must be 1D arrays')
192
+ if self.target_x.size == 0 or self.target_y.size == 0:
193
+ raise ValueError('target_x and target_y must not be empty')
194
+ if self.pixel_size[0] <= 0 or self.pixel_size[1] <= 0:
195
+ raise ValueError('pixel_size must be positive')
196
+ if not isinstance(self.transform, TransformationMatrix):
197
+ self.transform = TransformationMatrix.from_matrix(
198
+ asarray(self.transform, dtype=float))
199
+
200
+ # Match MosaicImageTransform coordinate-index behavior for consistency.
201
+ self._source_x0 = float(self.source_x[0])
202
+ self._source_y0 = float(self.source_y[0])
203
+ self._inv_dx = 0.0
204
+ self._inv_dy = 0.0
205
+ self._use_linear_x = False
206
+ self._use_linear_y = False
207
+
208
+ if self.source_x.size >= 2:
209
+ dx = float(self.source_x[1] - self.source_x[0])
210
+ if dx != 0.0:
211
+ xdiff = asarray(
212
+ self.source_x[1:] - self.source_x[:-1], dtype=float)
213
+ xtol = max(1e-12, 1e-9 * abs(dx))
214
+ self._use_linear_x = bool((abs(xdiff - dx) <= xtol).all())
215
+ if self._use_linear_x:
216
+ self._inv_dx = 1.0 / dx
217
+
218
+ if self.source_y.size >= 2:
219
+ dy = float(self.source_y[1] - self.source_y[0])
220
+ if dy != 0.0:
221
+ ydiff = asarray(
222
+ self.source_y[1:] - self.source_y[:-1], dtype=float)
223
+ ytol = max(1e-12, 1e-9 * abs(dy))
224
+ self._use_linear_y = bool((abs(ydiff - dy) <= ytol).all())
225
+ if self._use_linear_y:
226
+ self._inv_dy = 1.0 / dy
227
+
228
+ def map_to_mosaic(
229
+ self,
230
+ image: NDArray,
231
+ order: int = 1,
232
+ cval: float = nan,
233
+ mode: str = 'constant',
234
+ ) -> Tuple[DataArray, PixelSizeType]:
235
+ """Render a 2D image onto the finalized full-resolution mosaic grid."""
236
+ image_data = asarray(image)
237
+ if image_data.ndim != 2:
238
+ raise ValueError('image must be a 2D array')
239
+ if self.source_x.size != image_data.shape[1]:
240
+ raise ValueError('source_x size must match image width')
241
+ if self.source_y.size != image_data.shape[0]:
242
+ raise ValueError('source_y size must match image height')
243
+
244
+ x_target = self.target_x
245
+ y_target = self.target_y
246
+ px, py = float(self.pixel_size[0]), float(self.pixel_size[1])
247
+
248
+ xx, yy = meshgrid(x_target, y_target)
249
+ inv = self.transform.affine().inverse.params
250
+ src_x = inv[0, 0] * xx + inv[0, 1] * yy + inv[0, 2]
251
+ src_y = inv[1, 0] * xx + inv[1, 1] * yy + inv[1, 2]
252
+
253
+ if self._use_linear_x:
254
+ col = self._coord_to_index_linear(
255
+ src_x, self._source_x0, self._inv_dx, self.source_x.size)
256
+ else:
257
+ col = self._coord_to_index(src_x, self.source_x)
258
+
259
+ if self._use_linear_y:
260
+ row = self._coord_to_index_linear(
261
+ src_y, self._source_y0, self._inv_dy, self.source_y.size)
262
+ else:
263
+ row = self._coord_to_index(src_y, self.source_y)
264
+ coords = stack((row, col), axis=0)
265
+
266
+ warped = warp(
267
+ image_data,
268
+ coords,
269
+ order=order,
270
+ cval=cval,
271
+ mode=mode,
272
+ preserve_range=True,
273
+ )
274
+ out = DataArray(
275
+ warped,
276
+ dims=('y', 'x'),
277
+ coords={
278
+ 'x': ('x', Quantity(x_target, u.mm), {'units': u.mm, 'description': 'Mosaic X coordinate'}),
279
+ 'y': ('y', Quantity(y_target, u.mm), {'units': u.mm, 'description': 'Mosaic Y coordinate'}),
280
+ },
281
+ ).astropy.quantify()
282
+ out.attrs['pixel_scale_x_mm_per_px'] = float(px)
283
+ out.attrs['pixel_scale_y_mm_per_px'] = float(py)
284
+ out.attrs['bounds_x_mm'] = self.bounds_x
285
+ out.attrs['bounds_y_mm'] = self.bounds_y
286
+ return out, (float(px), float(py))
287
+
288
+ @staticmethod
289
+ def _coord_to_index(coord: NDArray, axis_values: NDArray) -> NDArray:
290
+ """Map physical coordinates to floating pixel indices by linear interpolation.
291
+
292
+ The source axis may be ascending or descending.
293
+ Coordinates outside axis range map to ``-1`` and are handled by ``warp``
294
+ according to the selected boundary ``mode`` and ``cval``.
295
+ """
296
+ idx = arange(axis_values.size, dtype=float)
297
+ axis = asarray(axis_values, dtype=float)
298
+ if axis.size < 2:
299
+ return interp(coord, axis, idx, left=-1.0, right=-1.0)
300
+ if axis[0] > axis[-1]:
301
+ axis = axis[::-1]
302
+ idx = idx[::-1]
303
+ return interp(coord, axis, idx, left=-1.0, right=-1.0)
304
+
305
+ @staticmethod
306
+ def _coord_to_index_linear(
307
+ coord: NDArray,
308
+ axis0: float,
309
+ inv_step: float,
310
+ size: int,
311
+ ) -> NDArray:
312
+ """Map physical coordinates to floating indices for a uniform axis.
313
+
314
+ Coordinates outside the axis extent are mapped to ``-1`` so ``warp``
315
+ applies the configured boundary behavior.
316
+ """
317
+ # Fast O(1) index computation: idx = (coord - axis0) / step
318
+ idx = (coord - axis0) * inv_step
319
+ # Out-of-bounds indices are masked to -1, triggering warp's boundary mode.
320
+ return asarray(
321
+ where((idx < 0.0) | (idx > (size - 1)), -1.0, idx),
322
+ dtype=float,
323
+ )
324
+
325
+
326
+ class MosaicImageStraightener:
327
+ """Straighten images mapped onto the mosaic grid using curve maps associated with window names.
328
+ """
329
+
330
+ def __init__(
331
+ self,
332
+ imaps: Dict[str, List[Dataset]],
333
+ mapper: MosaicImageMapper,
334
+ ):
335
+ self._mapper = mapper
336
+ self._imaps = imaps
337
+ self._windows = list(imaps.keys())
338
+
339
+ @classmethod
340
+ def load(
341
+ cls,
342
+ archive_path: Path,
343
+ ) -> MosaicImageStraightener:
344
+ """Load a MosaicImageStraightener from a binary bundle containing a MosaicImageMapper and associated curve maps.
345
+
346
+ Args:
347
+ archive_path (Path): The path to the archive file containing the mosaic image straightener data.
348
+
349
+ Raises:
350
+ ValueError: If the archive file does not exist or if required data is missing.
351
+ ValueError: If the mosaic image mapper is not found in the archive.
352
+
353
+ Returns:
354
+ MosaicImageStraightener: The loaded mosaic image straightener.
355
+ """
356
+ if not archive_path.exists():
357
+ raise ValueError(f'Archive {archive_path} does not exist')
358
+ mapper = None
359
+ with tarfile.open(archive_path, "r:xz") as tar:
360
+ with TemporaryDirectory() as tmpdir:
361
+ tmpdir = Path(tmpdir)
362
+ tmpdir.mkdir(exist_ok=True, parents=True)
363
+ tar.extractall(path=tmpdir)
364
+ imaps = {}
365
+ for dsdir in tmpdir.iterdir():
366
+ if not dsdir.is_dir():
367
+ # Load the mapper
368
+ if dsdir.suffix == '.json' and '_mapper' in dsdir.stem:
369
+ mapper = MosaicImageMapper.from_json(dsdir.read_text())
370
+ else:
371
+ # Load the curve maps
372
+ win_name = dsdir.name
373
+ datasets = []
374
+ for dsfile in natsorted(dsdir.iterdir()):
375
+ if not dsfile.is_file() or dsfile.suffix != '.nc':
376
+ continue
377
+ ds = load_dataset(dsfile)
378
+ datasets.append(ds)
379
+ imaps[win_name] = datasets
380
+ if mapper is None:
381
+ raise ValueError('MosaicImageMapper not found in archive')
382
+ return cls(imaps, mapper)
383
+
384
+ @property
385
+ def windows(self) -> List[str]:
386
+ """Return the list of available window names for straightening.
387
+
388
+ Returns:
389
+ List[str]: The list of available window names for straightening.
390
+ """
391
+ return self._windows
392
+
393
+ def load_image(
394
+ self,
395
+ image: NDArray,
396
+ order: int = 1,
397
+ cval: float = nan,
398
+ mode: PaddingMode = 'constant',
399
+ ) -> DataArray:
400
+ """Load an image onto the mosaic grid using the mapper, preparing it for straightening.
401
+
402
+ Args:
403
+ image (NDArray): The input image to be mapped onto the mosaic grid. Must be a 2D array.
404
+ order (int, optional): The interpolation order to use. Defaults to 1 (bilinear). Available options are 0 (nearest), 1 (bilinear), 2 (biquadratic), 3 (bicubic), 4 (biquartic), and 5 (biquintic).
405
+ cval (float, optional): The constant value to use for padding. Defaults to nan.
406
+ mode (PaddingMode, optional): The padding mode to use. Defaults to 'constant'.
407
+
408
+ Returns:
409
+ DataArray: The image mapped onto the mosaic grid.
410
+ """
411
+ da, _ = self._mapper.map_to_mosaic(
412
+ image, order, cval, mode)
413
+ return da
414
+
415
+ @overload
416
+ def straighten_image(
417
+ self,
418
+ image: DataArray,
419
+ window: str,
420
+ *,
421
+ inplace: bool
422
+ ) -> DataArray: ...
423
+
424
+ @overload
425
+ def straighten_image(
426
+ self,
427
+ image: DataArray,
428
+ window: List[str],
429
+ *,
430
+ inplace: bool
431
+ ) -> Dict[str, DataArray]: ...
432
+
433
+ def straighten_image(
434
+ self,
435
+ image: DataArray,
436
+ window: str | List[str],
437
+ *,
438
+ inplace: bool = True
439
+ ) -> DataArray | Dict[str, DataArray]:
440
+ """Straighten the given image using the curve maps associated with the specified window name(s).
441
+
442
+ Args:
443
+ image (DataArray): The input image to be straightened, already mapped onto the mosaic grid.
444
+ window (str | Sequence[str]): The name(s) of the window(s) to use for straightening.
445
+ inplace (bool, optional): If True, the input image will be modified in place. Defaults to True.
446
+
447
+ Raises:
448
+ ValueError: If the straightener is not properly initialized or if window is invalid.
449
+
450
+ Returns:
451
+ DataArray | Dict[str, DataArray]: _description_
452
+ """
453
+ if self._imaps is None:
454
+ raise ValueError('Must setup first')
455
+ if isinstance(window, str):
456
+ ret: List = []
457
+ for ds in self._imaps.get(window, []):
458
+ # Trick to select the same location across different datasets
459
+ imageset = Dataset(
460
+ data_vars={
461
+ 'image': (('y', 'x'), image.values),
462
+ 'loc': (('y', 'x'), ds['loc'].data, ds['loc'].attrs),
463
+ },
464
+ coords={
465
+ 'y': (('y',), image['y'].values, {'units': u.mm, 'description': 'Mosaic coordinate, increasing from the bottom.'}),
466
+ 'x': (('x',), image['x'].values, {'units': u.mm, 'description': 'Mosaic coordinate, increasing from the left.'}),
467
+ },
468
+ ).astropy.quantify()
469
+ xran = ds.attrs['xran']
470
+ yran = ds.attrs['yran']
471
+ xran = (Quantity(xran[0], u.mm), Quantity(xran[1], u.mm))
472
+ yran = (Quantity(yran[0], u.mm), Quantity(yran[1], u.mm))
473
+ xform = ds['xform'].data
474
+ res = ds['resolution']
475
+ data = imageset.where(imageset['loc'], drop=True)['image']
476
+ data = data.sel(y=slice(*yran), x=slice(*xran))
477
+ if inplace:
478
+ try:
479
+ data /= res.data
480
+ except (MergeError, ValueError):
481
+ data = data.data / res.data
482
+ else:
483
+ data = data / res.data
484
+ data = warp(
485
+ data.values[:, :], xform, cval=nan)
486
+ out = DataArray(data*10, coords={
487
+ 'y': (
488
+ ('y',),
489
+ ds['wly'].data, # type: ignore
490
+ {
491
+ 'units': 'mm',
492
+ 'description': 'Height in the mosaic coordinate, increasing from the bottom.'
493
+ }
494
+ ),
495
+ 'wavelength': (
496
+ 'wavelength',
497
+ ds['wavelength'].data / 10.0, # type: ignore
498
+ {
499
+ 'units': 'nm',
500
+ 'description': 'Wavelength in nanometers',
501
+ }
502
+ ),
503
+ })
504
+ ret.append(out)
505
+ out: DataArray = concat(ret, dim='y', join='outer') # type: ignore
506
+ out = out.sortby('y')
507
+ out = out.sortby('wavelength')
508
+ if image.attrs.get('units') is not None:
509
+ out.attrs['units'] = image.attrs['units'] + ' / nm'
510
+ else:
511
+ out.attrs['units'] = '1 / nm'
512
+ return out
513
+ elif isinstance(window, Sequence):
514
+ if len(window) == 0:
515
+ window = self.windows
516
+ return {
517
+ name: self.straighten_image(image, name, inplace=inplace)
518
+ for name in window
519
+ }
520
+ else:
521
+ raise ValueError(
522
+ 'win_name must be a string or an iterable of strings')
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: mss-imageproc
3
+ Version: 0.0.1
4
+ Summary: Image processing utilities for MSS Designer Models
5
+ Author-email: "Sunip K. Mukherjee" <sunipkmukherjee@gmail.com>
6
+ License: MIT
7
+ Keywords: image-processing,remote-sensing,mosaic
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: astropy>=7.1
14
+ Requires-Dist: dacite>=1.8.1
15
+ Requires-Dist: natsort>=8.4.0
16
+ Requires-Dist: numpy>=1.26
17
+ Requires-Dist: scikit-image>=0.22
18
+ Requires-Dist: serde-dataclass>=0.0.4
19
+ Requires-Dist: tomlkit>=0.12.5
20
+ Requires-Dist: xarray>=2024.1.0
21
+ Requires-Dist: netcdf4>=1.6.3
22
+ Requires-Dist: astropy-xarray==0.1.0
23
+
24
+ # mss-imageproc
25
+ This repository contains the Python library `mss-imageproc`, which provides tools for processing images
26
+ from instruments modeled using the Multi-Slit Spectrograph (MSS) Designer. The library includes functions
27
+ applying image transformations to map detector images onto the focal plane of the instrument, as well as
28
+ remove diffraction slit curvature from the images. This library is designed to be used stand-alone, using
29
+ the instrument configuration files generated by the MSS Designer. This library is also used internally by
30
+ the MSS Designer.
31
+
32
+ # Installation
33
+ You can install the library using pip:
34
+ ```bash
35
+ pip install mss-imageproc
36
+ ```
37
+
38
+ # Usage
39
+ The library is used in the following way:
40
+ ```python
41
+ # Import the library
42
+ from mss_imageproc import MosaicImageStraightener
43
+
44
+ # Create a straightener object using the instrument configuration file
45
+ straightener = MosaicImageStraightener.from_instrument_config('path/to/instrument_curve_maps.bin')
46
+ # Load an image and map it onto the mosaic grid
47
+ image_array = ... # Load your image as a 2D NumPy array
48
+ mapped_image = straightener.load_image(image_array)
49
+ # Straighten the image by removing slit curvature
50
+ straightened_images = straightener.straighten_image(mapped_image)
51
+ # The result is a dictionary of straightened images, one for each window. You can access them like this:
52
+ for window_name, straightened_image in straightened_images.items():
53
+ print(f"Straightened image for window {window_name}:")
54
+ straightened_image.show() # Show the straightened image
55
+ ```
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/mss_imageproc/__init__.py
4
+ src/mss_imageproc/straighten_image.py
5
+ src/mss_imageproc.egg-info/PKG-INFO
6
+ src/mss_imageproc.egg-info/SOURCES.txt
7
+ src/mss_imageproc.egg-info/dependency_links.txt
8
+ src/mss_imageproc.egg-info/requires.txt
9
+ src/mss_imageproc.egg-info/top_level.txt
@@ -0,0 +1,10 @@
1
+ astropy>=7.1
2
+ dacite>=1.8.1
3
+ natsort>=8.4.0
4
+ numpy>=1.26
5
+ scikit-image>=0.22
6
+ serde-dataclass>=0.0.4
7
+ tomlkit>=0.12.5
8
+ xarray>=2024.1.0
9
+ netcdf4>=1.6.3
10
+ astropy-xarray==0.1.0
@@ -0,0 +1 @@
1
+ mss_imageproc