24 This module contains a Task to register (align) multiple images. 
   26 __all__ = [
"RegisterTask", 
"RegisterConfig"]
 
   31 from lsst.pex.config 
import Config, Field, ConfigField
 
   41     """Configuration for RegisterTask""" 
   42     matchRadius = Field(dtype=float, default=1.0, doc=
"Matching radius (arcsec)", check=
lambda x: x > 0)
 
   43     sipOrder = Field(dtype=int, default=4, doc=
"Order for SIP WCS", check=
lambda x: x > 1)
 
   44     sipIter = Field(dtype=int, default=3, doc=
"Rejection iterations for SIP WCS", check=
lambda x: x > 0)
 
   45     sipRej = Field(dtype=float, default=3.0, doc=
"Rejection threshold for SIP WCS", check=
lambda x: x > 0)
 
   46     warper = ConfigField(dtype=Warper.ConfigClass, doc=
"Configuration for warping")
 
   51     Task to register (align) multiple images. 
   53     The 'run' method provides a revised Wcs from matches and fitting sources. 
   54     Additional methods are provided as a convenience to warp an exposure 
   55     ('warpExposure') and sources ('warpSources') with the new Wcs. 
   57     ConfigClass = RegisterConfig
 
   59     def run(self, inputSources, inputWcs, inputBBox, templateSources):
 
   60         """Register (align) an input exposure to the template 
   62         The sources must have RA,Dec set, and accurate to within the 
   63         'matchRadius' of the configuration in order to facilitate source 
   64         matching.  We fit a new Wcs, but do NOT set it in the input exposure. 
   66         @param inputSources: Sources from input exposure 
   67         @param inputWcs: Wcs of input exposure 
   68         @param inputBBox: Bounding box of input exposure 
   69         @param templateSources: Sources from template exposure 
   70         @return Struct(matches: Matches between sources, 
   71                        wcs: Wcs for input in frame of template, 
   74         matches = self.
matchSources(inputSources, templateSources)
 
   75         wcs = self.
fitWcs(matches, inputWcs, inputBBox)
 
   76         return Struct(matches=matches, wcs=wcs)
 
   79         """Match sources between the input and template 
   81         The order of the input arguments matters (because the later Wcs 
   82         fitting assumes a particular order). 
   84         @param inputSources: Source catalog of the input frame 
   85         @param templateSources: Source of the target frame 
   89                                       self.
config.matchRadius*geom.arcseconds)
 
   90         self.
log.
info(
"Matching within %.1f arcsec: %d matches" % (self.
config.matchRadius, len(matches)))
 
   93             raise RuntimeError(
"Unable to match source catalogs")
 
   96     def fitWcs(self, matches, inputWcs, inputBBox):
 
   99         The fitting includes iterative sigma-clipping. 
  101         @param matches: List of matches (first is target, second is input) 
  102         @param inputWcs: Original input Wcs 
  103         @param inputBBox: Bounding box of input image 
  106         copyMatches = 
type(matches)(matches)
 
  107         refCoordKey = copyMatches[0].first.getTable().getCoordKey()
 
  108         inCentroidKey = copyMatches[0].second.getTable().getCentroidSlot().getMeasKey()
 
  109         for i 
in range(self.
config.sipIter):
 
  111             self.
log.
debug(
"Registration WCS RMS iteration %d: %f pixels",
 
  112                            i, sipFit.getScatterInPixels())
 
  113             wcs = sipFit.getNewWcs()
 
  114             dr = [m.first.get(refCoordKey).separation(
 
  115                 wcs.pixelToSky(m.second.get(inCentroidKey))).asArcseconds() 
for 
  118             rms = math.sqrt((dr*dr).mean())  
 
  119             rms = 
max(rms, 1.0e-9)  
 
  120             self.
log.
debug(
"Registration iteration %d: rms=%f", i, rms)
 
  121             good = numpy.where(dr < self.
config.sipRej*rms)[0]
 
  122             numBad = len(copyMatches) - len(good)
 
  123             self.
log.
debug(
"Registration iteration %d: rejected %d", i, numBad)
 
  126             copyMatches = 
type(matches)(copyMatches[i] 
for i 
in good)
 
  129         self.
log.
info(
"Registration WCS: final WCS RMS=%f pixels from %d matches" %
 
  130                       (sipFit.getScatterInPixels(), len(copyMatches)))
 
  131         self.
metadata.
set(
"SIP_RMS", sipFit.getScatterInPixels())
 
  133         self.
metadata.
set(
"SIP_REJECTED", len(matches) - len(copyMatches))
 
  134         wcs = sipFit.getNewWcs()
 
  138         """Warp input exposure to template frame 
  140         There are a variety of data attached to the exposure (e.g., PSF, PhotoCalib 
  141         and other metadata), but we do not attempt to warp these to the template 
  144         @param inputExp: Input exposure, to be warped 
  145         @param newWcs: Revised Wcs for input exposure 
  146         @param templateWcs: Target Wcs 
  147         @param templateBBox: Target bounding box 
  148         @return Warped exposure 
  150         warper = Warper.fromConfig(self.
config.warper)
 
  151         copyExp = inputExp.Factory(inputExp.getMaskedImage(), newWcs)
 
  152         alignedExp = warper.warpExposure(templateWcs, copyExp, destBBox=templateBBox)
 
  155     def warpSources(self, inputSources, newWcs, templateWcs, templateBBox):
 
  156         """Warp sources to the new frame 
  158         It would be difficult to transform all possible quantities of potential 
  159         interest between the two frames.  We therefore update only the sky and 
  162         @param inputSources: Sources on input exposure, to be warped 
  163         @param newWcs: Revised Wcs for input exposure 
  164         @param templateWcs: Target Wcs 
  165         @param templateBBox: Target bounding box 
  166         @return Warped sources 
  168         alignedSources = inputSources.copy(
True)
 
  172         table = alignedSources.getTable()
 
  173         coordKey = table.getCoordKey()
 
  174         centroidKey = table.getCentroidSlot().getMeasKey()
 
  176         for i, s 
in enumerate(alignedSources):
 
  177             oldCentroid = s.get(centroidKey)
 
  178             newCoord = newWcs.pixelToSky(oldCentroid)
 
  179             newCentroid = templateWcs.skyToPixel(newCoord)
 
  180             if not templateBBox.contains(newCentroid):
 
  183             s.set(coordKey, newCoord)
 
  184             s.set(centroidKey, newCentroid)
 
  186         for i 
in reversed(deleteList):  
 
  187             del alignedSources[i]
 
  189         return alignedSources