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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

# This file is part of obs_lsst 

# 

# Developed for the LSST Data Management System. 

# This product includes software developed by the LSST Project 

# (http://www.lsst.org). 

# See the COPYRIGHT file at the top-level directory of this distribution 

# for details of code ownership. 

# 

# This program is free software: you can redistribute it and/or modify 

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

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

# (at your option) any later version. 

# 

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

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

# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

# GNU General Public License for more details. 

# 

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

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

 

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

"fixAmpsAndAssemble", "readRawAmps") 

 

import lsst.log 

import lsst.afw.image as afwImage 

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

from lsst.geom import Box2I, Extent2I 

from lsst.ip.isr import AssembleCcdTask 

 

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

 

 

def attachRawWcsFromBoresight(exposure): 

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

an Exposure. 

 

Parameters 

---------- 

exposure : `lsst.afw.image.Exposure` 

Image object with attached metadata and detector components. 

 

Return 

------ 

attached : `bool` 

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

``exposure``. 

""" 

md = exposure.getMetadata() 

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

# tell us a specific translator to use. 

visitInfo = MakeRawVisitInfoViaObsInfo(logger)(md) 

exposure.getInfo().setVisitInfo(visitInfo) 

 

if visitInfo.getBoresightRaDec().isFinite(): 

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

return True 

 

return False 

 

 

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

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

 

Bounding box differences that are consistent with differences in overscan 

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

information to correct the camera geometry. 

 

Parameters 

---------- 

inAmp : `lsst.afw.cameraGeom.Amplifier` 

Amplifier description from camera geometry. 

bbox : `lsst.geom.Box2I` 

The on-disk bounding box of the amplifer image. 

metadata : `lsst.daf.base.PropertyList` 

FITS header metadata from the amplifier HDU. 

logCmd : `function`, optional 

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

should match arguments to be accepted by normal logging functions. 

 

Return 

------ 

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

modified : `bool` 

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

 

Raises 

------ 

RuntimeError 

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

with just a change in overscan. 

""" 

if logCmd is None: 

# Define a null log command 

def logCmd(*args): 

return 

 

modified = False 

 

outAmp = inAmp.rebuild() 

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

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

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

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

 

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

 

w, h = bbox.getDimensions() 

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

# 

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

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

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

# 

fromCamGeom = outAmp.getRawHorizontalOverscanBBox() 

hOverscanBBox = Box2I(fromCamGeom.getBegin(), 

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

fromCamGeom = outAmp.getRawVerticalOverscanBBox() 

vOverscanBBox = Box2I(fromCamGeom.getBegin(), 

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

outAmp.setRawBBox(bbox) 

outAmp.setRawHorizontalOverscanBBox(hOverscanBBox) 

outAmp.setRawVerticalOverscanBBox(vOverscanBBox) 

# 

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

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

# in the wrong places, i.e. 

# outAmp.getRawXYOffset() 

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

# 

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

 

x0, y0 = outAmp.getRawXYOffset() 

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

x0, y0 = ix*xRawExtent, iy*yRawExtent 

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

 

modified = True 

 

# 

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

# 

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

# 

d = metadata.toDict() 

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

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

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

 

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

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

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

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

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

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

 

return outAmp, modified 

 

 

def assembleUntrimmedCcd(ccd, exposures): 

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

 

Parameters 

---------- 

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

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

framework for the assembly of the input amplifier exposures. 

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

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

 

Returns 

------- 

ccd : `lsst.afw.image.Exposure` 

Assembled CCD image. 

""" 

ampDict = {} 

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

ampDict[amp.getName()] = exposure 

config = AssembleCcdTask.ConfigClass() 

config.doTrim = False 

assembleTask = AssembleCcdTask(config=config) 

return assembleTask.assembleCcd(ampDict) 

 

 

def fixAmpsAndAssemble(ampExps, msg): 

"""Fix amp geometry and assemble into exposure. 

 

Parameters 

---------- 

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

Per-amplifier images. 

msg : `str` 

Message to add to log and exception output. 

 

Returns 

------- 

exposure : `lsst.afw.image.Exposure` 

Exposure with the amps combined into a single image. 

 

Notes 

----- 

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

 

""" 

if not len(ampExps): 

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

 

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

# 

# Check that the geometry in the metadata matches cameraGeom 

# 

warned = False 

 

def logCmd(s, *args): 

nonlocal warned 

if warned: 

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

else: 

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

warned = True 

 

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

tempCcd = ccd.rebuild() 

tempCcd.clear() 

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

outAmp, modified = fixAmpGeometry(amp, 

bbox=ampExp.getBBox(), 

metadata=ampExp.getMetadata(), 

logCmd=logCmd) 

tempCcd.append(outAmp) 

 

ccd = tempCcd.finish() 

 

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

for ampExp in ampExps: 

ampExp.setDetector(ccd) 

 

exposure = assembleUntrimmedCcd(ccd, ampExps) 

return exposure 

 

 

def readRawAmps(fileName, detector): 

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

 

Parameters 

---------- 

fileName : `str` 

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

detector : `lsst.afw.cameraGeom.Detector` 

Detector to associate with the amps. 

 

Returns 

------- 

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

All the individual amps read from the file. 

""" 

amps = [] 

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

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

exp.setDetector(detector) 

amps.append(exp) 

return amps