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

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

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

524 

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 

574 

575 

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

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

578 

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 

583 

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 ) 

595 

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) 

605 

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. 

615 

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 

639 

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) 

645 

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) 

655 

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)