LSSTApplications  10.0+286,10.0+36,10.0+46,10.0-2-g4f67435,10.1+152,10.1+37,11.0,11.0+1,11.0-1-g47edd16,11.0-1-g60db491,11.0-1-g7418c06,11.0-2-g04d2804,11.0-2-g68503cd,11.0-2-g818369d,11.0-2-gb8b8ce7
LSSTDataManagementBasePackage
objectSizeStarSelector.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2015 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 import sys
23 
24 import numpy
25 try:
26  import matplotlib.pyplot as pyplot
27  fig = None
28 except ImportError:
29  pyplot = None
30 
31 import lsst.pex.config as pexConfig
32 import lsst.pex.logging as pexLogging
33 import lsst.afw.display.ds9 as ds9
34 import lsst.afw.math as afwMath
35 import lsst.afw.geom as afwGeom
36 import lsst.afw.geom.ellipses as geomEllip
37 import lsst.afw.cameraGeom as cameraGeom
38 from . import algorithmsLib
39 from lsst.meas.algorithms.starSelectorRegistry import starSelectorRegistry
40 
41 class ObjectSizeStarSelectorConfig(pexConfig.Config):
42  fluxMin = pexConfig.Field(
43  doc = "specify the minimum psfFlux for good Psf Candidates",
44  dtype = float,
45  default = 12500.0,
46 # minValue = 0.0,
47  check = lambda x: x >= 0.0,
48  )
49  fluxMax = pexConfig.Field(
50  doc = "specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
51  dtype = float,
52  default = 0.0,
53  check = lambda x: x >= 0.0,
54  )
55  kernelSize = pexConfig.Field(
56  doc = "size of the Psf kernel to create",
57  dtype = int,
58  default = 21,
59  )
60  borderWidth = pexConfig.Field(
61  doc = "number of pixels to ignore around the edge of PSF candidate postage stamps",
62  dtype = int,
63  default = 0,
64  )
65  badFlags = pexConfig.ListField(
66  doc = "List of flags which cause a source to be rejected as bad",
67  dtype = str,
68  default = ["base_PixelFlags_flag_edge",
69  "base_PixelFlags_flag_interpolatedCenter",
70  "base_PixelFlags_flag_saturatedCenter",
71  "base_PixelFlags_flag_crCenter",
72  "base_PixelFlags_flag_bad",
73  ],
74  )
75  widthMin = pexConfig.Field(
76  doc = "minimum width to include in histogram",
77  dtype = float,
78  default = 0.0,
79  check = lambda x: x >= 0.0,
80  )
81  widthMax = pexConfig.Field(
82  doc = "maximum width to include in histogram",
83  dtype = float,
84  default = 10.0,
85  check = lambda x: x >= 0.0,
86  )
87  sourceFluxField = pexConfig.Field(
88  doc = "Name of field in Source to use for flux measurement",
89  dtype = str,
90  default = "base_GaussianFlux_flux",
91  )
92  widthStdAllowed = pexConfig.Field(
93  doc = "Standard deviation of width allowed to be interpreted as good stars",
94  dtype = float,
95  default = 0.15,
96  check = lambda x: x >= 0.0,
97  )
98  nSigmaClip = pexConfig.Field(
99  doc = "Keep objects within this many sigma of cluster 0's median",
100  dtype = float,
101  default = 2.0,
102  check = lambda x: x >= 0.0,
103  )
104 
105  def validate(self):
106  pexConfig.Config.validate(self)
107  if self.widthMin > self.widthMax:
108  raise pexConfig.FieldValidationError("widthMin (%f) > widthMax (%f)"
109  % (self.widthMin, self.widthMax))
110 
111 class EventHandler(object):
112  """A class to handle key strokes with matplotlib displays"""
113  def __init__(self, axes, xs, ys, x, y, frames=[0]):
114  self.axes = axes
115  self.xs = xs
116  self.ys = ys
117  self.x = x
118  self.y = y
119  self.frames = frames
120 
121  self.cid = self.axes.figure.canvas.mpl_connect('key_press_event', self)
122 
123  def __call__(self, ev):
124  if ev.inaxes != self.axes:
125  return
126 
127  if ev.key and ev.key in ("p"):
128  dist = numpy.hypot(self.xs - ev.xdata, self.ys - ev.ydata)
129  dist[numpy.where(numpy.isnan(dist))] = 1e30
130 
131  which = numpy.where(dist == min(dist))
132 
133  x = self.x[which][0]
134  y = self.y[which][0]
135  for frame in self.frames:
136  ds9.pan(x, y, frame=frame)
137  ds9.cmdBuffer.flush()
138  else:
139  pass
140 
141 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
142 
143 def _assignClusters(yvec, centers):
144  """Return a vector of centerIds based on their distance to the centers"""
145  assert len(centers) > 0
146 
147  minDist = numpy.nan*numpy.ones_like(yvec)
148  clusterId = numpy.empty_like(yvec)
149  clusterId.dtype = int # zeros_like(..., dtype=int) isn't in numpy 1.5
150 
151  for i, mean in enumerate(centers):
152  dist = abs(yvec - mean)
153  if i == 0:
154  update = dist == dist # True for all points
155  else:
156  update = dist < minDist
157 
158  minDist[update] = dist[update]
159  clusterId[update] = i
160 
161  return clusterId
162 
163 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
164  """A classic k-means algorithm, clustering yvec into nCluster clusters
165 
166  Return the set of centres, and the cluster ID for each of the points
167 
168  If useMedian is true, use the median of the cluster as its centre, rather than
169  the traditional mean
170 
171  Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means:
172  "e.g. why not use the Forgy or random partition initialization methods"
173  however, the approach adopted here seems to work well for the particular sorts of things
174  we're clustering in this application
175  """
176 
177  assert nCluster > 0
178 
179  mean0 = sorted(yvec)[len(yvec)//10] # guess
180  delta = mean0 * widthStdAllowed * 2.0
181  centers = mean0 + delta * numpy.arange(nCluster)
182 
183  func = numpy.median if useMedian else numpy.mean
184 
185  clusterId = numpy.zeros_like(yvec) - 1 # which cluster the points are assigned to
186  clusterId.dtype = int # zeros_like(..., dtype=int) isn't in numpy 1.5
187  while True:
188  oclusterId = clusterId
189  clusterId = _assignClusters(yvec, centers)
190 
191  if numpy.all(clusterId == oclusterId):
192  break
193 
194  for i in range(nCluster):
195  centers[i] = func(yvec[clusterId == i])
196 
197  return centers, clusterId
198 
199 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
200  """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median"""
201 
202  nMember = sum(clusterId == clusterNum)
203  if nMember < 5: # can't compute meaningful interquartile range, so no chance of improvement
204  return clusterId
205  for iter in range(nIteration):
206  old_nMember = nMember
207 
208  inCluster0 = clusterId == clusterNum
209  yv = yvec[inCluster0]
210 
211  centers[clusterNum] = numpy.median(yv)
212  stdev = numpy.std(yv)
213 
214  syv = sorted(yv)
215  stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
216  median = syv[int(0.5*nMember)]
217 
218  sd = stdev if stdev < stdev_iqr else stdev_iqr
219 
220  if False:
221  print "sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv))
222  newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
223  clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
224  clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
225 
226  nMember = sum(clusterId == clusterNum)
227  # 'sd < widthStdAllowed * median' prevents too much rejections
228  if nMember == old_nMember or sd < widthStdAllowed * median:
229  break
230 
231  return clusterId
232 
233 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
234  magType="model", clear=True):
235 
236  global fig
237  if not fig:
238  fig = pyplot.figure()
239  else:
240  if clear:
241  fig.clf()
242 
243  axes = fig.add_axes((0.1, 0.1, 0.85, 0.80));
244 
245  xmin = sorted(mag)[int(0.05*len(mag))]
246  xmax = sorted(mag)[int(0.95*len(mag))]
247 
248  axes.set_xlim(-17.5, -13)
249  axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
250  axes.set_ylim(0, 10)
251 
252  colors = ["r", "g", "b", "c", "m", "k",]
253  for k, mean in enumerate(centers):
254  if k == 0:
255  axes.plot(axes.get_xlim(), (mean, mean,), "k%s" % ltype)
256 
257  l = (clusterId == k)
258  axes.plot(mag[l], width[l], marker, markersize=markersize, markeredgewidth=markeredgewidth,
259  color=colors[k%len(colors)])
260 
261  l = (clusterId == -1)
262  axes.plot(mag[l], width[l], marker, markersize=markersize, markeredgewidth=markeredgewidth,
263  color='k')
264 
265  if clear:
266  axes.set_xlabel("Instrumental %s mag" % magType)
267  axes.set_ylabel(r"$\sqrt{(I_{xx} + I_{yy})/2}$")
268 
269  return fig
270 
271 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
272 
274  """!
275  A measurePsfTask star selector
276  """
277  ConfigClass = ObjectSizeStarSelectorConfig
278 
279  def __init__(self, config):
280  """!
281  Construct a star selector that looks for a cluster of small objects in a size-magnitude plot.
282 
283  \param[in] config An instance of ObjectSizeStarSelectorConfig
284  """
285  self._kernelSize = config.kernelSize
286  self._borderWidth = config.borderWidth
287  self._widthMin = config.widthMin
288  self._widthMax = config.widthMax
289  self._fluxMin = config.fluxMin
290  self._fluxMax = config.fluxMax
291  self._badFlags = config.badFlags
292  self._sourceFluxField = config.sourceFluxField
293  self._widthStdAllowed = config.widthStdAllowed
294  self._nSigmaClip = config.nSigmaClip
295 
296  def selectStars(self, exposure, catalog, matches=None):
297  """!Return a list of PSF candidates that represent likely stars
298 
299  A list of PSF candidates may be used by a PSF fitter to construct a PSF.
300 
301  \param[in] exposure the exposure containing the sources
302  \param[in] catalog a SourceCatalog containing sources that may be stars
303  \param[in] matches astrometric matches; ignored by this star selector
304 
305  \return psfCandidateList a list of PSF candidates.
306  """
307  import lsstDebug
308  display = lsstDebug.Info(__name__).display
309  displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
310  plotMagSize = lsstDebug.Info(__name__).plotMagSize # display the magnitude-size relation
311  dumpData = lsstDebug.Info(__name__).dumpData # dump data to pickle file?
312 
313  # create a log for my application
314  logger = pexLogging.Log(pexLogging.getDefaultLog(), "meas.algorithms.objectSizeStarSelector")
315 
316  detector = exposure.getDetector()
317  pixToTanXYTransform = None
318  if detector is not None:
319  tanSys = detector.makeCameraSys(cameraGeom.TAN_PIXELS)
320  pixToTanXYTransform = detector.getTransformMap().get(tanSys)
321  #
322  # Look at the distribution of stars in the magnitude-size plane
323  #
324  flux = catalog.get(self._sourceFluxField)
325 
326  xx = numpy.empty(len(catalog))
327  xy = numpy.empty_like(xx)
328  yy = numpy.empty_like(xx)
329  for i, source in enumerate(catalog):
330  Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
331  if pixToTanXYTransform:
332  p = afwGeom.Point2D(source.getX(), source.getY())
333  linTransform = pixToTanXYTransform.linearizeForwardTransform(p).getLinear()
334  m = geomEllip.Quadrupole(Ixx, Iyy, Ixy)
335  m.transform(linTransform)
336  Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
337 
338  xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
339 
340  width = numpy.sqrt(0.5*(xx + yy))
341 
342  badFlags = self._badFlags
343 
344  bad = reduce(lambda x, y: numpy.logical_or(x, catalog.get(y)), badFlags, False)
345  bad = numpy.logical_or(bad, flux < self._fluxMin)
346  bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
347  bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
348  bad = numpy.logical_or(bad, width < self._widthMin)
349  bad = numpy.logical_or(bad, width > self._widthMax)
350  if self._fluxMax > 0:
351  bad = numpy.logical_or(bad, flux > self._fluxMax)
352  good = numpy.logical_not(bad)
353 
354  if not numpy.any(good):
355  raise RuntimeError("No objects passed our cuts for consideration as psf stars")
356 
357  mag = -2.5*numpy.log10(flux[good])
358  width = width[good]
359  #
360  # Look for the maximum in the size histogram, then search upwards for the minimum that separates
361  # the initial peak (of, we presume, stars) from the galaxies
362  #
363  if dumpData:
364  import os, cPickle as pickle
365  _ii = 0
366  while True:
367  pickleFile = os.path.expanduser(os.path.join("~", "widths-%d.pkl" % _ii))
368  if not os.path.exists(pickleFile):
369  break
370  _ii += 1
371 
372  with open(pickleFile, "wb") as fd:
373  pickle.dump(mag, fd, -1)
374  pickle.dump(width, fd, -1)
375 
376  centers, clusterId = _kcenters(width, nCluster=4, useMedian=True,
377  widthStdAllowed=self._widthStdAllowed)
378 
379  if display and plotMagSize and pyplot:
380  fig = plot(mag, width, centers, clusterId, magType=self._sourceFluxField.split(".")[-1].title(),
381  marker="+", markersize=3, markeredgewidth=None, ltype=':', clear=True)
382  else:
383  fig = None
384 
385  clusterId = _improveCluster(width, centers, clusterId,
386  nsigma = self._nSigmaClip, widthStdAllowed=self._widthStdAllowed)
387 
388  if display and plotMagSize and pyplot:
389  plot(mag, width, centers, clusterId, marker="x", markersize=3, markeredgewidth=None, clear=False)
390 
391  stellar = (clusterId == 0)
392  #
393  # We know enough to plot, if so requested
394  #
395  frame = 0
396 
397  if fig:
398  if display and displayExposure:
399  ds9.mtv(exposure.getMaskedImage(), frame=frame, title="PSF candidates")
400 
401  global eventHandler
402  eventHandler = EventHandler(fig.get_axes()[0], mag, width,
403  catalog.getX()[good], catalog.getY()[good], frames=[frame])
404 
405  fig.show()
406 
407  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
408 
409  while True:
410  try:
411  reply = raw_input("continue? [c h(elp) q(uit) p(db)] ").strip()
412  except EOFError:
413  reply = None
414  if not reply:
415  reply = "c"
416 
417  if reply:
418  if reply[0] == "h":
419  print """\
420  We cluster the points; red are the stellar candidates and the other colours are other clusters.
421  Points labelled + are rejects from the cluster (only for cluster 0).
422 
423  At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text
424 
425  If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in ds9.
426  """
427  elif reply[0] == "p":
428  import pdb; pdb.set_trace()
429  elif reply[0] == 'q':
430  sys.exit(1)
431  else:
432  break
433 
434  if display and displayExposure:
435  mi = exposure.getMaskedImage()
436 
437  with ds9.Buffering():
438  for i, source in enumerate(catalog):
439  if good[i]:
440  ctype = ds9.GREEN # star candidate
441  else:
442  ctype = ds9.RED # not star
443 
444  ds9.dot("+", source.getX() - mi.getX0(),
445  source.getY() - mi.getY0(), frame=frame, ctype=ctype)
446  #
447  # Time to use that stellar classification to generate psfCandidateList
448  #
449  with ds9.Buffering():
450  psfCandidateList = []
451  for isStellar, source in zip(stellar, [s for g, s in zip(good, catalog) if g]):
452  if not isStellar:
453  continue
454 
455  try:
456  psfCandidate = algorithmsLib.makePsfCandidate(source, exposure)
457  # The setXXX methods are class static, but it's convenient to call them on
458  # an instance as we don't know Exposure's pixel type
459  # (and hence psfCandidate's exact type)
460  if psfCandidate.getWidth() == 0:
461  psfCandidate.setBorderWidth(self._borderWidth)
462  psfCandidate.setWidth(self._kernelSize + 2*self._borderWidth)
463  psfCandidate.setHeight(self._kernelSize + 2*self._borderWidth)
464 
465  im = psfCandidate.getMaskedImage().getImage()
466  vmax = afwMath.makeStatistics(im, afwMath.MAX).getValue()
467  if not numpy.isfinite(vmax):
468  continue
469  psfCandidateList.append(psfCandidate)
470 
471  if display and displayExposure:
472  ds9.dot("o", source.getX() - mi.getX0(), source.getY() - mi.getY0(),
473  size=4, frame=frame, ctype=ds9.CYAN)
474  except Exception as err:
475  logger.logdebug("Failed to make a psfCandidate from source %d: %s" % (source.getId(), err))
476 
477  return psfCandidateList
478 
479 starSelectorRegistry.register("objectSize", ObjectSizeStarSelector)
def selectStars
Return a list of PSF candidates that represent likely stars.
a place to record messages and descriptions of the state of processing.
Definition: Log.h:154
boost::enable_if< typename ExpressionTraits< Scalar >::IsScalar, Scalar >::type sum(Scalar const &scalar)
Definition: operators.h:1250
def __init__
Construct a star selector that looks for a cluster of small objects in a size-magnitude plot...
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
Definition: Statistics.cc:1082