LSSTApplications  1.1.2+25,10.0+13,10.0+132,10.0+133,10.0+224,10.0+41,10.0+8,10.0-1-g0f53050+14,10.0-1-g4b7b172+19,10.0-1-g61a5bae+98,10.0-1-g7408a83+3,10.0-1-gc1e0f5a+19,10.0-1-gdb4482e+14,10.0-11-g3947115+2,10.0-12-g8719d8b+2,10.0-15-ga3f480f+1,10.0-2-g4f67435,10.0-2-gcb4bc6c+26,10.0-28-gf7f57a9+1,10.0-3-g1bbe32c+14,10.0-3-g5b46d21,10.0-4-g027f45f+5,10.0-4-g86f66b5+2,10.0-4-gc4fccf3+24,10.0-40-g4349866+2,10.0-5-g766159b,10.0-5-gca2295e+25,10.0-6-g462a451+1
LSSTDataManagementBasePackage
astrometry.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 
3 import math
4 
5 import numpy
6 
7 from lsst.daf.base import PropertyList
8 from lsst.afw.image import ExposureF
9 from lsst.afw.image.utils import getDistortedWcs
10 from lsst.afw.table import Point2DKey
11 from lsst.afw.geom import Box2D
12 import lsst.pex.config as pexConfig
13 import lsst.pipe.base as pipeBase
14 from .loadAstrometryNetObjects import LoadAstrometryNetObjectsTask
15 from .matchOptimisticB import MatchOptimisticBTask
16 from .fitTanSipWcs import FitTanSipWcsTask
17 
18 
19 class AstrometryConfig(pexConfig.Config):
20  refObjLoader = pexConfig.ConfigurableField(
21  target = LoadAstrometryNetObjectsTask,
22  doc = "reference object loader",
23  )
24  matcher = pexConfig.ConfigurableField(
25  target = MatchOptimisticBTask,
26  doc = "reference object/source matcher",
27  )
28  wcsFitter = pexConfig.ConfigurableField(
29  target = FitTanSipWcsTask,
30  doc = "WCS fitter",
31  )
32  forceKnownWcs = pexConfig.Field(
33  dtype = bool,
34  doc= "Assume that the input image's WCS is correct, without comparing it to any external reality " +
35  " (but still match reference objects to sources)",
36  default = False,
37  )
38  maxIter = pexConfig.RangeField(
39  doc = "maximum number of iterations of match sources and fit WCS; " +
40  "ignored if forceKnownWcs True",
41  dtype = int,
42  default = 3,
43  min = 1,
44  )
45 
46 # The following block adds links to this task from the Task Documentation page.
47 ## \addtogroup LSST_task_documentation
48 ## \{
49 ## \page measAstrom_astrometryTask
50 ## \ref AstrometryTask "AstrometryTask"
51 ## Match an input source catalog with objects from a reference catalog and solve for the WCS
52 ## \}
53 
54 class AstrometryTask(pipeBase.Task):
55  """!Match an input source catalog with objects from a reference catalog and solve for the WCS
56 
57  @anchor AstrometryTask_
58 
59  @section meas_astrom_astrometry_Contents Contents
60 
61  - @ref meas_astrom_astrometry_Purpose
62  - @ref meas_astrom_astrometry_Initialize
63  - @ref meas_astrom_astrometry_IO
64  - @ref meas_astrom_astrometry_Config
65  - @ref meas_astrom_astrometry_Example
66  - @ref meas_astrom_astrometry_Debug
67 
68  @section meas_astrom_astrometry_Purpose Description
69 
70  Match input sourceCat with a reference catalog and solve for the Wcs
71 
72  There are three steps, each performed by different subtasks:
73  - Find position reference stars that overlap the exposure
74  - Match sourceCat to position reference stars
75  - Fit a WCS based on the matches
76 
77  @section meas_astrom_astrometry_Initialize Task initialisation
78 
79  @copydoc \_\_init\_\_
80 
81  @section meas_astrom_astrometry_IO Invoking the Task
82 
83  @copydoc run
84 
85  @section meas_astrom_astrometry_Config Configuration parameters
86 
87  See @ref AstrometryConfig
88 
89  @section meas_astrom_astrometry_Example A complete example of using AstrometryTask
90 
91  See \ref meas_photocal_photocal_Example.
92 
93  @section meas_astrom_astrometry_Debug Debug variables
94 
95  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a
96  flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about
97  @b debug.py files.
98 
99  The available variables in AstrometryTask are:
100  <DL>
101  <DT> @c display (bool)
102  <DD> If True display information at three stages: after finding reference objects,
103  after matching sources to reference objects, and after fitting the WCS; defaults to False
104  <DT> @c frame (int)
105  <DD> ds9 frame to use to display the reference objects; the next two frames are used
106  to display the match list and the results of the final WCS; defaults to 0
107  </DL>
108 
109  To investigate the @ref meas_astrom_astrometry_Debug, put something like
110  @code{.py}
111  import lsstDebug
112  def DebugInfo(name):
113  debug = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
114  if name == "lsst.meas.astrom.astrometry":
115  debug.display = True
116 
117  return debug
118 
119  lsstDebug.Info = DebugInfo
120  @endcode
121  into your debug.py file and run this task with the @c --debug flag.
122  """
123  ConfigClass = AstrometryConfig
124  _DefaultName = "astrometricSolver"
125 
126  def __init__(self, schema=None, **kwargs):
127  """!Construct an AstrometryTask
128 
129  @param[in] schema ignored; available for compatibility with an older astrometry task
130  """
131  pipeBase.Task.__init__(self, **kwargs)
132  self.makeSubtask("refObjLoader")
133  self.makeSubtask("matcher")
134  self.makeSubtask("wcsFitter")
135 
136  @pipeBase.timeMethod
137  def run(self, exposure, sourceCat):
138  """!Fit a WCS given a source catalog and exposure
139 
140  @param[in,out] exposure exposure whose WCS is to be fit
141  The following are read only:
142  - bbox
143  - calib (may be absent)
144  - filter (may be unset)
145  - detector (if wcs is pure tangent; may be absent)
146  The following are updated:
147  - wcs (the initial value is used as an initial guess, and is required)
148  @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
149  @return the same struct as the "solve" method
150  """
151  bbox = exposure.getBBox()
152  exposureInfo = exposure.getInfo()
153  initWcs = getDistortedWcs(exposureInfo, log=self.log)
154  calib = exposureInfo.getCalib() if exposureInfo.hasCalib() else None
155  filterName = exposureInfo.getFilter().getName() or None
156 
157  retVal = self.solve(sourceCat=sourceCat, bbox=bbox, initWcs=initWcs, filterName=filterName,
158  calib=calib, exposure=exposure)
159  exposure.setWcs(retVal.wcs)
160  return retVal
161 
162  @pipeBase.timeMethod
163  def solve(self, sourceCat, bbox, initWcs, filterName=None, calib=None, exposure=None):
164  """!Fit a WCS given a source catalog and exposure matchMetadata
165 
166  @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
167  @param[in] bbox bounding box of exposure (an lsst.afw.geom.Box2I)
168  @param[in] wcs initial guess for WCS of exposure (an lsst.afw.image.Wcs)
169  @param[in] filterName filter name, or None or "" if unknown (a string)
170  @param[in] calib calibration for exposure, or None if unknown (an lsst.afw.image.Calib)
171  @param[in] exposure exposure whose WCS is to be fit, or None; used only for the debug display
172 
173  @return an lsst.pipe.base.Struct with these fields:
174  - refCat reference object catalog of objects that overlap the exposure (with some margin)
175  (an lsst::afw::table::SimpleCatalog)
176  - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
177  - initWcs initial WCS from exposure (possibly tweaked) (an lsst.afw.image.Wcs)
178  - wcs fit WCS (an lsst.afw.image.Wcs)
179  - matchMeta metadata about the field
180  """
181  import lsstDebug
182  debug = lsstDebug.Info(__name__)
183 
184  loadRes = self.refObjLoader.loadPixelBox(
185  bbox = bbox,
186  wcs = initWcs,
187  filterName = filterName,
188  calib = calib,
189  )
190  if debug.display:
191  frame = int(debug.frame)
193  refCat = loadRes.refCat,
194  sourceCat = sourceCat,
195  exposure = exposure,
196  bbox = bbox,
197  frame = frame,
198  title="Reference catalog",
199  )
200 
201  res = None
202  wcs = initWcs
203  for i in range(self.config.maxIter):
204  tryRes = self._matchAndFitWcs( # refCat, sourceCat, refFluxField, bbox, wcs, exposure=None
205  refCat = loadRes.refCat,
206  sourceCat = sourceCat,
207  refFluxField = loadRes.fluxField,
208  bbox = bbox,
209  wcs = wcs,
210  exposure = exposure,
211  )
212 
213  if self.config.forceKnownWcs:
214  # run just once; note that the number of matches has already logged
215  res = tryRes
216  break
217 
218  self.log.logdebug(
219  "Fit WCS iter %s: %s matches; median scatter = %g arcsec" % \
220  (i, len(tryRes.matches), tryRes.scatterOnSky.asArcseconds()),
221  )
222 
223  if res is not None and not self.config.forceKnownWcs:
224  if len(tryRes.matches) < len(res.matches):
225  self.log.info(
226  "Fit WCS: use iter %s because it had more matches than the next iter: %s vs. %s" % \
227  (i-1, len(res.matches), len(tryRes.matches)))
228  break
229  if len(tryRes.matches) == len(res.matches) and tryRes.scatterOnSky >= res.scatterOnSky:
230  self.log.info(
231  "Fit WCS: use iter %s because it had less scatter than the next iter: %g vs. %g arcsec" % \
232  (i-1, res.scatterOnSky.asArcseconds(), tryRes.scatterOnSky.asArcseconds()))
233  break
234 
235  res = tryRes
236  wcs = res.wcs
237 
238  return pipeBase.Struct(
239  refCat = loadRes.refCat,
240  matches = res.matches,
241  initWcs = initWcs,
242  wcs = res.wcs,
243  scatterOnSky = res.scatterOnSky,
244  matchMeta = self._createMatchMetadata(bbox=bbox, wcs=res.wcs, filterName=filterName)
245  )
246 
247  @pipeBase.timeMethod
248  def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, exposure=None):
249  """!Match sources to reference objects and fit a WCS
250 
251  @param[in] refCat catalog of reference objects
252  @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
253  @param[in] bbox bounding box of exposure (an lsst.afw.geom.Box2I)
254  @param[in] wcs initial guess for WCS of exposure (an lsst.afw.image.Wcs)
255  @param[in] exposure exposure whose WCS is to be fit, or None; used only for the debug display
256 
257  @return an lsst.pipe.base.Struct with these fields:
258  - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
259  - wcs the fit WCS as an lsst.afw.image.Wcs
260  - scatterOnSky median on-sky separation between reference objects and sources in "matches",
261  as an lsst.afw.geom.Angle, or None if config.forceKnownWcs is True
262  """
263  import lsstDebug
264  debug = lsstDebug.Info(__name__)
265  matchRes = self.matcher.matchObjectsToSources(
266  refCat = refCat,
267  sourceCat = sourceCat,
268  wcs = wcs,
269  refFluxField = refFluxField,
270  )
271  if debug.display:
272  frame = int(debug.frame)
274  refCat = refCat,
275  sourceCat = sourceCat,
276  matches = matchRes.matches,
277  exposure = exposure,
278  bbox = bbox,
279  frame = frame + 1,
280  title="Match list",
281  )
282 
283  if not self.config.forceKnownWcs:
284  self.log.info("Fitting WCS")
285  fitRes = self.wcsFitter.fitWcs(
286  matches = matchRes.matches,
287  initWcs = wcs,
288  bbox = bbox,
289  refCat = refCat,
290  sourceCat = sourceCat,
291  )
292  fitWcs = fitRes.wcs
293  scatterOnSky = fitRes.scatterOnSky
294  else:
295  self.log.info("Not fitting WCS (forceKnownWcs true); %s matches" % (len(matchRes.matches),))
296  fitWcs = wcs
297  scatterOnSky = None
298  if debug.display:
299  frame = int(debug.frame)
301  refCat = refCat,
302  sourceCat = sourceCat,
303  matches = matchRes.matches,
304  exposure = exposure,
305  bbox = bbox,
306  frame = frame + 2,
307  title="TAN-SIP WCS",
308  )
309 
310  return pipeBase.Struct(
311  matches = matchRes.matches,
312  wcs = fitWcs,
313  scatterOnSky = scatterOnSky,
314  )
315 
316  @staticmethod
317  def _createMatchMetadata(bbox, wcs, filterName):
318  """Create matchMeta metadata required for regenerating the catalog
319 
320  This is copied from Astrom and I'm not sure why it is needed.
321  I did not put this in any subtask because none have all the necessary info:
322  - The matcher does not have the fit wcs
323  - The fitter does not have the filter name
324 
325  @param bbox bounding box of exposure (an lsst.afw.geom.Box2I or Box2D)
326  @param wcs WCS of exposure
327  @param filterName Name of filter, used for magnitudes
328  @return Metadata
329  """
330  matchMeta = PropertyList()
331  bboxd = Box2D(bbox)
332  ctrPos = bboxd.getCenter()
333  ctrCoord = wcs.pixelToSky(ctrPos).toIcrs()
334  llCoord = wcs.pixelToSky(bboxd.getMin())
335  approxRadius = ctrCoord.angularSeparation(llCoord)
336  matchMeta.add('RA', ctrCoord.getRa().asDegrees(), 'field center in degrees')
337  matchMeta.add('DEC', ctrCoord.getDec().asDegrees(), 'field center in degrees')
338  matchMeta.add('RADIUS', approxRadius.asDegrees(), 'field radius in degrees, approximate')
339  matchMeta.add('SMATCHV', 1, 'SourceMatchVector version number')
340  if filterName is not None:
341  matchMeta.add('FILTER', filterName, 'filter name for tagalong data')
342  return matchMeta
343 
344 
345 def showAstrometry(refCat, sourceCat, bbox=None, exposure=None, matches=None, frame=1, title=""):
346  """Show an astrometry debug image
347 
348  @param[in] refCat reference object catalog; must have fields "centroid_x" and "centroid_y"
349  @param[in] sourceCat source catalog; must have field "slot_Centroid_x" and "slot_Centroid_y"
350  @param[in] exposure exposure to display, or None for a blank exposure
351  @param[in] bbox bounding box of exposure; required if exposure is None and ignored otherwise
352  @param[in] matches list of matches (an lsst.afw.table.ReferenceMatchVector), or None
353  @param[in] frame frame number for ds9 display
354  @param[in] title title for ds9 display
355 
356  @throw RuntimeError if exposure and bbox are both None
357  """
358  import lsst.afw.display.ds9 as ds9
359 
360  if exposure is None:
361  if bbox is None:
362  raise RuntimeError("must specify exposure or bbox")
363  exposure = ExposureF(bbox)
364  ds9.mtv(exposure, frame=frame, title=title)
365 
366  with ds9.Buffering():
367  refCentroidKey = Point2DKey(refCat.schema["centroid"])
368  for refObj in refCat:
369  x, y = refObj.get(refCentroidKey)
370  ds9.dot("x", x, y, size=10, frame=frame, ctype=ds9.RED)
371 
372  sourceCentroidKey = Point2DKey(sourceCat.schema["slot_Centroid"])
373  for source in sourceCat:
374  x, y = source.get(sourceCentroidKey)
375  ds9.dot("+", x, y, size=10, frame=frame, ctype=ds9.GREEN)
376 
377  if matches:
378  radArr = numpy.ndarray(len(matches))
379 
380  for i, m in enumerate(matches):
381  refCentroid = m.first.get(refCentroidKey)
382  sourceCentroid = m.second.get(sourceCentroidKey)
383  radArr[i] = math.hypot(*(refCentroid - sourceCentroid))
384  ds9.dot("o", x, y, size=10, frame=frame, ctype=ds9.YELLOW)
385 
386  print("<match radius> = %.4g +- %.4g [%d matches]" %
387  (radArr.mean(), radArr.std(), len(matches)))
def _matchAndFitWcs
Match sources to reference objects and fit a WCS.
Definition: astrometry.py:248
Class for storing ordered metadata with comments.
Definition: PropertyList.h:81
def __init__
Construct an AstrometryTask.
Definition: astrometry.py:126
def getDistortedWcs
Get a WCS from an exposureInfo, with distortion terms if possible.
Definition: utils.py:46
def run
Fit a WCS given a source catalog and exposure.
Definition: astrometry.py:137
PointKey< double > Point2DKey
Definition: aggregates.h:119
def solve
Fit a WCS given a source catalog and exposure matchMetadata.
Definition: astrometry.py:163
A floating-point coordinate rectangle geometry.
Definition: Box.h:271
Match an input source catalog with objects from a reference catalog and solve for the WCS...
Definition: astrometry.py:54