23 from contextlib
import contextmanager
31 from .anetBasicAstrometry
import ANetBasicAstrometryTask
32 from .sip
import makeCreateWcsWithSip
33 from .display
import displayAstrometry
36 solver = pexConfig.ConfigurableField(
37 target = ANetBasicAstrometryTask,
38 doc =
"Basic astrometry solver",
40 forceKnownWcs = pexConfig.Field(dtype=bool, doc=(
41 "Assume that the input image's WCS is correct, without comparing it to any external reality." +
42 " (In contrast to using Astrometry.net). NOTE, if you set this, you probably also want to" +
43 " un-set 'solver.calculateSip'; otherwise we'll still try to find a TAN-SIP WCS starting " +
44 " from the existing WCS"), default=
False)
45 rejectThresh = pexConfig.RangeField(dtype=float, default=3.0, doc=
"Rejection threshold for Wcs fitting",
46 min=0.0, inclusiveMin=
False)
47 rejectIter = pexConfig.RangeField(dtype=int, default=3, doc=
"Rejection iterations for Wcs fitting",
59 """!Use astrometry.net to match input sources with a reference catalog and solve for the Wcs
61 @anchor ANetAstrometryTask_
63 The actual matching and solving is done by the 'solver'; this Task
64 serves as a wrapper for taking into account the known optical distortion.
66 \section pipe_tasks_astrometry_Contents Contents
68 - \ref pipe_tasks_astrometry_Purpose
69 - \ref pipe_tasks_astrometry_Initialize
70 - \ref pipe_tasks_astrometry_IO
71 - \ref pipe_tasks_astrometry_Config
72 - \ref pipe_tasks_astrometry_Debug
73 - \ref pipe_tasks_astrometry_Example
75 \section pipe_tasks_astrometry_Purpose Description
77 \copybrief ANetAstrometryTask
79 \section pipe_tasks_astrometry_Initialize Task initialisation
83 \section pipe_tasks_astrometry_IO Invoking the Task
87 \section pipe_tasks_astrometry_Config Configuration parameters
89 See \ref ANetAstrometryConfig
91 \section pipe_tasks_astrometry_Debug Debug variables
93 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
94 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
96 The available variables in ANetAstrometryTask are:
99 <DD> If True call showAstrometry while iterating ANetAstrometryConfig.rejectIter times,
100 and also after converging; and call displayAstrometry after applying the distortion correction.
102 <DD> ds9 frame to use in showAstrometry and displayAstrometry
104 <DD> Pause after showAstrometry and displayAstrometry?
107 \section pipe_tasks_astrometry_Example A complete example of using ANetAstrometryTask
109 See \ref meas_photocal_photocal_Example.
111 To investigate the \ref pipe_tasks_astrometry_Debug, put something like
115 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
116 if name in ("lsst.pipe.tasks.anetAstrometry", "lsst.pipe.tasks.anetBasicAstrometry"):
123 lsstDebug.Info = DebugInfo
125 into your debug.py file and run photoCalTask.py with the \c --debug flag.
127 ConfigClass = ANetAstrometryConfig
130 """!Create the astrometric calibration task. Most arguments are simply passed onto pipe.base.Task.
132 \param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
133 \param **kwds keyword arguments to be passed to the lsst.pipe.base.task.Task constructor
135 A centroid field "centroid.distorted" (used internally during the Task's operation)
136 will be added to the schema.
138 pipeBase.Task.__init__(self, **kwds)
141 doc=
"centroid distorted for astrometry solver")
143 doc=
"centroid distorted for astrometry solver")
145 doc=
"centroid distorted err for astrometry solver")
147 doc=
"centroid distorted err for astrometry solver")
149 doc=
"centroid distorted flag astrometry solver")
156 def run(self, exposure, sourceCat):
157 """!Load reference objects, match sources and optionally fit a WCS
159 This is a thin layer around solve or loadAndMatch, depending on config.forceKnownWcs
161 @param[in,out] exposure exposure whose WCS is to be fit
162 The following are read only:
164 - calib (may be absent)
165 - filter (may be unset)
166 - detector (if wcs is pure tangent; may be absent)
167 The following are updated:
168 - wcs (the initial value is used as an initial guess, and is required)
169 @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
170 @return an lsst.pipe.base.Struct with these fields:
171 - refCat reference object catalog of objects that overlap the exposure (with some margin)
172 (an lsst::afw::table::SimpleCatalog)
173 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
174 - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
176 if self.config.forceKnownWcs:
177 return self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
179 return self.
solve(exposure=exposure, sourceCat=sourceCat)
182 def solve(self, exposure, sourceCat):
183 """!Match with reference sources and calculate an astrometric solution
185 \param[in,out] exposure Exposure to calibrate; wcs is updated
186 \param[in] sourceCat catalog of measured sources (an lsst.afw.table.SourceCatalog)
187 \return a pipeBase.Struct with fields:
188 - refCat reference object catalog of objects that overlap the exposure (with some margin)
189 (an lsst::afw::table::SimpleCatalog)
190 - matches: Astrometric matches, as an lsst.afw.table.ReferenceMatchVector
191 - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
193 The reference catalog actually used is up to the implementation
194 of the solver; it will be manifested in the returned matches as
195 a list of lsst.afw.table.ReferenceMatch objects (\em i.e. of lsst.afw.table.Match with
196 \c first being of type lsst.afw.table.SimpleRecord and \c second type lsst.afw.table.SourceRecord ---
197 the reference object and matched object respectively).
200 The input sources have the centroid slot moved to a new column "centroid.distorted"
201 which has the positions corrected for any known optical distortion;
202 the 'solver' (which is instantiated in the 'astrometry' member)
203 should therefore simply use the centroids provided by calling
204 afw.table.Source.getCentroid() on the individual source records. This column \em must
205 be present in the sources table.
207 \note ignores config.forceKnownWcs
210 results = self.
_astrometry(sourceCat=sourceCat, exposure=exposure, bbox=bbox)
213 self.
refitWcs(sourceCat=sourceCat, exposure=exposure, matches=results.matches)
219 """!Calculate distorted source positions
221 CCD images are often affected by optical distortion that makes
222 the astrometric solution higher order than linear. Unfortunately,
223 most (all?) matching algorithms require that the distortion be
224 small or zero, and so it must be removed. We do this by calculating
225 (un-)distorted positions, based on a known optical distortion model
228 The distortion correction moves sources, so we return the distorted bounding box.
230 \param[in] exposure Exposure to process
231 \param[in,out] sourceCat SourceCatalog; getX() and getY() will be used as inputs,
232 with distorted points in "centroid.distorted" field.
233 \return bounding box of distorted exposure
235 detector = exposure.getDetector()
236 pixToTanXYTransform =
None
238 self.log.warn(
"No detector associated with exposure; assuming null distortion")
240 tanSys = detector.makeCameraSys(TAN_PIXELS)
241 pixToTanXYTransform = detector.getTransformMap().get(tanSys)
243 if pixToTanXYTransform
is None:
244 self.log.info(
"Null distortion correction")
249 return exposure.getBBox()
252 self.log.info(
"Applying distortion correction")
254 centroid = pixToTanXYTransform.forwardTransform(s.getCentroid())
261 for corner
in detector.getCorners(TAN_PIXELS):
262 bboxD.include(corner)
269 exposure=exposure, frame=frame, pause=pause)
275 """!Context manager that applies and removes distortion
277 We move the "centroid" definition in the catalog table to
278 point to the distorted positions. This is undone on exit
281 The input Wcs is taken to refer to the coordinate system
282 with the distortion correction applied, and hence no shift
283 is required when the sources are distorted. However, after
284 Wcs fitting, the Wcs is in the distorted frame so when the
285 distortion correction is removed, the Wcs needs to be
286 shifted to compensate.
288 \param sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog
289 \param exposure Exposure holding Wcs, an lsst.afw.image.ExposureF or D
290 \return bounding box of distorted exposure
293 if exposure.getWcs().hasDistortion():
294 yield exposure.getBBox()
296 bbox = self.
distort(sourceCat=sourceCat, exposure=exposure)
297 oldCentroidName = sourceCat.table.getCentroidDefinition()
303 sourceCat.table.defineCentroid(oldCentroidName)
304 x0, y0 = exposure.getXY0()
305 wcs = exposure.getWcs()
307 wcs.shiftReferencePixel(-bbox.getMinX() + x0, -bbox.getMinY() + y0)
311 """!Load reference objects overlapping an exposure and match to sources detected on that exposure
313 @param[in] exposure exposure whose WCS is to be fit
314 @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
316 @return an lsst.pipe.base.Struct with these fields:
317 - refCat reference object catalog of objects that overlap the exposure (with some margin)
318 (an lsst::afw::table::SimpleCatalog)
319 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
320 - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
322 @note ignores config.forceKnownWcs
326 self.makeSubtask(
"solver")
328 astrom = self.solver.useKnownWcs(
329 sourceCat = sourceCat,
332 calculateSip =
False,
335 if astrom
is None or astrom.getWcs()
is None:
336 raise RuntimeError(
"Unable to solve astrometry")
338 matches = astrom.getMatches()
339 matchMeta = astrom.getMatchMetadata()
340 if matches
is None or len(matches) == 0:
341 raise RuntimeError(
"No astrometric matches")
342 self.log.info(
"%d astrometric matches" % (len(matches)))
344 self.display(
'astrometry', exposure=exposure, sources=sourceCat, matches=matches)
346 return pipeBase.Struct(
347 refCat = astrom.refCat,
349 matchMeta = matchMeta,
354 """!Solve astrometry to produce WCS
356 \param[in] sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog
357 \param[in,out] exposure Exposure to process, an lsst.afw.image.ExposureF or D; wcs is updated
358 \param[in] bbox Bounding box, or None to use exposure
359 \return a pipe.base.Struct with fields:
360 - refCat reference object catalog of objects that overlap the exposure (with some margin)
361 (an lsst::afw::table::SimpleCatalog)
362 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
363 - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
365 self.log.info(
"Solving astrometry")
367 bbox = exposure.getBBox()
370 self.makeSubtask(
"solver")
372 astrom = self.solver.determineWcs(sourceCat=sourceCat, exposure=exposure, bbox=bbox)
374 if astrom
is None or astrom.getWcs()
is None:
375 raise RuntimeError(
"Unable to solve astrometry")
377 matches = astrom.getMatches()
378 matchMeta = astrom.getMatchMetadata()
379 if matches
is None or len(matches) == 0:
380 raise RuntimeError(
"No astrometric matches")
381 self.log.info(
"%d astrometric matches" % (len(matches)))
384 exposure.setWcs(astrom.getWcs())
386 self.display(
'astrometry', exposure=exposure, sources=sourceCat, matches=matches)
388 return pipeBase.Struct(
389 refCat = astrom.refCat,
391 matchMeta = matchMeta,
396 """!A final Wcs solution after matching and removing distortion
398 Specifically, fitting the non-linear part, since the linear
399 part has been provided by the matching engine.
401 \param sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog
402 \param exposure Exposure of interest, an lsst.afw.image.ExposureF or D
403 \param matches Astrometric matches, as an lsst.afw.table.ReferenceMatchVector
405 \return the resolved-Wcs object, or None if config.solver.calculateSip is False.
408 if self.config.solver.calculateSip:
409 self.log.info(
"Refitting WCS")
410 origMatches = matches
411 wcs = exposure.getWcs()
418 def fitWcs(initialWcs, title=None):
419 """!Do the WCS fitting and display of the results"""
421 resultWcs = sip.getNewWcs()
423 showAstrometry(exposure, resultWcs, origMatches, matches, frame=frame,
424 title=title, pause=pause)
425 return resultWcs, sip.getScatterOnSky()
429 for i
in range(self.config.rejectIter):
430 wcs, scatter = fitWcs(wcs, title=
"Iteration %d" % i)
432 ref = numpy.array([wcs.skyToPixel(m.first.getCoord())
for m
in matches])
433 src = numpy.array([m.second.getCentroid()
for m
in matches])
437 for d, m
in zip(diff, matches):
438 if numpy.all(numpy.abs(d) < self.config.rejectThresh*rms):
442 if len(matches) == len(trimmed):
447 wcs, scatter = fitWcs(wcs, title=
"Final astrometry")
449 except lsst.pex.exceptions.LengthError
as e:
450 self.log.warn(
"Unable to fit SIP: %s" % e)
452 self.log.info(
"Astrometric scatter: %f arcsec (%s non-linear terms, %d matches, %d rejected)" %
453 (scatter.asArcseconds(),
"with" if wcs.hasDistortion()
else "without",
454 len(matches), numRejected))
458 for index, source
in enumerate(sourceCat):
459 sky = wcs.pixelToSky(source.getX(), source.getY())
462 self.log.warn(
"Not calculating a SIP solution; matches may be suspect")
464 self.display(
'astrometry', exposure=exposure, sources=sourceCat, matches=matches)
469 def showAstrometry(exposure, wcs, allMatches, useMatches, frame=0, title=None, pause=False):
470 """!Show results of astrometry fitting
472 \param exposure Image to display
473 \param wcs Astrometric solution
474 \param allMatches List of all astrometric matches (including rejects)
475 \param useMatches List of used astrometric matches
476 \param frame Frame number for display
477 \param title Title for display
478 \param pause Pause to allow viewing of the display and optional debugging?
480 - Matches are shown in yellow if used in the Wcs solution, otherwise red
481 - +: Detected objects
482 - x: Catalogue objects
485 ds9.mtv(exposure, frame=frame, title=title)
487 useIndices = set(m.second.getId()
for m
in useMatches)
490 with ds9.Buffering():
491 for i, m
in enumerate(allMatches):
492 x, y = m.second.getX(), m.second.getY()
493 pix = wcs.skyToPixel(m.first.getCoord())
495 isUsed = m.second.getId()
in useIndices
497 radii.append(numpy.hypot(pix[0] - x, pix[1] - y))
499 color = ds9.YELLOW
if isUsed
else ds9.RED
501 ds9.dot(
"+", x, y, size=10, frame=frame, ctype=color)
502 ds9.dot(
"x", pix[0], pix[1], size=10, frame=frame, ctype=color)
504 radii = numpy.array(radii)
505 print "<dr> = %.4g +- %.4g pixels [%d/%d matches]" % (radii.mean(), radii.std(),
506 len(useMatches), len(allMatches))
512 reply = raw_input(
"Debugging? [p]db [q]uit; any other key to continue... ").strip()
519 import pdb;pdb.set_trace()
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.
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
def _astrometry
Solve astrometry to produce WCS.
A floating-point coordinate rectangle geometry.
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.