LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
LSST Data Management Base Package
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 __all__ = ["RegisterTask", "RegisterConfig"]
27 
28 import math
29 import numpy
30 
31 from lsst.pex.config import Config, Field, ConfigField
32 from lsst.pipe.base import Task, Struct
33 from lsst.meas.astrom.sip import makeCreateWcsWithSip
34 from lsst.afw.math import Warper
35 
36 import lsst.geom as geom
37 import lsst.afw.table as afwTable
38 
39 
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")
47 
48 
49 class RegisterTask(Task):
50  """
51  Task to register (align) multiple images.
52 
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.
56  """
57  ConfigClass = RegisterConfig
58 
59  def run(self, inputSources, inputWcs, inputBBox, templateSources):
60  """Register (align) an input exposure to the template
61 
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.
65 
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,
72  )
73  """
74  matches = self.matchSourcesmatchSources(inputSources, templateSources)
75  wcs = self.fitWcsfitWcs(matches, inputWcs, inputBBox)
76  return Struct(matches=matches, wcs=wcs)
77 
78  def matchSources(self, inputSources, templateSources):
79  """Match sources between the input and template
80 
81  The order of the input arguments matters (because the later Wcs
82  fitting assumes a particular order).
83 
84  @param inputSources: Source catalog of the input frame
85  @param templateSources: Source of the target frame
86  @return Match list
87  """
88  matches = afwTable.matchRaDec(templateSources, inputSources,
89  self.config.matchRadius*geom.arcseconds)
90  self.log.info("Matching within %.1f arcsec: %d matches", self.config.matchRadius, len(matches))
91  self.metadata.set("MATCH_NUM", len(matches))
92  if len(matches) == 0:
93  raise RuntimeError("Unable to match source catalogs")
94  return matches
95 
96  def fitWcs(self, matches, inputWcs, inputBBox):
97  """Fit Wcs to matches
98 
99  The fitting includes iterative sigma-clipping.
100 
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
104  @return Wcs
105  """
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):
110  sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox)
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
116  m in copyMatches]
117  dr = numpy.array(dr)
118  rms = math.sqrt((dr*dr).mean()) # RMS from zero
119  rms = max(rms, 1.0e-9) # Don't believe any RMS smaller than this
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)
124  if numBad == 0:
125  break
126  copyMatches = type(matches)(copyMatches[i] for i in good)
127 
128  sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox)
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())
132  self.metadata.set("SIP_GOOD", len(copyMatches))
133  self.metadata.set("SIP_REJECTED", len(matches) - len(copyMatches))
134  wcs = sipFit.getNewWcs()
135  return wcs
136 
137  def warpExposure(self, inputExp, newWcs, templateWcs, templateBBox):
138  """Warp input exposure to template frame
139 
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
142  frame.
143 
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
149  """
150  warper = Warper.fromConfig(self.config.warper)
151  copyExp = inputExp.Factory(inputExp.getMaskedImage(), newWcs)
152  alignedExp = warper.warpExposure(templateWcs, copyExp, destBBox=templateBBox)
153  return alignedExp
154 
155  def warpSources(self, inputSources, newWcs, templateWcs, templateBBox):
156  """Warp sources to the new frame
157 
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
160  pixel coordinates.
161 
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
167  """
168  alignedSources = inputSources.copy(True)
169  if not isinstance(templateBBox, geom.Box2D):
170  # There is no method Box2I::contains(Point2D)
171  templateBBox = geom.Box2D(templateBBox)
172  table = alignedSources.getTable()
173  coordKey = table.getCoordKey()
174  centroidKey = table.getCentroidSlot().getMeasKey()
175  deleteList = []
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):
181  deleteList.append(i)
182  continue
183  s.set(coordKey, newCoord)
184  s.set(centroidKey, newCentroid)
185 
186  for i in reversed(deleteList): # Delete from back so we don't change indices
187  del alignedSources[i]
188 
189  return alignedSources
int max
table::Key< int > type
Definition: Detector.cc:163
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
def matchSources(self, inputSources, templateSources)
def fitWcs(self, matches, inputWcs, inputBBox)
def warpSources(self, inputSources, newWcs, templateWcs, templateBBox)
def run(self, inputSources, inputWcs, inputBBox, templateSources)
def warpExposure(self, inputExp, newWcs, templateWcs, templateBBox)
daf::base::PropertySet * set
Definition: fits.cc:912
std::vector< Match< typename Cat1::Record, typename Cat2::Record > > matchRaDec(Cat1 const &cat1, Cat2 const &cat2, lsst::geom::Angle radius, MatchControl const &mc=MatchControl())
Compute all tuples (s1,s2,d) where s1 belings to cat1, s2 belongs to cat2 and d, the distance between...
Definition: Match.cc:158
CreateWcsWithSip< MatchT > makeCreateWcsWithSip(std::vector< MatchT > const &matches, afw::geom::SkyWcs const &linearWcs, int const order, geom::Box2I const &bbox=geom::Box2I(), int const ngrid=0)
Factory function for CreateWcsWithSip.