lsst.pipe.tasks  13.0-66-gfbf2f2ce+5
registerImage.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2013 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 """
24 This module contains a Task to register (align) multiple images.
25 """
26 from __future__ import absolute_import, division, print_function
27 from builtins import range
28 
29 __all__ = ["RegisterTask", "RegisterConfig"]
30 
31 import math
32 import numpy
33 
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
38 
39 import lsst.afw.geom as afwGeom
40 import lsst.afw.table as afwTable
41 
42 
43 class RegisterConfig(Config):
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")
50 
51 
52 class RegisterTask(Task):
53  """
54  Task to register (align) multiple images.
55 
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.
59  """
60  ConfigClass = RegisterConfig
61 
62  def run(self, inputSources, inputWcs, inputBBox, templateSources):
63  """Register (align) an input exposure to the template
64 
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.
68 
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,
75  )
76  """
77  matches = self.matchSources(inputSources, templateSources)
78  wcs = self.fitWcs(matches, inputWcs, inputBBox)
79  return Struct(matches=matches, wcs=wcs)
80 
81  def matchSources(self, inputSources, templateSources):
82  """Match sources between the input and template
83 
84  The order of the input arguments matters (because the later Wcs
85  fitting assumes a particular order).
86 
87  @param inputSources: Source catalog of the input frame
88  @param templateSources: Source of the target frame
89  @return Match list
90  """
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))
95  if len(matches) == 0:
96  raise RuntimeError("Unable to match source catalogs")
97  return matches
98 
99  def fitWcs(self, matches, inputWcs, inputBBox):
100  """Fit Wcs to matches
101 
102  The fitting includes iterative sigma-clipping.
103 
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
107  @return Wcs
108  """
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
119  m in copyMatches]
120  dr = numpy.array(dr)
121  rms = math.sqrt((dr*dr).mean()) # RMS from zero
122  rms = max(rms, 1.0e-9) # Don't believe any RMS smaller than this
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)
127  if numBad == 0:
128  break
129  copyMatches = type(matches)(copyMatches[i] for i in good)
130 
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()
138  return wcs
139 
140  def warpExposure(self, inputExp, newWcs, templateWcs, templateBBox):
141  """Warp input exposure to template frame
142 
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
145  frame.
146 
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
152  """
153  warper = Warper.fromConfig(self.config.warper)
154  copyExp = inputExp.Factory(inputExp.getMaskedImage(), newWcs)
155  alignedExp = warper.warpExposure(templateWcs, copyExp, destBBox=templateBBox)
156  return alignedExp
157 
158  def warpSources(self, inputSources, newWcs, templateWcs, templateBBox):
159  """Warp sources to the new frame
160 
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
163  pixel coordinates.
164 
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
170  """
171  alignedSources = inputSources.copy(True)
172  if not isinstance(templateBBox, afwGeom.Box2D):
173  # There is no method Box2I::contains(Point2D)
174  templateBBox = afwGeom.Box2D(templateBBox)
175  table = alignedSources.getTable()
176  coordKey = table.getCoordKey()
177  centroidKey = table.getCentroidKey()
178  deleteList = []
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):
184  deleteList.append(i)
185  continue
186  s.set(coordKey, newCoord)
187  s.set(centroidKey, newCentroid)
188 
189  for i in reversed(deleteList): # Delete from back so we don't change indices
190  del alignedSources[i]
191 
192  return alignedSources
def warpSources(self, inputSources, newWcs, templateWcs, templateBBox)
def fitWcs(self, matches, inputWcs, inputBBox)
def warpExposure(self, inputExp, newWcs, templateWcs, templateBBox)
def run(self, inputSources, inputWcs, inputBBox, templateSources)
def matchSources(self, inputSources, templateSources)