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