Coverage for python/lsst/afw/cameraGeom/_assembleImage.py: 18%

72 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-19 04:04 -0700

1# This file is part of afw. 

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# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22__all__ = ['assembleAmplifierImage', 'assembleAmplifierRawImage', 

23 'makeUpdatedDetector', 'AmplifierIsolator'] 

24 

25# dict of doFlip: slice 

26_SliceDict = { 

27 False: slice(None, None, 1), 

28 True: slice(None, None, -1), 

29} 

30 

31 

32def _insertPixelChunk(outView, inView, amplifier): 

33 # For the sake of simplicity and robustness, this code does not short-circuit the case flipX=flipY=False. 

34 # However, it would save a bit of time, including the cost of making numpy array views. 

35 # If short circuiting is wanted, do it here. 

36 

37 xSlice = _SliceDict[amplifier.getRawFlipX()] 

38 ySlice = _SliceDict[amplifier.getRawFlipY()] 

39 if hasattr(inView, "image"): 

40 inArrList = (inView.image.array, inView.mask.array, inView.variance.array) 

41 outArrList = (outView.image.array, outView.mask.array, outView.variance.array) 

42 else: 

43 inArrList = [inView.array] 

44 outArrList = [outView.array] 

45 

46 for inArr, outArr in zip(inArrList, outArrList): 

47 # y,x because numpy arrays are transposed w.r.t. afw Images 

48 outArr[:] = inArr[ySlice, xSlice] 

49 

50 

51def assembleAmplifierImage(destImage, rawImage, amplifier): 

52 """Assemble the amplifier region of an image from a raw image. 

53 

54 Parameters 

55 ---------- 

56 destImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` 

57 Assembled image; the region amplifier.getBBox() is overwritten with 

58 the assembled amplifier image. 

59 rawImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` 

60 Raw image (same type as destImage). 

61 amplifier : `lsst.afw.cameraGeom.Amplifier` 

62 Amplifier geometry, with raw amplifier info. 

63 

64 Raises 

65 ------ 

66 RuntimeError 

67 Raised if image types do not match or amplifier has no raw amplifier info. 

68 """ 

69 if type(destImage.Factory) != type(rawImage.Factory): # noqa: E721 

70 raise RuntimeError(f"destImage type = {type(destImage.Factory).__name__} != " 

71 f"{type(rawImage.Factory).__name__} = rawImage type") 

72 inView = rawImage.Factory(rawImage, amplifier.getRawDataBBox()) 

73 outView = destImage.Factory(destImage, amplifier.getBBox()) 

74 

75 _insertPixelChunk(outView, inView, amplifier) 

76 

77 

78def assembleAmplifierRawImage(destImage, rawImage, amplifier): 

79 """Assemble the amplifier region of a raw CCD image. 

80 

81 For most cameras this is a no-op: the raw image already is an assembled 

82 CCD image. 

83 However, it is useful for camera such as LSST for which each amplifier 

84 image is a separate image. 

85 

86 Parameters 

87 ---------- 

88 destImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` 

89 CCD Image; the region amplifier.getRawAmplifier().getBBox() 

90 is overwritten with the raw amplifier image. 

91 rawImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` 

92 Raw image (same type as destImage). 

93 amplifier : `lsst.afw.cameraGeom.Amplifier` 

94 Amplifier geometry with raw amplifier info 

95 

96 Raises 

97 ------ 

98 RuntimeError 

99 Raised if image types do not match or amplifier has no raw amplifier info. 

100 """ 

101 if type(destImage.Factory) != type(rawImage.Factory): # noqa: E721 

102 raise RuntimeError(f"destImage type = {type(destImage.Factory).__name__} != " 

103 f"{type(rawImage.Factory).__name__} = rawImage type") 

104 inBBox = amplifier.getRawBBox() 

105 inView = rawImage.Factory(rawImage, inBBox) 

106 outBBox = amplifier.getRawBBox() 

107 outBBox.shift(amplifier.getRawXYOffset()) 

108 outView = destImage.Factory(destImage, outBBox) 

109 

110 _insertPixelChunk(outView, inView, amplifier) 

111 

112 

113def makeUpdatedDetector(ccd): 

114 """Return a Detector that has had the definitions of amplifier geometry 

115 updated post assembly. 

116 

117 Parameters 

118 ---------- 

119 ccd : `lsst.afw.image.Detector` 

120 The detector to copy and update. 

121 """ 

122 builder = ccd.rebuild() 

123 for amp in builder.getAmplifiers(): 

124 amp.transform() 

125 return builder.finish() 

126 

127 

128class AmplifierIsolator: 

129 """A class that can extracts single-amplifier subimages from trimmed or 

130 untrimmed assembled images and transforms them to a particular orientation 

131 and offset. 

132 

133 Callers who have a in-memory assembled `lsst.afw.image.Exposure` should 

134 generally just use the `apply` class method. Other methods can be used to 

135 implement subimage loads of on on-disk images (e.g. formatter classes in 

136 ``obs_base``) or obtain subsets from other image classes. 

137 

138 Parameters 

139 ---------- 

140 amplifier : `Amplifier` 

141 Amplifier object that identifies the amplifier to load and sets the 

142 orientation and offset of the returned subimage. 

143 parent_bbox : `lsst.geom.Box2I` 

144 Bounding box of the assembled parent image. This must be equal to 

145 either ``parent_detector.getBBox()`` or 

146 ``parent_detector.getRawBBox()``; which one is used to determine 

147 whether the parent image (and hence the amplifier subimages) is 

148 trimmed. 

149 parent_detector : `Detector` 

150 Detector object that describes the parent image. 

151 """ 

152 

153 def __init__(self, amplifier, parent_bbox, parent_detector): 

154 self._amplifier = amplifier 

155 self._parent_detector = parent_detector 

156 self._parent_amplifier = self._parent_detector[self._amplifier.getName()] 

157 self._is_parent_trimmed = (parent_bbox == self._parent_detector.getBBox()) 

158 self._amplifier_comparison = self._amplifier.compareGeometry(self._parent_amplifier) 

159 if self._is_parent_trimmed: 

160 # We only care about the final bounding box; don't check e.g. 

161 # overscan regions for consistency. 

162 if self._parent_amplifier.getBBox() != self._amplifier.getBBox(): 

163 raise ValueError( 

164 f"The given amplifier's trimmed bounding box ({self._amplifier.getBBox()}) is not the " 

165 "same as the trimmed bounding box of the same amplifier in the parent image " 

166 f"({self._parent_amplifier.getBBox()})." 

167 ) 

168 else: 

169 # Parent is untrimmed, so we need all regions to be consistent 

170 # between the amplifiers modulo flips and offsets. 

171 if self._amplifier_comparison & self._amplifier_comparison.REGIONS_DIFFER: 

172 raise ValueError( 

173 "The given amplifier's subregions are fundamentally incompatible with those of the " 

174 "parent image's amplifier." 

175 ) 

176 

177 @property 

178 def subimage_bbox(self): 

179 """The bounding box of the target amplifier in the parent image 

180 (`lsst.geom.Box2I`). 

181 """ 

182 if self._is_parent_trimmed: 

183 return self._parent_amplifier.getBBox() 

184 else: 

185 return self._parent_amplifier.getRawBBox() 

186 

187 def transform_subimage(self, subimage): 

188 """Transform an already-extracted subimage to match the orientation 

189 and offset of the target amplifier. 

190 

191 Parameters 

192 ---------- 

193 subimage : image-like 

194 The subimage to transform; may be any of `lsst.afw.image.Image`, 

195 `lsst.afw.image.Mask`, `lsst.afw.image.MaskedImage`, and 

196 `lsst.afw.image.Exposure`. 

197 

198 Returns 

199 ------- 

200 transformed : image-like 

201 Transformed image of the same type as ``subimage``. 

202 """ 

203 from lsst.afw.math import flipImage 

204 if hasattr(subimage, "getMaskedImage"): 

205 # flipImage doesn't support Exposure natively. 

206 # And sadly, there's no way to write to an existing MaskedImage, 

207 # so we need to make yet another copy. 

208 result = subimage.clone() 

209 result.setMaskedImage( 

210 flipImage( 

211 subimage.getMaskedImage(), 

212 self._amplifier_comparison & self._amplifier_comparison.FLIPPED_X, 

213 self._amplifier_comparison & self._amplifier_comparison.FLIPPED_Y, 

214 ) 

215 ) 

216 else: 

217 result = flipImage( 

218 subimage, 

219 self._amplifier_comparison & self._amplifier_comparison.FLIPPED_X, 

220 self._amplifier_comparison & self._amplifier_comparison.FLIPPED_Y, 

221 ) 

222 if self._is_parent_trimmed: 

223 result.setXY0(self._amplifier.getBBox().getMin()) 

224 else: 

225 result.setXY0(self._amplifier.getRawBBox().getMin() + self._amplifier.getRawXYOffset()) 

226 return result 

227 

228 def make_detector(self): 

229 """Create a single-amplifier detector that describes the transformed 

230 subimage. 

231 

232 Returns 

233 ------- 

234 detector : `Detector` 

235 Detector object with a single amplifier, a trimmed bounding box 

236 equal to the amplifier's trimmed bounding box, and no crosstalk. 

237 """ 

238 detector = self._parent_detector.rebuild() 

239 detector.clear() 

240 detector.append(self._amplifier.rebuild()) 

241 detector.setBBox(self._amplifier.getBBox()) 

242 detector.unsetCrosstalk() 

243 return detector.finish() 

244 

245 @classmethod 

246 def apply(cls, parent_exposure, amplifier): 

247 """Obtain a single-amplifier `lsst.afw.image.Exposure` subimage that 

248 masquerades as full-detector image for a single-amp detector. 

249 

250 Parameters 

251 ---------- 

252 parent_exposure : `lsst.afw.image.Exposure` 

253 Parent image to obtain a subset from. 

254 `~lsst.afw.image.Exposure.getDetector` must not return `None`. 

255 amplifier : `Amplifier` 

256 Target amplifier for the subimage. May differ from the amplifier 

257 obtained by ``parent_exposure.getDetector()[amplifier.getName()]`` 

258 only by flips and differences in `~Amplifier.getRawXYOffset`. 

259 

260 Returns 

261 ------- 

262 subimage : `lsst.afw.image.Exposure` 

263 Exposure subimage for the target amplifier, with the 

264 orientation and XY0 described by that amplifier, and a single-amp 

265 detector holding a copy of that amplifier. 

266 

267 Notes 

268 ----- 

269 Because we use the target amplifier's bounding box as the bounding box 

270 of the detector attached to the returned exposure, other exposure 

271 components that are passed through unmodified (e.g. the WCS) should 

272 still be valid for the single-amp exposure after it is trimmed and 

273 "assembled". Unlike most trimmed+assembled images, however, it will 

274 have a nonzero XY0, and code that (incorrectly!) does not pay attention 

275 to XY0 may break. 

276 """ 

277 instance = cls(amplifier, parent_bbox=parent_exposure.getBBox(), 

278 parent_detector=parent_exposure.getDetector()) 

279 result = instance.transform_subimage(parent_exposure[instance.subimage_bbox]) 

280 result.setDetector(instance.make_detector()) 

281 return result