LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
LSSTDataManagementBasePackage
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 builtins import range
27 
28 __all__ = ["RegisterTask", "RegisterConfig"]
29 
30 import math
31 import numpy
32 
33 from lsst.pex.config import Config, Field, ConfigField
34 from lsst.pipe.base import Task, Struct
35 from lsst.meas.astrom.sip import makeCreateWcsWithSip
36 from lsst.afw.math import Warper
37 
38 import lsst.afw.geom as afwGeom
39 import lsst.afw.table as afwTable
40 
41 
42 class RegisterConfig(Config):
43  """Configuration for RegisterTask"""
44  matchRadius = Field(dtype=float, default=1.0, doc="Matching radius (arcsec)", check=lambda x: x > 0)
45  sipOrder = Field(dtype=int, default=4, doc="Order for SIP WCS", check=lambda x: x > 1)
46  sipIter = Field(dtype=int, default=3, doc="Rejection iterations for SIP WCS", check=lambda x: x > 0)
47  sipRej = Field(dtype=float, default=3.0, doc="Rejection threshold for SIP WCS", check=lambda x: x > 0)
48  warper = ConfigField(dtype=Warper.ConfigClass, doc="Configuration for warping")
49 
50 
51 class RegisterTask(Task):
52  """
53  Task to register (align) multiple images.
54 
55  The 'run' method provides a revised Wcs from matches and fitting sources.
56  Additional methods are provided as a convenience to warp an exposure
57  ('warpExposure') and sources ('warpSources') with the new Wcs.
58  """
59  ConfigClass = RegisterConfig
60 
61  def run(self, inputSources, inputWcs, inputBBox, templateSources):
62  """Register (align) an input exposure to the template
63 
64  The sources must have RA,Dec set, and accurate to within the
65  'matchRadius' of the configuration in order to facilitate source
66  matching. We fit a new Wcs, but do NOT set it in the input exposure.
67 
68  @param inputSources: Sources from input exposure
69  @param inputWcs: Wcs of input exposure
70  @param inputBBox: Bounding box of input exposure
71  @param templateSources: Sources from template exposure
72  @return Struct(matches: Matches between sources,
73  wcs: Wcs for input in frame of template,
74  )
75  """
76  matches = self.matchSources(inputSources, templateSources)
77  wcs = self.fitWcs(matches, inputWcs, inputBBox)
78  return Struct(matches=matches, wcs=wcs)
79 
80  def matchSources(self, inputSources, templateSources):
81  """Match sources between the input and template
82 
83  The order of the input arguments matters (because the later Wcs
84  fitting assumes a particular order).
85 
86  @param inputSources: Source catalog of the input frame
87  @param templateSources: Source of the target frame
88  @return Match list
89  """
90  matches = afwTable.matchRaDec(templateSources, inputSources,
91  self.config.matchRadius*afwGeom.arcseconds)
92  self.log.info("Matching within %.1f arcsec: %d matches" % (self.config.matchRadius, len(matches)))
93  self.metadata.set("MATCH_NUM", len(matches))
94  if len(matches) == 0:
95  raise RuntimeError("Unable to match source catalogs")
96  return matches
97 
98  def fitWcs(self, matches, inputWcs, inputBBox):
99  """Fit Wcs to matches
100 
101  The fitting includes iterative sigma-clipping.
102 
103  @param matches: List of matches (first is target, second is input)
104  @param inputWcs: Original input Wcs
105  @param inputBBox: Bounding box of input image
106  @return Wcs
107  """
108  copyMatches = type(matches)(matches)
109  refCoordKey = copyMatches[0].first.getTable().getCoordKey()
110  inCentroidKey = copyMatches[0].second.getTable().getCentroidKey()
111  for i in range(self.config.sipIter):
112  sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox)
113  self.log.debug("Registration WCS RMS iteration %d: %f pixels",
114  i, sipFit.getScatterInPixels())
115  wcs = sipFit.getNewWcs()
116  dr = [m.first.get(refCoordKey).angularSeparation(
117  wcs.pixelToSky(m.second.get(inCentroidKey))).asArcseconds() for
118  m in copyMatches]
119  dr = numpy.array(dr)
120  rms = math.sqrt((dr*dr).mean()) # RMS from zero
121  rms = max(rms, 1.0e-9) # Don't believe any RMS smaller than this
122  self.log.debug("Registration iteration %d: rms=%f", i, rms)
123  good = numpy.where(dr < self.config.sipRej*rms)[0]
124  numBad = len(copyMatches) - len(good)
125  self.log.debug("Registration iteration %d: rejected %d", i, numBad)
126  if numBad == 0:
127  break
128  copyMatches = type(matches)(copyMatches[i] for i in good)
129 
130  sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox)
131  self.log.info("Registration WCS: final WCS RMS=%f pixels from %d matches" %
132  (sipFit.getScatterInPixels(), len(copyMatches)))
133  self.metadata.set("SIP_RMS", sipFit.getScatterInPixels())
134  self.metadata.set("SIP_GOOD", len(copyMatches))
135  self.metadata.set("SIP_REJECTED", len(matches) - len(copyMatches))
136  wcs = sipFit.getNewWcs()
137  return wcs
138 
139  def warpExposure(self, inputExp, newWcs, templateWcs, templateBBox):
140  """Warp input exposure to template frame
141 
142  There are a variety of data attached to the exposure (e.g., PSF, Calib
143  and other metadata), but we do not attempt to warp these to the template
144  frame.
145 
146  @param inputExp: Input exposure, to be warped
147  @param newWcs: Revised Wcs for input exposure
148  @param templateWcs: Target Wcs
149  @param templateBBox: Target bounding box
150  @return Warped exposure
151  """
152  warper = Warper.fromConfig(self.config.warper)
153  copyExp = inputExp.Factory(inputExp.getMaskedImage(), newWcs)
154  alignedExp = warper.warpExposure(templateWcs, copyExp, destBBox=templateBBox)
155  return alignedExp
156 
157  def warpSources(self, inputSources, newWcs, templateWcs, templateBBox):
158  """Warp sources to the new frame
159 
160  It would be difficult to transform all possible quantities of potential
161  interest between the two frames. We therefore update only the sky and
162  pixel coordinates.
163 
164  @param inputSources: Sources on input exposure, to be warped
165  @param newWcs: Revised Wcs for input exposure
166  @param templateWcs: Target Wcs
167  @param templateBBox: Target bounding box
168  @return Warped sources
169  """
170  alignedSources = inputSources.copy(True)
171  if not isinstance(templateBBox, afwGeom.Box2D):
172  # There is no method Box2I::contains(Point2D)
173  templateBBox = afwGeom.Box2D(templateBBox)
174  table = alignedSources.getTable()
175  coordKey = table.getCoordKey()
176  centroidKey = table.getCentroidKey()
177  deleteList = []
178  for i, s in enumerate(alignedSources):
179  oldCentroid = s.get(centroidKey)
180  newCoord = newWcs.pixelToSky(oldCentroid)
181  newCentroid = templateWcs.skyToPixel(newCoord)
182  if not templateBBox.contains(newCentroid):
183  deleteList.append(i)
184  continue
185  s.set(coordKey, newCoord)
186  s.set(centroidKey, newCentroid)
187 
188  for i in reversed(deleteList): # Delete from back so we don't change indices
189  del alignedSources[i]
190 
191  return alignedSources
std::vector< Match< typename Cat::Record, typename Cat::Record > > matchRaDec(Cat const &cat, Angle radius, bool symmetric)
CreateWcsWithSip< MatchT > makeCreateWcsWithSip(std::vector< MatchT > const &matches, afw::image::Wcs const &linearWcs, int const order, afw::geom::Box2I const &bbox=afw::geom::Box2I(), int const ngrid=0)
Factory function for CreateWcsWithSip.
A floating-point coordinate rectangle geometry.
Definition: Box.h:271