2 __all__ = [
"FitSipDistortionTask",
"FitSipDistortionConfig"]
11 from .scaledPolynomialTransformFitter
import ScaledPolynomialTransformFitter, OutlierRejectionControl
12 from .sipTransform
import SipForwardTransform, SipReverseTransform, makeWcs
13 from .makeMatchStatistics
import makeMatchStatisticsInRadians
15 from .setMatchDistance
import setMatchDistance
19 order = lsst.pex.config.RangeField(
20 doc=
"Order of SIP polynomial",
25 numRejIter = lsst.pex.config.RangeField(
26 doc=
"Number of rejection iterations",
31 rejSigma = lsst.pex.config.RangeField(
32 doc=
"Number of standard deviations for clipping level",
37 nClipMin = lsst.pex.config.Field(
38 doc=
"Minimum number of matches to reject when sigma-clipping",
42 nClipMax = lsst.pex.config.Field(
43 doc=
"Maximum number of matches to reject when sigma-clipping",
47 maxScatterArcsec = lsst.pex.config.RangeField(
48 doc=
"Maximum median scatter of a WCS fit beyond which the fit fails (arcsec); " +
49 "be generous, as this is only intended to catch catastrophic failures",
54 refUncertainty = lsst.pex.config.Field(
55 doc=
"RMS uncertainty in reference catalog positions, in pixels. Will be added " +
56 "in quadrature with measured uncertainties in the fit.",
60 nGridX = lsst.pex.config.Field(
61 doc=
"Number of X grid points used to invert the SIP reverse transform.",
65 nGridY = lsst.pex.config.Field(
66 doc=
"Number of Y grid points used to invert the SIP reverse transform.",
70 gridBorder = lsst.pex.config.Field(
71 doc=
"When setting the gird region, how much to extend the image " +
72 "bounding box (in pixels) before transforming it to intermediate " +
73 "world coordinates using the initial WCS.",
80 """Fit a TAN-SIP WCS given a list of reference object/source matches 82 FitSipDistortionTask is a drop-in replacement for 83 :py:class:`lsst.meas.astrom.FitTanSipWcsTask`. It is built on fundamentally 84 stronger fitting algorithms, but has received significantly less testing. 86 Like :py:class:`lsst.meas.astrom.FitTanSipWcsTask`, this task is most 87 easily used as the wcsFitter component of 88 :py:class:`lsst.meas.astrom.AstrometryTask`; it can be enabled in a config 93 from lsst.meas.astrom import FitSipDistortionTask 94 config.(...).astometry.wcsFitter.retarget(FitSipDistortionTask) 99 The algorithm used by FitSipDistortionTask involves three steps: 101 - We set the CRVAL and CRPIX reference points to the mean positions of 102 the matches, while holding the CD matrix fixed to the value passed in 103 to the run() method. This work is done by the makeInitialWcs method. 105 - We fit the SIP "reverse transform" (the AP and BP polynomials that map 106 "intermediate world coordinates" to pixels). This happens iteratively; 107 while fitting for the polynomial coefficients given a set of matches is 108 a linear operation that can be done without iteration, outlier 109 rejection using sigma-clipping and estimation of the intrinsic scatter 110 are not. By fitting the reverse transform first, we can do outlier 111 rejection in pixel coordinates, where we can better handle the source 112 measurement uncertainties that contribute to the overall scatter. This 114 :cpp:class:`lsst::meas::astrom::ScaledPolynomialTransform`, which is 115 somewhat more general than the SIP reverse transform in that it allows 116 an affine transform both before and after the polynomial. This is 117 somewhat more numerically stable than the SIP form, which applies only 118 a linear transform (with no offset) before the polynomial and only a 119 shift afterwards. We only convert to SIP form once the fitting is 120 complete. This conversion is exact (though it may be subject to 121 significant round-off error) as long as we do not attempt to null the 122 low-order SIP polynomial terms (we do not). 124 - Once the SIP reverse transform has been fit, we use it to populate a 125 grid of points that we use as the data points for fitting its inverse, 126 the SIP forward transform. Because our "data" here is artificial, 127 there is no need for outlier rejection or uncertainty handling. We 128 again fit a general scaled polynomial, and only convert to SIP form 129 when the fit is complete. 135 Enabling DEBUG-level logging on this task will report the number of 136 outliers rejected and the current estimate of intrinsic scatter at each 139 FitSipDistortionTask also supports the following lsstDebug variables to 140 control diagnostic displays: 141 - FitSipDistortionTask.display: if True, enable display diagnostics. 142 - FitSipDistortionTask.frame: frame to which the display will be sent 143 - FitSipDistortionTask.pause: whether to pause (by dropping into pdb) 144 between iterations (default is True). If 145 False, multiple frames will be used, 146 starting at the given number. 148 The diagnostic display displays the image (or an empty image if 149 exposure=None) overlaid with the positions of sources and reference 150 objects will be shown for every iteration in the reverse transform fit. 151 The legend for the overlay is: 154 Reference sources transformed without SIP distortion terms; this 155 uses a TAN WCS whose CRPIX, CRVAL and CD matrix are the same 156 as those in the TAN-SIP WCS being fit. These are not expected to 157 line up with sources unless distortion is small. 160 Same as Red X, but for matches that were rejected as outliers. 163 Reference sources using the current best-fit TAN-SIP WCS. These 164 are connected to the corresponding non-distorted WCS position by 165 a red line, and should be a much better fit to source positions 169 Same as Red O, but for matches that were rejected as outliers. 172 Source positions and their error ellipses, including the current 173 estimate of the intrinsic scatter. 176 Same as Green Ellipse, but for matches that were rejected as outliers. 181 See :py:class:`lsst.pipe.base.Task`; FitSipDistortionTask does not add any 182 additional constructor parameters. 186 ConfigClass = FitSipDistortionConfig
187 _DefaultName =
"fitWcs" 190 lsst.pipe.base.Task.__init__(self, **kwds)
196 @lsst.pipe.base.timeMethod
197 def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None):
198 """Fit a TAN-SIP WCS from a list of reference object/source matches 203 matches : list of :cpp:class:`lsst::afw::table::ReferenceMatch` 204 A sequence of reference object/source matches. 205 The following fields are read: 206 - match.first (reference object) coord 207 - match.second (source) centroid 208 The following fields are written: 209 - match.first (reference object) centroid, 210 - match.second (source) centroid 211 - match.distance (on sky separation, in radians) 212 initWcs : :cpp:class:`lsst::afw::geom::SkyWcs` 213 An initial WCS whose CD matrix is used as the final CD matrix. 214 bbox : :cpp:class:`lsst::afw::geom::Box2I` 215 The region over which the WCS will be valid (PARENT pixel coordinates); 216 if None or an empty box then computed from matches 217 refCat : :cpp:class:`lsst::afw::table::SimpleCatalog` 218 Reference object catalog, or None. 219 If provided then all centroids are updated with the new WCS, 220 otherwise only the centroids for ref objects in matches are updated. 221 Required fields are "centroid_x", "centroid_y", "coord_ra", and "coord_dec". 222 sourceCat : :cpp:class:`lsst::afw::table::SourceCatalog` 223 Source catalog, or None. 224 If provided then coords are updated with the new WCS; 225 otherwise only the coords for sources in matches are updated. 226 Required input fields are "slot_Centroid_x", "slot_Centroid_y", 227 "slot_Centroid_xSigma", "slot_Centroid_ySigma", and optionally 228 "slot_Centroid_x_y_Cov". The "coord_ra" and "coord_dec" fields 229 will be updated but are not used as input. 230 exposure : :cpp:class:`lsst::afw::image::Exposure` 231 An Exposure or other displayable image on which matches can be 232 overplotted. Ignored (and may be None) if display-based debugging 233 is not enabled via lsstDebug. 238 An lsst.pipe.base.Struct with the following fields: 240 wcs : :cpp:class:`lsst::afw::geom::SkyWcs` 242 scatterOnSky : :cpp:class:`lsst::afw::geom::Angle` 243 The median on-sky separation between reference objects and 244 sources in "matches", as an lsst.afw.geom.Angle 253 for match
in matches:
254 bbox.include(match.second.getCentroid())
266 revFitter = ScaledPolynomialTransformFitter.fromMatches(self.config.order, matches, wcs,
267 self.config.refUncertainty)
269 for nIter
in range(self.config.numRejIter):
270 revFitter.updateModel()
271 intrinsicScatter = revFitter.updateIntrinsicScatter()
274 "Iteration {0}: intrinsic scatter is {1:4.3f} pixels, " 275 "rejected {2} outliers at {3:3.2f} sigma.".format(
276 nIter+1, intrinsicScatter, nRejected, clippedSigma
280 displayFrame = self.
display(revFitter, exposure=exposure, bbox=bbox,
281 frame=displayFrame, displayPause=displayPause)
283 revScaledPoly = revFitter.getTransform()
287 sipReverse = SipReverseTransform.convert(revScaledPoly, wcs.getPixelOrigin(), cdMatrix)
295 gridBBoxPix.grow(self.config.gridBorder)
301 for point
in gridBBoxPix.getCorners():
303 gridBBoxIwc.include(cdMatrix(point))
304 fwdFitter = ScaledPolynomialTransformFitter.fromGrid(self.config.order, gridBBoxIwc,
305 self.config.nGridX, self.config.nGridY,
309 fwdScaledPoly = fwdFitter.getTransform()
310 sipForward = SipForwardTransform.convert(fwdScaledPoly, wcs.getPixelOrigin(), cdMatrix)
314 wcs =
makeWcs(sipForward, sipReverse, wcs.getSkyOrigin())
316 if refCat
is not None:
317 self.log.debug(
"Updating centroids in refCat")
320 self.log.warn(
"Updating reference object centroids in match list; refCat is None")
323 if sourceCat
is not None:
324 self.log.debug(
"Updating coords in sourceCat")
327 self.log.warn(
"Updating source coords in match list; sourceCat is None")
330 self.log.debug(
"Updating distance in match list")
334 scatterOnSky = stats.getValue()*lsst.afw.geom.radians
336 if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec:
337 raise lsst.pipe.base.TaskError(
338 "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" %
339 (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec))
341 return lsst.pipe.base.Struct(
343 scatterOnSky=scatterOnSky,
346 def display(self, revFitter, exposure=None, bbox=None, frame=0, pause=True):
347 """Display positions and outlier status overlaid on an image. 349 This method is called by fitWcs when display debugging is enabled. It 350 always drops into pdb before returning to allow interactive inspection, 351 and hence it should never be called in non-interactive contexts. 356 revFitter : :cpp:class:`lsst::meas::astrom::ScaledPolynomialTransformFitter` 357 Fitter object initialized with `fromMatches` for fitting a "reverse" 358 distortion: the mapping from intermediate world coordinates to 360 exposure : :cpp:class:`lsst::afw::image::Exposure` 361 An Exposure or other displayable image on which matches can be 363 bbox : :cpp:class:`lsst::afw::geom::Box2I` 364 Bounding box of the region on which matches should be plotted. 366 data = revFitter.getData()
367 disp = lsst.afw.display.getDisplay(frame=frame)
368 if exposure
is not None:
370 elif bbox
is not None:
371 disp.mtv(exposure=lsst.afw.image.ExposureF(bbox))
373 raise TypeError(
"At least one of 'exposure' and 'bbox' must be provided.")
374 data = revFitter.getData()
376 srcErrKey = lsst.afw.table.CovarianceMatrix2fKey(data.schema[
"src"], [
"x",
"y"])
379 rejectedKey = data.schema.find(
"rejected").key
380 with disp.Buffering():
382 colors = ((lsst.afw.display.RED, lsst.afw.display.GREEN)
383 if not record.get(rejectedKey)
else 384 (lsst.afw.display.MAGENTA, lsst.afw.display.CYAN))
385 rx, ry = record.get(refKey)
386 disp.dot(
"x", rx, ry, size=10, ctype=colors[0])
387 mx, my = record.get(modelKey)
388 disp.dot(
"o", mx, my, size=10, ctype=colors[0])
389 disp.line([(rx, ry), (mx, my)], ctype=colors[0])
390 sx, sy = record.get(srcKey)
391 sErr = record.get(srcErrKey)
392 sEllipse = lsst.afw.geom.Quadrupole(sErr[0, 0], sErr[1, 1], sErr[0, 1])
393 disp.dot(sEllipse, sx, sy, ctype=colors[1])
394 if pause
or pause
is None:
395 print(
"Dropping into debugger to allow inspection of display. Type 'continue' when done.")
403 """Generate a guess Wcs from the astrometric matches 405 We create a Wcs anchored at the center of the matches, with the scale 406 of the input Wcs. This is necessary because the Wcs may have a very 407 approximation position (as is common with telescoped-generated Wcs). 408 We're using the best of each: positions from the matches, and scale 413 matches : list of :cpp:class:`lsst::afw::table::ReferenceMatch` 414 A sequence of reference object/source matches. 415 The following fields are read: 416 - match.first (reference object) coord 417 - match.second (source) centroid 418 wcs : :cpp:class:`lsst::afw::geom::SkyWcs` 419 An initial WCS whose CD matrix is used as the CD matrix of the 425 A new :cpp:class:`lsst::afw::geom::SkyWcs`. 431 crval += mm.first.getCoord().getVector()
432 crpix /= len(matches)
433 crval /= len(matches)
434 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)