Coverage for python/lsst/images/_image.py: 63%

233 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-09-30 11:30 +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. 

11 

12from __future__ import annotations 

13 

14__all__ = ("Image", "ImageSerializationModel") 

15 

16from collections.abc import Callable, Sequence 

17from contextlib import ExitStack 

18from types import EllipsisType 

19from typing import TYPE_CHECKING, Any, ClassVar, final 

20 

21import astropy.io.fits 

22import astropy.units 

23import astropy.wcs 

24import numpy as np 

25import numpy.typing as npt 

26import pydantic 

27 

28from lsst.resources import ResourcePath, ResourcePathExpression 

29 

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 

48 

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] 

54 

55 

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""" 

61 

62 

63@final 

64class Image(GeneralizedImage): 

65 """A 2-d array that may be augmented with units and a nonzero origin. 

66 

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. 

91 

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. 

98 

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:: 

103 

104 a[box] = b 

105 

106 is a shortcut for 

107 

108 a[box].quantity = b[box].quantity 

109 

110 An ellipsis (``...``) can be used instead of a `Box` to assign to the full 

111 image. 

112 """ 

113 

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 

153 

154 @property 

155 def array(self) -> np.ndarray: 

156 """The low-level array (`numpy.ndarray`). 

157 

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 

162 

163 @array.setter 

164 def array(self, value: np.ndarray | int | float) -> None: 

165 self._array[...] = value 

166 

167 @property 

168 def quantity(self) -> astropy.units.Quantity: 

169 """The low-level array with units (`astropy.units.Quantity`). 

170 

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) 

175 

176 @quantity.setter 

177 def quantity(self, value: astropy.units.Quantity) -> None: 

178 self.quantity[...] = value 

179 

180 @property 

181 def bbox(self) -> Box: 

182 """Bounding box for the image (`Box`).""" 

183 return self._bbox 

184 

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 

191 

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`). 

196 

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 

203 

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 ) 

210 

211 def __setitem__(self, bbox: Box | EllipsisType, value: Image) -> None: 

212 self[bbox].quantity[...] = value.quantity 

213 

214 def _describe(self, options: DescribeOptions = DescribeOptions(), /) -> Report: 

215 """Return a `Report` describing this image. 

216 

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 ) 

252 

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 ) 

261 

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 ) 

267 

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. 

276 

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 ...: 

290 unit = self._unit 

291 if sky_projection is ...: 

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)) 

296 

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. 

308 

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 """ 

336 

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) 

352 

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 ) 

366 

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 

375 

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 """ 

380 

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. 

389 

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 ) 

403 

404 def to_legacy(self, *, copy: bool | None = None) -> LegacyImage: 

405 """Convert to an `lsst.afw.image.Image` instance. 

406 

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 

417 

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.") 

426 

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 ) 

433 

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. 

443 

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. 

448 

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. 

459 

460 Returns 

461 ------- 

462 `~lsst.images.Image` 

463 The reconstructed image, ready to be re-serialized as a normal 

464 ``lsst.images`` file. 

465 

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 

478 

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`. 

488 

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 native_metadata = opaque_metadata.extract_legacy_primary_header(hdu_list[0].header) 

512 bintable_hdu: astropy.io.fits.BinTableHDU | None = None 

513 if preserve_quantization: 513 ↛ 514line 513 didn't jump to line 514 because the condition on line 513 was never true

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 result._metadata = native_metadata 

524 return result 

525 

526 @staticmethod 

527 def _read_legacy_hdu( 

528 hdu: astropy.io.fits.ImageHDU | astropy.io.fits.CompImageHDU, 

529 opaque_metadata: fits.FitsOpaqueMetadata, 

530 *, 

531 preserve_bintable: astropy.io.fits.BinTableHDU | None, 

532 fits_wcs_frame: Frame | None = None, 

533 ) -> Image: 

534 unit: astropy.units.UnitBase | None = None 

535 if (fits_unit := hdu.header.pop("BUNIT", None)) is not None: 

536 try: 

537 unit = astropy.units.Unit(fits_unit, format="fits") 

538 except ValueError: 

539 # Accept non-FITS units by assuming Astropy can still figure 

540 # them out if we don't specify the format. 

541 unit = astropy.units.Unit(fits_unit) 

542 if opaque_metadata.get_instrumental_unit() == astropy.units.electron: 542 ↛ 544line 542 didn't jump to line 544 because the condition on line 542 was never true

543 # Fix incorrect BUNIT='adu' in LSST preliminary_visit_image. 

544 if unit == astropy.units.adu: 

545 unit = astropy.units.electron 

546 if unit == astropy.units.adu**2: 

547 unit = astropy.units.electron**2 

548 yx0 = fits.read_yx0(hdu.header) 

549 hdu.header.remove("LTV1", ignore_missing=True) 

550 hdu.header.remove("LTV2", ignore_missing=True) 

551 read_only: bool = False 

552 if preserve_bintable is not None: 552 ↛ 553line 552 didn't jump to line 553 because the condition on line 552 was never true

553 opaque_metadata.precompressed[hdu.name] = fits.PrecompressedImage.from_bintable(preserve_bintable) 

554 read_only = True 

555 sky_projection: SkyProjection | None = None 

556 if fits_wcs_frame is not None: 

557 try: 

558 fits_wcs = astropy.wcs.WCS(hdu.header) 

559 except KeyError: 

560 pass 

561 else: 

562 sky_projection = SkyProjection.from_fits_wcs( 

563 fits_wcs, pixel_frame=fits_wcs_frame, x0=yx0.x, y0=yx0.y 

564 ) 

565 # FITS data is big-endian; uncompressed HDUs are returned that way. 

566 array = hdu.data.astype(hdu.data.dtype.newbyteorder("="), copy=False) 

567 image = Image(array, yx0=yx0, unit=unit, sky_projection=sky_projection) 

568 if read_only: 568 ↛ 569line 568 didn't jump to line 569 because the condition on line 568 was never true

569 image._array.flags["WRITEABLE"] = False 

570 fits.strip_wcs_cards(hdu.header) 

571 hdu.header.strip() 

572 hdu.header.remove("EXTTYPE", ignore_missing=True) 

573 hdu.header.remove("INHERIT", ignore_missing=True) 

574 hdu.header.remove("UZSCALE", ignore_missing=True) 

575 opaque_metadata.add_header(hdu.header) 

576 return image 

577 

578 

579class ImageSerializationModel[P: pydantic.BaseModel](ArchiveTree): 

580 """Pydantic model used to represent the serialized form of an `.Image`.""" 

581 

582 SCHEMA_NAME: ClassVar[str] = "image" 

583 SCHEMA_VERSION: ClassVar[str] = "1.0.0" 

584 MIN_READ_VERSION: ClassVar[int] = 1 

585 PUBLIC_TYPE: ClassVar[type] = Image 

586 

587 data: ArrayReferenceQuantityModel | ArrayReferenceModel | InlineArrayModel | InlineArrayQuantityModel = ( 

588 pydantic.Field(description="Reference to pixel data.") 

589 ) 

590 yx0: list[int] = pydantic.Field( 

591 description="Coordinate of the first pixels in the array, ordered (y, x)." 

592 ) 

593 sky_projection: SkyProjectionSerializationModel[P] | None = pydantic.Field( 

594 default=None, 

595 exclude_if=is_none, 

596 description="Projection that maps the logical pixel grid onto the sky.", 

597 ) 

598 

599 @property 

600 def bbox(self) -> Box: 

601 """The bounding box of the image.""" 

602 match self.data: 

603 case ArrayReferenceQuantityModel() | InlineArrayQuantityModel(): 

604 shape = self.data.value.shape 

605 case ArrayReferenceModel() | InlineArrayModel(): 605 ↛ 607line 605 didn't jump to line 607 because the pattern on line 605 always matched

606 shape = self.data.shape 

607 return Box.from_shape(shape, self.yx0) 

608 

609 def deserialize( 

610 self, 

611 archive: InputArchive[Any], 

612 *, 

613 bbox: Box | None = None, 

614 strip_header: Callable[[astropy.io.fits.Header], None] = no_header_updates, 

615 **kwargs: Any, 

616 ) -> Image: 

617 """Deserialize an image from an input archive. 

618 

619 Parameters 

620 ---------- 

621 archive 

622 Archive to read from. 

623 bbox 

624 Bounding box of a subimage to read instead. 

625 strip_header 

626 A callable that strips out any FITS header cards added by the 

627 ``update_header`` argument in the corresponding call to 

628 `Image.serialize`. 

629 **kwargs 

630 Unsupported keyword arguments are accepted only to provide better 

631 error messages (raising `serialization.InvalidParameterError`). 

632 """ 

633 if kwargs: 633 ↛ 634line 633 didn't jump to line 634 because the condition on line 633 was never true

634 raise InvalidParameterError(f"Unrecognized parameters for Image: {set(kwargs.keys())}.") 

635 array_model: ArrayReferenceModel | InlineArrayModel 

636 unit: astropy.units.UnitBase | None = None 

637 if isinstance(self.data, ArrayReferenceQuantityModel | InlineArrayQuantityModel): 

638 array_model = self.data.value 

639 unit = self.data.unit 

640 else: 

641 array_model = self.data 

642 

643 def _strip_header(header: astropy.io.fits.Header) -> None: 

644 if unit is not None: 

645 header.pop("BUNIT", None) 

646 fits.strip_wcs_cards(header) 

647 strip_header(header) 

648 

649 slices = bbox.slice_within(self.bbox) if bbox is not None else ... 

650 array = archive.get_array(array_model, strip_header=_strip_header, slices=slices) 

651 sky_projection = self.sky_projection.deserialize(archive) if self.sky_projection is not None else None 

652 return Image( 

653 array, 

654 yx0=self.yx0 if bbox is None else bbox.start, 

655 unit=unit, 

656 sky_projection=sky_projection, 

657 )._finish_deserialize(self) 

658 

659 def deserialize_component(self, component: str, archive: InputArchive[Any], **kwargs: Any) -> Any: 

660 if kwargs: 

661 raise InvalidParameterError(f"Unsupported parameters for Image components: {set(kwargs.keys())}.") 

662 return super().deserialize_component(component, archive)