23 from builtins
import range
24 from builtins
import object
30 from lsst.afw.cameraGeom
import PIXELS, TAN_PIXELS
31 from lsst.afw.geom.ellipses
import Quadrupole
32 from lsst.afw.table
import SourceCatalog, SourceTable
33 from lsst.pipe.base
import Struct
34 import lsst.pex.config
as pexConfig
36 import lsst.afw.display.ds9
as ds9
38 import lsst.afw.math
as afwMath
40 from .psfCandidate
import makePsfCandidate
41 from .doubleGaussianPsf
import DoubleGaussianPsf
42 from lsst.meas.base
import SingleFrameMeasurementTask, SingleFrameMeasurementConfig
43 from .starSelector
import BaseStarSelectorTask, starSelectorRegistry
47 fluxLim = pexConfig.Field(
48 doc=
"specify the minimum psfFlux for good Psf Candidates",
51 check=
lambda x: x >= 0.0,
53 fluxMax = pexConfig.Field(
54 doc=
"specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
57 check=
lambda x: x >= 0.0,
59 clumpNSigma = pexConfig.Field(
60 doc=
"candidate PSF's shapes must lie within this many sigma of the average shape",
63 check=
lambda x: x >= 0.0,
65 histSize = pexConfig.Field(
66 doc=
"Number of bins in moment histogram",
69 check=
lambda x: x > 0,
71 histMomentMax = pexConfig.Field(
72 doc=
"Maximum moment to consider",
75 check=
lambda x: x > 0,
77 histMomentMaxMultiplier = pexConfig.Field(
78 doc=
"Multiplier of mean for maximum moments histogram range",
81 check=
lambda x: x > 0,
83 histMomentClip = pexConfig.Field(
84 doc=
"Clipping threshold for moments histogram range",
87 check=
lambda x: x > 0,
89 histMomentMinMultiplier = pexConfig.Field(
90 doc=
"Multiplier of mean for minimum moments histogram range",
93 check=
lambda x: x > 0,
97 BaseStarSelectorTask.ConfigClass.setDefaults(self)
99 "base_PixelFlags_flag_edge",
100 "base_PixelFlags_flag_interpolatedCenter",
101 "base_PixelFlags_flag_saturatedCenter",
102 "base_PixelFlags_flag_crCenter",
106 Clump = collections.namedtuple(
'Clump', [
'peak',
'x',
'y',
'ixx',
'ixy',
'iyy',
'a',
'b',
'c'])
111 """A functor to check whether a source has any flags set that should cause it to be labeled bad."""
113 def __init__(self, table, badFlags, fluxLim, fluxMax):
114 self.
keys = [table.getSchema().find(name).key
for name
in badFlags]
115 self.keys.append(table.getCentroidFlagKey())
123 if self.
fluxLim is not None and source.getPsfFlux() < self.
fluxLim:
138 """!A star selector based on second moments
140 @anchor SecondMomentStarSelectorTask_
142 @section meas_algorithms_secondMomentStarSelector_Contents Contents
144 - @ref meas_algorithms_secondMomentStarSelector_Purpose
145 - @ref meas_algorithms_secondMomentStarSelector_Initialize
146 - @ref meas_algorithms_secondMomentStarSelector_IO
147 - @ref meas_algorithms_secondMomentStarSelector_Config
148 - @ref meas_algorithms_secondMomentStarSelector_Debug
150 @section meas_algorithms_secondMomentStarSelector_Purpose Description
152 A star selector based on second moments.
154 @warning This is a naive algorithm; use with caution.
156 @section meas_algorithms_secondMomentStarSelector_Initialize Task initialisation
158 @copydoc \_\_init\_\_
160 @section meas_algorithms_secondMomentStarSelector_IO Invoking the Task
162 Like all star selectors, the main method is `run`.
164 @section meas_algorithms_secondMomentStarSelector_Config Configuration parameters
166 See @ref SecondMomentStarSelectorConfig
168 @section meas_algorithms_secondMomentStarSelector_Debug Debug variables
170 SecondMomentStarSelectorTask has a debug dictionary with the following keys:
173 <dd>bool; if True display debug information
175 display = lsstDebug.Info(__name__).display
176 displayExposure = lsstDebug.Info(__name__).displayExposure
177 pauseAtEnd = lsstDebug.Info(__name__).pauseAtEnd
179 For example, put something like:
183 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
184 if name.endswith("catalogStarSelector"):
189 lsstDebug.Info = DebugInfo
191 into your `debug.py` file and run your task with the `--debug` flag.
193 ConfigClass = SecondMomentStarSelectorConfig
197 """!Return a list of PSF candidates that represent likely stars
199 A list of PSF candidates may be used by a PSF fitter to construct a PSF.
201 @param[in] exposure the exposure containing the sources
202 @param[in] sourceCat catalog of sources that may be stars (an lsst.afw.table.SourceCatalog)
203 @param[in] matches astrometric matches; ignored by this star selector
205 @return an lsst.pipe.base.Struct containing:
206 - starCat catalog of selected stars (a subset of sourceCat)
209 display = lsstDebug.Info(__name__).display
211 isGoodSource =
CheckSource(sourceCat.getTable(), self.config.badFlags, self.config.fluxLim,
214 detector = exposure.getDetector()
216 mi = exposure.getMaskedImage()
224 ixx, iyy = s.getIxx(), s.getIyy()
226 if (ixx == ixx
and ixx < self.config.histMomentMax
and
227 iyy == iyy
and iyy < self.config.histMomentMax
and
229 iqqList.append(s.getIxx())
230 iqqList.append(s.getIyy())
231 stat = afwMath.makeStatistics(iqqList, afwMath.MEANCLIP | afwMath.STDEVCLIP | afwMath.MAX)
232 iqqMean = stat.getValue(afwMath.MEANCLIP)
233 iqqStd = stat.getValue(afwMath.STDEVCLIP)
234 iqqMax = stat.getValue(afwMath.MAX)
236 iqqLimit = max(iqqMean + self.config.histMomentClip*iqqStd,
237 self.config.histMomentMaxMultiplier*iqqMean)
239 if iqqLimit > iqqMax:
240 iqqLimit = max(self.config.histMomentMinMultiplier*iqqMean, iqqMax)
243 xSize=self.config.histSize, ySize=self.config.histSize,
244 ixxMax=iqqLimit, iyyMax=iqqLimit)
248 ds9.mtv(mi, frame=frame, title=
"PSF candidates")
251 for source
in sourceCat:
252 good = isGoodSource(source)
254 notRejected = psfHist.insert(source)
258 ctypes.append(ds9.GREEN)
260 ctypes.append(ds9.MAGENTA)
262 ctypes.append(ds9.RED)
265 with ds9.Buffering():
266 for source, ctype
in zip(sourceCat, ctypes):
267 ds9.dot(
"o", source.getX() - mi.getX0(), source.getY() - mi.getY0(),
268 frame=frame, ctype=ctype)
270 clumps = psfHist.getClumps(display=display)
278 starCat = SourceCatalog(sourceCat.table)
281 if detector
is not None:
282 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
287 for source
in sourceCat:
288 if not isGoodSource(source):
290 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
292 p = afwGeom.Point2D(source.getX(), source.getY())
293 linTransform = afwGeom.linearizeTransform(pixToTanPix, p).getLinear()
294 m = Quadrupole(Ixx, Iyy, Ixy)
295 m.transform(linTransform)
296 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
298 x, y = psfHist.momentsToPixel(Ixx, Iyy)
300 dx, dy = (x - clump.x), (y - clump.y)
302 if math.sqrt(clump.a*dx*dx + 2*clump.b*dx*dy + clump.c*dy*dy) < 2*self.config.clumpNSigma:
304 if not isGoodSource(source):
312 if psfCandidate.getWidth() == 0:
313 psfCandidate.setBorderWidth(self.config.borderWidth)
314 psfCandidate.setWidth(self.config.kernelSize + 2*self.config.borderWidth)
315 psfCandidate.setHeight(self.config.kernelSize + 2*self.config.borderWidth)
317 im = psfCandidate.getMaskedImage().getImage()
318 if not numpy.isfinite(afwMath.makeStatistics(im, afwMath.MAX).getValue()):
320 starCat.append(source)
323 ds9.dot(
"o", source.getX() - mi.getX0(), source.getY() - mi.getY0(),
324 size=4, frame=frame, ctype=ds9.CYAN)
325 except Exception
as err:
326 self.log.error(
"Failed on source %s: %s" % (source.getId(), err))
336 """A class to represent a histogram of (Ixx, Iyy)
339 def __init__(self, xSize=32, ySize=32, ixxMax=30, iyyMax=30, detector=None, xy0=afwGeom.Point2D(0, 0)):
340 """Construct a _PsfShapeHistogram
342 The maximum seeing FWHM that can be tolerated is [xy]Max/2.35 pixels.
343 The 'resolution' of stars vs galaxies/CRs is provided by [xy]Size/[xy]Max.
344 A larger (better) resolution may thresh the peaks, but a smaller (worse)
345 resolution will allow stars and galaxies/CRs to mix. The disadvantages of
346 a larger (better) resolution can be compensated (some) by using multiple
349 @input[in] [xy]Size: the size of the psfImage (in pixels)
350 @input[in] ixxMax, iyyMax: the maximum values for I[xy][xy]
352 self._xSize, self.
_ySize = xSize, ySize
353 self._xMax, self.
_yMax = ixxMax, iyyMax
354 self.
_psfImage = afwImage.ImageF(afwGeom.ExtentI(xSize, ySize), 0)
363 """Insert source into the histogram."""
365 ixx, iyy, ixy = source.getIxx(), source.getIyy(), source.getIxy()
367 tanSys = self.detector.makeCameraSys(TAN_PIXELS)
368 if tanSys
in self.detector.getTransformMap():
369 pixToTanPix = self.detector.getTransform(PIXELS, TAN_PIXELS)
370 p = afwGeom.Point2D(source.getX(), source.getY())
371 linTransform = afwGeom.linearizeTransform(pixToTanPix, p).getLinear()
372 m = Quadrupole(ixx, iyy, ixy)
373 m.transform(linTransform)
374 ixx, iyy, ixy = m.getIxx(), m.getIyy(), m.getIxy()
383 if i
in range(0, self._xSize)
and j
in range(0, self.
_ySize):
385 self._psfImage.set(i, j, self._psfImage.get(i, j) + 1)
394 x = ixx * self._xSize / self._xMax
399 """Given a peak position in self._psfImage, return the corresponding (Ixx, Iyy)"""
403 ixx = x*self._xMax/self._xSize
409 raise RuntimeError(
"No candidate PSF sources")
415 width, height = psfImage.getWidth(), psfImage.getHeight()
416 largeImg = psfImage.Factory(afwGeom.ExtentI(2*width, 2*height))
419 bbox = afwGeom.BoxI(afwGeom.PointI(width, height), afwGeom.ExtentI(width, height))
420 largeImg.assign(psfImage, bbox, afwImage.LOCAL)
424 msk = afwImage.Mask(largeImg.getDimensions())
426 var = afwImage.ImageF(largeImg.getDimensions())
428 mpsfImage = afwImage.MaskedImageF(largeImg, msk, var)
429 mpsfImage.setXY0(afwGeom.PointI(-width, -height))
432 exposure = afwImage.makeExposure(mpsfImage)
437 maxVal = afwMath.makeStatistics(psfImage, afwMath.MAX).getValue()
438 threshold = maxVal - sigma*math.sqrt(maxVal)
442 threshold = afwDetection.Threshold(threshold)
444 ds = afwDetection.FootprintSet(mpsfImage, threshold,
"DETECTED")
449 schema = SourceTable.makeMinimalSchema()
450 psfImageConfig = SingleFrameMeasurementConfig()
451 psfImageConfig.slots.centroid =
"base_SdssCentroid"
452 psfImageConfig.plugins[
"base_SdssCentroid"].doFootprintCheck =
False
453 psfImageConfig.slots.psfFlux =
None
454 psfImageConfig.slots.apFlux =
"base_CircularApertureFlux_3_0"
455 psfImageConfig.slots.modelFlux =
None
456 psfImageConfig.slots.instFlux =
None
457 psfImageConfig.slots.calibFlux =
None
458 psfImageConfig.slots.shape =
"base_SdssShape"
461 psfImageConfig.algorithms.names = [
"base_SdssCentroid",
"base_CircularApertureFlux",
"base_SdssShape"]
462 psfImageConfig.algorithms[
"base_CircularApertureFlux"].radii = [3.0]
463 psfImageConfig.validate()
464 task = SingleFrameMeasurementTask(schema, config=psfImageConfig)
466 sourceCat = SourceCatalog(schema)
469 exposure.setPsf(DoubleGaussianPsf(11, 11, gaussianWidth))
471 ds.makeSources(sourceCat)
477 dispImage = mpsfImage.Factory(mpsfImage, afwGeom.BoxI(afwGeom.PointI(width, height),
478 afwGeom.ExtentI(width, height)),
480 ds9.mtv(dispImage, title=
"PSF Selection Image", frame=frame)
485 IzzMax = (self._xSize/8.0)**2
487 task.run(sourceCat, exposure)
488 for i, source
in enumerate(sourceCat):
489 if source.getCentroidFlag():
491 x, y = source.getX(), source.getY()
493 apFluxes.append(source.getApFlux())
495 val = mpsfImage.getImage().get(int(x) + width, int(y) + height)
497 psfClumpIxx = source.getIxx()
498 psfClumpIxy = source.getIxy()
499 psfClumpIyy = source.getIyy()
503 ds9.pan(x, y, frame=frame)
505 ds9.dot(
"+", x, y, ctype=ds9.YELLOW, frame=frame)
506 ds9.dot(
"@:%g,%g,%g" % (psfClumpIxx, psfClumpIxy, psfClumpIyy), x, y,
507 ctype=ds9.YELLOW, frame=frame)
509 if psfClumpIxx < IzzMin
or psfClumpIyy < IzzMin:
510 psfClumpIxx = max(psfClumpIxx, IzzMin)
511 psfClumpIyy = max(psfClumpIyy, IzzMin)
513 ds9.dot(
"@:%g,%g,%g" % (psfClumpIxx, psfClumpIxy, psfClumpIyy), x, y,
514 ctype=ds9.RED, frame=frame)
516 det = psfClumpIxx*psfClumpIyy - psfClumpIxy*psfClumpIxy
518 a, b, c = psfClumpIyy/det, -psfClumpIxy/det, psfClumpIxx/det
519 except ZeroDivisionError:
520 a, b, c = 1e4, 0, 1e4
522 clumps.append(
Clump(peak=val, x=x, y=y, a=a, b=b, c=c,
523 ixx=psfClumpIxx, ixy=psfClumpIxy, iyy=psfClumpIyy))
526 msg =
"Failed to determine center of PSF clump"
529 raise RuntimeError(msg)
543 if clump.ixx < IzzMax
and clump.iyy < IzzMax:
544 goodClumps.append(clump)
547 if len(goodClumps) == 0:
551 iBestClump = numpy.argsort(apFluxes)[0]
552 clumps = [clumps[iBestClump]]
556 starSelectorRegistry.register(
"secondMoment", SecondMomentStarSelectorTask)
def selectStars
Return a list of PSF candidates that represent likely stars.
A star selector based on second moments.
std::shared_ptr< PsfCandidate< PixelT > > makePsfCandidate(boost::shared_ptr< afw::table::SourceRecord > const &source, boost::shared_ptr< afw::image::Exposure< PixelT > > image)
Return a PsfCandidate of the right sort.