1 from __future__
import absolute_import, division, print_function
3 __all__ = [
"FitSipDistortionTask",
"FitSipDistortionConfig"]
5 from builtins
import range
12 from .scaledPolynomialTransformFitter
import ScaledPolynomialTransformFitter, OutlierRejectionControl
13 from .sipTransform
import SipForwardTransform, SipReverseTransform, makeWcs
14 from .makeMatchStatistics
import makeMatchStatisticsInRadians
16 from .setMatchDistance
import setMatchDistance
20 order = lsst.pex.config.RangeField(
21 doc=
"Order of SIP polynomial",
26 numRejIter = lsst.pex.config.RangeField(
27 doc=
"Number of rejection iterations",
32 rejSigma = lsst.pex.config.RangeField(
33 doc=
"Number of standard deviations for clipping level",
38 nClipMin = lsst.pex.config.Field(
39 doc=
"Minimum number of matches to reject when sigma-clipping",
43 nClipMax = lsst.pex.config.Field(
44 doc=
"Maximum number of matches to reject when sigma-clipping",
48 maxScatterArcsec = lsst.pex.config.RangeField(
49 doc=
"Maximum median scatter of a WCS fit beyond which the fit fails (arcsec); " +
50 "be generous, as this is only intended to catch catastrophic failures",
55 refUncertainty = lsst.pex.config.Field(
56 doc=
"RMS uncertainty in reference catalog positions, in pixels. Will be added " +
57 "in quadrature with measured uncertainties in the fit.",
61 nGridX = lsst.pex.config.Field(
62 doc=
"Number of X grid points used to invert the SIP reverse transform.",
66 nGridY = lsst.pex.config.Field(
67 doc=
"Number of Y grid points used to invert the SIP reverse transform.",
71 gridBorder = lsst.pex.config.Field(
72 doc=
"When setting the gird region, how much to extend the image " +
73 "bounding box (in pixels) before transforming it to intermediate " +
74 "world coordinates using the initial WCS.",
81 """Fit a TAN-SIP WCS given a list of reference object/source matches 83 FitSipDistortionTask is a drop-in replacement for 84 :py:class:`lsst.meas.astrom.FitTanSipWcsTask`. It is built on fundamentally 85 stronger fitting algorithms, but has received significantly less testing. 87 Like :py:class:`lsst.meas.astrom.FitTanSipWcsTask`, this task is most 88 easily used as the wcsFitter component of 89 :py:class:`lsst.meas.astrom.AstrometryTask`; it can be enabled in a config 94 from lsst.meas.astrom import FitSipDistortionTask 95 config.(...).astometry.wcsFitter.retarget(FitSipDistortionTask) 100 The algorithm used by FitSipDistortionTask involves three steps: 102 - We set the CRVAL and CRPIX reference points to the mean positions of 103 the matches, while holding the CD matrix fixed to the value passed in 104 to the run() method. This work is done by the makeInitialWcs method. 106 - We fit the SIP "reverse transform" (the AP and BP polynomials that map 107 "intermediate world coordinates" to pixels). This happens iteratively; 108 while fitting for the polynomial coefficients given a set of matches is 109 a linear operation that can be done without iteration, outlier 110 rejection using sigma-clipping and estimation of the intrinsic scatter 111 are not. By fitting the reverse transform first, we can do outlier 112 rejection in pixel coordinates, where we can better handle the source 113 measurement uncertainties that contribute to the overall scatter. This 115 :cpp:class:`lsst::meas::astrom::ScaledPolynomialTransform`, which is 116 somewhat more general than the SIP reverse transform in that it allows 117 an affine transform both before and after the polynomial. This is 118 somewhat more numerically stable than the SIP form, which applies only 119 a linear transform (with no offset) before the polynomial and only a 120 shift afterwards. We only convert to SIP form once the fitting is 121 complete. This conversion is exact (though it may be subject to 122 significant round-off error) as long as we do not attempt to null the 123 low-order SIP polynomial terms (we do not). 125 - Once the SIP reverse transform has been fit, we use it to populate a 126 grid of points that we use as the data points for fitting its inverse, 127 the SIP forward transform. Because our "data" here is artificial, 128 there is no need for outlier rejection or uncertainty handling. We 129 again fit a general scaled polynomial, and only convert to SIP form 130 when the fit is complete. 136 Enabling DEBUG-level logging on this task will report the number of 137 outliers rejected and the current estimate of intrinsic scatter at each 140 FitSipDistortionTask also supports the following lsstDebug variables to 141 control diagnostic displays: 142 - FitSipDistortionTask.display: if True, enable display diagnostics. 143 - FitSipDistortionTask.frame: frame to which the display will be sent 144 - FitSipDistortionTask.pause: whether to pause (by dropping into pdb) 145 between iterations (default is True). If 146 False, multiple frames will be used, 147 starting at the given number. 149 The diagnostic display displays the image (or an empty image if 150 exposure=None) overlaid with the positions of sources and reference 151 objects will be shown for every iteration in the reverse transform fit. 152 The legend for the overlay is: 155 Reference sources transformed without SIP distortion terms; this 156 uses a TAN WCS whose CRPIX, CRVAL and CD matrix are the same 157 as those in the TAN-SIP WCS being fit. These are not expected to 158 line up with sources unless distortion is small. 161 Same as Red X, but for matches that were rejected as outliers. 164 Reference sources using the current best-fit TAN-SIP WCS. These 165 are connected to the corresponding non-distorted WCS position by 166 a red line, and should be a much better fit to source positions 170 Same as Red O, but for matches that were rejected as outliers. 173 Source positions and their error ellipses, including the current 174 estimate of the intrinsic scatter. 177 Same as Green Ellipse, but for matches that were rejected as outliers. 182 See :py:class:`lsst.pipe.base.Task`; FitSipDistortionTask does not add any 183 additional constructor parameters. 187 ConfigClass = FitSipDistortionConfig
188 _DefaultName =
"fitWcs" 191 lsst.pipe.base.Task.__init__(self, **kwds)
197 @lsst.pipe.base.timeMethod
198 def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None):
199 """Fit a TAN-SIP WCS from a list of reference object/source matches 204 matches : list of :cpp:class:`lsst::afw::table::ReferenceMatch` 205 A sequence of reference object/source matches. 206 The following fields are read: 207 - match.first (reference object) coord 208 - match.second (source) centroid 209 The following fields are written: 210 - match.first (reference object) centroid, 211 - match.second (source) centroid 212 - match.distance (on sky separation, in radians) 213 initWcs : :cpp:class:`lsst::afw::geom::SkyWcs` 214 An initial WCS whose CD matrix is used as the final CD matrix. 215 bbox : :cpp:class:`lsst::afw::geom::Box2I` 216 The region over which the WCS will be valid (PARENT pixel coordinates); 217 if None or an empty box then computed from matches 218 refCat : :cpp:class:`lsst::afw::table::SimpleCatalog` 219 Reference object catalog, or None. 220 If provided then all centroids are updated with the new WCS, 221 otherwise only the centroids for ref objects in matches are updated. 222 Required fields are "centroid_x", "centroid_y", "coord_ra", and "coord_dec". 223 sourceCat : :cpp:class:`lsst::afw::table::SourceCatalog` 224 Source catalog, or None. 225 If provided then coords are updated with the new WCS; 226 otherwise only the coords for sources in matches are updated. 227 Required input fields are "slot_Centroid_x", "slot_Centroid_y", 228 "slot_Centroid_xSigma", "slot_Centroid_ySigma", and optionally 229 "slot_Centroid_x_y_Cov". The "coord_ra" and "coord_dec" fields 230 will be updated but are not used as input. 231 exposure : :cpp:class:`lsst::afw::image::Exposure` 232 An Exposure or other displayable image on which matches can be 233 overplotted. Ignored (and may be None) if display-based debugging 234 is not enabled via lsstDebug. 239 An lsst.pipe.base.Struct with the following fields: 241 wcs : :cpp:class:`lsst::afw::geom::SkyWcs` 243 scatterOnSky : :cpp:class:`lsst::afw::geom::Angle` 244 The median on-sky separation between reference objects and 245 sources in "matches", as an lsst.afw.geom.Angle 254 for match
in matches:
255 bbox.include(match.second.getCentroid())
267 revFitter = ScaledPolynomialTransformFitter.fromMatches(self.config.order, matches, wcs,
268 self.config.refUncertainty)
270 for nIter
in range(self.config.numRejIter):
271 revFitter.updateModel()
272 intrinsicScatter = revFitter.updateIntrinsicScatter()
275 "Iteration {0}: intrinsic scatter is {1:4.3f} pixels, " 276 "rejected {2} outliers at {3:3.2f} sigma.".format(
277 nIter+1, intrinsicScatter, nRejected, clippedSigma
281 displayFrame = self.
display(revFitter, exposure=exposure, bbox=bbox,
282 frame=displayFrame, displayPause=displayPause)
284 revScaledPoly = revFitter.getTransform()
288 sipReverse = SipReverseTransform.convert(revScaledPoly, wcs.getPixelOrigin(), cdMatrix)
296 gridBBoxPix.grow(self.config.gridBorder)
302 for point
in gridBBoxPix.getCorners():
304 gridBBoxIwc.include(cdMatrix(point))
305 fwdFitter = ScaledPolynomialTransformFitter.fromGrid(self.config.order, gridBBoxIwc,
306 self.config.nGridX, self.config.nGridY,
310 fwdScaledPoly = fwdFitter.getTransform()
311 sipForward = SipForwardTransform.convert(fwdScaledPoly, wcs.getPixelOrigin(), cdMatrix)
315 wcs =
makeWcs(sipForward, sipReverse, wcs.getSkyOrigin())
317 if refCat
is not None:
318 self.log.debug(
"Updating centroids in refCat")
321 self.log.warn(
"Updating reference object centroids in match list; refCat is None")
324 if sourceCat
is not None:
325 self.log.debug(
"Updating coords in sourceCat")
328 self.log.warn(
"Updating source coords in match list; sourceCat is None")
331 self.log.debug(
"Updating distance in match list")
335 scatterOnSky = stats.getValue()*lsst.afw.geom.radians
337 if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec:
338 raise lsst.pipe.base.TaskError(
339 "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" %
340 (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec))
342 return lsst.pipe.base.Struct(
344 scatterOnSky=scatterOnSky,
347 def display(self, revFitter, exposure=None, bbox=None, frame=0, pause=True):
348 """Display positions and outlier status overlaid on an image. 350 This method is called by fitWcs when display debugging is enabled. It 351 always drops into pdb before returning to allow interactive inspection, 352 and hence it should never be called in non-interactive contexts. 357 revFitter : :cpp:class:`lsst::meas::astrom::ScaledPolynomialTransformFitter` 358 Fitter object initialized with `fromMatches` for fitting a "reverse" 359 distortion: the mapping from intermediate world coordinates to 361 exposure : :cpp:class:`lsst::afw::image::Exposure` 362 An Exposure or other displayable image on which matches can be 364 bbox : :cpp:class:`lsst::afw::geom::Box2I` 365 Bounding box of the region on which matches should be plotted. 367 data = revFitter.getData()
368 disp = lsst.afw.display.getDisplay(frame=frame)
369 if exposure
is not None:
371 elif bbox
is not None:
372 disp.mtv(exposure=lsst.afw.image.ExposureF(bbox))
374 raise TypeError(
"At least one of 'exposure' and 'bbox' must be provided.")
375 data = revFitter.getData()
377 srcErrKey = lsst.afw.table.CovarianceMatrix2fKey(data.schema[
"src"], [
"x",
"y"])
380 rejectedKey = data.schema.find(
"rejected").key
381 with disp.Buffering():
383 colors = ((lsst.afw.display.RED, lsst.afw.display.GREEN)
384 if not record.get(rejectedKey)
else 385 (lsst.afw.display.MAGENTA, lsst.afw.display.CYAN))
386 rx, ry = record.get(refKey)
387 disp.dot(
"x", rx, ry, size=10, ctype=colors[0])
388 mx, my = record.get(modelKey)
389 disp.dot(
"o", mx, my, size=10, ctype=colors[0])
390 disp.line([(rx, ry), (mx, my)], ctype=colors[0])
391 sx, sy = record.get(srcKey)
392 sErr = record.get(srcErrKey)
393 sEllipse = lsst.afw.geom.Quadrupole(sErr[0, 0], sErr[1, 1], sErr[0, 1])
394 disp.dot(sEllipse, sx, sy, ctype=colors[1])
395 if pause
or pause
is None:
396 print(
"Dropping into debugger to allow inspection of display. Type 'continue' when done.")
404 """Generate a guess Wcs from the astrometric matches 406 We create a Wcs anchored at the center of the matches, with the scale 407 of the input Wcs. This is necessary because the Wcs may have a very 408 approximation position (as is common with telescoped-generated Wcs). 409 We're using the best of each: positions from the matches, and scale 414 matches : list of :cpp:class:`lsst::afw::table::ReferenceMatch` 415 A sequence of reference object/source matches. 416 The following fields are read: 417 - match.first (reference object) coord 418 - match.second (source) centroid 419 wcs : :cpp:class:`lsst::afw::geom::SkyWcs` 420 An initial WCS whose CD matrix is used as the CD matrix of the 426 A new :cpp:class:`lsst::afw::geom::SkyWcs`. 433 crpix /= len(matches)
434 crval /= len(matches)
435 cd = wcs.getCdMatrix()
def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None)
def display(self, revFitter, exposure=None, bbox=None, frame=0, pause=True)
std::shared_ptr< afw::geom::SkyWcs > makeWcs(SipForwardTransform const &sipForward, SipReverseTransform const &sipReverse, afw::geom::SpherePoint const &skyOrigin)
Create a new TAN SIP Wcs from a pair of SIP transforms and the sky origin.
void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList)
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList)
def makeInitialWcs(self, matches, wcs)
std::shared_ptr< SkyWcs > makeSkyWcs(Point2D const &crpix, SpherePoint const &crval, Eigen::Matrix2d const &cdMatrix, std::string const &projection="TAN")
afw::math::Statistics makeMatchStatisticsInRadians(afw::geom::SkyWcs 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.
def setMatchDistance(matches)