22__all__ = [
"ObjectSizeStarSelectorConfig",
"ObjectSizeStarSelectorTask"]
28from functools
import reduce
31from lsst.pipe.base
import Struct
37from .sourceSelector
import BaseSourceSelectorTask, sourceSelectorRegistry
39afwDisplay.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?",
68 signalToNoiseMin = pexConfig.Field(
69 doc=
"specify the minimum signal-to-noise for good Psf Candidates "
70 "(value should take into consideration the detection thresholds "
71 "set for the catalog of interest)",
74 check=
lambda x: x >= 0.0,
76 signalToNoiseMax = pexConfig.Field(
77 doc=
"specify the maximum signal-to-noise for good Psf Candidates (ignored if == 0)",
80 check=
lambda x: x >= 0.0,
82 widthMin = pexConfig.Field(
83 doc=
"minimum width to include in histogram",
86 check=
lambda x: x >= 0.0,
88 widthMax = pexConfig.Field(
89 doc=
"maximum width to include in histogram",
92 check=
lambda x: x >= 0.0,
94 sourceFluxField = pexConfig.Field(
95 doc=
"Name of field in Source to use for flux measurement",
97 default=
"base_GaussianFlux_instFlux",
99 widthStdAllowed = pexConfig.Field(
100 doc=
"Standard deviation of width allowed to be interpreted as good stars",
103 check=
lambda x: x >= 0.0,
105 nSigmaClip = pexConfig.Field(
106 doc=
"Keep objects within this many sigma of cluster 0's median",
109 check=
lambda x: x >= 0.0,
111 badFlags = pexConfig.ListField(
112 doc=
"List of flags which cause a source to be rejected as bad",
115 "base_PixelFlags_flag_edge",
116 "base_PixelFlags_flag_interpolatedCenter",
117 "base_PixelFlags_flag_saturatedCenter",
118 "base_PixelFlags_flag_crCenter",
119 "base_PixelFlags_flag_bad",
120 "base_PixelFlags_flag_interpolated",
125 BaseSourceSelectorTask.ConfigClass.validate(self)
127 msg = f
"widthMin ({self.widthMin}) > widthMax ({self.widthMax})"
128 raise pexConfig.FieldValidationError(ObjectSizeStarSelectorConfig.widthMin, self, msg)
132 """A class to handle key strokes with matplotlib displays.
135 def __init__(self, axes, xs, ys, x, y, frames=[0]):
143 self.
cidcid = self.
axesaxes.figure.canvas.mpl_connect(
'key_press_event', self)
146 if ev.inaxes != self.
axesaxes:
149 if ev.key
and ev.key
in (
"p"):
150 dist = numpy.hypot(self.
xsxs - ev.xdata, self.
ysys - ev.ydata)
151 dist[numpy.where(numpy.isnan(dist))] = 1e30
153 which = numpy.where(dist == min(dist))
155 x = self.
xx[which][0]
156 y = self.
yy[which][0]
157 for frame
in self.
framesframes:
158 disp = afwDisplay.Display(frame=frame)
165def _assignClusters(yvec, centers):
166 """Return a vector of centerIds based on their distance to the centers.
168 assert len(centers) > 0
170 minDist = numpy.nan*numpy.ones_like(yvec)
171 clusterId = numpy.empty_like(yvec)
172 clusterId.dtype = int
173 dbl = Log.getLogger(
"objectSizeStarSelector._assignClusters")
174 dbl.setLevel(dbl.INFO)
177 oldSettings = numpy.seterr(all=
"warn")
178 with warnings.catch_warnings(record=
True)
as w:
179 warnings.simplefilter(
"always")
180 for i, mean
in enumerate(centers):
181 dist = abs(yvec - mean)
183 update = dist == dist
185 update = dist < minDist
187 dbl.trace(str(w[-1]))
189 minDist[update] = dist[update]
190 clusterId[update] = i
191 numpy.seterr(**oldSettings)
196def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
197 """A classic k-means algorithm, clustering yvec into nCluster clusters
199 Return the set of centres, and the cluster ID
for each of the points
201 If useMedian
is true, use the median of the cluster
as its centre, rather than
204 Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means:
205 "e.g. why not use the Forgy or random partition initialization methods"
206 however, the approach adopted here seems to work well
for the particular sorts of things
207 we
're clustering in this application
212 mean0 = sorted(yvec)[len(yvec)//10]
213 delta = mean0 * widthStdAllowed * 2.0
214 centers = mean0 + delta * numpy.arange(nCluster)
216 func = numpy.median
if useMedian
else numpy.mean
218 clusterId = numpy.zeros_like(yvec) - 1
219 clusterId.dtype = int
221 oclusterId = clusterId
222 clusterId = _assignClusters(yvec, centers)
224 if numpy.all(clusterId == oclusterId):
227 for i
in range(nCluster):
229 pointsInCluster = (clusterId == i)
230 if numpy.any(pointsInCluster):
231 centers[i] = func(yvec[pointsInCluster])
233 centers[i] = numpy.nan
235 return centers, clusterId
238def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
239 """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median.
242 nMember = sum(clusterId == clusterNum)
245 for iter
in range(nIteration):
246 old_nMember = nMember
248 inCluster0 = clusterId == clusterNum
249 yv = yvec[inCluster0]
251 centers[clusterNum] = numpy.median(yv)
252 stdev = numpy.std(yv)
255 stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
256 median = syv[int(0.5*nMember)]
258 sd = stdev
if stdev < stdev_iqr
else stdev_iqr
261 print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
262 newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
263 clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
264 clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
266 nMember = sum(clusterId == clusterNum)
268 if nMember == old_nMember
or sd < widthStdAllowed * median:
274def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
275 magType="model", clear=True):
277 log = Log.getLogger(
"objectSizeStarSelector.plot")
279 import matplotlib.pyplot
as plt
280 except ImportError
as e:
281 log.warning(
"Unable to import matplotlib: %s", e)
292 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
294 xmin = sorted(mag)[int(0.05*len(mag))]
295 xmax = sorted(mag)[int(0.95*len(mag))]
297 axes.set_xlim(-17.5, -13)
298 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
301 colors = [
"r",
"g",
"b",
"c",
"m",
"k", ]
302 for k, mean
in enumerate(centers):
304 axes.plot(axes.get_xlim(), (mean, mean,),
"k%s" % ltype)
306 li = (clusterId == k)
307 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
308 color=colors[k % len(colors)])
310 li = (clusterId == -1)
311 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
315 axes.set_xlabel(
"Instrumental %s mag" % magType)
316 axes.set_ylabel(
r"$\sqrt{(I_{xx} + I_{yy})/2}$")
321@pexConfig.registerConfigurable("objectSize", sourceSelectorRegistry)
323 r"""A star selector that looks for a cluster of small objects in a size-magnitude plot.
325 ConfigClass = ObjectSizeStarSelectorConfig
329 """Return a selection of PSF candidates that represent likely stars.
331 A list of PSF candidates may be used by a PSF fitter to construct a PSF.
336 Catalog of sources to select from.
337 This catalog must be contiguous
in memory.
339 Ignored
in this SourceSelector.
341 The exposure the catalog was built
from; used to get the detector
342 to transform to TanPix,
and for debug display.
346 struct : `lsst.pipe.base.Struct`
347 The struct contains the following data:
349 - selected : `array` of `bool``
350 Boolean array of sources that were selected, same length
as
362 detector = exposure.getDetector()
364 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
368 flux = sourceCat.get(self.config.sourceFluxField)
369 fluxErr = sourceCat.get(self.config.sourceFluxField +
"Err")
371 xx = numpy.empty(len(sourceCat))
372 xy = numpy.empty_like(xx)
373 yy = numpy.empty_like(xx)
374 for i, source
in enumerate(sourceCat):
375 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
378 linTransform = afwGeom.linearizeTransform(pixToTanPix, p).getLinear()
379 m = afwGeom.Quadrupole(Ixx, Iyy, Ixy)
380 m.transform(linTransform)
381 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
383 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
385 width = numpy.sqrt(0.5*(xx + yy))
386 with numpy.errstate(invalid=
"ignore"):
387 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
388 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
389 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
390 if self.config.doFluxLimit:
391 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
392 if self.config.fluxMax > 0:
393 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
394 if self.config.doSignalToNoiseLimit:
395 bad = numpy.logical_or(bad, flux/fluxErr < self.config.signalToNoiseMin)
396 if self.config.signalToNoiseMax > 0:
397 bad = numpy.logical_or(bad, flux/fluxErr > self.config.signalToNoiseMax)
398 bad = numpy.logical_or(bad, width < self.config.widthMin)
399 bad = numpy.logical_or(bad, width > self.config.widthMax)
400 good = numpy.logical_not(bad)
402 if not numpy.any(good):
403 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
405 mag = -2.5*numpy.log10(flux[good])
413 import pickle
as pickle
416 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
417 if not os.path.exists(pickleFile):
421 with open(pickleFile,
"wb")
as fd:
422 pickle.dump(mag, fd, -1)
423 pickle.dump(width, fd, -1)
425 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
426 widthStdAllowed=self.config.widthStdAllowed)
428 if display
and plotMagSize:
429 fig =
plot(mag, width, centers, clusterId,
430 magType=self.config.sourceFluxField.split(
".")[-1].title(),
431 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
435 clusterId = _improveCluster(width, centers, clusterId,
436 nsigma=self.config.nSigmaClip,
437 widthStdAllowed=self.config.widthStdAllowed)
439 if display
and plotMagSize:
440 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
442 stellar = (clusterId == 0)
449 if display
and displayExposure:
450 disp = afwDisplay.Display(frame=frame)
451 disp.mtv(exposure.getMaskedImage(), title=
"PSF candidates")
454 eventHandler =
EventHandler(fig.get_axes()[0], mag, width,
455 sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
461 reply = input(
"continue? [c h(elp) q(uit) p(db)] ").strip()
470 We cluster the points; red are the stellar candidates and the other colours are other clusters.
471 Points labelled + are rejects
from the cluster (only
for cluster 0).
473 At this prompt, you can
continue with almost any key;
'p' enters pdb,
and 'h' prints this text
475 If displayExposure
is true, you can put the cursor on a point
and hit
'p' to see it
in the
478 elif reply[0] ==
"p":
481 elif reply[0] ==
'q':
486 if display
and displayExposure:
487 mi = exposure.getMaskedImage()
488 with disp.Buffering():
489 for i, source
in enumerate(sourceCat):
491 ctype = afwDisplay.GREEN
493 ctype = afwDisplay.RED
495 disp.dot(
"+", source.getX() - mi.getX0(), source.getY() - mi.getY0(), ctype=ctype)
501 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)