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)
315 @param[in] bbox bounding box go use for finding reference objects; if None, use exposure's bbox
317 @return an lsst.pipe.base.Struct with these fields:
318 - refCat reference object catalog of objects that overlap the exposure (with some margin)
319 (an lsst::afw::table::SimpleCatalog)
320 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
321 - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
323 @note ignores config.forceKnownWcs
327 self.makeSubtask(
"solver")
329 astrom = self.solver.useKnownWcs(
330 sourceCat = sourceCat,
333 calculateSip =
False,
336 if astrom
is None or astrom.getWcs()
is None:
337 raise RuntimeError(
"Unable to solve astrometry")
339 matches = astrom.getMatches()
340 matchMeta = astrom.getMatchMetadata()
341 if matches
is None or len(matches) == 0:
342 raise RuntimeError(
"No astrometric matches")
343 self.log.info(
"%d astrometric matches" % (len(matches)))
345 self.display(
'astrometry', exposure=exposure, sources=sourceCat, matches=matches)
347 return pipeBase.Struct(
348 refCat = astrom.refCat,
350 matchMeta = matchMeta,
355 """!Solve astrometry to produce WCS
357 \param[in] sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog
358 \param[in,out] exposure Exposure to process, an lsst.afw.image.ExposureF or D; wcs is updated
359 \param[in] bbox Bounding box, or None to use exposure
360 \return a pipe.base.Struct with fields:
361 - refCat reference object catalog of objects that overlap the exposure (with some margin)
362 (an lsst::afw::table::SimpleCatalog)
363 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
364 - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
366 self.log.info(
"Solving astrometry")
368 bbox = exposure.getBBox()
371 self.makeSubtask(
"solver")
373 astrom = self.solver.determineWcs(sourceCat=sourceCat, exposure=exposure, bbox=bbox)
375 if astrom
is None or astrom.getWcs()
is None:
376 raise RuntimeError(
"Unable to solve astrometry")
378 matches = astrom.getMatches()
379 matchMeta = astrom.getMatchMetadata()
380 if matches
is None or len(matches) == 0:
381 raise RuntimeError(
"No astrometric matches")
382 self.log.info(
"%d astrometric matches" % (len(matches)))
385 exposure.setWcs(astrom.getWcs())
387 self.display(
'astrometry', exposure=exposure, sources=sourceCat, matches=matches)
389 return pipeBase.Struct(
390 refCat = astrom.refCat,
392 matchMeta = matchMeta,
397 """!A final Wcs solution after matching and removing distortion
399 Specifically, fitting the non-linear part, since the linear
400 part has been provided by the matching engine.
402 \param sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog
403 \param exposure Exposure of interest, an lsst.afw.image.ExposureF or D
404 \param matches Astrometric matches, as an lsst.afw.table.ReferenceMatchVector
406 \return the resolved-Wcs object, or None if config.solver.calculateSip is False.
409 if self.config.solver.calculateSip:
410 self.log.info(
"Refitting WCS")
411 origMatches = matches
412 wcs = exposure.getWcs()
419 def fitWcs(initialWcs, title=None):
420 """!Do the WCS fitting and display of the results"""
422 resultWcs = sip.getNewWcs()
424 showAstrometry(exposure, resultWcs, origMatches, matches, frame=frame,
425 title=title, pause=pause)
426 return resultWcs, sip.getScatterOnSky()
430 for i
in range(self.config.rejectIter):
431 wcs, scatter = fitWcs(wcs, title=
"Iteration %d" % i)
433 ref = numpy.array([wcs.skyToPixel(m.first.getCoord())
for m
in matches])
434 src = numpy.array([m.second.getCentroid()
for m
in matches])
438 for d, m
in zip(diff, matches):
439 if numpy.all(numpy.abs(d) < self.config.rejectThresh*rms):
443 if len(matches) == len(trimmed):
448 wcs, scatter = fitWcs(wcs, title=
"Final astrometry")
450 except lsst.pex.exceptions.LengthError
as e:
451 self.log.warn(
"Unable to fit SIP: %s" % e)
453 self.log.info(
"Astrometric scatter: %f arcsec (%s non-linear terms, %d matches, %d rejected)" %
454 (scatter.asArcseconds(),
"with" if wcs.hasDistortion()
else "without",
455 len(matches), numRejected))
459 for index, source
in enumerate(sourceCat):
460 sky = wcs.pixelToSky(source.getX(), source.getY())
463 self.log.warn(
"Not calculating a SIP solution; matches may be suspect")
465 self.display(
'astrometry', exposure=exposure, sources=sourceCat, matches=matches)
470 def showAstrometry(exposure, wcs, allMatches, useMatches, frame=0, title=None, pause=False):
471 """!Show results of astrometry fitting
473 \param exposure Image to display
474 \param wcs Astrometric solution
475 \param allMatches List of all astrometric matches (including rejects)
476 \param useMatches List of used astrometric matches
477 \param frame Frame number for display
478 \param title Title for display
479 \param pause Pause to allow viewing of the display and optional debugging?
481 - Matches are shown in yellow if used in the Wcs solution, otherwise red
482 - +: Detected objects
483 - x: Catalogue objects
486 ds9.mtv(exposure, frame=frame, title=title)
488 useIndices = set(m.second.getId()
for m
in useMatches)
491 with ds9.Buffering():
492 for i, m
in enumerate(allMatches):
493 x, y = m.second.getX(), m.second.getY()
494 pix = wcs.skyToPixel(m.first.getCoord())
496 isUsed = m.second.getId()
in useIndices
498 radii.append(numpy.hypot(pix[0] - x, pix[1] - y))
500 color = ds9.YELLOW
if isUsed
else ds9.RED
502 ds9.dot(
"+", x, y, size=10, frame=frame, ctype=color)
503 ds9.dot(
"x", pix[0], pix[1], size=10, frame=frame, ctype=color)
505 radii = numpy.array(radii)
506 print "<dr> = %.4g +- %.4g pixels [%d/%d matches]" % (radii.mean(), radii.std(),
507 len(useMatches), len(allMatches))
513 reply = raw_input(
"Debugging? [p]db [q]uit; any other key to continue... ").strip()
520 import pdb;pdb.set_trace()
def refitWcs
A final Wcs solution after matching and removing distortion.
def __init__
Create the astrometric calibration task.
PointKey< double > Point2DKey
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...
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.