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

72 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-11 01:19 -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, hasArrays): 

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

40 # MaskedImage 

41 inArrList = inView.getArrays() 

42 outArrList = outView.getArrays() 

43 else: 

44 inArrList = [inView.getArray()] 

45 outArrList = [outView.getArray()] 

46 

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

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

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

50 

51 

52def assembleAmplifierImage(destImage, rawImage, amplifier): 

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

54 

55 Parameters 

56 ---------- 

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

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

59 the assembled amplifier image. 

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

61 Raw image (same type as destImage). 

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

63 Amplifier geometry, with raw amplifier info. 

64 

65 Raises 

66 ------ 

67 RuntimeError 

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

69 """ 

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

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

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

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

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

75 

76 _insertPixelChunk(outView, inView, amplifier, 

77 hasattr(rawImage, "getArrays")) 

78 

79 

80def assembleAmplifierRawImage(destImage, rawImage, amplifier): 

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

82 

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

84 CCD image. 

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

86 image is a separate image. 

87 

88 Parameters 

89 ---------- 

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

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

92 is overwritten with the raw amplifier image. 

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

94 Raw image (same type as destImage). 

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

96 Amplifier geometry with raw amplifier info 

97 

98 Raises 

99 ------ 

100 RuntimeError 

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

102 """ 

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

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

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

106 inBBox = amplifier.getRawBBox() 

107 inView = rawImage.Factory(rawImage, inBBox) 

108 outBBox = amplifier.getRawBBox() 

109 outBBox.shift(amplifier.getRawXYOffset()) 

110 outView = destImage.Factory(destImage, outBBox) 

111 

112 _insertPixelChunk(outView, inView, amplifier, 

113 hasattr(rawImage, "getArrays")) 

114 

115 

116def makeUpdatedDetector(ccd): 

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

118 updated post assembly. 

119 

120 Parameters 

121 ---------- 

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

123 The detector to copy and update. 

124 """ 

125 builder = ccd.rebuild() 

126 for amp in builder.getAmplifiers(): 

127 amp.transform() 

128 return builder.finish() 

129 

130 

131class AmplifierIsolator: 

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

133 untrimmed assembled images and transforms them to a particular orientation 

134 and offset. 

135 

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

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

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

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

140 

141 Parameters 

142 ---------- 

143 amplifier : `Amplifier` 

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

145 orientation and offset of the returned subimage. 

146 parent_bbox : `lsst.geom.Box2I` 

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

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

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

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

151 trimmed. 

152 parent_detector : `Detector` 

153 Detector object that describes the parent image. 

154 """ 

155 

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

157 self._amplifier = amplifier 

158 self._parent_detector = parent_detector 

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

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

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

162 if self._is_parent_trimmed: 

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

164 # overscan regions for consistency. 

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

166 raise ValueError( 

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

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

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

170 ) 

171 else: 

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

173 # between the amplifiers modulo flips and offsets. 

174 if self._amplifier_comparison & self._amplifier_comparison.REGIONS_DIFFER: 

175 raise ValueError( 

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

177 "parent image's amplifier." 

178 ) 

179 

180 @property 

181 def subimage_bbox(self): 

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

183 (`lsst.geom.Box2I`). 

184 """ 

185 if self._is_parent_trimmed: 

186 return self._parent_amplifier.getBBox() 

187 else: 

188 return self._parent_amplifier.getRawBBox() 

189 

190 def transform_subimage(self, subimage): 

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

192 and offset of the target amplifier. 

193 

194 Parameters 

195 ---------- 

196 subimage : image-like 

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

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

199 `lsst.afw.image.Exposure`. 

200 

201 Returns 

202 ------- 

203 transformed : image-like 

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

205 """ 

206 from lsst.afw.math import flipImage 

207 if hasattr(subimage, "getMaskedImage"): 

208 # flipImage doesn't support Exposure natively. 

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

210 # so we need to make yet another copy. 

211 result = subimage.clone() 

212 result.setMaskedImage( 

213 flipImage( 

214 subimage.getMaskedImage(), 

215 self._amplifier_comparison & self._amplifier_comparison.FLIPPED_X, 

216 self._amplifier_comparison & self._amplifier_comparison.FLIPPED_Y, 

217 ) 

218 ) 

219 else: 

220 result = flipImage( 

221 subimage, 

222 self._amplifier_comparison & self._amplifier_comparison.FLIPPED_X, 

223 self._amplifier_comparison & self._amplifier_comparison.FLIPPED_Y, 

224 ) 

225 if self._is_parent_trimmed: 

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

227 else: 

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

229 return result 

230 

231 def make_detector(self): 

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

233 subimage. 

234 

235 Returns 

236 ------- 

237 detector : `Detector` 

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

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

240 """ 

241 detector = self._parent_detector.rebuild() 

242 detector.clear() 

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

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

245 detector.unsetCrosstalk() 

246 return detector.finish() 

247 

248 @classmethod 

249 def apply(cls, parent_exposure, amplifier): 

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

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

252 

253 Parameters 

254 ---------- 

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

256 Parent image to obtain a subset from. 

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

258 amplifier : `Amplifier` 

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

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

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

262 

263 Returns 

264 ------- 

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

266 Exposure subimage for the target amplifier, with the 

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

268 detector holding a copy of that amplifier. 

269 

270 Notes 

271 ----- 

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

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

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

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

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

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

278 to XY0 may break. 

279 """ 

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

281 parent_detector=parent_exposure.getDetector()) 

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

283 result.setDetector(instance.make_detector()) 

284 return result