1 from __future__
import absolute_import, division, print_function
2 from builtins
import range
9 from .astromLib
import (
10 ScaledPolynomialTransformFitter,
11 OutlierRejectionControl,
14 makeMatchStatisticsInRadians,
18 from .setMatchDistance
import setMatchDistance
20 __all__ = [
"FitSipDistortionTask",
"FitSipDistortionConfig"]
24 order = lsst.pex.config.RangeField(
25 doc=
"Order of SIP polynomial",
30 numRejIter = lsst.pex.config.RangeField(
31 doc=
"Number of rejection iterations",
36 rejSigma = lsst.pex.config.RangeField(
37 doc=
"Number of standard deviations for clipping level",
42 nClipMin = lsst.pex.config.Field(
43 doc=
"Minimum number of matches to reject when sigma-clipping",
47 nClipMax = lsst.pex.config.Field(
48 doc=
"Maximum number of matches to reject when sigma-clipping",
52 maxScatterArcsec = lsst.pex.config.RangeField(
53 doc=
"Maximum median scatter of a WCS fit beyond which the fit fails (arcsec); " +
54 "be generous, as this is only intended to catch catastrophic failures",
59 refUncertainty = lsst.pex.config.Field(
60 doc=
"RMS uncertainty in reference catalog positions, in pixels. Will be added " +
61 "in quadrature with measured uncertainties in the fit.",
65 nGridX = lsst.pex.config.Field(
66 doc=
"Number of X grid points used to invert the SIP reverse transform.",
70 nGridY = lsst.pex.config.Field(
71 doc=
"Number of Y grid points used to invert the SIP reverse transform.",
75 gridBorder = lsst.pex.config.Field(
76 doc=
"When setting the gird region, how much to extend the image " +
77 "bounding box (in pixels) before transforming it to intermediate " +
78 "world coordinates using the initial WCS.",
85 """Fit a TAN-SIP WCS given a list of reference object/source matches
87 FitSipDistortionTask is a drop-in replacement for
88 :py:class:`lsst.meas.astrom.FitTanSinWcsTask`. It is built on fundamentally
89 stronger fitting algorithms, but has received significantly less testing.
91 Like :py:class:`lsst.meas.astrom.FitTanSinWcsTask`, this task is most
92 easily used as the wcsFitter component of
93 :py:class:`lsst.meas.astrom.AstrometryTask`; it can be enabled in a config
98 from lsst.meas.astrom import FitSipDistortionTask
99 config.(...).astometry.wcsFitter.retarget(FitSipDistortionTask)
104 The algorithm used by FitSipDistortionTask involves three steps:
106 - We set the CRVAL and CRPIX reference points to the mean positions of
107 the matches, while holding the CD matrix fixed to the value passed in
108 to the run() method. This work is done by the makeInitialWcs method.
110 - We fit the SIP "reverse transform" (the AP and BP polynomials that map
111 "intermediate world coordinates" to pixels). This happens iteratively;
112 while fitting for the polynomial coefficients given a set of matches is
113 a linear operation that can be done without iteration, outlier
114 rejection using sigma-clipping and estimation of the intrinsic scatter
115 are not. By fitting the reverse transform first, we can do outlier
116 rejection in pixel coordinates, where we can better handle the source
117 measurement uncertainties that contribute to the overall scatter. This
119 :cpp:class:`lsst::meas::astrom::ScaledPolynomialTransform`, which is
120 somewhat more general than the SIP reverse transform in that it allows
121 an affine transform both before and after the polynomial. This is
122 somewhat more numerically stable than the SIP form, which applies only
123 a linear transform (with no offset) before the polynomial and only a
124 shift afterwards. We only convert to SIP form once the fitting is
125 complete. This conversion is exact (though it may be subject to
126 significant round-off error) as long as we do not attempt to null the
127 low-order SIP polynomial terms (we do not).
129 - Once the SIP reverse transform has been fit, we use it to populate a
130 grid of points that we use as the data points for fitting its inverse,
131 the SIP forward transform. Because our "data" here is artificial,
132 there is no need for outlier rejection or uncertainty handling. We
133 again fit a general scaled polynomial, and only convert to SIP form
134 when the fit is complete.
140 Enabling DEBUG-level logging on this task will report the number of
141 outliers rejected and the current estimate of intrinsic scatter at each
144 FitSipDistortionTask also supports the following lsstDebug variables to
145 control diagnostic displays:
146 - FitSipDistortionTask.display: if True, enable display diagnostics.
147 - FitSipDistortionTask.frame: frame to which the display will be sent
148 - FitSipDistortionTask.pause: whether to pause (by dropping into pdb)
149 between iterations (default is True). If
150 False, multiple frames will be used,
151 starting at the given number.
153 The diagnostic display displays the image (or an empty image if
154 exposure=None) overlaid with the positions of sources and reference
155 objects will be shown for every iteration in the reverse transform fit.
156 The legend for the overlay is:
159 Reference sources transformed without SIP distortion terms; this
160 uses a TAN WCS whose CRPIX, CRVAL and CD matrix are the same
161 as those in the TAN-SIP WCS being fit. These are not expected to
162 line up with sources unless distortion is small.
165 Same as Red X, but for matches that were rejected as outliers.
168 Reference sources using the current best-fit TAN-SIP WCS. These
169 are connected to the corresponding non-distorted WCS position by
170 a red line, and should be a much better fit to source positions
174 Same as Red O, but for matches that were rejected as outliers.
177 Source positions and their error ellipses, including the current
178 estimate of the intrinsic scatter.
181 Same as Green Ellipse, but for matches that were rejected as outliers.
186 See :py:class:`lsst.pipe.base.Task`; FitSipDistortionTask does not add any
187 additional constructor parameters.
191 ConfigClass = FitSipDistortionConfig
192 _DefaultName =
"fitWcs"
195 lsst.pipe.base.Task.__init__(self, **kwds)
197 self.outlierRejectionCtrl.nClipMin = self.config.nClipMin
198 self.outlierRejectionCtrl.nClipMax = self.config.nClipMax
199 self.outlierRejectionCtrl.nSigma = self.config.rejSigma
201 @lsst.pipe.base.timeMethod
202 def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None):
203 """Fit a TAN-SIP WCS from a list of reference object/source matches
208 matches : :cpp:class:`lsst::afw::table::ReferenceMatchVector`
209 A sequence of reference object/source matches.
210 The following fields are read:
211 - match.first (reference object) coord
212 - match.second (source) centroid
213 The following fields are written:
214 - match.first (reference object) centroid,
215 - match.second (source) centroid
216 - match.distance (on sky separation, in radians)
217 initWcs : :cpp:class:`lsst::afw::image::Wcs`
218 An initial WCS whose CD matrix is used as the final CD matrix.
219 bbox : :cpp:class:`lsst::afw::geom::Box2I`
220 The region over which the WCS will be valid (PARENT pixel coordinates);
221 if None or an empty box then computed from matches
222 refCat : :cpp:class:`lsst::afw::table::SimpleCatalog`
223 Reference object catalog, or None.
224 If provided then all centroids are updated with the new WCS,
225 otherwise only the centroids for ref objects in matches are updated.
226 Required fields are "centroid_x", "centroid_y", "coord_ra", and "coord_dec".
227 sourceCat : :cpp:class:`lsst::afw::table::SourceCatalog`
228 Source catalog, or None.
229 If provided then coords are updated with the new WCS;
230 otherwise only the coords for sources in matches are updated.
231 Required input fields are "slot_Centroid_x", "slot_Centroid_y",
232 "slot_Centroid_xSigma", "slot_Centroid_ySigma", and optionally
233 "slot_Centroid_x_y_Cov". The "coord_ra" and "coord_dec" fields
234 will be updated but are not used as input.
235 exposure : :cpp:class:`lsst::afw::image::Exposure`
236 An Exposure or other displayable image on which matches can be
237 overplotted. Ignored (and may be None) if display-based debugging
238 is not enabled via lsstDebug.
243 An lsst.pipe.base.Struct with the following fields:
245 wcs : :cpp:class:`lsst::afw::image::TanWcs`
247 scatterOnSky : :cpp:class:`lsst::afw::geom::Angle`
248 The median on-sky separation between reference objects and
249 sources in "matches", as an lsst.afw.geom.Angle
258 for match
in matches:
259 bbox.include(match.second.getCentroid())
271 revFitter = ScaledPolynomialTransformFitter.fromMatches(self.config.order, matches, wcs,
272 self.config.refUncertainty)
274 for nIter
in range(self.config.numRejIter):
275 revFitter.updateModel()
276 intrinsicScatter = revFitter.updateIntrinsicScatter()
279 "Iteration {0}: intrinsic scatter is {1:4.3f} pixels, "
280 "rejected {2} outliers at {3:3.2f} sigma.".
format(
281 nIter+1, intrinsicScatter, nRejected, clippedSigma
285 displayFrame = self.
display(revFitter, exposure=exposure, bbox=bbox,
286 frame=displayFrame, displayPause=displayPause)
288 revScaledPoly = revFitter.getTransform()
292 sipReverse = SipReverseTransform.convert(revScaledPoly, wcs.getPixelOrigin(), cdMatrix)
300 gridBBoxPix.grow(self.config.gridBorder)
306 for point
in gridBBoxPix.getCorners():
308 gridBBoxIwc.include(cdMatrix(point))
309 fwdFitter = ScaledPolynomialTransformFitter.fromGrid(self.config.order, gridBBoxIwc,
310 self.config.nGridX, self.config.nGridY,
314 fwdScaledPoly = fwdFitter.getTransform()
315 sipForward = SipForwardTransform.convert(fwdScaledPoly, wcs.getPixelOrigin(), cdMatrix)
319 wcs =
makeWcs(sipForward, sipReverse, wcs.getSkyOrigin())
321 if refCat
is not None:
322 self.log.debug(
"Updating centroids in refCat")
323 lsst.afw.table.updateRefCentroids(wcs, refList=refCat)
325 self.log.warn(
"Updating reference object centroids in match list; refCat is None")
326 lsst.afw.table.updateRefCentroids(wcs, refList=[match.first
for match
in matches])
328 if sourceCat
is not None:
329 self.log.debug(
"Updating coords in sourceCat")
330 lsst.afw.table.updateSourceCoords(wcs, sourceList=sourceCat)
332 self.log.warn(
"Updating source coords in match list; sourceCat is None")
333 lsst.afw.table.updateSourceCoords(wcs, sourceList=[match.second
for match
in matches])
335 self.log.debug(
"Updating distance in match list")
339 scatterOnSky = stats.getValue()*lsst.afw.geom.radians
341 if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec:
342 raise lsst.pipe.base.TaskError(
343 "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" %
344 (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec))
346 return lsst.pipe.base.Struct(
348 scatterOnSky=scatterOnSky,
351 def display(self, revFitter, exposure=None, bbox=None, frame=0, pause=True):
352 """Display positions and outlier status overlaid on an image.
354 This method is called by fitWcs when display debugging is enabled. It
355 always drops into pdb before returning to allow interactive inspection,
356 and hence it should never be called in non-interactive contexts.
361 revFitter : :cpp:class:`lsst::meas::astrom::ScaledPolynomialTransformFitter`
362 Fitter object initialized with `fromMatches` for fitting a "reverse"
363 distortion: the mapping from intermediate world coordinates to
365 exposure : :cpp:class:`lsst::afw::image::Exposure`
366 An Exposure or other displayable image on which matches can be
368 bbox : :cpp:class:`lsst::afw::geom::Box2I`
369 Bounding box of the region on which matches should be plotted.
371 data = revFitter.getData()
372 disp = lsst.afw.display.getDisplay(frame=frame)
373 if exposure
is not None:
375 elif bbox
is not None:
376 disp.mtv(exposure=lsst.afw.image.ExposureF(bbox))
378 raise TypeError(
"At least one of 'exposure' and 'bbox' must be provided.")
379 data = revFitter.getData()
381 srcErrKey = lsst.afw.table.CovarianceMatrix2fKey(data.schema[
"src"], [
"x",
"y"])
384 rejectedKey = data.schema.find(
"rejected").key
385 with disp.Buffering():
387 colors = ((lsst.afw.display.RED, lsst.afw.display.GREEN)
388 if not record.get(rejectedKey)
else
389 (lsst.afw.display.MAGENTA, lsst.afw.display.CYAN))
390 rx, ry = record.get(refKey)
391 disp.dot(
"x", rx, ry, size=10, ctype=colors[0])
392 mx, my = record.get(modelKey)
393 disp.dot(
"o", mx, my, size=10, ctype=colors[0])
394 disp.line([(rx, ry), (mx, my)], ctype=colors[0])
395 sx, sy = record.get(srcKey)
396 sErr = record.get(srcErrKey)
398 disp.dot(sEllipse, sx, sy, ctype=colors[1])
399 if pause
or pause
is None:
400 print(
"Dropping into debugger to allow inspection of display. Type 'continue' when done.")
408 """Generate a guess Wcs from the astrometric matches
410 We create a Wcs anchored at the center of the matches, with the scale
411 of the input Wcs. This is necessary because the Wcs may have a very
412 approximation position (as is common with telescoped-generated Wcs).
413 We're using the best of each: positions from the matches, and scale
418 matches : :cpp:class:`lsst::afw::table::ReferenceMatchVector`
419 A sequence of reference object/source matches.
420 The following fields are read:
421 - match.first (reference object) coord
422 - match.second (source) centroid
423 wcs : :cpp:class:`lsst::afw::image::Wcs`
424 An initial WCS whose CD matrix is used as the CD matrix of the
430 A new :cpp:class:`lsst::afw::image::Wcs`.
437 crpix /= len(matches)
438 crval /= len(matches)
439 cd = wcs.getCDMatrix()
An ellipse core with quadrupole moments as parameters.
A coordinate class intended to represent absolute positions.
An integer coordinate rectangle.
boost::shared_ptr< Wcs > makeWcs(coord::Coord const &crval, geom::Point2D const &crpix, double CD11, double CD12, double CD21, double CD22)
Create a Wcs object from crval, crpix, CD, using CD elements (useful from python) ...
std::shared_ptr< afw::image::TanWcs > makeWcs(SipForwardTransform const &sipForward, SipReverseTransform const &sipReverse, afw::coord::Coord const &skyOrigin)
Create a new TAN SIP Wcs from a pair of SIP transforms and the sky origin.
afw::math::Statistics makeMatchStatisticsInRadians(afw::image::Wcs const &wcs, std::vector< MatchT > const &matchList, int const flags, afw::math::StatisticsControl const &sctrl=afw::math::StatisticsControl())
Compute statistics of on-sky radial separation for a match list, in radians.
A floating-point coordinate rectangle geometry.
Control object for outlier rejection in ScaledPolynomialTransformFitter.
A class to handle Icrs coordinates (inherits from Coord)