getframes 2.0.0__py3-none-any.whl
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.
- getframes/__about__.py +4 -0
- getframes/__init__.py +91 -0
- getframes/analysis/__init__.py +18 -0
- getframes/analysis/apertures.py +92 -0
- getframes/analysis/ptc.py +109 -0
- getframes/calibrate.py +182 -0
- getframes/camera.py +649 -0
- getframes/cli.py +214 -0
- getframes/config.py +420 -0
- getframes/dataset.py +294 -0
- getframes/frame.py +107 -0
- getframes/noise.py +637 -0
- getframes/observation.py +162 -0
- getframes/presets/__init__.py +90 -0
- getframes/presets/data/__init__.py +3 -0
- getframes/presets/data/andor_ikon_m934.toml +22 -0
- getframes/presets/data/andor_ixon_ultra_888.toml +22 -0
- getframes/presets/data/generic_ccd.toml +18 -0
- getframes/presets/data/generic_cmos.toml +18 -0
- getframes/presets/data/generic_eapd.toml +20 -0
- getframes/presets/data/generic_emccd.toml +20 -0
- getframes/presets/data/generic_scmos.toml +21 -0
- getframes/presets/data/hamamatsu_orca_fusion.toml +25 -0
- getframes/presets/data/leonardo_saphira.toml +32 -0
- getframes/presets/data/zwo_asi2600mm.toml +20 -0
- getframes/py.typed +0 -0
- getframes/scene/__init__.py +51 -0
- getframes/scene/optics.py +180 -0
- getframes/scene/photometry.py +311 -0
- getframes/scene/psf.py +371 -0
- getframes/scene/scene.py +205 -0
- getframes/scene/sources.py +683 -0
- getframes/scene/thermal.py +114 -0
- getframes/scene/wcs.py +110 -0
- getframes/spectral.py +449 -0
- getframes-2.0.0.dist-info/METADATA +218 -0
- getframes-2.0.0.dist-info/RECORD +40 -0
- getframes-2.0.0.dist-info/WHEEL +4 -0
- getframes-2.0.0.dist-info/entry_points.txt +2 -0
- getframes-2.0.0.dist-info/licenses/LICENSE +21 -0
getframes/dataset.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Scalable raw + ground-truth dataset generation (roadmap phase 1.6).
|
|
3
|
+
|
|
4
|
+
The library's reason to exist is *paired* data: a realistic raw frame and the
|
|
5
|
+
noise-free signal it was drawn from. :func:`pairs` turns a camera and a stream of
|
|
6
|
+
:class:`~getframes.scene.scene.Scene` objects into a reproducible sequence of
|
|
7
|
+
``{"raw": ADU, "truth": electrons}`` pairs — training data for denoising,
|
|
8
|
+
deconvolution, or calibration networks — and streams it to disk in ``float32``
|
|
9
|
+
without ever holding the whole set in memory.
|
|
10
|
+
|
|
11
|
+
:func:`random_star_fields` is a convenience generator of random star-field scenes
|
|
12
|
+
to feed it, but any iterable of scenes (matching the camera's resolution) works.
|
|
13
|
+
|
|
14
|
+
>>> import getframes as gf
|
|
15
|
+
>>> cam = gf.Camera.from_preset("andor_ikon_m934", precision="float32")
|
|
16
|
+
>>> scenes = gf.dataset.random_star_fields(n=4, shape=cam.resolution, seed=0)
|
|
17
|
+
>>> ds = gf.dataset.pairs(camera=cam, scenes=scenes, exposure=10.0, seed=1)
|
|
18
|
+
>>> pair = next(iter(ds))
|
|
19
|
+
>>> sorted(pair)
|
|
20
|
+
['raw', 'truth']
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Iterable, Iterator
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import TYPE_CHECKING, Any
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
from .scene import Bandpass, GaussianPSF, PointSource, Scene, Sky, Telescope
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from numpy.typing import DTypeLike, NDArray
|
|
35
|
+
|
|
36
|
+
from .camera import Camera
|
|
37
|
+
from .scene.psf import PSF
|
|
38
|
+
|
|
39
|
+
# Sub-stream salt for the per-frame dataset seeds, kept distinct from other streams.
|
|
40
|
+
_DATASET_STREAM = 0x44415441 # "DATA"
|
|
41
|
+
|
|
42
|
+
Pair = dict[str, "NDArray[np.floating[Any]]"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _default_optics() -> Telescope:
|
|
46
|
+
"""A generic small-telescope optic for synthetic star fields (Johnson V)."""
|
|
47
|
+
return Telescope(
|
|
48
|
+
aperture_diameter_m=0.5,
|
|
49
|
+
plate_scale_arcsec_per_pixel=1.0,
|
|
50
|
+
throughput=0.5,
|
|
51
|
+
band=Bandpass.johnson("V"),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RandomStarFields:
|
|
56
|
+
"""A reproducible, re-iterable stream of random star-field :class:`Scene` objects.
|
|
57
|
+
|
|
58
|
+
Each scene is a field of uniformly placed point sources with magnitudes drawn
|
|
59
|
+
uniformly from ``mag_range`` and an optional uniform sky. The number of stars per
|
|
60
|
+
field is fixed (``int``) or drawn per field from a ``(low, high)`` range. The
|
|
61
|
+
stream is deterministic for a given ``seed`` (each field gets its own derived
|
|
62
|
+
seed) and can be iterated more than once.
|
|
63
|
+
|
|
64
|
+
Construct via :func:`random_star_fields`.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
n:
|
|
69
|
+
Number of scenes in the stream.
|
|
70
|
+
shape:
|
|
71
|
+
Scene size ``(height, width)``; must match the camera it is observed with.
|
|
72
|
+
optics, psf:
|
|
73
|
+
The :class:`~getframes.scene.optics.Telescope` and
|
|
74
|
+
:class:`~getframes.scene.psf.PSF` shared by every field. Sensible generic
|
|
75
|
+
defaults are used when omitted.
|
|
76
|
+
n_stars:
|
|
77
|
+
Stars per field — a fixed count, or a ``(low, high)`` range sampled per field.
|
|
78
|
+
mag_range:
|
|
79
|
+
``(bright, faint)`` magnitude bounds for the uniform brightness draw.
|
|
80
|
+
sky_mag_arcsec2:
|
|
81
|
+
Optional uniform sky surface brightness (mag/arcsec^2); ``None`` for no sky.
|
|
82
|
+
seed:
|
|
83
|
+
Base seed; field ``i`` uses a distinct derived seed so the stream repeats.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
n: int,
|
|
89
|
+
shape: tuple[int, int],
|
|
90
|
+
*,
|
|
91
|
+
optics: Telescope | None = None,
|
|
92
|
+
psf: PSF | None = None,
|
|
93
|
+
n_stars: int | tuple[int, int] = (20, 200),
|
|
94
|
+
mag_range: tuple[float, float] = (16.0, 22.0),
|
|
95
|
+
sky_mag_arcsec2: float | None = 21.0,
|
|
96
|
+
seed: int | None = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
if n < 0:
|
|
99
|
+
raise ValueError("n must be non-negative.")
|
|
100
|
+
if len(shape) != 2 or any(s <= 0 for s in shape):
|
|
101
|
+
raise ValueError(f"shape must be two positive ints, got {shape!r}.")
|
|
102
|
+
self.n = int(n)
|
|
103
|
+
self.shape = (int(shape[0]), int(shape[1]))
|
|
104
|
+
self.optics = optics if optics is not None else _default_optics()
|
|
105
|
+
self.psf = psf if psf is not None else GaussianPSF(fwhm_arcsec=2.5)
|
|
106
|
+
self.n_stars = n_stars
|
|
107
|
+
self.mag_range = (float(mag_range[0]), float(mag_range[1]))
|
|
108
|
+
self.sky_mag_arcsec2 = sky_mag_arcsec2
|
|
109
|
+
self.seed = seed
|
|
110
|
+
|
|
111
|
+
def __len__(self) -> int:
|
|
112
|
+
return self.n
|
|
113
|
+
|
|
114
|
+
def _field_count(self, rng: np.random.Generator) -> int:
|
|
115
|
+
if isinstance(self.n_stars, tuple):
|
|
116
|
+
low, high = self.n_stars
|
|
117
|
+
return int(rng.integers(low, high + 1))
|
|
118
|
+
return int(self.n_stars)
|
|
119
|
+
|
|
120
|
+
def _scene(self, rng: np.random.Generator) -> Scene:
|
|
121
|
+
height, width = self.shape
|
|
122
|
+
k = self._field_count(rng)
|
|
123
|
+
xs = rng.uniform(0.0, width - 1, size=k)
|
|
124
|
+
ys = rng.uniform(0.0, height - 1, size=k)
|
|
125
|
+
mags = rng.uniform(self.mag_range[0], self.mag_range[1], size=k)
|
|
126
|
+
sources = [
|
|
127
|
+
PointSource(x=float(x), y=float(y), magnitude=float(m)) for x, y, m in zip(xs, ys, mags)
|
|
128
|
+
]
|
|
129
|
+
sky = None if self.sky_mag_arcsec2 is None else Sky(self.sky_mag_arcsec2)
|
|
130
|
+
return Scene(shape=self.shape, optics=self.optics, psf=self.psf, sources=sources, sky=sky)
|
|
131
|
+
|
|
132
|
+
def __iter__(self) -> Iterator[Scene]:
|
|
133
|
+
seeds: Iterable[np.random.SeedSequence | None]
|
|
134
|
+
if self.seed is None:
|
|
135
|
+
seeds = [None] * self.n
|
|
136
|
+
else:
|
|
137
|
+
seeds = np.random.SeedSequence(self.seed).spawn(self.n)
|
|
138
|
+
for ss in seeds:
|
|
139
|
+
yield self._scene(np.random.default_rng(ss))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def random_star_fields(
|
|
143
|
+
n: int,
|
|
144
|
+
shape: tuple[int, int],
|
|
145
|
+
*,
|
|
146
|
+
optics: Telescope | None = None,
|
|
147
|
+
psf: PSF | None = None,
|
|
148
|
+
n_stars: int | tuple[int, int] = (20, 200),
|
|
149
|
+
mag_range: tuple[float, float] = (16.0, 22.0),
|
|
150
|
+
sky_mag_arcsec2: float | None = 21.0,
|
|
151
|
+
seed: int | None = None,
|
|
152
|
+
) -> RandomStarFields:
|
|
153
|
+
"""Build a reproducible :class:`RandomStarFields` stream of ``n`` star-field scenes.
|
|
154
|
+
|
|
155
|
+
A convenience source of scenes for :func:`pairs`; see :class:`RandomStarFields`
|
|
156
|
+
for the parameters.
|
|
157
|
+
"""
|
|
158
|
+
return RandomStarFields(
|
|
159
|
+
n,
|
|
160
|
+
shape,
|
|
161
|
+
optics=optics,
|
|
162
|
+
psf=psf,
|
|
163
|
+
n_stars=n_stars,
|
|
164
|
+
mag_range=mag_range,
|
|
165
|
+
sky_mag_arcsec2=sky_mag_arcsec2,
|
|
166
|
+
seed=seed,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class PairDataset:
|
|
171
|
+
"""A lazy, reproducible sequence of raw + truth pairs (see :func:`pairs`).
|
|
172
|
+
|
|
173
|
+
Iterating yields ``{"raw": ADU, "truth": electrons}`` dicts, one per input
|
|
174
|
+
scene, each cast to :attr:`dtype`. The stream is single-pass when its scenes are
|
|
175
|
+
a one-shot iterator; pass a re-iterable scene source (e.g.
|
|
176
|
+
:class:`RandomStarFields`) to iterate more than once. Materialise to disk with
|
|
177
|
+
:meth:`to_npz` or into stacked arrays with :meth:`to_arrays`.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
camera: Camera,
|
|
183
|
+
scenes: Iterable[Scene],
|
|
184
|
+
exposure: float,
|
|
185
|
+
*,
|
|
186
|
+
temperature: float | None = None,
|
|
187
|
+
dtype: DTypeLike = np.float32,
|
|
188
|
+
seed: int | None = None,
|
|
189
|
+
) -> None:
|
|
190
|
+
self.camera = camera
|
|
191
|
+
self.scenes = scenes
|
|
192
|
+
self.exposure = float(exposure)
|
|
193
|
+
self.temperature = temperature
|
|
194
|
+
self.dtype = np.dtype(dtype)
|
|
195
|
+
self.seed = seed
|
|
196
|
+
|
|
197
|
+
def __len__(self) -> int:
|
|
198
|
+
try:
|
|
199
|
+
return len(self.scenes) # type: ignore[arg-type]
|
|
200
|
+
except TypeError as exc: # pragma: no cover - depends on the scene source
|
|
201
|
+
raise TypeError("This PairDataset's scene source has no length.") from exc
|
|
202
|
+
|
|
203
|
+
def _frame_seed(self, index: int) -> int | None:
|
|
204
|
+
if self.seed is None:
|
|
205
|
+
return None
|
|
206
|
+
ss = np.random.SeedSequence([int(self.seed), _DATASET_STREAM, index])
|
|
207
|
+
return int(ss.generate_state(1)[0])
|
|
208
|
+
|
|
209
|
+
def __iter__(self) -> Iterator[Pair]:
|
|
210
|
+
for i, scene in enumerate(self.scenes):
|
|
211
|
+
frame = self.camera.observe(
|
|
212
|
+
scene,
|
|
213
|
+
self.exposure,
|
|
214
|
+
self.temperature,
|
|
215
|
+
seed=self._frame_seed(i),
|
|
216
|
+
include_truth=True,
|
|
217
|
+
)
|
|
218
|
+
assert frame.truth is not None # include_truth=True
|
|
219
|
+
yield {
|
|
220
|
+
"raw": np.asarray(frame.data, dtype=self.dtype),
|
|
221
|
+
"truth": np.asarray(frame.truth.mean_electrons, dtype=self.dtype),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def to_npz(self, directory: str, *, prefix: str = "pair", compress: bool = False) -> list[str]:
|
|
225
|
+
"""Write each pair to ``{directory}/{prefix}_{i:06d}.npz`` and return the paths.
|
|
226
|
+
|
|
227
|
+
Each archive holds ``raw`` (ADU) and ``truth`` (electrons) arrays in
|
|
228
|
+
:attr:`dtype`. Streams pair by pair, so the whole set is never resident in
|
|
229
|
+
memory. ``compress`` uses :func:`numpy.savez_compressed`.
|
|
230
|
+
"""
|
|
231
|
+
out = Path(directory)
|
|
232
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
writer = np.savez_compressed if compress else np.savez
|
|
234
|
+
paths: list[str] = []
|
|
235
|
+
for i, pair in enumerate(self):
|
|
236
|
+
path = out / f"{prefix}_{i:06d}.npz"
|
|
237
|
+
writer(path, raw=pair["raw"], truth=pair["truth"])
|
|
238
|
+
paths.append(str(path))
|
|
239
|
+
return paths
|
|
240
|
+
|
|
241
|
+
def to_arrays(self) -> tuple[NDArray[np.floating[Any]], NDArray[np.floating[Any]]]:
|
|
242
|
+
"""Stack the whole dataset into ``(raw, truth)`` arrays of shape ``(N, H, W)``.
|
|
243
|
+
|
|
244
|
+
Convenient for small sets; holds everything in memory, unlike :meth:`to_npz`.
|
|
245
|
+
"""
|
|
246
|
+
raws: list[NDArray[np.floating[Any]]] = []
|
|
247
|
+
truths: list[NDArray[np.floating[Any]]] = []
|
|
248
|
+
for pair in self:
|
|
249
|
+
raws.append(pair["raw"])
|
|
250
|
+
truths.append(pair["truth"])
|
|
251
|
+
if not raws:
|
|
252
|
+
raise ValueError("Dataset is empty; nothing to stack.")
|
|
253
|
+
return np.stack(raws, axis=0), np.stack(truths, axis=0)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def pairs(
|
|
257
|
+
*,
|
|
258
|
+
camera: Camera,
|
|
259
|
+
scenes: Iterable[Scene],
|
|
260
|
+
exposure: float,
|
|
261
|
+
temperature: float | None = None,
|
|
262
|
+
dtype: DTypeLike = np.float32,
|
|
263
|
+
seed: int | None = None,
|
|
264
|
+
) -> PairDataset:
|
|
265
|
+
"""Build a :class:`PairDataset` of raw + truth pairs from a camera and scenes.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
camera:
|
|
270
|
+
The :class:`~getframes.camera.Camera` that observes each scene. Construct it
|
|
271
|
+
with ``precision="float32"`` to render the signal chain in the fast path too.
|
|
272
|
+
scenes:
|
|
273
|
+
Any iterable of :class:`~getframes.scene.scene.Scene` matching the camera's
|
|
274
|
+
resolution (e.g. :func:`random_star_fields`).
|
|
275
|
+
exposure:
|
|
276
|
+
Integration time in seconds for every frame.
|
|
277
|
+
temperature:
|
|
278
|
+
Sensor temperature (deg C); defaults to the camera's.
|
|
279
|
+
dtype:
|
|
280
|
+
Storage dtype for the ``raw``/``truth`` arrays (``float32`` by default to
|
|
281
|
+
halve on-disk size; the ADU are exact integers either way).
|
|
282
|
+
seed:
|
|
283
|
+
Base seed; frame ``i`` draws a distinct derived seed, so the whole dataset is
|
|
284
|
+
reproducible yet the frames are independent.
|
|
285
|
+
"""
|
|
286
|
+
return PairDataset(camera, scenes, exposure, temperature=temperature, dtype=dtype, seed=seed)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
__all__ = [
|
|
290
|
+
"PairDataset",
|
|
291
|
+
"RandomStarFields",
|
|
292
|
+
"pairs",
|
|
293
|
+
"random_star_fields",
|
|
294
|
+
]
|
getframes/frame.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""The :class:`Frame` container returned by frame-generation methods."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from numpy.typing import NDArray
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class FrameTruth:
|
|
17
|
+
"""Noise-free ground truth a :class:`Frame` was generated from.
|
|
18
|
+
|
|
19
|
+
Useful for validating analysis pipelines against exactly what went in. All
|
|
20
|
+
arrays are in electrons unless noted, shaped like the frame.
|
|
21
|
+
|
|
22
|
+
Attributes
|
|
23
|
+
----------
|
|
24
|
+
mean_electrons:
|
|
25
|
+
Noise-free total signal (photo + dark) per pixel, in electrons. This is
|
|
26
|
+
the expectation value before shot noise, gain, and read noise.
|
|
27
|
+
mean_photoelectrons:
|
|
28
|
+
Noise-free photo signal per pixel, in electrons (i.e. excluding dark).
|
|
29
|
+
photon_rate:
|
|
30
|
+
The incident photon rate the frame was exposed to, in photons/s/pixel, as
|
|
31
|
+
provided by the caller (a scalar for uniform illumination, else an array).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
mean_electrons: NDArray[np.float64]
|
|
35
|
+
mean_photoelectrons: NDArray[np.float64]
|
|
36
|
+
photon_rate: NDArray[np.float64] | float
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Frame:
|
|
41
|
+
"""A single simulated image plus the metadata describing how it was made.
|
|
42
|
+
|
|
43
|
+
The pixel values live in :attr:`data` as a 2-D NumPy array in ADU. The object
|
|
44
|
+
is array-like: ``np.asarray(frame)`` and most NumPy operations work directly.
|
|
45
|
+
|
|
46
|
+
Attributes
|
|
47
|
+
----------
|
|
48
|
+
data:
|
|
49
|
+
2-D array of pixel values in ADU, shaped ``(height, width)``.
|
|
50
|
+
metadata:
|
|
51
|
+
Free-form dictionary describing the simulation (camera name, exposure,
|
|
52
|
+
temperature, frame type, etc.). Suitable for writing to a FITS header.
|
|
53
|
+
truth:
|
|
54
|
+
Optional :class:`FrameTruth` holding the noise-free signal the frame was
|
|
55
|
+
built from, for ground-truth comparisons. ``None`` if not requested.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
data: NDArray[np.floating[Any] | np.integer[Any]]
|
|
59
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
truth: FrameTruth | None = None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def shape(self) -> tuple[int, ...]:
|
|
64
|
+
return tuple(self.data.shape)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def dtype(self) -> np.dtype[Any]:
|
|
68
|
+
return self.data.dtype
|
|
69
|
+
|
|
70
|
+
def __array__(self, dtype: Any = None) -> NDArray[Any]:
|
|
71
|
+
return np.asarray(self.data, dtype=dtype)
|
|
72
|
+
|
|
73
|
+
def stats(self) -> dict[str, float]:
|
|
74
|
+
"""Common summary statistics of the pixel values (mean/median/std/min/max)."""
|
|
75
|
+
arr = np.asarray(self.data, dtype=float)
|
|
76
|
+
return {
|
|
77
|
+
"mean": float(arr.mean()),
|
|
78
|
+
"median": float(np.median(arr)),
|
|
79
|
+
"std": float(arr.std()),
|
|
80
|
+
"min": float(arr.min()),
|
|
81
|
+
"max": float(arr.max()),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def to_fits(self, path: str, overwrite: bool = False) -> None:
|
|
85
|
+
"""Write the frame to a FITS file (requires ``astropy``).
|
|
86
|
+
|
|
87
|
+
Metadata keys are written to the FITS header where they fit the 8-character
|
|
88
|
+
keyword and value-type constraints.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
from astropy.io import fits
|
|
92
|
+
except ImportError as exc: # pragma: no cover - astropy is a core dependency
|
|
93
|
+
raise ImportError(
|
|
94
|
+
"Writing FITS files requires astropy (a core dependency of getframes); "
|
|
95
|
+
"reinstall with: pip install getframes"
|
|
96
|
+
) from exc
|
|
97
|
+
|
|
98
|
+
hdu = fits.PrimaryHDU(data=np.asarray(self.data))
|
|
99
|
+
for key, value in self.metadata.items():
|
|
100
|
+
if isinstance(value, (str, int, float, bool)):
|
|
101
|
+
hdu.header[key[:8].upper()] = value
|
|
102
|
+
hdu.writeto(path, overwrite=overwrite)
|
|
103
|
+
|
|
104
|
+
def __repr__(self) -> str:
|
|
105
|
+
ftype = self.metadata.get("frame_type", "frame")
|
|
106
|
+
cam = self.metadata.get("camera", "?")
|
|
107
|
+
return f"Frame(type={ftype!r}, camera={cam!r}, shape={self.shape}, dtype={self.dtype})"
|