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
anetAstrometry.py
Go to the documentation of this file.
1 from __future__ import print_function
2 from builtins import input
3 from builtins import zip
4 from builtins import range
5 #
6 # LSST Data Management System
7 # Copyright 2008-2016 AURA/LSST.
8 #
9 # This product includes software developed by the
10 # LSST Project (http://www.lsst.org/).
11 #
12 # This program is free software: you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation, either version 3 of the License, or
15 # (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the LSST License Statement and
23 # the GNU General Public License along with this program. If not,
24 # see <http://www.lsstcorp.org/LegalNotices/>.
25 #
26 from contextlib import contextmanager
27 
28 import numpy as np
29 
30 import lsstDebug
32 import lsst.afw.geom as afwGeom
33 from lsst.afw.cameraGeom import TAN_PIXELS
34 from lsst.afw.table import Point2DKey, CovarianceMatrix2fKey
35 import lsst.pex.config as pexConfig
36 import lsst.pipe.base as pipeBase
37 from .anetBasicAstrometry import ANetBasicAstrometryTask
38 from .sip import makeCreateWcsWithSip
39 from .display import displayAstrometry
40 
41 
42 class ANetAstrometryConfig(pexConfig.Config):
43  solver = pexConfig.ConfigurableField(
44  target=ANetBasicAstrometryTask,
45  doc="Basic astrometry solver",
46  )
47  forceKnownWcs = pexConfig.Field(dtype=bool, doc=(
48  "Assume that the input image's WCS is correct, without comparing it to any external reality." +
49  " (In contrast to using Astrometry.net). NOTE, if you set this, you probably also want to" +
50  " un-set 'solver.calculateSip'; otherwise we'll still try to find a TAN-SIP WCS starting " +
51  " from the existing WCS"), default=False)
52  rejectThresh = pexConfig.RangeField(dtype=float, default=3.0, doc="Rejection threshold for Wcs fitting",
53  min=0.0, inclusiveMin=False)
54  rejectIter = pexConfig.RangeField(dtype=int, default=3, doc="Rejection iterations for Wcs fitting",
55  min=0)
56 
57  @property
58  def refObjLoader(self):
59  """An alias, for a uniform interface with the standard AstrometryTask"""
60  return self.solver
61 
62  # \addtogroup LSST_task_documentation
63  # \{
64  # \page measAstrom_anetAstrometryTask
65  # \ref ANetAstrometryTask_ "ANetAstrometryTask"
66  # Use astrometry.net to match input sources with a reference catalog and solve for the Wcs
67  # \}
68 
69 
70 class ANetAstrometryTask(pipeBase.Task):
71  """!Use astrometry.net to match input sources with a reference catalog and solve for the Wcs
72 
73  @anchor ANetAstrometryTask_
74 
75  The actual matching and solving is done by the 'solver'; this Task
76  serves as a wrapper for taking into account the known optical distortion.
77 
78  \section pipe_tasks_astrometry_Contents Contents
79 
80  - \ref pipe_tasks_astrometry_Purpose
81  - \ref pipe_tasks_astrometry_Initialize
82  - \ref pipe_tasks_astrometry_IO
83  - \ref pipe_tasks_astrometry_Config
84  - \ref pipe_tasks_astrometry_Debug
85  - \ref pipe_tasks_astrometry_Example
86 
87  \section pipe_tasks_astrometry_Purpose Description
88 
89  \copybrief ANetAstrometryTask
90 
91  \section pipe_tasks_astrometry_Initialize Task initialisation
92 
93  \copydoc \_\_init\_\_
94 
95  \section pipe_tasks_astrometry_IO Invoking the Task
96 
97  \copydoc run
98 
99  \section pipe_tasks_astrometry_Config Configuration parameters
100 
101  See \ref ANetAstrometryConfig
102 
103  \section pipe_tasks_astrometry_Debug Debug variables
104 
105  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
106  flag \c -d to import \b debug.py from your \c PYTHONPATH;
107  see \ref baseDebug for more about \b debug.py files.
108 
109  The available variables in ANetAstrometryTask are:
110  <DL>
111  <DT> \c display
112  <DD> If True call showAstrometry while iterating ANetAstrometryConfig.rejectIter times,
113  and also after converging; and call displayAstrometry after applying the distortion correction.
114  <DT> \c frame
115  <DD> ds9 frame to use in showAstrometry and displayAstrometry
116  <DT> \c pause
117  <DD> Pause after showAstrometry and displayAstrometry?
118  </DL>
119 
120  \section pipe_tasks_astrometry_Example A complete example of using ANetAstrometryTask
121 
122  See \ref meas_photocal_photocal_Example.
123 
124  To investigate the \ref pipe_tasks_astrometry_Debug, put something like
125  \code{.py}
126  import lsstDebug
127  def DebugInfo(name):
128  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
129  if name in ("lsst.pipe.tasks.anetAstrometry", "lsst.pipe.tasks.anetBasicAstrometry"):
130  di.display = 1
131  di.frame = 1
132  di.pause = True
133 
134  return di
135 
136  lsstDebug.Info = DebugInfo
137  \endcode
138  into your debug.py file and run photoCalTask.py with the \c --debug flag.
139  """
140  ConfigClass = ANetAstrometryConfig
141 
142  def __init__(self, schema, refObjLoader=None, **kwds):
143  """!Create the astrometric calibration task. Most arguments are simply passed onto pipe.base.Task.
144 
145  \param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
146  \param refObjLoader The AstrometryTask constructor requires a refObjLoader. In order to make this
147  task retargettable for AstrometryTask it needs to take the same arguments. This argument will be
148  ignored since it uses its own internal loader.
149  \param **kwds keyword arguments to be passed to the lsst.pipe.base.task.Task constructor
150 
151  A centroid field "centroid.distorted" (used internally during the Task's operation)
152  will be added to the schema.
153  """
154  pipeBase.Task.__init__(self, **kwds)
155  self.distortedName = "astrom_distorted"
156  self.centroidXKey = schema.addField(self.distortedName + "_x", type="D",
157  doc="centroid distorted for astrometry solver")
158  self.centroidYKey = schema.addField(self.distortedName + "_y", type="D",
159  doc="centroid distorted for astrometry solver")
160  self.centroidXErrKey = schema.addField(self.distortedName + "_xSigma", type="F",
161  doc="centroid distorted err for astrometry solver")
162  self.centroidYErrKey = schema.addField(self.distortedName + "_ySigma", type="F",
163  doc="centroid distorted err for astrometry solver")
164  self.centroidFlagKey = schema.addField(self.distortedName + "_flag", type="Flag",
165  doc="centroid distorted flag astrometry solver")
167  self.centroidErrKey = CovarianceMatrix2fKey((self.centroidXErrKey, self.centroidYErrKey))
168  # postpone making the solver subtask because it may not be needed and is expensive to create
169  self.solver = None
170 
171  @pipeBase.timeMethod
172  def run(self, exposure, sourceCat):
173  """!Load reference objects, match sources and optionally fit a WCS
174 
175  This is a thin layer around solve or loadAndMatch, depending on config.forceKnownWcs
176 
177  @param[in,out] exposure exposure whose WCS is to be fit
178  The following are read only:
179  - bbox
180  - calib (may be absent)
181  - filter (may be unset)
182  - detector (if wcs is pure tangent; may be absent)
183  The following are updated:
184  - wcs (the initial value is used as an initial guess, and is required)
185  @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
186  @return an lsst.pipe.base.Struct with these fields:
187  - refCat reference object catalog of objects that overlap the exposure (with some margin)
188  (an lsst::afw::table::SimpleCatalog)
189  - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
190  - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
191  """
192  if self.config.forceKnownWcs:
193  return self.loadAndMatch(exposure=exposure, sourceCat=sourceCat)
194  else:
195  return self.solve(exposure=exposure, sourceCat=sourceCat)
196 
197  @pipeBase.timeMethod
198  def solve(self, exposure, sourceCat):
199  """!Match with reference sources and calculate an astrometric solution
200 
201  \param[in,out] exposure Exposure to calibrate; wcs is updated
202  \param[in] sourceCat catalog of measured sources (an lsst.afw.table.SourceCatalog)
203  \return a pipeBase.Struct with fields:
204  - refCat reference object catalog of objects that overlap the exposure (with some margin)
205  (an lsst::afw::table::SimpleCatalog)
206  - matches: Astrometric matches, as an lsst.afw.table.ReferenceMatchVector
207  - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
208 
209  The reference catalog actually used is up to the implementation
210  of the solver; it will be manifested in the returned matches as
211  a list of lsst.afw.table.ReferenceMatch objects (\em i.e. of lsst.afw.table.Match with
212  \c first being of type lsst.afw.table.SimpleRecord and \c second type lsst.afw.table.SourceRecord ---
213  the reference object and matched object respectively).
214 
215  \note
216  The input sources have the centroid slot moved to a new column "centroid.distorted"
217  which has the positions corrected for any known optical distortion;
218  the 'solver' (which is instantiated in the 'astrometry' member)
219  should therefore simply use the centroids provided by calling
220  afw.table.Source.getCentroid() on the individual source records. This column \em must
221  be present in the sources table.
222 
223  \note ignores config.forceKnownWcs
224  """
225  with self.distortionContext(sourceCat=sourceCat, exposure=exposure) as bbox:
226  results = self._astrometry(sourceCat=sourceCat, exposure=exposure, bbox=bbox)
227 
228  if results.matches:
229  self.refitWcs(sourceCat=sourceCat, exposure=exposure, matches=results.matches)
230 
231  return results
232 
233  @pipeBase.timeMethod
234  def distort(self, sourceCat, exposure):
235  """!Calculate distorted source positions
236 
237  CCD images are often affected by optical distortion that makes
238  the astrometric solution higher order than linear. Unfortunately,
239  most (all?) matching algorithms require that the distortion be
240  small or zero, and so it must be removed. We do this by calculating
241  (un-)distorted positions, based on a known optical distortion model
242  in the Ccd.
243 
244  The distortion correction moves sources, so we return the distorted bounding box.
245 
246  \param[in] exposure Exposure to process
247  \param[in,out] sourceCat SourceCatalog; getX() and getY() will be used as inputs,
248  with distorted points in "centroid.distorted" field.
249  \return bounding box of distorted exposure
250  """
251  detector = exposure.getDetector()
252  pixToTanXYTransform = None
253  if detector is None:
254  self.log.warn("No detector associated with exposure; assuming null distortion")
255  else:
256  tanSys = detector.makeCameraSys(TAN_PIXELS)
257  pixToTanXYTransform = detector.getTransformMap().get(tanSys)
258 
259  if pixToTanXYTransform is None:
260  self.log.info("Null distortion correction")
261  for s in sourceCat:
262  s.set(self.centroidKey, s.getCentroid())
263  s.set(self.centroidErrKey, s.getCentroidErr())
264  s.set(self.centroidFlagKey, s.getCentroidFlag())
265  return exposure.getBBox()
266 
267  # Distort source positions
268  self.log.info("Applying distortion correction")
269  for s in sourceCat:
270  centroid = pixToTanXYTransform.forwardTransform(s.getCentroid())
271  s.set(self.centroidKey, centroid)
272  s.set(self.centroidErrKey, s.getCentroidErr())
273  s.set(self.centroidFlagKey, s.getCentroidFlag())
274 
275  # Get distorted image size so that astrometry_net does not clip.
276  bboxD = afwGeom.Box2D()
277  for corner in detector.getCorners(TAN_PIXELS):
278  bboxD.include(corner)
279 
280  if lsstDebug.Info(__name__).display:
281  frame = lsstDebug.Info(__name__).frame
282  pause = lsstDebug.Info(__name__).pause
283  displayAstrometry(sourceCat=sourceCat, distortedCentroidKey=self.centroidKey,
284  exposure=exposure, frame=frame, pause=pause)
285 
286  return afwGeom.Box2I(bboxD)
287 
288  @contextmanager
289  def distortionContext(self, sourceCat, exposure):
290  """!Context manager that applies and removes distortion
291 
292  We move the "centroid" definition in the catalog table to
293  point to the distorted positions. This is undone on exit
294  from the context.
295 
296  The input Wcs is taken to refer to the coordinate system
297  with the distortion correction applied, and hence no shift
298  is required when the sources are distorted. However, after
299  Wcs fitting, the Wcs is in the distorted frame so when the
300  distortion correction is removed, the Wcs needs to be
301  shifted to compensate.
302 
303  \param sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog
304  \param exposure Exposure holding Wcs, an lsst.afw.image.ExposureF or D
305  \return bounding box of distorted exposure
306  """
307  # Apply distortion, if not already present in the exposure's WCS
308  if exposure.getWcs().hasDistortion():
309  yield exposure.getBBox()
310  else:
311  bbox = self.distort(sourceCat=sourceCat, exposure=exposure)
312  oldCentroidName = sourceCat.table.getCentroidDefinition()
313  sourceCat.table.defineCentroid(self.distortedName)
314  try:
315  yield bbox # Execute 'with' block, providing bbox to 'as' variable
316  finally:
317  # Un-apply distortion
318  sourceCat.table.defineCentroid(oldCentroidName)
319  x0, y0 = exposure.getXY0()
320  wcs = exposure.getWcs()
321  if wcs:
322  wcs.shiftReferencePixel(-bbox.getMinX() + x0, -bbox.getMinY() + y0)
323 
324  @pipeBase.timeMethod
325  def loadAndMatch(self, exposure, sourceCat, bbox=None):
326  """!Load reference objects overlapping an exposure and match to sources detected on that exposure
327 
328  @param[in] exposure exposure whose WCS is to be fit
329  @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
330  @param[in] bbox bounding box go use for finding reference objects; if None, use exposure's bbox
331 
332  @return an lsst.pipe.base.Struct with these fields:
333  - refCat reference object catalog of objects that overlap the exposure (with some margin)
334  (an lsst::afw::table::SimpleCatalog)
335  - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
336  - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
337 
338  @note ignores config.forceKnownWcs
339  """
340  with self.distortionContext(sourceCat=sourceCat, exposure=exposure) as bbox:
341  if not self.solver:
342  self.makeSubtask("solver")
343 
344  astrom = self.solver.useKnownWcs(
345  sourceCat=sourceCat,
346  exposure=exposure,
347  bbox=bbox,
348  calculateSip=False,
349  )
350 
351  if astrom is None or astrom.getWcs() is None:
352  raise RuntimeError("Unable to solve astrometry")
353 
354  matches = astrom.getMatches()
355  matchMeta = astrom.getMatchMetadata()
356  if matches is None or len(matches) == 0:
357  raise RuntimeError("No astrometric matches")
358  self.log.info("%d astrometric matches" % (len(matches)))
359 
360  if self._display:
361  frame = lsstDebug.Info(__name__).frame
362  displayAstrometry(exposure=exposure, sourceCat=sourceCat, matches=matches,
363  frame=frame, pause=False)
364 
365  return pipeBase.Struct(
366  refCat=astrom.refCat,
367  matches=matches,
368  matchMeta=matchMeta,
369  )
370 
371  @pipeBase.timeMethod
372  def _astrometry(self, sourceCat, exposure, bbox=None):
373  """!Solve astrometry to produce WCS
374 
375  \param[in] sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog
376  \param[in,out] exposure Exposure to process, an lsst.afw.image.ExposureF or D; wcs is updated
377  \param[in] bbox Bounding box, or None to use exposure
378  \return a pipe.base.Struct with fields:
379  - refCat reference object catalog of objects that overlap the exposure (with some margin)
380  (an lsst::afw::table::SimpleCatalog)
381  - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
382  - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
383  """
384  self.log.info("Solving astrometry")
385  if bbox is None:
386  bbox = exposure.getBBox()
387 
388  if not self.solver:
389  self.makeSubtask("solver")
390 
391  astrom = self.solver.determineWcs(sourceCat=sourceCat, exposure=exposure, bbox=bbox)
392 
393  if astrom is None or astrom.getWcs() is None:
394  raise RuntimeError("Unable to solve astrometry")
395 
396  matches = astrom.getMatches()
397  matchMeta = astrom.getMatchMetadata()
398  if matches is None or len(matches) == 0:
399  raise RuntimeError("No astrometric matches")
400  self.log.info("%d astrometric matches" % (len(matches)))
401 
402  # Note that this is the Wcs for the provided positions, which may be distorted
403  exposure.setWcs(astrom.getWcs())
404 
405  if self._display:
406  frame = lsstDebug.Info(__name__).frame
407  displayAstrometry(exposure=exposure, sourceCat=sourceCat, matches=matches,
408  frame=frame, pause=False)
409 
410  return pipeBase.Struct(
411  refCat=astrom.refCat,
412  matches=matches,
413  matchMeta=matchMeta,
414  )
415 
416  @pipeBase.timeMethod
417  def refitWcs(self, sourceCat, exposure, matches):
418  """!A final Wcs solution after matching and removing distortion
419 
420  Specifically, fitting the non-linear part, since the linear
421  part has been provided by the matching engine.
422 
423  \param sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog
424  \param exposure Exposure of interest, an lsst.afw.image.ExposureF or D
425  \param matches Astrometric matches, as an lsst.afw.table.ReferenceMatchVector
426 
427  \return the resolved-Wcs object, or None if config.solver.calculateSip is False.
428  """
429  sip = None
430  if self.config.solver.calculateSip:
431  self.log.info("Refitting WCS")
432  origMatches = matches
433  wcs = exposure.getWcs()
434 
435  import lsstDebug
436  display = lsstDebug.Info(__name__).display
437  frame = lsstDebug.Info(__name__).frame
438  pause = lsstDebug.Info(__name__).pause
439 
440  def fitWcs(initialWcs, title=None):
441  """!Do the WCS fitting and display of the results"""
442  sip = makeCreateWcsWithSip(matches, initialWcs, self.config.solver.sipOrder)
443  resultWcs = sip.getNewWcs()
444  if display:
445  showAstrometry(exposure, resultWcs, origMatches, matches, frame=frame,
446  title=title, pause=pause)
447  return resultWcs, sip.getScatterOnSky()
448 
449  numRejected = 0
450  try:
451  for i in range(self.config.rejectIter):
452  wcs, scatter = fitWcs(wcs, title="Iteration %d" % i)
453 
454  ref = np.array([wcs.skyToPixel(m.first.getCoord()) for m in matches])
455  src = np.array([m.second.getCentroid() for m in matches])
456  diff = ref - src
457  rms = diff.std()
458  trimmed = []
459  for d, m in zip(diff, matches):
460  if np.all(np.abs(d) < self.config.rejectThresh*rms):
461  trimmed.append(m)
462  else:
463  numRejected += 1
464  if len(matches) == len(trimmed):
465  break
466  matches = trimmed
467 
468  # Final fit after rejection iterations
469  wcs, scatter = fitWcs(wcs, title="Final astrometry")
470 
471  except lsst.pex.exceptions.LengthError as e:
472  self.log.warn("Unable to fit SIP: %s" % e)
473 
474  self.log.info("Astrometric scatter: %f arcsec (%s non-linear terms, %d matches, %d rejected)" %
475  (scatter.asArcseconds(), "with" if wcs.hasDistortion() else "without",
476  len(matches), numRejected))
477  exposure.setWcs(wcs)
478 
479  # Apply WCS to sources
480  for index, source in enumerate(sourceCat):
481  sky = wcs.pixelToSky(source.getX(), source.getY())
482  source.setCoord(sky)
483  else:
484  self.log.warn("Not calculating a SIP solution; matches may be suspect")
485 
486  if self._display:
487  frame = lsstDebug.Info(__name__).frame
488  displayAstrometry(exposure=exposure, sourceCat=sourceCat, matches=matches,
489  frame=frame, pause=False)
490 
491  return sip
492 
493 
494 def showAstrometry(exposure, wcs, allMatches, useMatches, frame=0, title=None, pause=False):
495  """!Show results of astrometry fitting
496 
497  \param exposure Image to display
498  \param wcs Astrometric solution
499  \param allMatches List of all astrometric matches (including rejects)
500  \param useMatches List of used astrometric matches
501  \param frame Frame number for display
502  \param title Title for display
503  \param pause Pause to allow viewing of the display and optional debugging?
504 
505  - Matches are shown in yellow if used in the Wcs solution, otherwise red
506  - +: Detected objects
507  - x: Catalogue objects
508  """
509  import lsst.afw.display.ds9 as ds9
510  ds9.mtv(exposure, frame=frame, title=title)
511 
512  useIndices = set(m.second.getId() for m in useMatches)
513 
514  radii = []
515  with ds9.Buffering():
516  for i, m in enumerate(allMatches):
517  x, y = m.second.getX(), m.second.getY()
518  pix = wcs.skyToPixel(m.first.getCoord())
519 
520  isUsed = m.second.getId() in useIndices
521  if isUsed:
522  radii.append(np.hypot(pix[0] - x, pix[1] - y))
523 
524  color = ds9.YELLOW if isUsed else ds9.RED
525 
526  ds9.dot("+", x, y, size=10, frame=frame, ctype=color)
527  ds9.dot("x", pix[0], pix[1], size=10, frame=frame, ctype=color)
528 
529  radii = np.array(radii)
530  print("<dr> = %.4g +- %.4g pixels [%d/%d matches]" % (radii.mean(), radii.std(),
531  len(useMatches), len(allMatches)))
532 
533  if pause:
534  import sys
535  while True:
536  try:
537  reply = input("Debugging? [p]db [q]uit; any other key to continue... ").strip()
538  except EOFError:
539  reply = ""
540 
541  if len(reply) > 1:
542  reply = reply[0]
543  if reply == "p":
544  import pdb
545  pdb.set_trace()
546  elif reply == "q":
547  sys.exit(1)
548  else:
549  break
def refitWcs
A final Wcs solution after matching and removing distortion.
def __init__
Create the astrometric calibration task.
def solve
Match with reference sources and calculate an astrometric solution.
An integer coordinate rectangle.
Definition: Box.h:53
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.
def distortionContext
Context manager that applies and removes distortion.
def loadAndMatch
Load reference objects overlapping an exposure and match to sources detected on that exposure...
Use astrometry.net to match input sources with a reference catalog and solve for the Wcs...
PointKey< double > Point2DKey
Definition: aggregates.h:111
def _astrometry
Solve astrometry to produce WCS.
metadata input
A floating-point coordinate rectangle geometry.
Definition: Box.h:271
def run
Load reference objects, match sources and optionally fit a WCS.
def distort
Calculate distorted source positions.
def showAstrometry
Show results of astrometry fitting.