Coverage for python/lsst/images/_image.py: 58%
231 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 08:38 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 08:38 +0000
1# This file is part of lsst-images.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14__all__ = ("Image", "ImageSerializationModel")
16from collections.abc import Callable, Sequence
17from contextlib import ExitStack
18from types import EllipsisType
19from typing import TYPE_CHECKING, Any, ClassVar, final
21import astropy.io.fits
22import astropy.units
23import astropy.wcs
24import numpy as np
25import numpy.typing as npt
26import pydantic
28from lsst.resources import ResourcePath, ResourcePathExpression
30from . import fits
31from ._generalized_image import GeneralizedImage
32from ._geom import YX, Box
33from ._transforms import Frame, GeneralFrame, SkyProjection, SkyProjectionSerializationModel
34from .describe import DescribeOptions, FieldRole, Report, ReportField
35from .serialization import (
36 ArchiveTree,
37 ArrayReferenceModel,
38 ArrayReferenceQuantityModel,
39 InlineArrayModel,
40 InlineArrayQuantityModel,
41 InputArchive,
42 InvalidParameterError,
43 MetadataValue,
44 OutputArchive,
45 no_header_updates,
46)
47from .utils import is_none
49if TYPE_CHECKING:
50 try:
51 from lsst.afw.image import Image as LegacyImage
52 except ImportError:
53 type LegacyImage = Any # type: ignore[no-redef]
56DEFAULT_PIXEL_FRAME = GeneralFrame(unit=astropy.units.pix)
57"""The pixel-grid `Frame` assumed when reconstructing a `SkyProjection` from a
58FITS header that does not otherwise identify its pixel frame, consistent with
59the FITS standard's notion of a plain pixel axis.
60"""
63@final
64class Image(GeneralizedImage):
65 """A 2-d array that may be augmented with units and a nonzero origin.
67 Parameters
68 ----------
69 array_or_fill
70 Array or fill value for the image. If a fill value, ``bbox`` or
71 ``shape`` must be provided.
72 bbox
73 Bounding box for the image.
74 yx0
75 Logical coordinates of the first pixel in the array, ordered ``y``,
76 ``x`` (unless an `XY` instance is passed). Ignored if
77 ``bbox`` is provided. Defaults to zeros.
78 shape
79 Leading dimensions of the array, ordered ``y``, ``x`` (unless an `XY`
80 instance is passed). Only needed if ``array_or_fill`` is not an
81 array and ``bbox`` is not provided. Like the bbox, this does not
82 include the last dimension of the array.
83 dtype
84 Pixel data type override.
85 unit
86 Units for the image's pixel values.
87 sky_projection
88 Projection that maps the pixel grid to the sky.
89 metadata
90 Arbitrary flexible metadata to associate with the image.
92 Notes
93 -----
94 Indexing the `array` attribute of an `Image` does not take into account its
95 ``yx0`` offset, but accessing a subimage by indexing an `Image` with a
96 `Box` does, and the `bbox` of the subimage is set to match its location
97 within the original image.
99 Indexed assignment to a subimage requires consistency between the
100 coordinate systems and units of both operands, but it will automatically
101 select a subimage of the right-hand side and convert compatible units when
102 possible. In other words::
104 a[box] = b
106 is a shortcut for
108 a[box].quantity = b[box].quantity
110 An ellipsis (``...``) can be used instead of a `Box` to assign to the full
111 image.
112 """
114 def __init__(
115 self,
116 array_or_fill: np.ndarray | int | float = 0,
117 /,
118 *,
119 bbox: Box | None = None,
120 yx0: Sequence[int] | None = None,
121 shape: Sequence[int] | None = None,
122 dtype: npt.DTypeLike | None = None,
123 unit: astropy.units.UnitBase | None = None,
124 sky_projection: SkyProjection[Any] | None = None,
125 metadata: dict[str, MetadataValue] | None = None,
126 ) -> None:
127 super().__init__(metadata)
128 if isinstance(array_or_fill, np.ndarray):
129 if dtype is not None:
130 array = np.array(array_or_fill, dtype=dtype, copy=None)
131 else:
132 array = array_or_fill
133 if bbox is None:
134 bbox = Box.from_shape(array.shape, start=yx0)
135 elif bbox.shape != array.shape:
136 raise ValueError(
137 f"Explicit bbox shape {bbox.shape} does not match array with shape {array.shape}."
138 )
139 if shape is not None and shape != array.shape:
140 raise ValueError(f"Explicit shape {shape} does not match array with shape {array.shape}.")
141 else:
142 if bbox is None:
143 if shape is None:
144 raise TypeError("No bbox, shape, or array provided.")
145 bbox = Box.from_shape(shape, start=yx0)
146 elif shape is not None and shape != bbox.shape:
147 raise ValueError(f"Explicit shape {shape} does not match bbox shape {bbox.shape}.")
148 array = np.full(bbox.shape, array_or_fill, dtype=dtype)
149 self._array: np.ndarray = array
150 self._bbox: Box = bbox
151 self._unit = unit
152 self._sky_projection = sky_projection
154 @property
155 def array(self) -> np.ndarray:
156 """The low-level array (`numpy.ndarray`).
158 Assigning to this attribute modifies the existing array in place; the
159 bounding box and underlying data pointer are never changed.
160 """
161 return self._array
163 @array.setter
164 def array(self, value: np.ndarray | int | float) -> None:
165 self._array[...] = value
167 @property
168 def quantity(self) -> astropy.units.Quantity:
169 """The low-level array with units (`astropy.units.Quantity`).
171 Assigning to this attribute modifies the existing array in place; the
172 bounding box and underlying data pointer are never changed.
173 """
174 return astropy.units.Quantity(self._array, self._unit, copy=False)
176 @quantity.setter
177 def quantity(self, value: astropy.units.Quantity) -> None:
178 self.quantity[...] = value
180 @property
181 def bbox(self) -> Box:
182 """Bounding box for the image (`Box`)."""
183 return self._bbox
185 @property
186 def unit(self) -> astropy.units.UnitBase | None:
187 """Units for the image's pixel values (`astropy.units.Unit` or
188 `None`).
189 """
190 return self._unit
192 @property
193 def sky_projection(self) -> SkyProjection[Any] | None:
194 """The projection that maps this image's pixel grid to the sky
195 (`SkyProjection` | `None`).
197 Notes
198 -----
199 The pixel coordinates used by this projection account for the bounding
200 box ``start``; they are not just array indices.
201 """
202 return self._sky_projection
204 def __getitem__(self, bbox: Box | EllipsisType) -> Image:
205 bbox, indices = self._handle_getitem_args(bbox)
206 return self._transfer_metadata(
207 Image(self._array[indices], bbox=bbox, unit=self._unit, sky_projection=self._sky_projection),
208 bbox=bbox,
209 )
211 def __setitem__(self, bbox: Box | EllipsisType, value: Image) -> None:
212 self[bbox].quantity[...] = value.quantity
214 def _describe(self, options: DescribeOptions = DescribeOptions(), /) -> Report:
215 """Return a `Report` describing this image.
217 Parameters
218 ----------
219 options : `DescribeOptions`, optional
220 Rendering options. ``"bbox"`` and ``"sky_projection"`` are
221 recognized in `DescribeOptions.exclude`.
222 """
223 dtype_name = self.array.dtype.type.__name__
224 # When a composite excludes the shared bbox, this image is a child that
225 # only needs to convey its dtype, so it collapses to a single line.
226 if "bbox" in options.exclude:
227 return Report(
228 type_name="Image",
229 summary=f"(dtype {dtype_name})",
230 inline=True,
231 )
232 children = {}
233 if not options.brief and "sky_projection" not in options.exclude and self._sky_projection is not None: 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true
234 children["sky_projection"] = self._sky_projection._describe(options.for_child(), bbox=self._bbox)
235 fields = [
236 ReportField(
237 label="array",
238 value="<array>",
239 repr_value="...",
240 positional=True,
241 role=FieldRole.REPR_ONLY,
242 ),
243 ReportField(label="bbox", value=self.bbox, repr_value=repr(self.bbox)),
244 ReportField(label="dtype", value=str(self.array.dtype), repr_value=repr(self.array.dtype)),
245 ]
246 return Report(
247 type_name="Image",
248 summary=f"Image({self.bbox!s}, {dtype_name})",
249 fields=fields,
250 children=children,
251 )
253 def __eq__(self, other: object) -> bool:
254 if not isinstance(other, Image):
255 return NotImplemented
256 return (
257 self._bbox == other._bbox
258 and self._unit == other._unit
259 and np.array_equal(self._array, other._array, equal_nan=True)
260 )
262 def copy(self) -> Image:
263 return self._transfer_metadata(
264 Image(self._array.copy(), bbox=self._bbox, unit=self._unit, sky_projection=self._sky_projection),
265 copy=True,
266 )
268 def view(
269 self,
270 *,
271 unit: astropy.units.UnitBase | None | EllipsisType = ...,
272 sky_projection: SkyProjection | None | EllipsisType = ...,
273 yx0: Sequence[int] | EllipsisType = ...,
274 ) -> Image:
275 """Make a view of the image, with optional updates.
277 Parameters
278 ----------
279 unit
280 Units for the view's pixel values. Defaults to the units of this
281 image.
282 sky_projection
283 Projection that maps the pixel grid to the sky. Defaults to the
284 projection of this image.
285 yx0
286 Logical coordinates of the first pixel, ordered ``y``, ``x``.
287 Defaults to the ``start`` of this image's bounding box.
288 """
289 if unit is ...: 289 ↛ 291line 289 didn't jump to line 291 because the condition on line 289 was always true
290 unit = self._unit
291 if sky_projection is ...: 291 ↛ 292line 291 didn't jump to line 292 because the condition on line 291 was never true
292 sky_projection = self._sky_projection
293 if yx0 is ...: 293 ↛ 295line 293 didn't jump to line 295 because the condition on line 293 was always true
294 yx0 = self._bbox.start
295 return self._transfer_metadata(Image(self._array, yx0=yx0, unit=unit, sky_projection=sky_projection))
297 def serialize[P: pydantic.BaseModel](
298 self,
299 archive: OutputArchive[P],
300 *,
301 update_header: Callable[[astropy.io.fits.Header], None] = no_header_updates,
302 save_projection: bool = True,
303 add_offset_wcs: str | None = "A",
304 tile_shape: tuple[int, ...] | None = None,
305 options_name: str | None = None,
306 ) -> ImageSerializationModel[P]:
307 """Serialize the image to an output archive.
309 Parameters
310 ----------
311 archive
312 Archive to write to.
313 update_header
314 A callback that will be given the FITS header for the HDU
315 containing this image in order to add keys to it. This callback
316 may be provided but will not be called if the output format is not
317 FITS.
318 save_projection
319 If `True`, save the `SkyProjection` attached to the image, if there
320 is one. This does not affect whether a FITS WCS corresponding to
321 the projection is written (it always is, if available, and if
322 ``add_offset_wcs`` is not ``" "``).
323 add_offset_wcs
324 A FITS WCS single-character suffix to use when adding a linear
325 WCS that maps the FITS array to the logical pixel coordinates
326 defined by ``bbox.start``. Set to `None` to not write this WCS.
327 If this is set to ``" "``, it will prevent the `SkyProjection` from
328 being saved as a FITS WCS.
329 tile_shape
330 The recommended shape of each tile, if the archive will save
331 the array in distinct tiles for faster subarray retrieval.
332 This is a hint; archives are not required to use this value.
333 options_name
334 Use this name to look up archive options.
335 """
337 def _update_header(header: astropy.io.fits.Header) -> None:
338 update_header(header)
339 if self.unit is not None:
340 try:
341 header["BUNIT"] = self.unit.to_string(format="fits")
342 except ValueError:
343 # Units not supported by FITS; write it anyway because
344 # the accepted units are just a recommendation in the
345 # standard.
346 header["BUNIT"] = self.unit.to_string()
347 if self.sky_projection is not None and add_offset_wcs != " ":
348 if self.fits_wcs:
349 header.update(self.fits_wcs.to_header(relax=True))
350 if add_offset_wcs is not None: 350 ↛ exitline 350 didn't return from function '_update_header' because the condition on line 350 was always true
351 fits.add_offset_wcs(header, x=self.bbox.x.start, y=self.bbox.y.start, key=add_offset_wcs)
353 array_model = archive.add_array(
354 self.array, update_header=_update_header, tile_shape=tile_shape, options_name=options_name
355 )
356 serialized_projection: SkyProjectionSerializationModel[P] | None = None
357 if save_projection and self.sky_projection is not None:
358 serialized_projection = archive.serialize_direct("sky_projection", self.sky_projection.serialize)
359 data = array_model if self.unit is None else array_model.with_units(self.unit)
360 return ImageSerializationModel.model_construct(
361 data=data,
362 yx0=list(self.bbox.start),
363 sky_projection=serialized_projection,
364 metadata=self.metadata,
365 )
367 @staticmethod
368 def _get_archive_tree_type[P: pydantic.BaseModel](
369 pointer_type: type[P],
370 ) -> type[ImageSerializationModel[P]]:
371 """Return the serialization model type for this object for an archive
372 type that uses the given pointer type.
373 """
374 return ImageSerializationModel[pointer_type] # type: ignore
376 _archive_default_name: ClassVar[str] = "image"
377 """The name this object should be serialized with when written as the
378 top-level object.
379 """
381 @staticmethod
382 def from_legacy(
383 legacy: LegacyImage,
384 unit: astropy.units.UnitBase | None = None,
385 *,
386 sky_projection: SkyProjection[Any] | None = None,
387 ) -> Image:
388 """Convert from an `lsst.afw.image.Image` instance.
390 Parameters
391 ----------
392 legacy
393 An `lsst.afw.image.Image` instance that will share pixel data with
394 the returned object.
395 unit
396 Units of the image.
397 sky_projection
398 Projection from pixels to xky.
399 """
400 return Image(
401 legacy.array, yx0=YX(y=legacy.getY0(), x=legacy.getX0()), unit=unit, sky_projection=sky_projection
402 )
404 def to_legacy(self, *, copy: bool | None = None) -> LegacyImage:
405 """Convert to an `lsst.afw.image.Image` instance.
407 Parameters
408 ----------
409 copy
410 If `True`, always copy the pixel data. If `False`, return a view,
411 and raise `TypeError` if the pixel data is read-only (this is not
412 supported by afw). If `None`, only copy if the pixel data is
413 read-only.
414 """
415 import lsst.afw.image
416 import lsst.geom
418 array = self._array
419 if copy: 419 ↛ 420line 419 didn't jump to line 420 because the condition on line 419 was never true
420 array = array.copy()
421 elif not self._array.flags.writeable: 421 ↛ 422line 421 didn't jump to line 422 because the condition on line 421 was never true
422 if copy is None:
423 array = array.copy()
424 else:
425 raise TypeError("Cannot create a legacy lsst.afw.image.Image view into a read-only array.")
427 return lsst.afw.image.Image(
428 array,
429 deep=False,
430 dtype=array.dtype.type,
431 xy0=lsst.geom.Point2I(self._bbox.x.min, self._bbox.y.min),
432 )
434 @classmethod
435 def from_hdu_list(
436 cls,
437 hdu_list: astropy.io.fits.HDUList,
438 *,
439 fits_wcs_frame: Frame | None = DEFAULT_PIXEL_FRAME,
440 ) -> Image:
441 """Reconstruct an `~lsst.images.Image` from a cut-down ``lsst.images``
442 HDU list.
444 This reads only the first two HDUs (the primary HDU and the image
445 HDU), as written for the image-only cut-outs produced by
446 ``dax_images_cutout``: a real ``lsst.images`` file with its JSON-tree,
447 index, and any nested-archive HDUs dropped.
449 Parameters
450 ----------
451 hdu_list
452 HDU list whose first HDU is the primary header and whose second
453 HDU holds the image pixels.
454 fits_wcs_frame
455 Pixel-grid `~lsst.images.Frame` for the
456 `~lsst.images.SkyProjection` reconstructed from the image HDU's
457 FITS WCS. Defaults to a plain pixel frame; pass `None` to skip
458 attaching a projection.
460 Returns
461 -------
462 `~lsst.images.Image`
463 The reconstructed image, ready to be re-serialized as a normal
464 ``lsst.images`` file.
466 Notes
467 -----
468 The headers of the consumed HDUs are modified in place (WCS and other
469 interpreted cards are stripped), as in `read_legacy`.
470 """
471 opaque_metadata = fits.FitsOpaqueMetadata()
472 opaque_metadata.add_cutdown_primary_header(hdu_list[0].header)
473 result = cls._read_legacy_hdu(
474 hdu_list[1], opaque_metadata, preserve_bintable=None, fits_wcs_frame=fits_wcs_frame
475 )
476 result._opaque_metadata = opaque_metadata
477 return result
479 @staticmethod
480 def read_legacy(
481 uri: ResourcePathExpression,
482 *,
483 preserve_quantization: bool = False,
484 ext: str | int = 1,
485 fits_wcs_frame: Frame | None = None,
486 ) -> Image:
487 """Read a FITS file written by `lsst.afw.image.Image.writeFits`.
489 Parameters
490 ----------
491 uri
492 URI or file name.
493 preserve_quantization
494 If `True`, ensure that writing the image back out again will
495 exactly preserve quantization-compressed pixel values. This causes
496 the arrays to be marked as read-only and stores the original binary
497 table data for those planes in memory. If the `Image` is copied,
498 the precompressed pixel values are not transferred to the copy.
499 ext
500 Name or index of the FITS HDU to read.
501 fits_wcs_frame
502 If not `None` and the HDU containing the image has a FITS WCS,
503 attach a `SkyProjection` to the returned image by converting that
504 WCS.
505 """
506 opaque_metadata = fits.FitsOpaqueMetadata()
507 with ExitStack() as exit_stack:
508 fs, fspath = ResourcePath(uri).to_fsspec()
509 stream = exit_stack.enter_context(fs.open(fspath))
510 hdu_list = exit_stack.enter_context(astropy.io.fits.open(stream))
511 opaque_metadata.extract_legacy_primary_header(hdu_list[0].header)
512 bintable_hdu: astropy.io.fits.BinTableHDU | None = None
513 if preserve_quantization:
514 bintable_stream = exit_stack.enter_context(fs.open(fspath))
515 bintable_hdu_list = exit_stack.enter_context(
516 astropy.io.fits.open(bintable_stream, disable_image_compression=True)
517 )
518 bintable_hdu = bintable_hdu_list[ext]
519 result = Image._read_legacy_hdu(
520 hdu_list[ext], opaque_metadata, preserve_bintable=bintable_hdu, fits_wcs_frame=fits_wcs_frame
521 )
522 result._opaque_metadata = opaque_metadata
523 return result
525 @staticmethod
526 def _read_legacy_hdu(
527 hdu: astropy.io.fits.ImageHDU | astropy.io.fits.CompImageHDU,
528 opaque_metadata: fits.FitsOpaqueMetadata,
529 *,
530 preserve_bintable: astropy.io.fits.BinTableHDU | None,
531 fits_wcs_frame: Frame | None = None,
532 ) -> Image:
533 unit: astropy.units.UnitBase | None = None
534 if (fits_unit := hdu.header.pop("BUNIT", None)) is not None:
535 try:
536 unit = astropy.units.Unit(fits_unit, format="fits")
537 except ValueError:
538 # Accept non-FITS units by assuming Astropy can still figure
539 # them out if we don't specify the format.
540 unit = astropy.units.Unit(fits_unit)
541 if opaque_metadata.get_instrumental_unit() == astropy.units.electron: 541 ↛ 543line 541 didn't jump to line 543 because the condition on line 541 was never true
542 # Fix incorrect BUNIT='adu' in LSST preliminary_visit_image.
543 if unit == astropy.units.adu:
544 unit = astropy.units.electron
545 if unit == astropy.units.adu**2:
546 unit = astropy.units.electron**2
547 yx0 = fits.read_yx0(hdu.header)
548 hdu.header.remove("LTV1", ignore_missing=True)
549 hdu.header.remove("LTV2", ignore_missing=True)
550 read_only: bool = False
551 if preserve_bintable is not None: 551 ↛ 552line 551 didn't jump to line 552 because the condition on line 551 was never true
552 opaque_metadata.precompressed[hdu.name] = fits.PrecompressedImage.from_bintable(preserve_bintable)
553 read_only = True
554 sky_projection: SkyProjection | None = None
555 if fits_wcs_frame is not None:
556 try:
557 fits_wcs = astropy.wcs.WCS(hdu.header)
558 except KeyError:
559 pass
560 else:
561 sky_projection = SkyProjection.from_fits_wcs(
562 fits_wcs, pixel_frame=fits_wcs_frame, x0=yx0.x, y0=yx0.y
563 )
564 image = Image(hdu.data, yx0=yx0, unit=unit, sky_projection=sky_projection)
565 if read_only: 565 ↛ 566line 565 didn't jump to line 566 because the condition on line 565 was never true
566 image._array.flags["WRITEABLE"] = False
567 fits.strip_wcs_cards(hdu.header)
568 hdu.header.strip()
569 hdu.header.remove("EXTTYPE", ignore_missing=True)
570 hdu.header.remove("INHERIT", ignore_missing=True)
571 hdu.header.remove("UZSCALE", ignore_missing=True)
572 opaque_metadata.add_header(hdu.header)
573 return image
576class ImageSerializationModel[P: pydantic.BaseModel](ArchiveTree):
577 """Pydantic model used to represent the serialized form of an `.Image`."""
579 SCHEMA_NAME: ClassVar[str] = "image"
580 SCHEMA_VERSION: ClassVar[str] = "1.0.0"
581 MIN_READ_VERSION: ClassVar[int] = 1
582 PUBLIC_TYPE: ClassVar[type] = Image
584 data: ArrayReferenceQuantityModel | ArrayReferenceModel | InlineArrayModel | InlineArrayQuantityModel = (
585 pydantic.Field(description="Reference to pixel data.")
586 )
587 yx0: list[int] = pydantic.Field(
588 description="Coordinate of the first pixels in the array, ordered (y, x)."
589 )
590 sky_projection: SkyProjectionSerializationModel[P] | None = pydantic.Field(
591 default=None,
592 exclude_if=is_none,
593 description="Projection that maps the logical pixel grid onto the sky.",
594 )
596 @property
597 def bbox(self) -> Box:
598 """The bounding box of the image."""
599 match self.data:
600 case ArrayReferenceQuantityModel() | InlineArrayQuantityModel():
601 shape = self.data.value.shape
602 case ArrayReferenceModel() | InlineArrayModel(): 602 ↛ 604line 602 didn't jump to line 604 because the pattern on line 602 always matched
603 shape = self.data.shape
604 return Box.from_shape(shape, self.yx0)
606 def deserialize(
607 self,
608 archive: InputArchive[Any],
609 *,
610 bbox: Box | None = None,
611 strip_header: Callable[[astropy.io.fits.Header], None] = no_header_updates,
612 **kwargs: Any,
613 ) -> Image:
614 """Deserialize an image from an input archive.
616 Parameters
617 ----------
618 archive
619 Archive to read from.
620 bbox
621 Bounding box of a subimage to read instead.
622 strip_header
623 A callable that strips out any FITS header cards added by the
624 ``update_header`` argument in the corresponding call to
625 `Image.serialize`.
626 **kwargs
627 Unsupported keyword arguments are accepted only to provide better
628 error messages (raising `serialization.InvalidParameterError`).
629 """
630 if kwargs: 630 ↛ 631line 630 didn't jump to line 631 because the condition on line 630 was never true
631 raise InvalidParameterError(f"Unrecognized parameters for Image: {set(kwargs.keys())}.")
632 array_model: ArrayReferenceModel | InlineArrayModel
633 unit: astropy.units.UnitBase | None = None
634 if isinstance(self.data, ArrayReferenceQuantityModel | InlineArrayQuantityModel):
635 array_model = self.data.value
636 unit = self.data.unit
637 else:
638 array_model = self.data
640 def _strip_header(header: astropy.io.fits.Header) -> None:
641 if unit is not None:
642 header.pop("BUNIT", None)
643 fits.strip_wcs_cards(header)
644 strip_header(header)
646 slices = bbox.slice_within(self.bbox) if bbox is not None else ...
647 array = archive.get_array(array_model, strip_header=_strip_header, slices=slices)
648 sky_projection = self.sky_projection.deserialize(archive) if self.sky_projection is not None else None
649 return Image(
650 array,
651 yx0=self.yx0 if bbox is None else bbox.start,
652 unit=unit,
653 sky_projection=sky_projection,
654 )._finish_deserialize(self)
656 def deserialize_component(self, component: str, archive: InputArchive[Any], **kwargs: Any) -> Any:
657 if kwargs:
658 raise InvalidParameterError(f"Unsupported parameters for Image components: {set(kwargs.keys())}.")
659 return super().deserialize_component(component, archive)