Coverage for python / lsst / images / _generalized_image.py: 73%

46 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-03 00:49 -0800

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__ = ("AbsoluteSliceProxy", "GeneralizedImage", "LocalSliceProxy") 

15 

16from abc import ABC, abstractmethod 

17from functools import cached_property 

18from types import EllipsisType 

19from typing import Any, Self, TypeVar 

20 

21import astropy.wcs 

22 

23from ._geom import Box 

24from ._transforms import Projection, ProjectionAstropyView 

25 

26T = TypeVar("T", bound="GeneralizedImage") # for sphinx 

27 

28 

29class GeneralizedImage(ABC): 

30 """A base class for types that represent one or more 2-d image-like arrays 

31 with the same pixel grid and projection. 

32 """ 

33 

34 @property 

35 @abstractmethod 

36 def bbox(self) -> Box: 

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

38 raise NotImplementedError() 

39 

40 @property 

41 @abstractmethod 

42 def projection(self) -> Projection[Any] | None: 

43 """The projection that maps this image's pixel grid to the sky 

44 (`Projection` | `None`). 

45 

46 Notes 

47 ----- 

48 The pixel coordinates used by this projection account for the bounding 

49 box ``start``; they are not just array indices. 

50 """ 

51 raise NotImplementedError() 

52 

53 @property 

54 def astropy_wcs(self) -> ProjectionAstropyView | None: 

55 """An Astropy WCS for this image's pixel array. 

56 

57 Notes 

58 ----- 

59 As expected for Astropy WCS objects, this defines pixel coordinates 

60 such that the first row and column in any associated arrays are 

61 ``(0, 0)``, not ``bbox.start``, as is the case for `projection`. 

62 

63 This object satisfies the `astropy.wcs.wcsapi.BaseHighLevelWCS` and 

64 `astropy.wcs.wcsapi.BaseLowLevelWCS` interfaces, but it is not an 

65 `astropy.wcs.WCS` (use `fits_wcs` for that). 

66 """ 

67 return self.projection.as_astropy(self.bbox) if self.projection is not None else None 

68 

69 @cached_property 

70 def fits_wcs(self) -> astropy.wcs.WCS | None: 

71 """An Astropy FITS WCS for this image's pixel array. 

72 

73 Notes 

74 ----- 

75 As expected for Astropy WCS objects, this defines pixel coordinates 

76 such that the first row and column in any associated arrays are 

77 ``(0, 0)``, not ``bbox.start``, as is the case for `projection`. 

78 

79 This may be an approximation or absent if `projection` is not 

80 naturally representable as a FITS WCS. 

81 """ 

82 return ( 

83 self.projection.as_fits_wcs(self.bbox, allow_approximation=True) 

84 if self.projection is not None 

85 else None 

86 ) 

87 

88 @property 

89 def local(self) -> LocalSliceProxy[Self]: 

90 """A proxy object for slicing a generalized image using "local" or 

91 "array" pixel coordinates. 

92 

93 Notes 

94 ----- 

95 In this convention, the first row and column of the pixel grid is 

96 always at ``(0, 0)``. This is also the convention used by 

97 `astropy.wcs` objects. When a subimage is created from a parent image, 

98 its "local" coordinate system is offset from the original. 

99 

100 Note that most `lsst.images` types (e.g. `~lsst.images.Box`, 

101 `~lsst.images.Projection`, `~lsst.images.psfs.PointSpreadFunction`) 

102 operate instead in "absolute" coordinates, which is shared by subimage 

103 and their parents. 

104 

105 See Also 

106 -------- 

107 lsst.images.BoxSliceFactory 

108 lsst.images.IntervalSliceFactory 

109 """ 

110 return LocalSliceProxy(self) 

111 

112 @property 

113 def absolute(self) -> AbsoluteSliceProxy[Self]: 

114 """A proxy object for slicing a generalized image using absolute pixel 

115 coordinates. 

116 

117 Notes 

118 ----- 

119 In this convention, the first row and column of the pixel grid is 

120 ``bbox.start``. A subimage and its parent image share the same 

121 absolute pixel coordinate system, and most `lsst.images` types (e.g. 

122 `~lsst.images.Box`, `~lsst.images.Projection`, 

123 `~lsst.images.psfs.PointSpreadFunction`) operate exclusively in this 

124 system. 

125 

126 Note that `astropy.wcs` and `numpy.ndarray` are not aware of the 

127 ``bbox.start`` offset that defines tihs coordinates system; use 

128 `local` slicing for indices obtained from those. 

129 

130 See Also 

131 -------- 

132 lsst.images.BoxSliceFactory 

133 lsst.images.IntervalSliceFactory 

134 """ 

135 return AbsoluteSliceProxy(self) 

136 

137 # Subclasses should delegate to super().__getitem__ for some user-friendly 

138 # argument type-checking before providing their own implementation. 

139 @abstractmethod 

140 def __getitem__(self, bbox: Box | EllipsisType) -> Self: 

141 if not isinstance(bbox, Box): 

142 raise TypeError( 

143 "Only Box objects can be used to subset image objects directly; " 

144 "use .local[y, x] or .absolute[y, x] proxies for slice-based subsets." 

145 ) 

146 return self 

147 

148 @abstractmethod 

149 def copy(self) -> Self: 

150 """Deep-copy the image.""" 

151 raise NotImplementedError() 

152 

153 

154class LocalSliceProxy[T: GeneralizedImage]: 

155 """A proxy object for obtraining a generalized image subset using 

156 local slicing. 

157 

158 See `GeneralizedImage.local` for more information. 

159 """ 

160 

161 def __init__(self, parent: T): 

162 self._parent = parent 

163 

164 def __getitem__(self, slices: tuple[slice, slice]) -> T: 

165 return self._parent[self._parent.bbox.local[slices]] 

166 

167 

168class AbsoluteSliceProxy[T: GeneralizedImage]: 

169 """A proxy object for obtraining a generalized image subset using 

170 absolute slicing. 

171 

172 See `GeneralizedImage.absolute` for more information. 

173 """ 

174 

175 def __init__(self, parent: T): 

176 self._parent = parent 

177 

178 def __getitem__(self, slices: tuple[slice, slice]) -> T: 

179 return self._parent[self._parent.bbox.absolute[slices]]