22 __all__ = [
"ObjectSizeStarSelectorConfig",
"ObjectSizeStarSelectorTask"]
28 from functools
import reduce
31 from lsst.pipe.base
import Struct
37 from .sourceSelector
import BaseSourceSelectorTask, sourceSelectorRegistry
39 afwDisplay.setDefaultMaskTransparency(75)
43 doFluxLimit = pexConfig.Field(
44 doc=
"Apply flux limit to Psf Candidate selection?",
48 fluxMin = pexConfig.Field(
49 doc=
"specify the minimum psfFlux for good Psf Candidates",
52 check=
lambda x: x >= 0.0,
54 fluxMax = pexConfig.Field(
55 doc=
"specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
58 check=
lambda x: x >= 0.0,
60 doSignalToNoiseLimit = pexConfig.Field(
61 doc=
"Apply signal-to-noise (i.e. flux/fluxErr) limit to Psf Candidate selection?",
65 signalToNoiseMin = pexConfig.Field(
66 doc=
"specify the minimum signal-to-noise for good Psf Candidates",
69 check=
lambda x: x >= 0.0,
71 signalToNoiseMax = pexConfig.Field(
72 doc=
"specify the maximum signal-to-noise for good Psf Candidates (ignored if == 0)",
75 check=
lambda x: x >= 0.0,
77 widthMin = pexConfig.Field(
78 doc=
"minimum width to include in histogram",
81 check=
lambda x: x >= 0.0,
83 widthMax = pexConfig.Field(
84 doc=
"maximum width to include in histogram",
87 check=
lambda x: x >= 0.0,
89 sourceFluxField = pexConfig.Field(
90 doc=
"Name of field in Source to use for flux measurement",
92 default=
"base_GaussianFlux_instFlux",
94 widthStdAllowed = pexConfig.Field(
95 doc=
"Standard deviation of width allowed to be interpreted as good stars",
98 check=
lambda x: x >= 0.0,
100 nSigmaClip = pexConfig.Field(
101 doc=
"Keep objects within this many sigma of cluster 0's median",
104 check=
lambda x: x >= 0.0,
106 badFlags = pexConfig.ListField(
107 doc=
"List of flags which cause a source to be rejected as bad",
110 "base_PixelFlags_flag_edge",
111 "base_PixelFlags_flag_interpolatedCenter",
112 "base_PixelFlags_flag_saturatedCenter",
113 "base_PixelFlags_flag_crCenter",
114 "base_PixelFlags_flag_bad",
115 "base_PixelFlags_flag_interpolated",
120 BaseSourceSelectorTask.ConfigClass.validate(self)
122 msg = f
"widthMin ({self.widthMin}) > widthMax ({self.widthMax})"
123 raise pexConfig.FieldValidationError(ObjectSizeStarSelectorConfig.widthMin, self, msg)
127 """A class to handle key strokes with matplotlib displays.
130 def __init__(self, axes, xs, ys, x, y, frames=[0]):
138 self.
cidcid = self.
axesaxes.figure.canvas.mpl_connect(
'key_press_event', self)
141 if ev.inaxes != self.
axesaxes:
144 if ev.key
and ev.key
in (
"p"):
145 dist = numpy.hypot(self.
xsxs - ev.xdata, self.
ysys - ev.ydata)
146 dist[numpy.where(numpy.isnan(dist))] = 1e30
148 which = numpy.where(dist == min(dist))
150 x = self.
xx[which][0]
151 y = self.
yy[which][0]
152 for frame
in self.
framesframes:
153 disp = afwDisplay.Display(frame=frame)
160 def _assignClusters(yvec, centers):
161 """Return a vector of centerIds based on their distance to the centers.
163 assert len(centers) > 0
165 minDist = numpy.nan*numpy.ones_like(yvec)
166 clusterId = numpy.empty_like(yvec)
167 clusterId.dtype = int
168 dbl = Log.getLogger(
"objectSizeStarSelector._assignClusters")
169 dbl.setLevel(dbl.INFO)
172 oldSettings = numpy.seterr(all=
"warn")
173 with warnings.catch_warnings(record=
True)
as w:
174 warnings.simplefilter(
"always")
175 for i, mean
in enumerate(centers):
176 dist = abs(yvec - mean)
178 update = dist == dist
180 update = dist < minDist
182 dbl.trace(str(w[-1]))
184 minDist[update] = dist[update]
185 clusterId[update] = i
186 numpy.seterr(**oldSettings)
191 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
192 """A classic k-means algorithm, clustering yvec into nCluster clusters
194 Return the set of centres, and the cluster ID for each of the points
196 If useMedian is true, use the median of the cluster as its centre, rather than
199 Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means:
200 "e.g. why not use the Forgy or random partition initialization methods"
201 however, the approach adopted here seems to work well for the particular sorts of things
202 we're clustering in this application
207 mean0 = sorted(yvec)[len(yvec)//10]
208 delta = mean0 * widthStdAllowed * 2.0
209 centers = mean0 + delta * numpy.arange(nCluster)
211 func = numpy.median
if useMedian
else numpy.mean
213 clusterId = numpy.zeros_like(yvec) - 1
214 clusterId.dtype = int
216 oclusterId = clusterId
217 clusterId = _assignClusters(yvec, centers)
219 if numpy.all(clusterId == oclusterId):
222 for i
in range(nCluster):
224 pointsInCluster = (clusterId == i)
225 if numpy.any(pointsInCluster):
226 centers[i] = func(yvec[pointsInCluster])
228 centers[i] = numpy.nan
230 return centers, clusterId
233 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
234 """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median.
237 nMember = sum(clusterId == clusterNum)
240 for iter
in range(nIteration):
241 old_nMember = nMember
243 inCluster0 = clusterId == clusterNum
244 yv = yvec[inCluster0]
246 centers[clusterNum] = numpy.median(yv)
247 stdev = numpy.std(yv)
250 stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
251 median = syv[int(0.5*nMember)]
253 sd = stdev
if stdev < stdev_iqr
else stdev_iqr
256 print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
257 newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
258 clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
259 clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
261 nMember = sum(clusterId == clusterNum)
263 if nMember == old_nMember
or sd < widthStdAllowed * median:
269 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
270 magType="model", clear=True):
272 log = Log.getLogger(
"objectSizeStarSelector.plot")
274 import matplotlib.pyplot
as plt
275 except ImportError
as e:
276 log.warning(
"Unable to import matplotlib: %s", e)
287 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
289 xmin = sorted(mag)[int(0.05*len(mag))]
290 xmax = sorted(mag)[int(0.95*len(mag))]
292 axes.set_xlim(-17.5, -13)
293 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
296 colors = [
"r",
"g",
"b",
"c",
"m",
"k", ]
297 for k, mean
in enumerate(centers):
299 axes.plot(axes.get_xlim(), (mean, mean,),
"k%s" % ltype)
301 li = (clusterId == k)
302 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
303 color=colors[k % len(colors)])
305 li = (clusterId == -1)
306 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
310 axes.set_xlabel(
"Instrumental %s mag" % magType)
311 axes.set_ylabel(
r"$\sqrt{(I_{xx} + I_{yy})/2}$")
316 @pexConfig.registerConfigurable("objectSize", sourceSelectorRegistry)
318 r"""A star selector that looks for a cluster of small objects in a size-magnitude plot.
320 ConfigClass = ObjectSizeStarSelectorConfig
324 """Return a selection of PSF candidates that represent likely stars.
326 A list of PSF candidates may be used by a PSF fitter to construct a PSF.
330 sourceCat : `lsst.afw.table.SourceCatalog`
331 Catalog of sources to select from.
332 This catalog must be contiguous in memory.
333 matches : `list` of `lsst.afw.table.ReferenceMatch` or None
334 Ignored in this SourceSelector.
335 exposure : `lsst.afw.image.Exposure` or None
336 The exposure the catalog was built from; used to get the detector
337 to transform to TanPix, and for debug display.
341 struct : `lsst.pipe.base.Struct`
342 The struct contains the following data:
344 - selected : `array` of `bool``
345 Boolean array of sources that were selected, same length as
357 detector = exposure.getDetector()
359 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
363 flux = sourceCat.get(self.config.sourceFluxField)
364 fluxErr = sourceCat.get(self.config.sourceFluxField +
"Err")
366 xx = numpy.empty(len(sourceCat))
367 xy = numpy.empty_like(xx)
368 yy = numpy.empty_like(xx)
369 for i, source
in enumerate(sourceCat):
370 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
373 linTransform = afwGeom.linearizeTransform(pixToTanPix, p).getLinear()
374 m = afwGeom.Quadrupole(Ixx, Iyy, Ixy)
375 m.transform(linTransform)
376 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
378 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
380 width = numpy.sqrt(0.5*(xx + yy))
381 with numpy.errstate(invalid=
"ignore"):
382 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
383 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
384 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
385 if self.config.doFluxLimit:
386 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
387 if self.config.fluxMax > 0:
388 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
389 if self.config.doSignalToNoiseLimit:
390 bad = numpy.logical_or(bad, flux/fluxErr < self.config.signalToNoiseMin)
391 if self.config.signalToNoiseMax > 0:
392 bad = numpy.logical_or(bad, flux/fluxErr > self.config.signalToNoiseMax)
393 bad = numpy.logical_or(bad, width < self.config.widthMin)
394 bad = numpy.logical_or(bad, width > self.config.widthMax)
395 good = numpy.logical_not(bad)
397 if not numpy.any(good):
398 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
400 mag = -2.5*numpy.log10(flux[good])
408 import pickle
as pickle
411 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
412 if not os.path.exists(pickleFile):
416 with open(pickleFile,
"wb")
as fd:
417 pickle.dump(mag, fd, -1)
418 pickle.dump(width, fd, -1)
420 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
421 widthStdAllowed=self.config.widthStdAllowed)
423 if display
and plotMagSize:
424 fig =
plot(mag, width, centers, clusterId,
425 magType=self.config.sourceFluxField.split(
".")[-1].title(),
426 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
430 clusterId = _improveCluster(width, centers, clusterId,
431 nsigma=self.config.nSigmaClip,
432 widthStdAllowed=self.config.widthStdAllowed)
434 if display
and plotMagSize:
435 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
437 stellar = (clusterId == 0)
444 if display
and displayExposure:
445 disp = afwDisplay.Display(frame=frame)
446 disp.mtv(exposure.getMaskedImage(), title=
"PSF candidates")
449 eventHandler =
EventHandler(fig.get_axes()[0], mag, width,
450 sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
456 reply = input(
"continue? [c h(elp) q(uit) p(db)] ").strip()
465 We cluster the points; red are the stellar candidates and the other colours are other clusters.
466 Points labelled + are rejects from the cluster (only for cluster 0).
468 At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text
470 If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in the
473 elif reply[0] ==
"p":
476 elif reply[0] ==
'q':
481 if display
and displayExposure:
482 mi = exposure.getMaskedImage()
483 with disp.Buffering():
484 for i, source
in enumerate(sourceCat):
486 ctype = afwDisplay.GREEN
488 ctype = afwDisplay.RED
490 disp.dot(
"+", source.getX() - mi.getX0(), source.getY() - mi.getY0(), ctype=ctype)
496 return Struct(selected=good)
def __init__(self, axes, xs, ys, x, y, frames=[0])
def selectSources(self, sourceCat, matches=None, exposure=None)
def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-', magType="model", clear=True)