Coverage for python/lsst/obs/lsst/assembly.py : 11%

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/>.
22__all__ = ("attachRawWcsFromBoresight", "fixAmpGeometry", "assembleUntrimmedCcd",
23 "fixAmpsAndAssemble", "readRawAmps")
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
31logger = lsst.log.Log.getLogger("LsstCamAssembler")
34def attachRawWcsFromBoresight(exposure):
35 """Attach a WCS by extracting boresight, rotation, and camera geometry from
36 an Exposure.
38 Parameters
39 ----------
40 exposure : `lsst.afw.image.Exposure`
41 Image object with attached metadata and detector components.
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)
55 if visitInfo.getBoresightRaDec().isFinite():
56 exposure.setWcs(createInitialSkyWcs(visitInfo, exposure.getDetector()))
57 return True
59 return False
62def fixAmpGeometry(inAmp, bbox, metadata, logCmd=None):
63 """Make sure a camera geometry amplifier matches an on-disk bounding box.
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.
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.
81 Return
82 ------
83 outAmp : `~lsst.afw.cameraGeom.Amplifier.Builder`
84 modified : `bool`
85 `True` if ``amp`` was modified; `False` otherwise.
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
98 modified = False
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()))
106 logCmd("outAmp.getRawBBox() != data.getBBox(); patching. (%s v. %s)", outAmp.getRawBBox(), bbox)
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()
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))
138 modified = True
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
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)
157 return outAmp, modified
160def assembleUntrimmedCcd(ccd, exposures):
161 """Assemble an untrimmmed CCD from per-amp Exposure objects.
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``.
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)
185def fixAmpsAndAssemble(ampExps, msg):
186 """Fix amp geometry and assemble into exposure.
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.
195 Returns
196 -------
197 exposure : `lsst.afw.image.Exposure`
198 Exposure with the amps combined into a single image.
200 Notes
201 -----
202 The returned exposure does not have any metadata or WCS attached.
204 """
205 if not len(ampExps):
206 raise RuntimeError(f"Unable to read raw_amps for {msg}")
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
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
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)
232 ccd = tempCcd.finish()
234 # Update the data to be combined to point to the newly rebuilt detector.
235 for ampExp in ampExps:
236 ampExp.setDetector(ccd)
238 exposure = assembleUntrimmedCcd(ccd, ampExps)
239 return exposure
242def readRawAmps(fileName, detector):
243 """Given a file name read the amps and attach the detector.
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.
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