1 from __future__
import print_function
2 from builtins
import input
3 from builtins
import zip
4 from builtins
import range
26 from contextlib
import contextmanager
37 from .anetBasicAstrometry
import ANetBasicAstrometryTask
38 from .sip
import makeCreateWcsWithSip
39 from .display
import displayAstrometry
43 solver = pexConfig.ConfigurableField(
44 target=ANetBasicAstrometryTask,
45 doc=
"Basic astrometry solver",
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",
59 """An alias, for a uniform interface with the standard AstrometryTask"""
71 """!Use astrometry.net to match input sources with a reference catalog and solve for the Wcs
73 @anchor ANetAstrometryTask_
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.
78 \section pipe_tasks_astrometry_Contents Contents
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
87 \section pipe_tasks_astrometry_Purpose Description
89 \copybrief ANetAstrometryTask
91 \section pipe_tasks_astrometry_Initialize Task initialisation
95 \section pipe_tasks_astrometry_IO Invoking the Task
99 \section pipe_tasks_astrometry_Config Configuration parameters
101 See \ref ANetAstrometryConfig
103 \section pipe_tasks_astrometry_Debug Debug variables
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.
109 The available variables in ANetAstrometryTask are:
112 <DD> If True call showAstrometry while iterating ANetAstrometryConfig.rejectIter times,
113 and also after converging; and call displayAstrometry after applying the distortion correction.
115 <DD> ds9 frame to use in showAstrometry and displayAstrometry
117 <DD> Pause after showAstrometry and displayAstrometry?
120 \section pipe_tasks_astrometry_Example A complete example of using ANetAstrometryTask
122 See \ref meas_photocal_photocal_Example.
124 To investigate the \ref pipe_tasks_astrometry_Debug, put something like
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"):
136 lsstDebug.Info = DebugInfo
138 into your debug.py file and run photoCalTask.py with the \c --debug flag.
140 ConfigClass = ANetAstrometryConfig
142 def __init__(self, schema, refObjLoader=None, **kwds):
143 """!Create the astrometric calibration task. Most arguments are simply passed onto pipe.base.Task.
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
151 A centroid field "centroid.distorted" (used internally during the Task's operation)
152 will be added to the schema.
154 pipeBase.Task.__init__(self, **kwds)
157 doc=
"centroid distorted for astrometry solver")
159 doc=
"centroid distorted for astrometry solver")
161 doc=
"centroid distorted err for astrometry solver")
163 doc=
"centroid distorted err for astrometry solver")
165 doc=
"centroid distorted flag astrometry solver")
172 def run(self, exposure, sourceCat):
173 """!Load reference objects, match sources and optionally fit a WCS
175 This is a thin layer around solve or loadAndMatch, depending on config.forceKnownWcs
177 @param[in,out] exposure exposure whose WCS is to be fit
178 The following are read only:
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)
192 if self.config.forceKnownWcs:
193 return self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
195 return self.
solve(exposure=exposure, sourceCat=sourceCat)
198 def solve(self, exposure, sourceCat):
199 """!Match with reference sources and calculate an astrometric solution
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)
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).
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.
223 \note ignores config.forceKnownWcs
226 results = self.
_astrometry(sourceCat=sourceCat, exposure=exposure, bbox=bbox)
229 self.
refitWcs(sourceCat=sourceCat, exposure=exposure, matches=results.matches)
235 """!Calculate distorted source positions
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
244 The distortion correction moves sources, so we return the distorted bounding box.
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
251 detector = exposure.getDetector()
252 pixToTanXYTransform =
None
254 self.log.warn(
"No detector associated with exposure; assuming null distortion")
256 tanSys = detector.makeCameraSys(TAN_PIXELS)
257 pixToTanXYTransform = detector.getTransformMap().get(tanSys)
259 if pixToTanXYTransform
is None:
260 self.log.info(
"Null distortion correction")
265 return exposure.getBBox()
268 self.log.info(
"Applying distortion correction")
270 centroid = pixToTanXYTransform.forwardTransform(s.getCentroid())
277 for corner
in detector.getCorners(TAN_PIXELS):
278 bboxD.include(corner)
284 exposure=exposure, frame=frame, pause=pause)
290 """!Context manager that applies and removes distortion
292 We move the "centroid" definition in the catalog table to
293 point to the distorted positions. This is undone on exit
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.
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
308 if exposure.getWcs().hasDistortion():
309 yield exposure.getBBox()
311 bbox = self.
distort(sourceCat=sourceCat, exposure=exposure)
312 oldCentroidName = sourceCat.table.getCentroidDefinition()
318 sourceCat.table.defineCentroid(oldCentroidName)
319 x0, y0 = exposure.getXY0()
320 wcs = exposure.getWcs()
322 wcs.shiftReferencePixel(-bbox.getMinX() + x0, -bbox.getMinY() + y0)
326 """!Load reference objects overlapping an exposure and match to sources detected on that exposure
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
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)
338 @note ignores config.forceKnownWcs
342 self.makeSubtask(
"solver")
344 astrom = self.solver.useKnownWcs(
351 if astrom
is None or astrom.getWcs()
is None:
352 raise RuntimeError(
"Unable to solve astrometry")
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)))
363 frame=frame, pause=
False)
365 return pipeBase.Struct(
366 refCat=astrom.refCat,
373 """!Solve astrometry to produce WCS
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)
384 self.log.info(
"Solving astrometry")
386 bbox = exposure.getBBox()
389 self.makeSubtask(
"solver")
391 astrom = self.solver.determineWcs(sourceCat=sourceCat, exposure=exposure, bbox=bbox)
393 if astrom
is None or astrom.getWcs()
is None:
394 raise RuntimeError(
"Unable to solve astrometry")
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)))
403 exposure.setWcs(astrom.getWcs())
408 frame=frame, pause=
False)
410 return pipeBase.Struct(
411 refCat=astrom.refCat,
418 """!A final Wcs solution after matching and removing distortion
420 Specifically, fitting the non-linear part, since the linear
421 part has been provided by the matching engine.
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
427 \return the resolved-Wcs object, or None if config.solver.calculateSip is False.
430 if self.config.solver.calculateSip:
431 self.log.info(
"Refitting WCS")
432 origMatches = matches
433 wcs = exposure.getWcs()
440 def fitWcs(initialWcs, title=None):
441 """!Do the WCS fitting and display of the results"""
443 resultWcs = sip.getNewWcs()
445 showAstrometry(exposure, resultWcs, origMatches, matches, frame=frame,
446 title=title, pause=pause)
447 return resultWcs, sip.getScatterOnSky()
451 for i
in range(self.config.rejectIter):
452 wcs, scatter = fitWcs(wcs, title=
"Iteration %d" % i)
454 ref = np.array([wcs.skyToPixel(m.first.getCoord())
for m
in matches])
455 src = np.array([m.second.getCentroid()
for m
in matches])
459 for d, m
in zip(diff, matches):
460 if np.all(np.abs(d) < self.config.rejectThresh*rms):
464 if len(matches) == len(trimmed):
469 wcs, scatter = fitWcs(wcs, title=
"Final astrometry")
471 except lsst.pex.exceptions.LengthError
as e:
472 self.log.warn(
"Unable to fit SIP: %s" % e)
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))
480 for index, source
in enumerate(sourceCat):
481 sky = wcs.pixelToSky(source.getX(), source.getY())
484 self.log.warn(
"Not calculating a SIP solution; matches may be suspect")
489 frame=frame, pause=
False)
494 def showAstrometry(exposure, wcs, allMatches, useMatches, frame=0, title=None, pause=False):
495 """!Show results of astrometry fitting
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?
505 - Matches are shown in yellow if used in the Wcs solution, otherwise red
506 - +: Detected objects
507 - x: Catalogue objects
510 ds9.mtv(exposure, frame=frame, title=title)
512 useIndices = set(m.second.getId()
for m
in useMatches)
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())
520 isUsed = m.second.getId()
in useIndices
522 radii.append(np.hypot(pix[0] - x, pix[1] - y))
524 color = ds9.YELLOW
if isUsed
else ds9.RED
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)
529 radii = np.array(radii)
530 print(
"<dr> = %.4g +- %.4g pixels [%d/%d matches]" % (radii.mean(), radii.std(),
531 len(useMatches), len(allMatches)))
537 reply =
input(
"Debugging? [p]db [q]uit; any other key to continue... ").strip()
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.