24 This module contains a Task to register (align) multiple images.
26 from __future__
import absolute_import, division, print_function
27 from builtins
import range
29 __all__ = [
"RegisterTask",
"RegisterConfig"]
34 from lsst.pex.config
import Config, Field, ConfigField
35 from lsst.pipe.base
import Task, Struct
36 from lsst.meas.astrom.sip
import makeCreateWcsWithSip
37 from lsst.afw.math
import Warper
39 import lsst.afw.geom
as afwGeom
40 import lsst.afw.table
as afwTable
44 """Configuration for RegisterTask"""
45 matchRadius = Field(dtype=float, default=1.0, doc=
"Matching radius (arcsec)", check=
lambda x: x > 0)
46 sipOrder = Field(dtype=int, default=4, doc=
"Order for SIP WCS", check=
lambda x: x > 1)
47 sipIter = Field(dtype=int, default=3, doc=
"Rejection iterations for SIP WCS", check=
lambda x: x > 0)
48 sipRej = Field(dtype=float, default=3.0, doc=
"Rejection threshold for SIP WCS", check=
lambda x: x > 0)
49 warper = ConfigField(dtype=Warper.ConfigClass, doc=
"Configuration for warping")
54 Task to register (align) multiple images.
56 The 'run' method provides a revised Wcs from matches and fitting sources.
57 Additional methods are provided as a convenience to warp an exposure
58 ('warpExposure') and sources ('warpSources') with the new Wcs.
60 ConfigClass = RegisterConfig
62 def run(self, inputSources, inputWcs, inputBBox, templateSources):
63 """Register (align) an input exposure to the template
65 The sources must have RA,Dec set, and accurate to within the
66 'matchRadius' of the configuration in order to facilitate source
67 matching. We fit a new Wcs, but do NOT set it in the input exposure.
69 @param inputSources: Sources from input exposure
70 @param inputWcs: Wcs of input exposure
71 @param inputBBox: Bounding box of input exposure
72 @param templateSources: Sources from template exposure
73 @return Struct(matches: Matches between sources,
74 wcs: Wcs for input in frame of template,
77 matches = self.
matchSources(inputSources, templateSources)
78 wcs = self.
fitWcs(matches, inputWcs, inputBBox)
79 return Struct(matches=matches, wcs=wcs)
82 """Match sources between the input and template
84 The order of the input arguments matters (because the later Wcs
85 fitting assumes a particular order).
87 @param inputSources: Source catalog of the input frame
88 @param templateSources: Source of the target frame
91 matches = afwTable.matchRaDec(templateSources, inputSources,
92 self.config.matchRadius*afwGeom.arcseconds)
93 self.log.info(
"Matching within %.1f arcsec: %d matches" % (self.config.matchRadius, len(matches)))
94 self.metadata.set(
"MATCH_NUM", len(matches))
96 raise RuntimeError(
"Unable to match source catalogs")
99 def fitWcs(self, matches, inputWcs, inputBBox):
100 """Fit Wcs to matches
102 The fitting includes iterative sigma-clipping.
104 @param matches: List of matches (first is target, second is input)
105 @param inputWcs: Original input Wcs
106 @param inputBBox: Bounding box of input image
109 copyMatches = type(matches)(matches)
110 refCoordKey = copyMatches[0].first.getTable().getCoordKey()
111 inCentroidKey = copyMatches[0].second.getTable().getCentroidKey()
112 for i
in range(self.config.sipIter):
113 sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox)
114 self.log.debug(
"Registration WCS RMS iteration %d: %f pixels",
115 i, sipFit.getScatterInPixels())
116 wcs = sipFit.getNewWcs()
117 dr = [m.first.get(refCoordKey).angularSeparation(
118 wcs.pixelToSky(m.second.get(inCentroidKey))).asArcseconds()
for
121 rms = math.sqrt((dr*dr).mean())
122 rms = max(rms, 1.0e-9)
123 self.log.debug(
"Registration iteration %d: rms=%f", i, rms)
124 good = numpy.where(dr < self.config.sipRej*rms)[0]
125 numBad = len(copyMatches) - len(good)
126 self.log.debug(
"Registration iteration %d: rejected %d", i, numBad)
129 copyMatches = type(matches)(copyMatches[i]
for i
in good)
131 sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox)
132 self.log.info(
"Registration WCS: final WCS RMS=%f pixels from %d matches" %
133 (sipFit.getScatterInPixels(), len(copyMatches)))
134 self.metadata.set(
"SIP_RMS", sipFit.getScatterInPixels())
135 self.metadata.set(
"SIP_GOOD", len(copyMatches))
136 self.metadata.set(
"SIP_REJECTED", len(matches) - len(copyMatches))
137 wcs = sipFit.getNewWcs()
141 """Warp input exposure to template frame
143 There are a variety of data attached to the exposure (e.g., PSF, Calib
144 and other metadata), but we do not attempt to warp these to the template
147 @param inputExp: Input exposure, to be warped
148 @param newWcs: Revised Wcs for input exposure
149 @param templateWcs: Target Wcs
150 @param templateBBox: Target bounding box
151 @return Warped exposure
153 warper = Warper.fromConfig(self.config.warper)
154 copyExp = inputExp.Factory(inputExp.getMaskedImage(), newWcs)
155 alignedExp = warper.warpExposure(templateWcs, copyExp, destBBox=templateBBox)
158 def warpSources(self, inputSources, newWcs, templateWcs, templateBBox):
159 """Warp sources to the new frame
161 It would be difficult to transform all possible quantities of potential
162 interest between the two frames. We therefore update only the sky and
165 @param inputSources: Sources on input exposure, to be warped
166 @param newWcs: Revised Wcs for input exposure
167 @param templateWcs: Target Wcs
168 @param templateBBox: Target bounding box
169 @return Warped sources
171 alignedSources = inputSources.copy(
True)
172 if not isinstance(templateBBox, afwGeom.Box2D):
174 templateBBox = afwGeom.Box2D(templateBBox)
175 table = alignedSources.getTable()
176 coordKey = table.getCoordKey()
177 centroidKey = table.getCentroidKey()
179 for i, s
in enumerate(alignedSources):
180 oldCentroid = s.get(centroidKey)
181 newCoord = newWcs.pixelToSky(oldCentroid)
182 newCentroid = templateWcs.skyToPixel(newCoord)
183 if not templateBBox.contains(newCentroid):
186 s.set(coordKey, newCoord)
187 s.set(centroidKey, newCentroid)
189 for i
in reversed(deleteList):
190 del alignedSources[i]
192 return alignedSources