Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of obs_lsst 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://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 <http://www.gnu.org/licenses/>. 

21 

22__all__ = ("attachRawWcsFromBoresight", "fixAmpGeometry", "assembleUntrimmedCcd", 

23 "fixAmpsAndAssemble", "readRawAmps") 

24 

25import lsst.log 

26import lsst.afw.image as afwImage 

27from lsst.obs.base import bboxFromIraf, MakeRawVisitInfoViaObsInfo, createInitialSkyWcs 

28from lsst.geom import Box2I, Extent2I 

29from lsst.ip.isr import AssembleCcdTask 

30 

31logger = lsst.log.Log.getLogger("LsstCamAssembler") 

32 

33 

34def attachRawWcsFromBoresight(exposure): 

35 """Attach a WCS by extracting boresight, rotation, and camera geometry from 

36 an Exposure. 

37 

38 Parameters 

39 ---------- 

40 exposure : `lsst.afw.image.Exposure` 

41 Image object with attached metadata and detector components. 

42 

43 Return 

44 ------ 

45 attached : `bool` 

46 If True, a WCS component was successfully created and attached to 

47 ``exposure``. 

48 """ 

49 md = exposure.getMetadata() 

50 # Use the generic version since we do not have a mapper available to 

51 # tell us a specific translator to use. 

52 visitInfo = MakeRawVisitInfoViaObsInfo(logger)(md) 

53 exposure.getInfo().setVisitInfo(visitInfo) 

54 

55 if visitInfo.getBoresightRaDec().isFinite(): 

56 exposure.setWcs(createInitialSkyWcs(visitInfo, exposure.getDetector())) 

57 return True 

58 

59 return False 

60 

61 

62def fixAmpGeometry(inAmp, bbox, metadata, logCmd=None): 

63 """Make sure a camera geometry amplifier matches an on-disk bounding box. 

64 

65 Bounding box differences that are consistent with differences in overscan 

66 regions are assumed to be overscan regions, which gives us enough 

67 information to correct the camera geometry. 

68 

69 Parameters 

70 ---------- 

71 inAmp : `lsst.afw.cameraGeom.Amplifier` 

72 Amplifier description from camera geometry. 

73 bbox : `lsst.geom.Box2I` 

74 The on-disk bounding box of the amplifer image. 

75 metadata : `lsst.daf.base.PropertyList` 

76 FITS header metadata from the amplifier HDU. 

77 logCmd : `function`, optional 

78 Call back to use to issue log messages. Arguments to this function 

79 should match arguments to be accepted by normal logging functions. 

80 

81 Return 

82 ------ 

83 outAmp : `~lsst.afw.cameraGeom.Amplifier.Builder` 

84 modified : `bool` 

85 `True` if ``amp`` was modified; `False` otherwise. 

86 

87 Raises 

88 ------ 

89 RuntimeError 

90 Raised if the bounding boxes differ in a way that is not consistent 

91 with just a change in overscan. 

92 """ 

93 if logCmd is None: 

94 # Define a null log command 

95 def logCmd(*args): 

96 return 

97 

98 modified = False 

99 

100 outAmp = inAmp.rebuild() 

101 if outAmp.getRawBBox() != bbox: # Oh dear. cameraGeom is wrong -- probably overscan 

102 if outAmp.getRawDataBBox().getDimensions() != outAmp.getBBox().getDimensions(): 

103 raise RuntimeError("Active area is the wrong size: %s v. %s" % 

104 (outAmp.getRawDataBBox().getDimensions(), outAmp.getBBox().getDimensions())) 

105 

106 logCmd("outAmp.getRawBBox() != data.getBBox(); patching. (%s v. %s)", outAmp.getRawBBox(), bbox) 

107 

108 w, h = bbox.getDimensions() 

109 ow, oh = outAmp.getRawBBox().getDimensions() # "old" (cameraGeom) dimensions 

110 # 

111 # We could trust the BIASSEC keyword, or we can just assume that 

112 # they've changed the number of overscan pixels (serial and/or 

113 # parallel). As Jim Chiang points out, the latter is safer 

114 # 

115 fromCamGeom = outAmp.getRawHorizontalOverscanBBox() 

116 hOverscanBBox = Box2I(fromCamGeom.getBegin(), 

117 Extent2I(w - fromCamGeom.getBeginX(), fromCamGeom.getHeight())) 

118 fromCamGeom = outAmp.getRawVerticalOverscanBBox() 

119 vOverscanBBox = Box2I(fromCamGeom.getBegin(), 

120 Extent2I(fromCamGeom.getWidth(), h - fromCamGeom.getBeginY())) 

121 outAmp.setRawBBox(bbox) 

122 outAmp.setRawHorizontalOverscanBBox(hOverscanBBox) 

123 outAmp.setRawVerticalOverscanBBox(vOverscanBBox) 

124 # 

125 # This gets all the geometry right for the amplifier, but the size 

126 # of the untrimmed image will be wrong and we'll put the amp sections 

127 # in the wrong places, i.e. 

128 # outAmp.getRawXYOffset() 

129 # will be wrong. So we need to recalculate the offsets. 

130 # 

131 xRawExtent, yRawExtent = outAmp.getRawBBox().getDimensions() 

132 

133 x0, y0 = outAmp.getRawXYOffset() 

134 ix, iy = x0//ow, y0/oh 

135 x0, y0 = ix*xRawExtent, iy*yRawExtent 

136 outAmp.setRawXYOffset(Extent2I(ix*xRawExtent, iy*yRawExtent)) 

137 

138 modified = True 

139 

140 # 

141 # Check the "IRAF" keywords, but don't abort if they're wrong 

142 # 

143 # Only warn about the first amp, use debug for the others 

144 # 

145 d = metadata.toDict() 

146 detsec = bboxFromIraf(d["DETSEC"]) if "DETSEC" in d else None 

147 datasec = bboxFromIraf(d["DATASEC"]) if "DATASEC" in d else None 

148 biassec = bboxFromIraf(d["BIASSEC"]) if "BIASSEC" in d else None 

149 

150 if detsec and outAmp.getBBox() != detsec: 

151 logCmd("DETSEC doesn't match (%s != %s)", outAmp.getBBox(), detsec) 

152 if datasec and outAmp.getRawDataBBox() != datasec: 

153 logCmd("DATASEC doesn't match for (%s != %s)", outAmp.getRawDataBBox(), detsec) 

154 if biassec and outAmp.getRawHorizontalOverscanBBox() != biassec: 

155 logCmd("BIASSEC doesn't match for (%s != %s)", outAmp.getRawHorizontalOverscanBBox(), detsec) 

156 

157 return outAmp, modified 

158 

159 

160def assembleUntrimmedCcd(ccd, exposures): 

161 """Assemble an untrimmmed CCD from per-amp Exposure objects. 

162 

163 Parameters 

164 ---------- 

165 ccd : `~lsst.afw.cameraGeom.Detector` 

166 The detector geometry for this ccd that will be used as the 

167 framework for the assembly of the input amplifier exposures. 

168 exposures : sequence of `lsst.afw.image.Exposure` 

169 Per-amplifier images, in the same order as ``amps``. 

170 

171 Returns 

172 ------- 

173 ccd : `lsst.afw.image.Exposure` 

174 Assembled CCD image. 

175 """ 

176 ampDict = {} 

177 for amp, exposure in zip(ccd, exposures): 

178 ampDict[amp.getName()] = exposure 

179 config = AssembleCcdTask.ConfigClass() 

180 config.doTrim = False 

181 assembleTask = AssembleCcdTask(config=config) 

182 return assembleTask.assembleCcd(ampDict) 

183 

184 

185def fixAmpsAndAssemble(ampExps, msg): 

186 """Fix amp geometry and assemble into exposure. 

187 

188 Parameters 

189 ---------- 

190 ampExps : sequence of `lsst.afw.image.Exposure` 

191 Per-amplifier images. 

192 msg : `str` 

193 Message to add to log and exception output. 

194 

195 Returns 

196 ------- 

197 exposure : `lsst.afw.image.Exposure` 

198 Exposure with the amps combined into a single image. 

199 

200 Notes 

201 ----- 

202 The returned exposure does not have any metadata or WCS attached. 

203 

204 """ 

205 if not len(ampExps): 

206 raise RuntimeError(f"Unable to read raw_amps for {msg}") 

207 

208 ccd = ampExps[0].getDetector() # the same (full, CCD-level) Detector is attached to all ampExps 

209 # 

210 # Check that the geometry in the metadata matches cameraGeom 

211 # 

212 warned = False 

213 

214 def logCmd(s, *args): 

215 nonlocal warned 

216 if warned: 

217 logger.debug(f"{msg}: {s}", *args) 

218 else: 

219 logger.warn(f"{msg}: {s}", *args) 

220 warned = True 

221 

222 # Rebuild the detector and the amplifiers to use their corrected geometry. 

223 tempCcd = ccd.rebuild() 

224 tempCcd.clear() 

225 for amp, ampExp in zip(ccd, ampExps): 

226 outAmp, modified = fixAmpGeometry(amp, 

227 bbox=ampExp.getBBox(), 

228 metadata=ampExp.getMetadata(), 

229 logCmd=logCmd) 

230 tempCcd.append(outAmp) 

231 

232 ccd = tempCcd.finish() 

233 

234 # Update the data to be combined to point to the newly rebuilt detector. 

235 for ampExp in ampExps: 

236 ampExp.setDetector(ccd) 

237 

238 exposure = assembleUntrimmedCcd(ccd, ampExps) 

239 return exposure 

240 

241 

242def readRawAmps(fileName, detector): 

243 """Given a file name read the amps and attach the detector. 

244 

245 Parameters 

246 ---------- 

247 fileName : `str` 

248 The full path to a file containing data from a single CCD. 

249 detector : `lsst.afw.cameraGeom.Detector` 

250 Detector to associate with the amps. 

251 

252 Returns 

253 ------- 

254 ampExps : `list` of `lsst.afw.image.Exposure` 

255 All the individual amps read from the file. 

256 """ 

257 amps = [] 

258 for hdu in range(1, 16+1): 

259 exp = afwImage.makeExposure(afwImage.makeMaskedImage(afwImage.ImageF(fileName, hdu=hdu))) 

260 exp.setDetector(detector) 

261 amps.append(exp) 

262 return amps