22 from __future__
import absolute_import, division, print_function
24 __all__ = [
"ANetAstrometryConfig",
"ANetAstrometryTask",
"showAstrometry"]
26 from builtins
import input
27 from builtins
import zip
28 from builtins
import range
36 from lsst.afw.table import Point2DKey, CovarianceMatrix2fKey, updateSourceCoords
41 from .anetBasicAstrometry
import ANetBasicAstrometryTask
45 solver = pexConfig.ConfigurableField(
46 target=ANetBasicAstrometryTask,
47 doc=
"Basic astrometry solver",
49 forceKnownWcs = pexConfig.Field(dtype=bool, doc=(
50 "Assume that the input image's WCS is correct, without comparing it to any external reality." 51 " (In contrast to using Astrometry.net). NOTE, if you set this, you probably also want to" 52 " un-set 'solver.calculateSip'; otherwise we'll still try to find a TAN-SIP WCS starting " 53 " from the existing WCS"), default=
False)
54 rejectThresh = pexConfig.RangeField(dtype=float, default=3.0, doc=
"Rejection threshold for Wcs fitting",
55 min=0.0, inclusiveMin=
False)
56 rejectIter = pexConfig.RangeField(dtype=int, default=3, doc=
"Rejection iterations for Wcs fitting",
61 """An alias, for a uniform interface with the standard AstrometryTask""" 73 r"""!Use astrometry.net to match input sources with a reference catalog and solve for the Wcs 75 @anchor ANetAstrometryTask_ 77 The actual matching and solving is done by the 'solver'; this Task 78 serves as a wrapper for taking into account the known optical distortion. 80 \section pipe_tasks_astrometry_Contents Contents 82 - \ref pipe_tasks_astrometry_Purpose 83 - \ref pipe_tasks_astrometry_Initialize 84 - \ref pipe_tasks_astrometry_IO 85 - \ref pipe_tasks_astrometry_Config 86 - \ref pipe_tasks_astrometry_Debug 87 - \ref pipe_tasks_astrometry_Example 89 \section pipe_tasks_astrometry_Purpose Description 91 \copybrief ANetAstrometryTask 93 \section pipe_tasks_astrometry_Initialize Task initialisation 97 \section pipe_tasks_astrometry_IO Invoking the Task 101 \section pipe_tasks_astrometry_Config Configuration parameters 103 See \ref ANetAstrometryConfig 105 \section pipe_tasks_astrometry_Debug Debug variables 107 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a 108 flag \c -d to import \b debug.py from your \c PYTHONPATH; 109 see \ref baseDebug for more about \b debug.py files. 111 The available variables in ANetAstrometryTask are: 114 <DD> If True call showAstrometry while iterating ANetAstrometryConfig.rejectIter times, 115 and also after converging; and call displayAstrometry after applying the distortion correction. 117 <DD> display frame to use in showAstrometry and displayAstrometry 119 <DD> Pause after showAstrometry and displayAstrometry? 122 \section pipe_tasks_astrometry_Example A complete example of using ANetAstrometryTask 124 See \ref pipe_tasks_photocal_Example. 126 To investigate the \ref pipe_tasks_astrometry_Debug, put something like 130 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 131 if name in ("lsst.pipe.tasks.anetAstrometry", "lsst.pipe.tasks.anetBasicAstrometry"): 138 lsstDebug.Info = DebugInfo 140 into your debug.py file and run photoCalTask.py with the \c --debug flag. 142 ConfigClass = ANetAstrometryConfig
143 _DefaultName =
"astrometricSolver" 145 def __init__(self, schema, refObjLoader=None, **kwds):
146 r"""!Create the astrometric calibration task. Most arguments are simply passed onto pipe.base.Task. 148 \param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog 149 \param refObjLoader The AstrometryTask constructor requires a refObjLoader. In order to make this 150 task retargettable for AstrometryTask it needs to take the same arguments. This argument will be 151 ignored since it uses its own internal loader. 152 \param **kwds keyword arguments to be passed to the lsst.pipe.base.task.Task constructor 154 A centroid field "centroid.distorted" (used internally during the Task's operation) 155 will be added to the schema. 157 pipeBase.Task.__init__(self, **kwds)
160 doc=
"centroid distorted for astrometry solver")
162 doc=
"centroid distorted for astrometry solver")
164 doc=
"centroid distorted err for astrometry solver")
166 doc=
"centroid distorted err for astrometry solver")
168 doc=
"centroid distorted flag astrometry solver")
175 def run(self, exposure, sourceCat):
176 """!Load reference objects, match sources and optionally fit a WCS 178 This is a thin layer around solve or loadAndMatch, depending on config.forceKnownWcs 180 @param[in,out] exposure exposure whose WCS is to be fit 181 The following are read only: 183 - photoCalib (may be absent) 184 - filter (may be unset) 185 - detector (if wcs is pure tangent; may be absent) 186 The following are updated: 187 - wcs (the initial value is used as an initial guess, and is required) 188 @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog) 189 @return an lsst.pipe.base.Struct with these fields: 190 - refCat reference object catalog of objects that overlap the exposure (with some margin) 191 (an lsst::afw::table::SimpleCatalog) 192 - matches astrometric matches, a list of lsst.afw.table.ReferenceMatch 193 - matchMeta metadata about the field (an lsst.daf.base.PropertyList) 195 if self.config.forceKnownWcs:
196 return self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
198 return self.
solve(exposure=exposure, sourceCat=sourceCat)
201 def solve(self, exposure, sourceCat):
202 r"""!Match with reference sources and calculate an astrometric solution 204 \param[in,out] exposure Exposure to calibrate; wcs is updated 205 \param[in] sourceCat catalog of measured sources (an lsst.afw.table.SourceCatalog) 206 \return a pipeBase.Struct with fields: 207 - refCat reference object catalog of objects that overlap the exposure (with some margin) 208 (an lsst::afw::table::SimpleCatalog) 209 - matches astrometric matches, a list of lsst.afw.table.ReferenceMatch 210 - matchMeta metadata about the field (an lsst.daf.base.PropertyList) 212 The reference catalog actually used is up to the implementation 213 of the solver; it will be manifested in the returned matches as 214 a list of lsst.afw.table.ReferenceMatch objects (\em i.e. of lsst.afw.table.Match with 215 \c first being of type lsst.afw.table.SimpleRecord and \c second type lsst.afw.table.SourceRecord --- 216 the reference object and matched object respectively). 219 The input sources have the centroid slot moved to a new column "centroid.distorted" 220 which has the positions corrected for any known optical distortion; 221 the 'solver' (which is instantiated in the 'astrometry' member) 222 should therefore simply use the centroids provided by calling 223 afw.table.Source.getCentroid() on the individual source records. This column \em must 224 be present in the sources table. 226 \note ignores config.forceKnownWcs 228 results = self.
_astrometry(sourceCat=sourceCat, exposure=exposure)
231 self.
refitWcs(sourceCat=sourceCat, exposure=exposure, matches=results.matches)
237 r"""!Calculate distorted source positions 239 CCD images are often affected by optical distortion that makes 240 the astrometric solution higher order than linear. Unfortunately, 241 most (all?) matching algorithms require that the distortion be 242 small or zero, and so it must be removed. We do this by calculating 243 (un-)distorted positions, based on a known optical distortion model 246 The distortion correction moves sources, so we return the distorted bounding box. 248 \param[in] exposure Exposure to process 249 \param[in,out] sourceCat SourceCatalog; getX() and getY() will be used as inputs, 250 with distorted points in "centroid.distorted" field. 251 \return bounding box of distorted exposure 253 detector = exposure.getDetector()
254 pixToTanXYTransform =
None 256 self.log.
warn(
"No detector associated with exposure; assuming null distortion")
258 pixToTanXYTransform = detector.getTransform(PIXELS, TAN_PIXELS)
260 if pixToTanXYTransform
is None:
261 self.log.
info(
"Null distortion correction")
266 return exposure.getBBox()
269 self.log.
info(
"Applying distortion correction")
271 centroid = pixToTanXYTransform.forwardTransform(s.getCentroid())
278 for corner
in detector.getCorners(TAN_PIXELS):
279 bboxD.include(corner)
285 exposure=exposure, frame=frame, pause=pause)
291 """!Load reference objects overlapping an exposure and match to sources detected on that exposure 293 @param[in] exposure exposure whose WCS is to be fit 294 @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog) 295 @param[in] bbox bounding box go use for finding reference objects; if None, use exposure's bbox 297 @return an lsst.pipe.base.Struct with these fields: 298 - refCat reference object catalog of objects that overlap the exposure (with some margin) 299 (an lsst::afw::table::SimpleCatalog) 300 - matches astrometric matches, a list of lsst.afw.table.ReferenceMatch 301 - matchMeta metadata about the field (an lsst.daf.base.PropertyList) 303 @note ignores config.forceKnownWcs 305 bbox = exposure.getBBox()
307 self.makeSubtask(
"solver")
309 astrom = self.
solver.useKnownWcs(
316 if astrom
is None or astrom.getWcs()
is None:
317 raise RuntimeError(
"Unable to solve astrometry")
319 matches = astrom.getMatches()
320 matchMeta = astrom.getMatchMetadata()
321 if matches
is None or len(matches) == 0:
322 raise RuntimeError(
"No astrometric matches")
323 self.log.
info(
"%d astrometric matches", len(matches))
328 frame=frame, pause=
False)
330 return pipeBase.Struct(
331 refCat=astrom.refCat,
337 def _astrometry(self, sourceCat, exposure, bbox=None):
338 r"""!Solve astrometry to produce WCS 340 \param[in] sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog 341 \param[in,out] exposure Exposure to process, an lsst.afw.image.ExposureF or D; wcs is updated 342 \param[in] bbox Bounding box, or None to use exposure 343 \return a pipe.base.Struct with fields: 344 - refCat reference object catalog of objects that overlap the exposure (with some margin) 345 (an lsst::afw::table::SimpleCatalog) 346 - matches astrometric matches, a list of lsst.afw.table.ReferenceMatch 347 - matchMeta metadata about the field (an lsst.daf.base.PropertyList) 349 self.log.
info(
"Solving astrometry")
351 bbox = exposure.getBBox()
354 self.makeSubtask(
"solver")
356 astrom = self.
solver.determineWcs(sourceCat=sourceCat, exposure=exposure, bbox=bbox)
358 if astrom
is None or astrom.getWcs()
is None:
359 raise RuntimeError(
"Unable to solve astrometry")
361 matches = astrom.getMatches()
362 matchMeta = astrom.getMatchMetadata()
363 if matches
is None or len(matches) == 0:
364 raise RuntimeError(
"No astrometric matches")
365 self.log.
info(
"%d astrometric matches", len(matches))
368 exposure.setWcs(astrom.getWcs())
373 frame=frame, pause=
False)
375 return pipeBase.Struct(
376 refCat=astrom.refCat,
383 """!A final Wcs solution after matching and removing distortion 385 Specifically, fitting the non-linear part, since the linear 386 part has been provided by the matching engine. 388 @param sourceCat Sources on exposure, an lsst.afw.table.SourceCatalog 389 @param exposure Exposure of interest, an lsst.afw.image.ExposureF or D 390 @param matches Astrometric matches, as a list of lsst.afw.table.ReferenceMatch 392 @return the resolved-Wcs object, or None if config.solver.calculateSip is False. 395 if self.config.solver.calculateSip:
396 self.log.
info(
"Refitting WCS")
397 origMatches = matches
398 wcs = exposure.getWcs()
405 def fitWcs(initialWcs, title=None):
406 """!Do the WCS fitting and display of the results""" 408 resultWcs = sip.getNewWcs()
410 showAstrometry(exposure, resultWcs, origMatches, matches, frame=frame,
411 title=title, pause=pause)
412 return resultWcs, sip.getScatterOnSky()
416 for i
in range(self.config.rejectIter):
417 wcs, scatter = fitWcs(wcs, title=
"Iteration %d" % i)
419 ref = np.array([wcs.skyToPixel(m.first.getCoord())
for m
in matches])
420 src = np.array([m.second.getCentroid()
for m
in matches])
424 for d, m
in zip(diff, matches):
425 if np.all(np.abs(d) < self.config.rejectThresh*rms):
429 if len(matches) == len(trimmed):
434 wcs, scatter = fitWcs(wcs, title=
"Final astrometry")
437 self.log.
warn(
"Unable to fit SIP: %s", e)
439 self.log.
info(
"Astrometric scatter: %f arcsec (%d matches, %d rejected)",
440 scatter.asArcseconds(), len(matches), numRejected)
446 self.log.
warn(
"Not calculating a SIP solution; matches may be suspect")
451 frame=frame, pause=
False)
456 def showAstrometry(exposure, wcs, allMatches, useMatches, frame=0, title=None, pause=False):
457 r"""!Show results of astrometry fitting 459 \param exposure Image to display 460 \param wcs Astrometric solution 461 \param allMatches List of all astrometric matches (including rejects) 462 \param useMatches List of used astrometric matches 463 \param frame Frame number for display 464 \param title Title for display 465 \param pause Pause to allow viewing of the display and optional debugging? 467 - Matches are shown in yellow if used in the Wcs solution, otherwise red 468 - +: Detected objects 469 - x: Catalogue objects 472 disp = afwDisplay.Display(frame=frame)
473 disp.mtv(exposure, title=title)
475 useIndices =
set(m.second.getId()
for m
in useMatches)
478 with disp.Buffering():
479 for i, m
in enumerate(allMatches):
480 x, y = m.second.getX(), m.second.getY()
481 pix = wcs.skyToPixel(m.first.getCoord())
483 isUsed = m.second.getId()
in useIndices
485 radii.append(np.hypot(pix[0] - x, pix[1] - y))
487 color = afwDisplay.YELLOW
if isUsed
else afwDisplay.RED
489 disp.dot(
"+", x, y, size=10, ctype=color)
490 disp.dot(
"x", pix[0], pix[1], size=10, ctype=color)
492 radii = np.array(radii)
493 print(
"<dr> = %.4g +- %.4g pixels [%d/%d matches]" % (radii.mean(), radii.std(),
494 len(useMatches), len(allMatches)))
500 reply = input(
"Debugging? [p]db [q]uit; any other key to continue... ").
strip()
A floating-point coordinate rectangle geometry.
Reports attempts to exceed implementation-defined length limits for some classes. ...
def loadAndMatch(self, exposure, sourceCat, bbox=None)
Load reference objects overlapping an exposure and match to sources detected on that exposure...
def solve(self, exposure, sourceCat)
Match with reference sources and calculate an astrometric solution.
PointKey< double > Point2DKey
def distort(self, sourceCat, exposure)
Calculate distorted source positions.
def showAstrometry(exposure, wcs, allMatches, useMatches, frame=0, title=None, pause=False)
Show results of astrometry fitting.
daf::base::PropertySet * set
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList)
Update sky coordinates in a collection of source objects.
def refitWcs(self, sourceCat, exposure, matches)
A final Wcs solution after matching and removing distortion.
def _astrometry(self, sourceCat, exposure, bbox=None)
Solve astrometry to produce WCS.
def run(self, exposure, sourceCat)
Load reference objects, match sources and optionally fit a WCS.
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
CreateWcsWithSip< MatchT > makeCreateWcsWithSip(std::vector< MatchT > const &matches, afw::geom::SkyWcs const &linearWcs, int const order, geom::Box2I const &bbox=geom::Box2I(), int const ngrid=0)
Factory function for CreateWcsWithSip.
An integer coordinate rectangle.
def __init__(self, schema, refObjLoader=None, kwds)
Create the astrometric calibration task.
Use astrometry.net to match input sources with a reference catalog and solve for the Wcs...