1 from __future__
import absolute_import, division, print_function
14 from .loadAstrometryNetObjects
import LoadAstrometryNetObjectsTask
15 from .matchOptimisticB
import MatchOptimisticBTask
16 from .fitTanSipWcs
import FitTanSipWcsTask
20 refObjLoader = pexConfig.ConfigurableField(
21 target = LoadAstrometryNetObjectsTask,
22 doc =
"reference object loader",
24 matcher = pexConfig.ConfigurableField(
25 target = MatchOptimisticBTask,
26 doc =
"reference object/source matcher",
28 wcsFitter = pexConfig.ConfigurableField(
29 target = FitTanSipWcsTask,
32 forceKnownWcs = pexConfig.Field(
34 doc=
"Assume that the input image's WCS is correct, without comparing it to any external reality " +
35 " (but still match reference objects to sources)",
38 maxIter = pexConfig.RangeField(
39 doc =
"maximum number of iterations of match sources and fit WCS; " +
40 "ignored if forceKnownWcs True",
55 """!Match an input source catalog with objects from a reference catalog and solve for the WCS
57 @anchor AstrometryTask_
59 @section meas_astrom_astrometry_Contents Contents
61 - @ref meas_astrom_astrometry_Purpose
62 - @ref meas_astrom_astrometry_Initialize
63 - @ref meas_astrom_astrometry_IO
64 - @ref meas_astrom_astrometry_Config
65 - @ref meas_astrom_astrometry_Example
66 - @ref meas_astrom_astrometry_Debug
68 @section meas_astrom_astrometry_Purpose Description
70 Match input sourceCat with a reference catalog and solve for the Wcs
72 There are three steps, each performed by different subtasks:
73 - Find position reference stars that overlap the exposure
74 - Match sourceCat to position reference stars
75 - Fit a WCS based on the matches
77 @section meas_astrom_astrometry_Initialize Task initialisation
81 @section meas_astrom_astrometry_IO Invoking the Task
85 @section meas_astrom_astrometry_Config Configuration parameters
87 See @ref AstrometryConfig
89 @section meas_astrom_astrometry_Example A complete example of using AstrometryTask
91 See \ref meas_photocal_photocal_Example.
93 @section meas_astrom_astrometry_Debug Debug variables
95 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a
96 flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about
99 The available variables in AstrometryTask are:
101 <DT> @c display (bool)
102 <DD> If True display information at three stages: after finding reference objects,
103 after matching sources to reference objects, and after fitting the WCS; defaults to False
105 <DD> ds9 frame to use to display the reference objects; the next two frames are used
106 to display the match list and the results of the final WCS; defaults to 0
109 To investigate the @ref meas_astrom_astrometry_Debug, put something like
113 debug = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
114 if name == "lsst.meas.astrom.astrometry":
119 lsstDebug.Info = DebugInfo
121 into your debug.py file and run this task with the @c --debug flag.
123 ConfigClass = AstrometryConfig
124 _DefaultName =
"astrometricSolver"
127 """!Construct an AstrometryTask
129 @param[in] schema ignored; available for compatibility with an older astrometry task
131 pipeBase.Task.__init__(self, **kwargs)
132 self.makeSubtask(
"refObjLoader")
133 self.makeSubtask(
"matcher")
134 self.makeSubtask(
"wcsFitter")
137 def run(self, exposure, sourceCat):
138 """!Fit a WCS given a source catalog and exposure
140 @param[in,out] exposure exposure whose WCS is to be fit
141 The following are read only:
143 - calib (may be absent)
144 - filter (may be unset)
145 - detector (if wcs is pure tangent; may be absent)
146 The following are updated:
147 - wcs (the initial value is used as an initial guess, and is required)
148 @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
149 @return the same struct as the "solve" method
151 bbox = exposure.getBBox()
152 exposureInfo = exposure.getInfo()
154 calib = exposureInfo.getCalib()
if exposureInfo.hasCalib()
else None
155 filterName = exposureInfo.getFilter().getName()
or None
157 retVal = self.
solve(sourceCat=sourceCat, bbox=bbox, initWcs=initWcs, filterName=filterName,
158 calib=calib, exposure=exposure)
159 exposure.setWcs(retVal.wcs)
163 def solve(self, sourceCat, bbox, initWcs, filterName=None, calib=None, exposure=None):
164 """!Fit a WCS given a source catalog and exposure matchMetadata
166 @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
167 @param[in] bbox bounding box of exposure (an lsst.afw.geom.Box2I)
168 @param[in] wcs initial guess for WCS of exposure (an lsst.afw.image.Wcs)
169 @param[in] filterName filter name, or None or "" if unknown (a string)
170 @param[in] calib calibration for exposure, or None if unknown (an lsst.afw.image.Calib)
171 @param[in] exposure exposure whose WCS is to be fit, or None; used only for the debug display
173 @return an lsst.pipe.base.Struct with these fields:
174 - refCat reference object catalog of objects that overlap the exposure (with some margin)
175 (an lsst::afw::table::SimpleCatalog)
176 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
177 - initWcs initial WCS from exposure (possibly tweaked) (an lsst.afw.image.Wcs)
178 - wcs fit WCS (an lsst.afw.image.Wcs)
179 - matchMeta metadata about the field
184 loadRes = self.refObjLoader.loadPixelBox(
187 filterName = filterName,
191 frame = int(debug.frame)
193 refCat = loadRes.refCat,
194 sourceCat = sourceCat,
198 title=
"Reference catalog",
203 for i
in range(self.config.maxIter):
205 refCat = loadRes.refCat,
206 sourceCat = sourceCat,
207 refFluxField = loadRes.fluxField,
213 if self.config.forceKnownWcs:
219 "Fit WCS iter %s: %s matches; median scatter = %g arcsec" % \
220 (i, len(tryRes.matches), tryRes.scatterOnSky.asArcseconds()),
223 if res
is not None and not self.config.forceKnownWcs:
224 if len(tryRes.matches) < len(res.matches):
226 "Fit WCS: use iter %s because it had more matches than the next iter: %s vs. %s" % \
227 (i-1, len(res.matches), len(tryRes.matches)))
229 if len(tryRes.matches) == len(res.matches)
and tryRes.scatterOnSky >= res.scatterOnSky:
231 "Fit WCS: use iter %s because it had less scatter than the next iter: %g vs. %g arcsec" % \
232 (i-1, res.scatterOnSky.asArcseconds(), tryRes.scatterOnSky.asArcseconds()))
238 return pipeBase.Struct(
239 refCat = loadRes.refCat,
240 matches = res.matches,
243 scatterOnSky = res.scatterOnSky,
249 """!Match sources to reference objects and fit a WCS
251 @param[in] refCat catalog of reference objects
252 @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
253 @param[in] bbox bounding box of exposure (an lsst.afw.geom.Box2I)
254 @param[in] wcs initial guess for WCS of exposure (an lsst.afw.image.Wcs)
255 @param[in] exposure exposure whose WCS is to be fit, or None; used only for the debug display
257 @return an lsst.pipe.base.Struct with these fields:
258 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
259 - wcs the fit WCS as an lsst.afw.image.Wcs
260 - scatterOnSky median on-sky separation between reference objects and sources in "matches",
261 as an lsst.afw.geom.Angle, or None if config.forceKnownWcs is True
265 matchRes = self.matcher.matchObjectsToSources(
267 sourceCat = sourceCat,
269 refFluxField = refFluxField,
272 frame = int(debug.frame)
275 sourceCat = sourceCat,
276 matches = matchRes.matches,
283 if not self.config.forceKnownWcs:
284 self.log.info(
"Fitting WCS")
285 fitRes = self.wcsFitter.fitWcs(
286 matches = matchRes.matches,
290 sourceCat = sourceCat,
293 scatterOnSky = fitRes.scatterOnSky
295 self.log.info(
"Not fitting WCS (forceKnownWcs true); %s matches" % (len(matchRes.matches),))
299 frame = int(debug.frame)
302 sourceCat = sourceCat,
303 matches = matchRes.matches,
310 return pipeBase.Struct(
311 matches = matchRes.matches,
313 scatterOnSky = scatterOnSky,
318 """Create matchMeta metadata required for regenerating the catalog
320 This is copied from Astrom and I'm not sure why it is needed.
321 I did not put this in any subtask because none have all the necessary info:
322 - The matcher does not have the fit wcs
323 - The fitter does not have the filter name
325 @param bbox bounding box of exposure (an lsst.afw.geom.Box2I or Box2D)
326 @param wcs WCS of exposure
327 @param filterName Name of filter, used for magnitudes
332 ctrPos = bboxd.getCenter()
333 ctrCoord = wcs.pixelToSky(ctrPos).toIcrs()
334 llCoord = wcs.pixelToSky(bboxd.getMin())
335 approxRadius = ctrCoord.angularSeparation(llCoord)
336 matchMeta.add(
'RA', ctrCoord.getRa().asDegrees(),
'field center in degrees')
337 matchMeta.add(
'DEC', ctrCoord.getDec().asDegrees(),
'field center in degrees')
338 matchMeta.add(
'RADIUS', approxRadius.asDegrees(),
'field radius in degrees, approximate')
339 matchMeta.add(
'SMATCHV', 1,
'SourceMatchVector version number')
340 if filterName
is not None:
341 matchMeta.add(
'FILTER', filterName,
'filter name for tagalong data')
345 def showAstrometry(refCat, sourceCat, bbox=None, exposure=None, matches=None, frame=1, title=""):
346 """Show an astrometry debug image
348 @param[in] refCat reference object catalog; must have fields "centroid_x" and "centroid_y"
349 @param[in] sourceCat source catalog; must have field "slot_Centroid_x" and "slot_Centroid_y"
350 @param[in] exposure exposure to display, or None for a blank exposure
351 @param[in] bbox bounding box of exposure; required if exposure is None and ignored otherwise
352 @param[in] matches list of matches (an lsst.afw.table.ReferenceMatchVector), or None
353 @param[in] frame frame number for ds9 display
354 @param[in] title title for ds9 display
356 @throw RuntimeError if exposure and bbox are both None
362 raise RuntimeError(
"must specify exposure or bbox")
363 exposure = ExposureF(bbox)
364 ds9.mtv(exposure, frame=frame, title=title)
366 with ds9.Buffering():
367 refCentroidKey =
Point2DKey(refCat.schema[
"centroid"])
368 for refObj
in refCat:
369 x, y = refObj.get(refCentroidKey)
370 ds9.dot(
"x", x, y, size=10, frame=frame, ctype=ds9.RED)
372 sourceCentroidKey =
Point2DKey(sourceCat.schema[
"slot_Centroid"])
373 for source
in sourceCat:
374 x, y = source.get(sourceCentroidKey)
375 ds9.dot(
"+", x, y, size=10, frame=frame, ctype=ds9.GREEN)
378 radArr = numpy.ndarray(len(matches))
380 for i, m
in enumerate(matches):
381 refCentroid = m.first.get(refCentroidKey)
382 sourceCentroid = m.second.get(sourceCentroidKey)
383 radArr[i] = math.hypot(*(refCentroid - sourceCentroid))
384 ds9.dot(
"o", x, y, size=10, frame=frame, ctype=ds9.YELLOW)
386 print(
"<match radius> = %.4g +- %.4g [%d matches]" %
387 (radArr.mean(), radArr.std(), len(matches)))
def _matchAndFitWcs
Match sources to reference objects and fit a WCS.
Class for storing ordered metadata with comments.
def __init__
Construct an AstrometryTask.
def getDistortedWcs
Get a WCS from an exposureInfo, with distortion terms if possible.
def run
Fit a WCS given a source catalog and exposure.
PointKey< double > Point2DKey
def solve
Fit a WCS given a source catalog and exposure matchMetadata.
A floating-point coordinate rectangle geometry.
Match an input source catalog with objects from a reference catalog and solve for the WCS...