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

# 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(amp, 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 

---------- 

amp : `lsst.afw.table.AmpInfoRecord` 

Amplifier description from camera gemoetry. Will be modified in-place. 

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 

------ 

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: 

logCmd = lambda x, *args: None # noqa 

modified = False 

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

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

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

(amp.getRawDataBBox().getDimensions(), amp.getBBox().getDimensions())) 

 

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

 

w, h = bbox.getDimensions() 

ow, oh = amp.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 = amp.getRawHorizontalOverscanBBox() 

hOverscanBBox = Box2I(fromCamGeom.getBegin(), 

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

fromCamGeom = amp.getRawVerticalOverscanBBox() 

vOverscanBBox = Box2I(fromCamGeom.getBegin(), 

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

amp.setRawBBox(bbox) 

amp.setRawHorizontalOverscanBBox(hOverscanBBox) 

amp.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. 

# amp.getRawXYOffset() 

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

# 

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

 

x0, y0 = amp.getRawXYOffset() 

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

x0, y0 = ix*xRawExtent, iy*yRawExtent 

amp.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 amp.getBBox() != detsec: 

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

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

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

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

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

 

return modified 

 

 

def assembleUntrimmedCcd(amps, exposures): 

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

 

Parameters 

---------- 

amps : sequence of `lsst.afw.table.AmpInfoRecord`. 

A deterministically-ordered container of camera geometry amplifier 

information. May be a `~lsst.afw.cameraGeom.Detector`, a 

`~lsst.afw.table.AmpInfoCatalog`, a `list`, or any other sequence. 

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(amps, 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 

 

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

fixAmpGeometry(amp, bbox=ampExp.getBBox(), metadata=ampExp.getMetadata(), logCmd=logCmd) 

 

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