1 from __future__
import absolute_import, division
18 from .loadAstrometryNetObjects
import LoadAstrometryNetObjectsTask, LoadMultiIndexes
20 from .
import sip
as astromSip
22 __all__ = [
"InitialAstrometry",
"ANetBasicAstrometryConfig",
"ANetBasicAstrometryTask"]
26 Object returned by Astrometry.determineWcs
28 getWcs(): sipWcs or tanWcs
29 getMatches(): sipMatches or tanMatches
32 solveQa (PropertyList)
34 tanMatches (MatchList)
36 sipMatches (MatchList)
37 matchMetadata (PropertyList)
49 Get "sipMatches" -- MatchList using the SIP WCS solution, if it
50 exists, or "tanMatches" -- MatchList using the TAN WCS solution
57 Returns the SIP WCS, if one was found, or a TAN WCS
61 matches = property(getMatches)
62 wcs = property(getWcs)
83 maxCpuTime = RangeField(
84 doc =
"Maximum CPU time to spend solving, in seconds",
89 matchThreshold = RangeField(
90 doc =
"Matching threshold for Astrometry.net solver (log-odds)",
92 default=math.log(1e12),
95 maxStars = RangeField(
96 doc =
"Maximum number of stars to use in Astrometry.net solving",
101 useWcsPixelScale = Field(
102 doc =
"Use the pixel scale from the input exposure\'s WCS headers?",
106 useWcsRaDecCenter = Field(
107 doc=
"Use the RA,Dec center information from the input exposure\'s WCS headers?",
111 useWcsParity = Field(
112 doc=
"Use the parity (flip / handedness) of the image from the input exposure\'s WCS headers?",
116 raDecSearchRadius = RangeField(
117 doc =
"When useWcsRaDecCenter=True, this is the radius, in degrees, around the RA,Dec center " +
118 "specified in the input exposure\'s WCS to search for a solution.",
123 pixelScaleUncertainty = RangeField(
124 doc =
"Range of pixel scales, around the value in the WCS header, to search. If the value of this field " +
125 "is X and the nominal scale is S, the range searched will be S/X to S*X",
130 catalogMatchDist = RangeField(
131 doc =
"Matching radius (arcsec) for matching sources to reference objects",
136 cleaningParameter = RangeField(
137 doc =
"Sigma-clipping parameter in sip/cleanBadPoints.py",
142 calculateSip = Field(
143 doc =
"Compute polynomial SIP distortion terms?",
147 sipOrder = RangeField(
148 doc =
"Polynomial order of SIP distortion terms",
153 badFlags = ListField(
154 doc =
"List of flags which cause a source to be rejected as bad",
157 "slot_Centroid_flag",
158 "base_PixelFlags_flag_edge",
159 "base_PixelFlags_flag_saturated",
160 "base_PixelFlags_flag_crCenter",
164 doc =
"Retrieve all available fluxes (and errors) from catalog?",
171 """!Basic implemeentation of the astrometry.net astrometrical fitter
173 A higher-level class ANetAstrometryTask takes care of dealing with the fact
174 that the initial WCS is probably only a pure TAN SIP, yet we may have
175 significant distortion and a good estimate for that distortion.
178 About Astrometry.net index files (astrometry_net_data):
180 There are three components of an index file: a list of stars
181 (stored as a star kd-tree), a list of quadrangles of stars ("quad
182 file") and a list of the shapes ("codes") of those quadrangles,
183 stored as a code kd-tree.
185 Each index covers a region of the sky, defined by healpix nside
186 and number, and a range of angular scales. In LSST, we share the
187 list of stars in a part of the sky between multiple indexes. That
188 is, the star kd-tree is shared between multiple indices (quads and
189 code kd-trees). In the astrometry.net code, this is called a
192 It is possible to "unload" and "reload" multiindex (and index)
193 objects. When "unloaded", they consume no FILE or mmap resources.
195 The multiindex object holds the star kd-tree and gives each index
196 object it holds a pointer to it, so it is necessary to
197 multiindex_reload_starkd() before reloading the indices it holds.
198 The multiindex_unload() method, on the other hand, unloads its
199 starkd and unloads each index it holds.
201 ConfigClass = ANetBasicAstrometryConfig
202 _DefaultName =
"aNetBasicAstrometry"
207 """!Construct an ANetBasicAstrometryTask
209 @param[in] config configuration (an instance of self.ConfigClass)
210 @param[in] andConfig astrometry.net data config (an instance of AstromNetDataConfig, or None);
211 if None then use andConfig.py in the astrometry_net_data product (which must be setup)
213 @throw RuntimeError if andConfig is None and the configuration cannot be found,
214 either because astrometry_net_data is not setup in eups
215 or because the setup version does not include the file "andConfig.py"
217 pipeBase.Task.__init__(self, config=config, **kwargs)
221 self.refObjLoader._readIndexFiles()
225 if self.log.getThreshold() > pexLog.Log.DEBUG:
227 from astrometry.util.ttime
import get_memusage
230 for key
in [
'VmPeak',
'VmSize',
'VmRSS',
'VmData']:
232 ss.append(key +
': ' +
' '.join(mu[key]))
234 ss.append(
'Mmaps: %i' % len(mu[
'mmaps']))
235 if 'mmaps_total' in mu:
236 ss.append(
'Mmaps: %i kB' % (mu[
'mmaps_total'] / 1024))
237 self.log.logdebug(prefix +
'Memory: ' +
', '.join(ss))
239 def _getImageParams(self, exposure=None, bbox=None, wcs=None, filterName=None, wcsRequired=True):
240 """Get image parameters
242 @param[in] exposure exposure (an afwImage.Exposure) or None
243 @param[in] bbox bounding box (an afwGeom.Box2I) or None; if None then bbox must be specified
244 @param[in] wcs WCS (an afwImage.Wcs) or None; if None then exposure must be specified
245 @param[in] filterName filter name, a string, or None; if None exposure must be specified
246 @param[in] wcsRequired if True then either wcs must be specified or exposure must contain a wcs;
247 if False then the returned wcs may be None
249 - bbox bounding box; guaranteed to be set
250 - wcs WCS if known, else None
251 - filterName filter name if known, else None
252 @throw RuntimeError if bbox cannot be determined, or wcs cannot be determined and wcsRequired True
254 if exposure
is not None:
256 bbox = exposure.getBBox()
257 self.log.logdebug(
"Setting bbox = %s from exposure metadata" % (bbox,))
259 self.log.logdebug(
"Setting wcs from exposure metadata")
260 wcs = exposure.getWcs()
261 if filterName
is None:
262 filterName = exposure.getFilter().getName()
263 self.log.logdebug(
"Setting filterName = %r from exposure metadata" % (filterName,))
265 raise RuntimeError(
"bbox or exposure must be specified")
266 if wcs
is None and wcsRequired:
267 raise RuntimeError(
"wcs or exposure (with a WCS) must be specified")
268 return bbox, wcs, filterName
270 def useKnownWcs(self, sourceCat, wcs=None, exposure=None, filterName=None, bbox=None):
271 """!Return an InitialAstrometry object, just like determineWcs,
272 but assuming the given input WCS is correct.
274 This is enabled by the ANetBasicAstrometryConfig
275 'forceKnownWcs' option. If you are using that option, you
276 probably also want to turn OFF 'calculateSip'.
278 This involves searching for reference sources within the WCS
279 area, and matching them to the given 'sourceCat'. If
280 'calculateSip' is set, we will try to compute a TAN-SIP
281 distortion correction.
283 @param[in] sourceCat list of detected sources in this image.
284 @param[in] wcs your known WCS, or None to get from exposure
285 @param[in] exposure the exposure holding metadata for this image;
286 if None then you must specify wcs, filterName and bbox
287 @param[in] filterName string, filter name, eg "i", or None to get from exposure`
288 @param[in] bbox bounding box of image, or None to get from exposure
290 @note this function is also called by 'determineWcs' (via 'determineWcs2'), since the steps are all
300 filterName = filterName,
303 refCat = self.refObjLoader.loadPixelBox(
306 filterName = filterName,
309 catids = [src.getId()
for src
in refCat]
311 self.log.logdebug(
'%i reference sources; %i unique IDs' % (len(catids), len(uids)))
313 uniq = set([sm.second.getId()
for sm
in matches])
314 if len(matches) != len(uniq):
315 self.log.warn((
'The list of matched stars contains duplicate reference source IDs ' +
316 '(%i sources, %i unique ids)') % (len(matches), len(uniq)))
317 if len(matches) == 0:
318 self.log.warn(
'No matches found between input sources and reference catalogue.')
321 self.log.logdebug(
'%i reference objects match input sources using input WCS' % (len(matches)))
322 astrom.tanMatches = matches
325 srcids = [s.getId()
for s
in sourceCat]
327 assert(m.second.getId()
in srcids)
328 assert(m.second
in sourceCat)
330 if self.config.calculateSip:
333 self.log.logdebug(
'Failed to find a SIP WCS better than the initial one.')
335 self.log.logdebug(
'%i reference objects match input sources using SIP WCS' %
337 astrom.sipWcs = sipwcs
338 astrom.sipMatches = matches
340 wcs = astrom.getWcs()
343 for src
in sourceCat:
352 """Find a WCS solution for the given 'sourceCat' in the given
353 'exposure', getting other parameters from config.
355 Valid kwargs include:
357 'radecCenter', an afw.coord.Coord giving the RA,Dec position
358 of the center of the field. This is used to limit the
359 search done by Astrometry.net (to make it faster and load
360 fewer index files, thereby using less memory). Defaults to
361 the RA,Dec center from the exposure's WCS; turn that off
362 with the boolean kwarg 'useRaDecCenter' or config option
365 'useRaDecCenter', a boolean. Don't use the RA,Dec center from
366 the exposure's initial WCS.
368 'searchRadius', in degrees, to search for a solution around
369 the given 'radecCenter'; default from config option
372 'useParity': parity is the 'flip' of the image. Knowing it
373 reduces the search space (hence time) for Astrometry.net.
374 The parity can be computed from the exposure's WCS (the
375 sign of the determinant of the CD matrix); this option
376 controls whether we do that or force Astrometry.net to
377 search both parities. Default from config.useWcsParity.
379 'pixelScale': afwGeom.Angle, estimate of the angle-per-pixel
380 (ie, arcseconds per pixel). Defaults to a value derived
381 from the exposure's WCS. If enabled, this value, plus or
382 minus config.pixelScaleUncertainty, will be used to limit
383 Astrometry.net's search.
385 'usePixelScale': boolean. Use the pixel scale to limit
386 Astrometry.net's search? Defaults to config.useWcsPixelScale.
388 'filterName', a string, the filter name of this image. Will
389 be mapped through the 'filterMap' config dictionary to a
390 column name in the astrometry_net_data index FITS files.
391 Defaults to the exposure.getFilter() value.
393 'bbox', bounding box of exposure; defaults to exposure.getBBox()
396 assert(exposure
is not None)
398 margs = kwargs.copy()
399 if not 'searchRadius' in margs:
400 margs.update(searchRadius = self.config.raDecSearchRadius * afwGeom.degrees)
401 if not 'usePixelScale' in margs:
402 margs.update(usePixelScale = self.config.useWcsPixelScale)
403 if not 'useRaDecCenter' in margs:
404 margs.update(useRaDecCenter = self.config.useWcsRaDecCenter)
405 if not 'useParity' in margs:
406 margs.update(useParity = self.config.useWcsParity)
407 margs.update(exposure=exposure)
411 """Get a blind astrometric solution for the given catalog of sources.
417 And if available, we can use:
418 -an initial Wcs estimate;
423 (all of which are metadata of Exposure).
426 imageSize: (W,H) integer tuple/iterable
427 pixelScale: afwGeom::Angle per pixel.
428 radecCenter: afwCoord::Coord
433 for key
in [
'exposure',
'bbox',
'filterName']:
435 kw[key] = kwargs[key]
436 astrom = self.
useKnownWcs(sourceCat, wcs=wcs, **kw)
453 searchRadiusScale=2.):
454 if not useRaDecCenter
and radecCenter
is not None:
455 raise RuntimeError(
'radecCenter is set, but useRaDecCenter is False. Make up your mind!')
456 if not usePixelScale
and pixelScale
is not None:
457 raise RuntimeError(
'pixelScale is set, but usePixelScale is False. Make up your mind!')
463 filterName = filterName,
468 xc, yc = bboxD.getCenter()
472 if pixelScale
is None:
474 pixelScale = wcs.pixelScale()
475 self.log.logdebug(
'Setting pixel scale estimate = %.3f from given WCS estimate' %
476 (pixelScale.asArcseconds()))
478 if radecCenter
is None:
480 radecCenter = wcs.pixelToSky(xc, yc)
481 self.log.logdebug((
'Setting RA,Dec center estimate = (%.3f, %.3f) from given WCS '
482 +
'estimate, using pixel center = (%.1f, %.1f)') %
483 (radecCenter.getLongitude().asDegrees(),
484 radecCenter.getLatitude().asDegrees(), xc, yc))
486 if searchRadius
is None:
488 assert(pixelScale
is not None)
489 pixRadius = math.hypot(*bboxD.getDimensions()) / 2
490 searchRadius = (pixelScale * pixRadius * searchRadiusScale)
491 self.log.logdebug((
'Using RA,Dec search radius = %.3f deg, from pixel scale, '
492 +
'image size, and searchRadiusScale = %g') %
493 (searchRadius, searchRadiusScale))
495 parity = wcs.isFlipped()
496 self.log.logdebug(
'Using parity = %s' % (parity
and 'True' or 'False'))
500 if exposure
is not None:
501 exposureBBoxD =
afwGeom.Box2D(exposure.getMaskedImage().getBBox())
503 exposureBBoxD = bboxD
505 self.log.logdebug(
"Trimming: kept %i of %i sources" % (n, len(sourceCat)))
508 sourceCat = sourceCat,
511 pixelScale = pixelScale,
512 radecCenter = radecCenter,
513 searchRadius = searchRadius,
515 filterName = filterName,
518 raise RuntimeError(
"Unable to match sources with catalog.")
519 self.log.info(
'Got astrometric solution from Astrometry.net')
521 rdc = wcs.pixelToSky(xc, yc)
522 self.log.logdebug(
'New WCS says image center pixel (%.1f, %.1f) -> RA,Dec (%.3f, %.3f)' %
523 (xc, yc, rdc.getLongitude().asDegrees(), rdc.getLatitude().asDegrees()))
527 """!Get a TAN-SIP WCS, starting from an existing WCS.
529 It uses your WCS to compute a fake grid of corresponding "stars" in pixel and sky coords,
530 and feeds that to the regular SIP code.
532 @param[in] wcs initial WCS
533 @param[in] bbox bounding box of image
534 @param[in] ngrid number of grid points along x and y for fitting (fit at ngrid^2 points)
535 @param[in] linearizeCenter if True, get a linear approximation of the input
536 WCS at the image center and use that as the TAN initialization for
537 the TAN-SIP solution. You probably want this if your WCS has its
538 CRPIX outside the image bounding box.
541 srcSchema = afwTable.SourceTable.makeMinimalSchema()
542 key = srcSchema.addField(
"centroid", type=
"PointD")
543 srcTable = afwTable.SourceTable.make(srcSchema)
544 srcTable.defineCentroid(
"centroid")
546 refs = afwTable.SimpleTable.make(afwTable.SimpleTable.makeMinimalSchema())
549 (W,H) = bbox.getDimensions()
550 x0, y0 = bbox.getMin()
551 for xx
in np.linspace(0., W, ngrid):
552 for yy
in np.linspace(0, H, ngrid):
553 src = srcs.makeRecord()
554 src.set(key.getX(), x0 + xx)
555 src.set(key.getY(), y0 + yy)
558 ref = refs.makeRecord()
562 if linearizeAtCenter:
567 crval = wcs.pixelToSky(crpix)
568 crval = crval.getPosition(afwGeom.degrees)
571 aff = wcs.linearizePixelToSky(crval)
572 cd = aff.getLinear().getMatrix()
580 """Produce a SIP solution given a list of known correspondences.
582 Unlike _calculateSipTerms, this does not iterate the solution;
583 it assumes you have given it a good sets of corresponding stars.
585 NOTE that "refCat" and "sourceCat" are assumed to be the same length;
586 entries "refCat[i]" and "sourceCat[i]" are assumed to be correspondences.
588 @param[in] origWcs the WCS to linearize in order to get the TAN part of the TAN-SIP WCS.
589 @param[in] refCat reference source catalog
590 @param[in] sourceCat source catalog
591 @param[in] bbox bounding box of image
593 sipOrder = self.config.sipOrder
595 for ci,si
in zip(refCat, sourceCat):
598 sipObject = astromSip.makeCreateWcsWithSip(matches, origWcs, sipOrder, bbox)
599 return sipObject.getNewWcs()
602 """!Iteratively calculate SIP distortions and regenerate matches based on improved WCS.
604 @param[in] origWcs original WCS object, probably (but not necessarily) a TAN WCS;
605 this is used to set the baseline when determining whether a SIP
606 solution is any better; it will be returned if no better SIP solution
608 @param[in] refCat reference source catalog
609 @param[in] sourceCat sources in the image to be solved
610 @param[in] matches list of supposedly matched sources, using the "origWcs".
611 @param[in] bbox bounding box of image, which is used when finding reverse SIP coefficients.
613 sipOrder = self.config.sipOrder
620 sipObject = astromSip.makeCreateWcsWithSip(matches, wcs, sipOrder, bbox)
621 if lastScatPix
is None:
622 lastScatPix = sipObject.getLinearScatterInPixels()
623 proposedWcs = sipObject.getNewWcs()
624 scatPix = sipObject.getScatterInPixels()
625 self.
plotSolution(matches, proposedWcs, bbox.getDimensions())
627 self.log.warn(
'Failed to calculate distortion terms. Error: ' + str(e))
630 matchSize = len(matches)
632 proposedMatchlist = self.
_getMatchList(sourceCat, refCat, proposedWcs)
634 self.log.logdebug(
'SIP iteration %i: %i objects match. Median scatter is %g arcsec = %g pixels (vs previous: %i matches, %g pixels)' %
635 (i, len(proposedMatchlist), sipObject.getScatterOnSky().asArcseconds(),
636 scatPix, matchSize, lastScatPix))
638 if len(proposedMatchlist) < matchSize:
640 "Fit WCS: use iter %s because it had more matches than the next iter: %s vs. %s" % \
641 (i, matchSize, len(proposedMatchlist)))
643 if len(proposedMatchlist) == matchSize
and scatPix >= lastScatPix:
645 "Fit WCS: use iter %s because it had less linear scatter than the next iter: %g vs. %g pixels" % \
646 (i, lastScatPix, scatPix))
650 matches = proposedMatchlist
651 lastScatPix = scatPix
652 matchSize = len(matches)
658 """Plot the solution, when debugging is turned on.
660 @param matches The list of matches
662 @param imageSize 2-tuple with the image size (W,H)
670 import matplotlib.pyplot
as plt
673 print >> sys.stderr,
"Unable to import matplotlib"
679 fig.canvas._tkcanvas._root().lift()
686 dx = numpy.zeros(num)
687 dy = numpy.zeros(num)
688 for i, m
in enumerate(matches):
689 x[i] = m.second.getX()
690 y[i] = m.second.getY()
691 pixel = wcs.skyToPixel(m.first.getCoord())
692 dx[i] = x[i] - pixel.getX()
693 dy[i] = y[i] - pixel.getY()
695 subplots = maUtils.makeSubplots(fig, 2, 2, xgutter=0.1, ygutter=0.1, pygutter=0.04)
697 def plotNext(x, y, xLabel, yLabel, xMax):
699 ax.set_autoscalex_on(
False)
700 ax.set_xbound(lower=0, upper=xMax)
702 ax.set_xlabel(xLabel)
703 ax.set_ylabel(yLabel)
706 plotNext(x, dx,
"x",
"dx", imageSize[0])
707 plotNext(x, dy,
"x",
"dy", imageSize[0])
708 plotNext(y, dx,
"y",
"dx", imageSize[1])
709 plotNext(y, dy,
"y",
"dy", imageSize[1])
715 reply = raw_input(
"Pausing for inspection, enter to continue... [hpQ] ").strip()
719 reply = reply.split()
725 if reply
in (
"",
"h",
"p",
"Q"):
727 print "h[elp] p[db] Q[uit]"
730 import pdb; pdb.set_trace()
736 dist = self.config.catalogMatchDist * afwGeom.arcseconds
737 clean = self.config.cleaningParameter
738 matcher = astromSip.MatchSrcToCatalogue(refCat, sourceCat, wcs, dist)
739 matches = matcher.getMatches()
742 X = [src.getX()
for src
in sourceCat]
743 Y = [src.getY()
for src
in sourceCat]
744 R1 = [src.getRa().asDegrees()
for src
in sourceCat]
745 D1 = [src.getDec().asDegrees()
for src
in sourceCat]
746 R2 = [src.getRa().asDegrees()
for src
in refCat]
747 D2 = [src.getDec().asDegrees()
for src
in refCat]
754 self.loginfo(
'_getMatchList: %i sources, %i reference sources' % (len(sourceCat), len(refCat)))
757 'Source range: x [%.1f, %.1f], y [%.1f, %.1f], RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
760 self.loginfo(
'Reference range: RA [%.3f, %.3f], Dec [%.3f, %.3f]' %
762 raise RuntimeError(
'No matches found between image and catalogue')
763 matches = astromSip.cleanBadPoints.clean(matches, wcs, nsigma=clean)
768 Returns the column name in the astrometry_net_data index file that will be used
769 for the given filter name.
771 @param filterName Name of filter used in exposure
772 @param columnMap Dict that maps filter names to column names
773 @param default Default column name
775 filterName = self.config.filterMap.get(filterName, filterName)
777 return columnMap[filterName]
779 self.log.warn(
"No column in configuration for filter '%s'; using default '%s'" %
780 (filterName, default))
783 def _solve(self, sourceCat, wcs, bbox, pixelScale, radecCenter,
784 searchRadius, parity, filterName=
None):
785 solver = self.refObjLoader._getSolver()
787 imageSize = bbox.getDimensions()
788 x0, y0 = bbox.getMin()
793 badkeys = [goodsources.getSchema().find(name).key
for name
in self.config.badFlags]
796 if np.isfinite(s.getX())
and np.isfinite(s.getY())
and np.isfinite(s.getPsfFlux()) \
798 goodsources.append(s)
800 self.log.info(
"Number of selected sources for astrometry : %d" %(len(goodsources)))
801 if len(goodsources) < len(sourceCat):
802 self.log.logdebug(
'Keeping %i of %i sources with finite X,Y positions and PSF flux' %
803 (len(goodsources), len(sourceCat)))
804 self.log.logdebug((
'Feeding sources in range x=[%.1f, %.1f], y=[%.1f, %.1f] ' +
805 '(after subtracting x0,y0 = %.1f,%.1f) to Astrometry.net') %
806 (xybb.getMinX(), xybb.getMaxX(), xybb.getMinY(), xybb.getMaxY(), x0, y0))
808 solver.setStars(goodsources, x0, y0)
809 solver.setMaxStars(self.config.maxStars)
810 solver.setImageSize(*imageSize)
811 solver.setMatchThreshold(self.config.matchThreshold)
813 if radecCenter
is not None:
814 raDecRadius = (radecCenter.getLongitude().asDegrees(),
815 radecCenter.getLatitude().asDegrees(),
816 searchRadius.asDegrees())
817 solver.setRaDecRadius(*raDecRadius)
818 self.log.logdebug(
'Searching for match around RA,Dec = (%g, %g) with radius %g deg' %
821 if pixelScale
is not None:
822 dscale = self.config.pixelScaleUncertainty
823 scale = pixelScale.asArcseconds()
826 solver.setPixelScaleRange(lo, hi)
828 'Searching for matches with pixel scale = %g +- %g %% -> range [%g, %g] arcsec/pix' %
829 (scale, 100.*(dscale-1.), lo, hi))
831 if parity
is not None:
832 solver.setParity(parity)
833 self.log.logdebug(
'Searching for match with parity = ' + str(parity))
836 if radecCenter
is not None:
837 multiInds = self.refObjLoader._getMIndexesWithinRange(radecCenter, searchRadius)
839 multiInds = self.refObjLoader.multiInds
840 qlo,qhi = solver.getQuadSizeLow(), solver.getQuadSizeHigh()
842 toload_multiInds = set()
845 for i
in range(len(mi)):
847 if not ind.overlapsScaleRange(qlo, qhi):
849 toload_multiInds.add(mi)
850 toload_inds.append(ind)
852 with LoadMultiIndexes(toload_multiInds):
853 solver.addIndices(toload_inds)
854 self.
memusage(
'Index files loaded: ')
856 cpulimit = self.config.maxCpuTime
861 self.
memusage(
'Index files unloaded: ')
863 if solver.didSolve():
864 self.log.logdebug(
'Solved!')
865 wcs = solver.getWcs()
866 self.log.logdebug(
'WCS: %s' % wcs.getFitsMetadata().toString())
868 if x0 != 0
or y0 != 0:
869 wcs.shiftReferencePixel(x0, y0)
870 self.log.logdebug(
'After shifting reference pixel by x0,y0 = (%i,%i), WCS is: %s' %
871 (x0, y0, wcs.getFitsMetadata().toString()))
874 self.log.warn(
'Did not get an astrometric solution from Astrometry.net')
880 if radecCenter
is not None:
881 self.refObjLoader.loadSkyCircle(radecCenter, searchRadius, filterName)
883 qa = solver.getSolveStats()
884 self.log.logdebug(
'qa: %s' % qa.toString())
889 if candsource.get(k):
895 """Remove elements from catalog whose xy positions are not within the given bbox.
897 sourceCat: a Catalog of SimpleRecord or SourceRecord objects
898 bbox: an afwImage.Box2D
899 wcs: if not None, will be used to compute the xy positions on-the-fly;
900 this is required when sources actually contains SimpleRecords.
903 a list of Source objects with xAstrom,yAstrom within the bbox.
905 keep = type(sourceCat)(sourceCat.table)
907 point = s.getCentroid()
if wcs
is None else wcs.skyToPixel(s.getCoord())
908 if bbox.contains(point):
914 This function is required to reconstitute a ReferenceMatchVector after being
915 unpersisted. The persisted form of a ReferenceMatchVector is the
916 normalized Catalog of IDs produced by afw.table.packMatches(), with the result of
917 InitialAstrometry.getMatchMetadata() in the associated tables\' metadata.
919 The "live" form of a matchlist has links to
920 the real record objects that are matched; it is "denormalized".
921 This function takes a normalized match catalog, along with the catalog of
922 sources to which the match catalog refers. It fetches the reference
923 sources that are within range, and then denormalizes the matches
924 -- sets the "matches[*].first" and "matches[*].second" entries
925 to point to the sources in the "sourceCat" argument, and to the
926 reference sources fetched from the astrometry_net_data files.
928 @param[in] packedMatches Unpersisted match list (an lsst.afw.table.BaseCatalog).
929 packedMatches.table.getMetadata() must contain the
930 values from InitialAstrometry.getMatchMetadata()
931 @param[in,out] sourceCat Source catalog used for the 'second' side of the matches
932 (an lsst.afw.table.SourceCatalog). As a side effect,
933 the catalog will be sorted by ID.
935 @return An lsst.afw.table.ReferenceMatchVector of denormalized matches.
937 matchmeta = packedMatches.table.getMetadata()
938 version = matchmeta.getInt(
'SMATCHV')
940 raise ValueError(
'SourceMatchVector version number is %i, not 1.' % version)
941 filterName = matchmeta.getString(
'FILTER').strip()
943 matchmeta.getDouble(
'RA') * afwGeom.degrees,
944 matchmeta.getDouble(
'DEC') * afwGeom.degrees,
946 rad = matchmeta.getDouble(
'RADIUS') * afwGeom.degrees
947 refCat = self.refObjLoader.loadSkyCircle(ctrCoord, rad, filterName).refCat
955 Create match metadata entries required for regenerating the catalog
957 @param bbox bounding box of image (pixels)
958 @param filterName Name of filter, used for magnitudes
964 cx, cy = bboxD.getCenter()
965 radec = wcs.pixelToSky(cx, cy).toIcrs()
966 meta.add(
'RA', radec.getRa().asDegrees(),
'field center in degrees')
967 meta.add(
'DEC', radec.getDec().asDegrees(),
'field center in degrees')
968 pixelRadius = math.hypot(*bboxD.getDimensions())/2.0
969 skyRadius = wcs.pixelScale() * pixelRadius
970 meta.add(
'RADIUS', skyRadius.asDegrees(),
971 'field radius in degrees, approximate')
972 meta.add(
'SMATCHV', 1,
'SourceMatchVector version number')
973 if filterName
is not None:
974 meta.add(
'FILTER', filterName,
'LSST filter name for tagalong data')
Basic implemeentation of the astrometry.net astrometrical fitter.
Class for storing ordered metadata with comments.
def getSipWcs
"Not very pythonic!" complains Paul.
Implementation of the WCS standard for a any projection.
def useKnownWcs
Return an InitialAstrometry object, just like determineWcs, but assuming the given input WCS is corre...
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Lightweight representation of a geometric match between two records.
def _calculateSipTerms
Iteratively calculate SIP distortions and regenerate matches based on improved WCS.
def getSipWcsFromCorrespondences
def getSipWcsFromWcs
Get a TAN-SIP WCS, starting from an existing WCS.
def joinMatchListWithCatalog
A floating-point coordinate rectangle geometry.
def __init__
Construct an ANetBasicAstrometryTask.
A class to handle Icrs coordinates (inherits from Coord)
std::vector< Match< typename Cat1::Record, typename Cat2::Record > > unpackMatches(BaseCatalog const &matches, Cat1 const &cat1, Cat2 const &cat2)
Reconstruct a MatchVector from a BaseCatalog representation of the matches and a pair of catalogs...