-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
AstrometryTask now iterates the match and fit WCS cycle in a smarter way: - It checks if each iteration has improved the fit and stops if not or if the fit is so good there is no point continuing. - If a WCS fit fails it either gives up (if it's the first fit) or uses the previous iteration - It reduces the maximum matching distance for each iteration based on the measured quality of the fit. Improved FitTanSipWcsTask in two ways: - It iterates the fit (since the existing C++ code fits X first, then Y, which is something we plan to fix at some point). - If the fit is terrible it raises an exception.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,9 @@ | ||
from __future__ import absolute_import, division, print_function | ||
|
||
from lsst.daf.base import PropertyList | ||
from lsst.afw.image import ExposureF | ||
from lsst.afw.image.utils import getDistortedWcs | ||
from lsst.afw.table import Point2DKey | ||
from lsst.afw.geom import Box2D | ||
from lsst.afw.geom import Box2D, radToArcsec | ||
import lsst.afw.math as afwMath | ||
import lsst.pex.config as pexConfig | ||
import lsst.pipe.base as pipeBase | ||
from .loadAstrometryNetObjects import LoadAstrometryNetObjectsTask | ||
|
@@ -35,7 +34,7 @@ class AstrometryConfig(pexConfig.Config): | |
doc = "maximum number of iterations of match sources and fit WCS; " + | ||
"ignored if forceKnownWcs True", | ||
dtype = int, | ||
default = 3, | ||
default = 6, | ||
min = 1, | ||
) | ||
|
||
|
@@ -196,40 +195,53 @@ def solve(self, sourceCat, bbox, initWcs, filterName=None, calib=None, exposure= | |
|
||
res = None | ||
wcs = initWcs | ||
maxMatchDistArcSec = None | ||
for i in range(self.config.maxIter): | ||
tryRes = self._matchAndFitWcs( # refCat, sourceCat, refFluxField, bbox, wcs, exposure=None | ||
refCat = loadRes.refCat, | ||
sourceCat = sourceCat, | ||
refFluxField = loadRes.fluxField, | ||
bbox = bbox, | ||
wcs = wcs, | ||
exposure = exposure, | ||
) | ||
try: | ||
tryRes = self._matchAndFitWcs( # refCat, sourceCat, refFluxField, bbox, wcs, exposure=None | ||
refCat = loadRes.refCat, | ||
sourceCat = sourceCat, | ||
refFluxField = loadRes.fluxField, | ||
bbox = bbox, | ||
wcs = wcs, | ||
exposure = exposure, | ||
maxMatchDistArcSec = maxMatchDistArcSec, | ||
) | ||
except Exception as e: | ||
# if we have had a succeessful iteration then use that; otherwise fail | ||
if i > 0: | ||
self.log.info("Fit WCS iter %d failed; using previous iteration: %s" % (i, e)) | ||
break | ||
else: | ||
raise | ||
|
||
if self.config.forceKnownWcs: | ||
# run just once; note that the number of matches has already logged | ||
res = tryRes | ||
break | ||
|
||
self.log.logdebug( | ||
"Fit WCS iter %s: %s matches; median scatter = %g arcsec" % \ | ||
(i, len(tryRes.matches), tryRes.scatterOnSky.asArcseconds()), | ||
) | ||
distRadList = [match.distance for match in tryRes.matches] | ||
distRadStats = afwMath.makeStatistics(distRadList, afwMath.MEANCLIP | afwMath.STDEVCLIP) | ||
distArcsecMean = radToArcsec(distRadStats.getValue(afwMath.MEANCLIP)) | ||
distArcsecStdDev = radToArcsec(distRadStats.getValue(afwMath.STDEVCLIP)) | ||
This comment has been minimized.
Sorry, something went wrong. |
||
|
||
if res is not None and not self.config.forceKnownWcs: | ||
if len(tryRes.matches) < len(res.matches): | ||
self.log.info( | ||
"Fit WCS: use iter %s because it had more matches than the next iter: %s vs. %s" % \ | ||
(i-1, len(res.matches), len(tryRes.matches))) | ||
break | ||
if len(tryRes.matches) == len(res.matches) and tryRes.scatterOnSky >= res.scatterOnSky: | ||
self.log.info( | ||
"Fit WCS: use iter %s because it had less scatter than the next iter: %g vs. %g arcsec" % \ | ||
(i-1, res.scatterOnSky.asArcseconds(), tryRes.scatterOnSky.asArcseconds())) | ||
self.log.info("Fit WCS iter %d: %d matches; scatter = %0.3f +- %0.3f arcsec" % | ||
(i, len(tryRes.matches), distArcsecMean, distArcsecStdDev)) | ||
This comment has been minimized.
Sorry, something went wrong.
PaulPrice
Contributor
|
||
|
||
newMaxMatchDistArcSec = distArcsecMean + 2.0*distArcsecStdDev | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
PaulPrice
Contributor
|
||
if maxMatchDistArcSec is not None: | ||
if newMaxMatchDistArcSec >= maxMatchDistArcSec: | ||
self.log.warn( | ||
"Iteration %d had no better mean + 2sigma scatter; using previous iteration" % (i,)) | ||
break | ||
|
||
maxMatchDistArcSec = newMaxMatchDistArcSec | ||
res = tryRes | ||
wcs = res.wcs | ||
if newMaxMatchDistArcSec < 0.001: | ||
self.log.info( | ||
"Iteration %d had mean + 2sigma scatter < 0.001 arcsec; that's good enough" % (i,)) | ||
break | ||
|
||
return pipeBase.Struct( | ||
refCat = loadRes.refCat, | ||
|
@@ -241,13 +253,16 @@ def solve(self, sourceCat, bbox, initWcs, filterName=None, calib=None, exposure= | |
) | ||
|
||
@pipeBase.timeMethod | ||
def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, exposure=None): | ||
def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, maxMatchDistArcSec=None, | ||
exposure=None): | ||
"""!Match sources to reference objects and fit a WCS | ||
@param[in] refCat catalog of reference objects | ||
@param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog) | ||
@param[in] bbox bounding box of exposure (an lsst.afw.geom.Box2I) | ||
@param[in] wcs initial guess for WCS of exposure (an lsst.afw.image.Wcs) | ||
@param[in] maxMatchDistArcSec maximum distance between reference objects and sources (arcsec); | ||
if None then use the matcher's default | ||
@param[in] exposure exposure whose WCS is to be fit, or None; used only for the debug display | ||
@return an lsst.pipe.base.Struct with these fields: | ||
|
@@ -263,7 +278,9 @@ def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, exposure=N | |
sourceCat = sourceCat, | ||
wcs = wcs, | ||
refFluxField = refFluxField, | ||
maxMatchDistArcSec = maxMatchDistArcSec, | ||
) | ||
self.log.info("Found %s matches" % (len(matchRes.matches),)) | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong. |
||
if debug.display: | ||
frame = int(debug.frame) | ||
displayAstrometry( | ||
|
@@ -288,7 +305,7 @@ def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, exposure=N | |
fitWcs = fitRes.wcs | ||
scatterOnSky = fitRes.scatterOnSky | ||
else: | ||
self.log.info("Not fitting WCS (forceKnownWcs true); %s matches" % (len(matchRes.matches),)) | ||
self.log.info("Not fitting WCS (forceKnownWcs true)") | ||
This comment has been minimized.
Sorry, something went wrong. |
||
fitWcs = wcs | ||
scatterOnSky = None | ||
if debug.display: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,9 +11,23 @@ | |
|
||
class FitTanSipWcsConfig(pexConfig.Config): | ||
order = pexConfig.RangeField( | ||
doc = "order of SIP polynomial (0 for pure TAN WCS)", | ||
doc = "order of SIP polynomial", | ||
dtype = int, | ||
default = 4, | ||
min = 0, | ||
) | ||
numIter = pexConfig.RangeField( | ||
doc = "number of iterations of fitter (which fits X and Y separately, and so benefits from " + \ | ||
"a few iterations", | ||
dtype = int, | ||
default = 3, | ||
This comment has been minimized.
Sorry, something went wrong.
PaulPrice
Contributor
|
||
min = 1, | ||
) | ||
maxScatterArcsec = pexConfig.RangeField( | ||
This comment has been minimized.
Sorry, something went wrong. |
||
doc = "maximum median scatter of a WCS fit beyond which the fit fails (arcsec); " + | ||
"be generous, as this is only intended to catch catastrophic failures", | ||
dtype = float, | ||
default = 10, | ||
min = 0, | ||
) | ||
|
||
|
@@ -101,8 +115,10 @@ def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None): | |
""" | ||
if bbox is None: | ||
bbox = afwGeom.Box2I() | ||
sipObject = makeCreateWcsWithSip(matches, initWcs, self.config.order, bbox) | ||
wcs = sipObject.getNewWcs() | ||
wcs = initWcs | ||
for i in range(self.config.numIter): | ||
sipObject = makeCreateWcsWithSip(matches, wcs, self.config.order, bbox) | ||
wcs = sipObject.getNewWcs() | ||
|
||
if refCat is not None: | ||
self.log.info("Updating centroids in refCat") | ||
|
@@ -121,9 +137,16 @@ def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None): | |
self.log.info("Updating distance in match list") | ||
setMatchDistance(matches) | ||
|
||
scatterOnSky = sipObject.getScatterOnSky() | ||
|
||
if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec: | ||
raise pipeBase.TaskError( | ||
"Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" % | ||
(scatterOnSky.asArcseconds(), self.config.maxScatterArcsec)) | ||
|
||
return pipeBase.Struct( | ||
wcs = wcs, | ||
scatterOnSky = sipObject.getScatterOnSky(), | ||
scatterOnSky = scatterOnSky, | ||
) | ||
|
||
@staticmethod | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,7 +26,7 @@ | |
import os | ||
import unittest | ||
|
||
import numpy | ||
import numpy as np | ||
This comment has been minimized.
Sorry, something went wrong. |
||
|
||
import eups | ||
import lsst.utils.tests as utilsTests | ||
|
@@ -116,7 +116,7 @@ def doTest(self, pixelsToTanPixels, order=3): | |
|
||
angSep = refCoord.angularSeparation(srcCoord) | ||
maxAngSep = max(maxAngSep, angSep) | ||
self.assertLess(refCoord.angularSeparation(srcCoord), 0.0025 * afwGeom.arcseconds) | ||
self.assertLess(refCoord.angularSeparation(srcCoord).asArcseconds(), 0.0025) | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
r-owen
Author
Contributor
|
||
|
||
pixSep = math.hypot(*(srcPixPos-refPixPos)) | ||
maxPixSep = max(maxPixSep, pixSep) | ||
|
@@ -136,8 +136,11 @@ def doTest(self, pixelsToTanPixels, order=3): | |
self.assertTrue(resultsNoFit.wcs is distortedWcs) | ||
self.assertTrue(resultsNoFit.initWcs is distortedWcs) | ||
self.assertTrue(resultsNoFit.scatterOnSky is None) | ||
# fitting may find a few more matches, since it matches again after fitting the WCS | ||
self.assertTrue(0 <= len(results.matches) - len(resultsNoFit.matches) < len(results.matches) * 0.1) | ||
|
||
# fitting should improve the quality of the matches | ||
meanFitDist = np.mean([match.distance for match in results.matches]) | ||
meanNoFitDist = np.mean([match.distance for match in results.matches]) | ||
This comment has been minimized.
Sorry, something went wrong.
PaulPrice
Contributor
|
||
self.assertLessEqual(meanFitDist, meanNoFitDist) | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
r-owen
Author
Contributor
|
||
|
||
def makeSourceCat(self, distortedWcs): | ||
"""Make a source catalog by reading the position reference stars and distorting the positions | ||
|
@@ -147,17 +150,18 @@ def makeSourceCat(self, distortedWcs): | |
loadRes = loader.loadPixelBox(bbox=self.bbox, wcs=distortedWcs, filterName="r") | ||
refCat = loadRes.refCat | ||
refCentroidKey = afwTable.Point2DKey(refCat.schema["centroid"]) | ||
refFluxRKey = refCat.schema["r_flux"].asKey() | ||
|
||
sourceSchema = afwTable.SourceTable.makeMinimalSchema() | ||
measBase.SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema | ||
sourceCat = afwTable.SourceCatalog(sourceSchema) | ||
sourceCentroidKey = afwTable.Point2DKey(sourceSchema["slot_Centroid"]) | ||
sourceFluxKey = sourceSchema["slot_ApFlux_flux"].asKey() | ||
|
||
for refObj in refCat: | ||
src = sourceCat.addNew() | ||
refPos = refObj.get(refCentroidKey) | ||
srcPos = refPos | ||
src.set(sourceCentroidKey, srcPos) | ||
src.set(sourceCentroidKey, refObj.get(refCentroidKey)) | ||
src.set(sourceFluxKey, refObj.get(refFluxRKey)) | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
r-owen
Author
Contributor
|
||
return sourceCat | ||
|
||
def assertWcssAlmostEqual(self, wcs0, wcs1, bbox, maxSkyErr=0.01 * afwGeom.arcseconds, maxPixErr = 0.02, | ||
|
@@ -177,8 +181,8 @@ def assertWcssAlmostEqual(self, wcs0, wcs1, bbox, maxSkyErr=0.01 * afwGeom.arcse | |
@throw AssertionError if the two WCSs do not match sufficiently closely | ||
""" | ||
bboxd = afwGeom.Box2D(bbox) | ||
xList = numpy.linspace(bboxd.getMinX(), bboxd.getMaxX(), nx) | ||
yList = numpy.linspace(bboxd.getMinY(), bboxd.getMaxY(), ny) | ||
xList = np.linspace(bboxd.getMinX(), bboxd.getMaxX(), nx) | ||
yList = np.linspace(bboxd.getMinY(), bboxd.getMaxY(), ny) | ||
maxSkyErrPixPos = [afwGeom.Angle(0), None] | ||
maxPixErrSkyPos = [0, None] | ||
for x in xList: | ||
|
Why are you using clipped statistics on the match list? Shouldn't they all be good matches?