23 from contextlib
import contextmanager
35 solver = pexConfig.ConfigField(
36 dtype = Astrometry.ConfigClass,
37 doc =
"Configuration for the astrometry solver",
39 forceKnownWcs = pexConfig.Field(dtype=bool, doc=(
40 "Assume that the input image's WCS is correct, without comparing it to any external reality." +
41 " (In contrast to using Astrometry.net). NOTE, if you set this, you probably also want to" +
42 " un-set 'solver.calculateSip'; otherwise we'll still try to find a TAN-SIP WCS starting " +
43 " from the existing WCS"), default=
False)
44 rejectThresh = pexConfig.RangeField(dtype=float, default=3.0, doc=
"Rejection threshold for Wcs fitting",
45 min=0.0, inclusiveMin=
False)
46 rejectIter = pexConfig.RangeField(dtype=int, default=3, doc=
"Rejection iterations for Wcs fitting",
59 \anchor AstrometryTask_
61 \brief %Match input sources with a reference catalog and solve for the Wcs
63 \warning This task is currently in \link lsst.pipe.tasks.astrometry\endlink;
64 it should be moved to lsst.meas.astrom.
66 The actual matching and solving is done by the 'solver'; this Task
67 serves as a wrapper for taking into account the known optical distortion.
69 \section pipe_tasks_astrometry_Contents Contents
71 - \ref pipe_tasks_astrometry_Purpose
72 - \ref pipe_tasks_astrometry_Initialize
73 - \ref pipe_tasks_astrometry_IO
74 - \ref pipe_tasks_astrometry_Config
75 - \ref pipe_tasks_astrometry_Debug
76 - \ref pipe_tasks_astrometry_Example
78 \section pipe_tasks_astrometry_Purpose Description
80 \copybrief AstrometryTask
82 Calculate an Exposure's zero-point given a set of flux measurements of stars matched to an input catalogue.
83 The type of flux to use is specified by AstrometryConfig.fluxField.
85 The algorithm clips outliers iteratively, with parameters set in the configuration.
87 \note This task can adds fields to the schema, so any code calling this task must ensure that
88 these columns are indeed present in the input match list; see \ref pipe_tasks_astrometry_Example
90 \section pipe_tasks_astrometry_Initialize Task initialisation
94 \section pipe_tasks_astrometry_IO Invoking the Task
98 \section pipe_tasks_astrometry_Config Configuration parameters
100 See \ref AstrometryConfig
102 \section pipe_tasks_astrometry_Debug Debug variables
104 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
105 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
107 The available variables in AstrometryTask are:
110 <DD> If True call astrometry.showAstrometry while iterating AstrometryConfig.rejectIter times,
111 and also after converging.
113 <DD> ds9 frame to use in astrometry.showAstrometry
115 <DD> Pause within astrometry.showAstrometry
118 \section pipe_tasks_astrometry_Example A complete example of using AstrometryTask
120 See \ref meas_photocal_photocal_Example.
122 To investigate the \ref pipe_tasks_astrometry_Debug, put something like
126 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
127 if name == "lsst.pipe.tasks.astrometry":
132 lsstDebug.Info = DebugInfo
134 into your debug.py file and run photoCalTask.py with the \c --debug flag.
136 ConfigClass = AstrometryConfig
140 """!Create the astrometric calibration task. Most arguments are simply passed onto pipe.base.Task.
142 \param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
143 \param **kwds keyword arguments to be passed to the lsst.pipe.base.task.Task constructor
145 A centroid field "centroid.distorted" (used internally during the Task's operation)
146 will be added to the schema.
148 pipeBase.Task.__init__(self, **kwds)
150 if schema.getVersion() == 0:
153 doc=
"centroid distorted for astrometry solver")
155 doc=
"centroid distorted err for astrometry solver")
157 doc=
"centroid distorted flag astrometry solver")
161 doc=
"centroid distorted for astrometry solver")
163 doc=
"centroid distorted for astrometry solver")
165 doc=
"centroid distorted err for astrometry solver")
167 doc=
"centroid distorted err for astrometry solver")
169 doc=
"centroid distorted flag astrometry solver")
176 def run(self, exposure, sources):
177 """!Match with reference sources and calculate an astrometric solution
179 \param exposure Exposure to calibrate
180 \param sources SourceCatalog of measured sources
181 \return a pipeBase.Struct with fields:
182 - matches: Astrometric matches
183 - matchMeta: Metadata for astrometric matches
185 The reference catalog actually used is up to the implementation
186 of the solver; it will be manifested in the returned matches as
187 a list of lsst.afw.table.ReferenceMatch objects (\em i.e. of lsst.afw.table.Match with
188 \c first being of type lsst.afw.table.SimpleRecord and \c second type lsst.afw.table.SourceRecord ---
189 the reference object and matched object respectively).
192 The input sources have the centroid slot moved to a new column "centroid.distorted"
193 which has the positions corrected for any known optical distortion;
194 the 'solver' (which is instantiated in the 'astrometry' member)
195 should therefore simply use the centroids provided by calling
196 afw.table.Source.getCentroid() on the individual source records. This column \em must
197 be present in the sources table.
200 results = self.
astrometry(exposure, sources, bbox=bbox)
203 self.
refitWcs(exposure, sources, results.matches)
209 """!Calculate distorted source positions
211 CCD images are often affected by optical distortion that makes
212 the astrometric solution higher order than linear. Unfortunately,
213 most (all?) matching algorithms require that the distortion be
214 small or zero, and so it must be removed. We do this by calculating
215 (un-)distorted positions, based on a known optical distortion model
218 The distortion correction moves sources, so we return the distorted
221 \param[in] exposure Exposure to process
222 \param[in,out] sources SourceCatalog; getX() and getY() will be used as inputs,
223 with distorted points in "centroid.distorted" field.
224 \return bounding box of distorted exposure
226 detector = exposure.getDetector()
227 pixToTanXYTransform =
None
229 self.log.warn(
"No detector associated with exposure; assuming null distortion")
231 tanSys = detector.makeCameraSys(TAN_PIXELS)
232 pixToTanXYTransform = detector.getTransformMap().get(tanSys)
234 if pixToTanXYTransform
is None:
235 self.log.info(
"Null distortion correction")
240 return exposure.getBBox()
243 self.log.info(
"Applying distortion correction")
245 centroid = pixToTanXYTransform.forwardTransform(s.getCentroid())
253 for corner
in detector.getCorners(TAN_PIXELS):
254 bboxD.include(corner)
259 """!Context manager that applies and removes distortion
261 We move the "centroid" definition in the catalog table to
262 point to the distorted positions. This is undone on exit
265 The input Wcs is taken to refer to the coordinate system
266 with the distortion correction applied, and hence no shift
267 is required when the sources are distorted. However, after
268 Wcs fitting, the Wcs is in the distorted frame so when the
269 distortion correction is removed, the Wcs needs to be
270 shifted to compensate.
272 \param exposure Exposure holding Wcs
273 \param sources Sources to correct for distortion
274 \return bounding box of distorted exposure
277 bbox = self.
distort(exposure, sources)
278 oldCentroidName = sources.table.getCentroidDefinition()
284 sources.table.defineCentroid(oldCentroidName)
285 x0, y0 = exposure.getXY0()
286 wcs = exposure.getWcs()
288 wcs.shiftReferencePixel(-bbox.getMinX() + x0, -bbox.getMinY() + y0)
292 """!Solve astrometry to produce WCS
294 \param exposure Exposure to process
295 \param sources Sources
296 \param bbox Bounding box, or None to use exposure
297 \return a pipe.base.Struct with fields:
298 - matches: star matches
299 - matchMeta: match metadata
301 if not self.config.forceKnownWcs:
302 self.log.info(
"Solving astrometry")
304 bbox = exposure.getBBox()
309 kwargs = dict(x0=bbox.getMinX(), y0=bbox.getMinY(), imageSize=bbox.getDimensions())
310 if self.config.forceKnownWcs:
311 self.log.info(
"Forcing the input exposure's WCS")
312 if self.config.solver.calculateSip:
313 self.log.warn(
"'forceKnownWcs' and 'solver.calculateSip' options are both set." +
314 " Will try to compute a TAN-SIP WCS starting from the input WCS.")
315 astrom = self.astrometer.useKnownWcs(sources, exposure=exposure, **kwargs)
317 astrom = self.astrometer.determineWcs(sources, exposure, **kwargs)
319 if astrom
is None or astrom.getWcs()
is None:
320 raise RuntimeError(
"Unable to solve astrometry")
322 matches = astrom.getMatches()
323 matchMeta = astrom.getMatchMetadata()
324 if matches
is None or len(matches) == 0:
325 raise RuntimeError(
"No astrometric matches")
326 self.log.info(
"%d astrometric matches" % (len(matches)))
328 if not self.config.forceKnownWcs:
330 exposure.setWcs(astrom.getWcs())
332 self.display(
'astrometry', exposure=exposure, sources=sources, matches=matches)
334 return pipeBase.Struct(matches=matches, matchMeta=matchMeta)
338 """!A final Wcs solution after matching and removing distortion
340 Specifically, fitting the non-linear part, since the linear
341 part has been provided by the matching engine.
343 \param exposure Exposure of interest
344 \param sources Sources on image (no distortion applied)
345 \param matches Astrometric matches
347 \return the resolved-Wcs object, or None if config.solver.calculateSip is False.
350 if self.config.solver.calculateSip:
351 self.log.info(
"Refitting WCS")
352 origMatches = matches
353 wcs = exposure.getWcs()
360 def fitWcs(initialWcs, title=None):
361 """!Do the WCS fitting and display of the results"""
363 resultWcs = sip.getNewWcs()
365 showAstrometry(exposure, resultWcs, origMatches, matches, frame=frame,
366 title=title, pause=pause)
367 return resultWcs, sip.getScatterOnSky()
371 for i
in range(self.config.rejectIter):
372 wcs, scatter = fitWcs(wcs, title=
"Iteration %d" % i)
374 ref = numpy.array([wcs.skyToPixel(m.first.getCoord())
for m
in matches])
375 src = numpy.array([m.second.getCentroid()
for m
in matches])
379 for d, m
in zip(diff, matches):
380 if numpy.all(numpy.abs(d) < self.config.rejectThresh*rms):
384 if len(matches) == len(trimmed):
389 wcs, scatter = fitWcs(wcs, title=
"Final astrometry")
391 except lsst.pex.exceptions.LengthError
as e:
392 self.log.warn(
"Unable to fit SIP: %s" % e)
394 self.log.info(
"Astrometric scatter: %f arcsec (%s non-linear terms, %d matches, %d rejected)" %
395 (scatter.asArcseconds(),
"with" if wcs.hasDistortion()
else "without",
396 len(matches), numRejected))
400 for index, source
in enumerate(sources):
401 sky = wcs.pixelToSky(source.getX(), source.getY())
404 self.log.warn(
"Not calculating a SIP solution; matches may be suspect")
406 self.display(
'astrometry', exposure=exposure, sources=sources, matches=matches)
411 def showAstrometry(exposure, wcs, allMatches, useMatches, frame=0, title=None, pause=False):
412 """!Show results of astrometry fitting
414 \param exposure Image to display
415 \param wcs Astrometric solution
416 \param allMatches List of all astrometric matches (including rejects)
417 \param useMatches List of used astrometric matches
418 \param frame Frame number for display
419 \param title Title for display
420 \param pause Pause to allow viewing of the display and optional debugging?
422 - Matches are shown in yellow if used in the Wcs solution, otherwise red
423 - +: Detected objects
424 - x: Catalogue objects
427 ds9.mtv(exposure, frame=frame, title=title)
429 useIndices = set(m.second.getId()
for m
in useMatches)
432 with ds9.Buffering():
433 for i, m
in enumerate(allMatches):
434 x, y = m.second.getX(), m.second.getY()
435 pix = wcs.skyToPixel(m.first.getCoord())
437 isUsed = m.second.getId()
in useIndices
439 radii.append(numpy.hypot(pix[0] - x, pix[1] - y))
441 color = ds9.YELLOW
if isUsed
else ds9.RED
443 ds9.dot(
"+", x, y, size=10, frame=frame, ctype=color)
444 ds9.dot(
"x", pix[0], pix[1], size=10, frame=frame, ctype=color)
446 radii = numpy.array(radii)
447 print "<dr> = %.4g +- %.4g pixels [%d/%d matches]" % (radii.mean(), radii.std(),
448 len(useMatches), len(allMatches))
454 reply = raw_input(
"Debugging? [p]db [q]uit; any other key to continue... ").strip()
458 reply = reply.split()
462 import pdb;pdb.set_trace()
def distort
Calculate distorted source positions.
def run
Match with reference sources and calculate an astrometric solution.
PointKey< double > Point2DKey
def astrometry
Solve astrometry to produce WCS.
def showAstrometry
Show results of astrometry fitting.
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 refitWcs
A final Wcs solution after matching and removing distortion.
def distortionContext
Context manager that applies and removes distortion.
A floating-point coordinate rectangle geometry.
def __init__
Create the astrometric calibration task.
Match input sources with a reference catalog and solve for the Wcs