22 __all__ = [
"ObjectSizeStarSelectorConfig",
"ObjectSizeStarSelectorTask"]
28 from functools
import reduce
30 from lsst.utils.logging
import getLogger
37 from .sourceSelector
import BaseSourceSelectorTask, sourceSelectorRegistry
39 afwDisplay.setDefaultMaskTransparency(75)
45 doFluxLimit = pexConfig.Field(
46 doc=
"Apply flux limit to Psf Candidate selection?",
50 fluxMin = pexConfig.Field(
51 doc=
"specify the minimum psfFlux for good Psf Candidates",
54 check=
lambda x: x >= 0.0,
56 fluxMax = pexConfig.Field(
57 doc=
"specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
60 check=
lambda x: x >= 0.0,
62 doSignalToNoiseLimit = pexConfig.Field(
63 doc=
"Apply signal-to-noise (i.e. flux/fluxErr) limit to Psf Candidate selection?",
70 signalToNoiseMin = pexConfig.Field(
71 doc=
"specify the minimum signal-to-noise for good Psf Candidates "
72 "(value should take into consideration the detection thresholds "
73 "set for the catalog of interest)",
76 check=
lambda x: x >= 0.0,
78 signalToNoiseMax = pexConfig.Field(
79 doc=
"specify the maximum signal-to-noise for good Psf Candidates (ignored if == 0)",
82 check=
lambda x: x >= 0.0,
84 widthMin = pexConfig.Field(
85 doc=
"minimum width to include in histogram",
88 check=
lambda x: x >= 0.0,
90 widthMax = pexConfig.Field(
91 doc=
"maximum width to include in histogram",
94 check=
lambda x: x >= 0.0,
96 sourceFluxField = pexConfig.Field(
97 doc=
"Name of field in Source to use for flux measurement",
99 default=
"base_GaussianFlux_instFlux",
101 widthStdAllowed = pexConfig.Field(
102 doc=
"Standard deviation of width allowed to be interpreted as good stars",
105 check=
lambda x: x >= 0.0,
107 nSigmaClip = pexConfig.Field(
108 doc=
"Keep objects within this many sigma of cluster 0's median",
111 check=
lambda x: x >= 0.0,
113 badFlags = pexConfig.ListField(
114 doc=
"List of flags which cause a source to be rejected as bad",
117 "base_PixelFlags_flag_edge",
118 "base_PixelFlags_flag_interpolatedCenter",
119 "base_PixelFlags_flag_saturatedCenter",
120 "base_PixelFlags_flag_crCenter",
121 "base_PixelFlags_flag_bad",
122 "base_PixelFlags_flag_interpolated",
127 BaseSourceSelectorTask.ConfigClass.validate(self)
129 msg = f
"widthMin ({self.widthMin}) > widthMax ({self.widthMax})"
130 raise pexConfig.FieldValidationError(ObjectSizeStarSelectorConfig.widthMin, self, msg)
134 """A class to handle key strokes with matplotlib displays.
137 def __init__(self, axes, xs, ys, x, y, frames=[0]):
145 self.
cidcid = self.
axesaxes.figure.canvas.mpl_connect(
'key_press_event', self)
148 if ev.inaxes != self.
axesaxes:
151 if ev.key
and ev.key
in (
"p"):
152 dist = numpy.hypot(self.
xsxs - ev.xdata, self.
ysys - ev.ydata)
153 dist[numpy.where(numpy.isnan(dist))] = 1e30
155 which = numpy.where(dist ==
min(dist))
157 x = self.
xx[which][0]
158 y = self.
yy[which][0]
159 for frame
in self.
framesframes:
160 disp = afwDisplay.Display(frame=frame)
167 def _assignClusters(yvec, centers):
168 """Return a vector of centerIds based on their distance to the centers.
170 assert len(centers) > 0
172 minDist = numpy.nan*numpy.ones_like(yvec)
173 clusterId = numpy.empty_like(yvec)
174 clusterId.dtype = int
175 dbl = _LOG.getChild(
"_assignClusters")
176 dbl.setLevel(dbl.INFO)
179 oldSettings = numpy.seterr(all=
"warn")
180 with warnings.catch_warnings(record=
True)
as w:
181 warnings.simplefilter(
"always")
182 for i, mean
in enumerate(centers):
183 dist =
abs(yvec - mean)
185 update = dist == dist
187 update = dist < minDist
189 dbl.trace(str(w[-1]))
191 minDist[update] = dist[update]
192 clusterId[update] = i
193 numpy.seterr(**oldSettings)
198 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
199 """A classic k-means algorithm, clustering yvec into nCluster clusters
201 Return the set of centres, and the cluster ID for each of the points
203 If useMedian is true, use the median of the cluster as its centre, rather than
206 Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means:
207 "e.g. why not use the Forgy or random partition initialization methods"
208 however, the approach adopted here seems to work well for the particular sorts of things
209 we're clustering in this application
214 mean0 = sorted(yvec)[len(yvec)//10]
215 delta = mean0 * widthStdAllowed * 2.0
216 centers = mean0 + delta * numpy.arange(nCluster)
218 func = numpy.median
if useMedian
else numpy.mean
220 clusterId = numpy.zeros_like(yvec) - 1
221 clusterId.dtype = int
223 oclusterId = clusterId
224 clusterId = _assignClusters(yvec, centers)
226 if numpy.all(clusterId == oclusterId):
229 for i
in range(nCluster):
231 pointsInCluster = (clusterId == i)
232 if numpy.any(pointsInCluster):
233 centers[i] = func(yvec[pointsInCluster])
235 centers[i] = numpy.nan
237 return centers, clusterId
240 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
241 """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median.
244 nMember = sum(clusterId == clusterNum)
247 for iter
in range(nIteration):
248 old_nMember = nMember
250 inCluster0 = clusterId == clusterNum
251 yv = yvec[inCluster0]
253 centers[clusterNum] = numpy.median(yv)
254 stdev = numpy.std(yv)
257 stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
258 median = syv[int(0.5*nMember)]
260 sd = stdev
if stdev < stdev_iqr
else stdev_iqr
263 print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
264 newCluster0 =
abs(yvec - centers[clusterNum]) < nsigma*sd
265 clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
266 clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
268 nMember = sum(clusterId == clusterNum)
270 if nMember == old_nMember
or sd < widthStdAllowed * median:
276 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
277 magType="model", clear=True):
279 log = _LOG.getChild(
"plot")
281 import matplotlib.pyplot
as plt
282 except ImportError
as e:
283 log.warning(
"Unable to import matplotlib: %s", e)
294 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
296 xmin = sorted(mag)[int(0.05*len(mag))]
297 xmax = sorted(mag)[int(0.95*len(mag))]
299 axes.set_xlim(-17.5, -13)
300 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
303 colors = [
"r",
"g",
"b",
"c",
"m",
"k", ]
304 for k, mean
in enumerate(centers):
306 axes.plot(axes.get_xlim(), (mean, mean,),
"k%s" % ltype)
308 li = (clusterId == k)
309 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
310 color=colors[k % len(colors)])
312 li = (clusterId == -1)
313 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
317 axes.set_xlabel(
"Instrumental %s mag" % magType)
318 axes.set_ylabel(
r"$\sqrt{(I_{xx} + I_{yy})/2}$")
323 @pexConfig.registerConfigurable("objectSize", sourceSelectorRegistry)
325 r"""A star selector that looks for a cluster of small objects in a size-magnitude plot.
327 ConfigClass = ObjectSizeStarSelectorConfig
331 """Return a selection of PSF candidates that represent likely stars.
333 A list of PSF candidates may be used by a PSF fitter to construct a PSF.
337 sourceCat : `lsst.afw.table.SourceCatalog`
338 Catalog of sources to select from.
339 This catalog must be contiguous in memory.
340 matches : `list` of `lsst.afw.table.ReferenceMatch` or None
341 Ignored in this SourceSelector.
342 exposure : `lsst.afw.image.Exposure` or None
343 The exposure the catalog was built from; used to get the detector
344 to transform to TanPix, and for debug display.
348 struct : `lsst.pipe.base.Struct`
349 The struct contains the following data:
351 - selected : `array` of `bool``
352 Boolean array of sources that were selected, same length as
364 detector = exposure.getDetector()
366 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
370 flux = sourceCat.get(self.config.sourceFluxField)
371 fluxErr = sourceCat.get(self.config.sourceFluxField +
"Err")
373 xx = numpy.empty(len(sourceCat))
374 xy = numpy.empty_like(xx)
375 yy = numpy.empty_like(xx)
376 for i, source
in enumerate(sourceCat):
377 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
382 m.transform(linTransform)
383 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
385 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
387 width = numpy.sqrt(0.5*(xx + yy))
388 with numpy.errstate(invalid=
"ignore"):
389 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
390 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
391 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
392 if self.config.doFluxLimit:
393 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
394 if self.config.fluxMax > 0:
395 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
396 if self.config.doSignalToNoiseLimit:
397 bad = numpy.logical_or(bad, flux/fluxErr < self.config.signalToNoiseMin)
398 if self.config.signalToNoiseMax > 0:
399 bad = numpy.logical_or(bad, flux/fluxErr > self.config.signalToNoiseMax)
400 bad = numpy.logical_or(bad, width < self.config.widthMin)
401 bad = numpy.logical_or(bad, width > self.config.widthMax)
402 good = numpy.logical_not(bad)
404 if not numpy.any(good):
405 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
407 mag = -2.5*numpy.log10(flux[good])
415 import pickle
as pickle
418 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
419 if not os.path.exists(pickleFile):
423 with open(pickleFile,
"wb")
as fd:
424 pickle.dump(mag, fd, -1)
425 pickle.dump(width, fd, -1)
427 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
428 widthStdAllowed=self.config.widthStdAllowed)
430 if display
and plotMagSize:
431 fig =
plot(mag, width, centers, clusterId,
432 magType=self.config.sourceFluxField.split(
".")[-1].title(),
433 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
437 clusterId = _improveCluster(width, centers, clusterId,
438 nsigma=self.config.nSigmaClip,
439 widthStdAllowed=self.config.widthStdAllowed)
441 if display
and plotMagSize:
442 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
444 stellar = (clusterId == 0)
451 if display
and displayExposure:
452 disp = afwDisplay.Display(frame=frame)
453 disp.mtv(exposure.getMaskedImage(), title=
"PSF candidates")
456 eventHandler =
EventHandler(fig.get_axes()[0], mag, width,
457 sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
463 reply = input(
"continue? [c h(elp) q(uit) p(db)] ").
strip()
472 We cluster the points; red are the stellar candidates and the other colours are other clusters.
473 Points labelled + are rejects from the cluster (only for cluster 0).
475 At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text
477 If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in the
480 elif reply[0] ==
"p":
483 elif reply[0] ==
'q':
488 if display
and displayExposure:
489 mi = exposure.getMaskedImage()
490 with disp.Buffering():
491 for i, source
in enumerate(sourceCat):
493 ctype = afwDisplay.GREEN
495 ctype = afwDisplay.RED
497 disp.dot(
"+", source.getX() - mi.getX0(), source.getY() - mi.getY0(), ctype=ctype)
503 return Struct(selected=good)
An ellipse core with quadrupole moments as parameters.
def __init__(self, axes, xs, ys, x, y, frames=[0])
def selectSources(self, sourceCat, matches=None, exposure=None)
lsst::geom::AffineTransform linearizeTransform(TransformPoint2ToPoint2 const &original, lsst::geom::Point2D const &inPoint)
Approximate a Transform by its local linearization.
def getLogger(loggername)
def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-', magType="model", clear=True)
Angle abs(Angle const &a)