LSSTApplications  10.0-2-g4f67435,11.0.rc2+1,11.0.rc2+12,11.0.rc2+3,11.0.rc2+4,11.0.rc2+5,11.0.rc2+6,11.0.rc2+7,11.0.rc2+8
LSSTDataManagementBasePackage
objectSizeStarSelector.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010 LSST Corporation.
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 <http://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  ]
73  )
74  widthMin = pexConfig.Field(
75  doc = "minimum width to include in histogram",
76  dtype = float,
77  default = 0.0,
78  check = lambda x: x >= 0.0,
79  )
80  widthMax = pexConfig.Field(
81  doc = "maximum width to include in histogram",
82  dtype = float,
83  default = 10.0,
84  check = lambda x: x >= 0.0,
85  )
86  sourceFluxField = pexConfig.Field(
87  doc = "Name of field in Source to use for flux measurement",
88  dtype = str,
89  default = "base_GaussianFlux_flux"
90  )
91 
92  def validate(self):
93  pexConfig.Config.validate(self)
94  if self.widthMin > self.widthMax:
95  raise pexConfig.FieldValidationError("widthMin (%f) > widthMax (%f)"
96  % (self.widthMin, self.widthMax))
97 
98 class EventHandler(object):
99  """A class to handle key strokes with matplotlib displays"""
100  def __init__(self, axes, xs, ys, x, y, frames=[0]):
101  self.axes = axes
102  self.xs = xs
103  self.ys = ys
104  self.x = x
105  self.y = y
106  self.frames = frames
107 
108  self.cid = self.axes.figure.canvas.mpl_connect('key_press_event', self)
109 
110  def __call__(self, ev):
111  if ev.inaxes != self.axes:
112  return
113 
114  if ev.key and ev.key in ("p"):
115  dist = numpy.hypot(self.xs - ev.xdata, self.ys - ev.ydata)
116  dist[numpy.where(numpy.isnan(dist))] = 1e30
117 
118  which = numpy.where(dist == min(dist))
119 
120  x = self.x[which][0]
121  y = self.y[which][0]
122  for frame in self.frames:
123  ds9.pan(x, y, frame=frame)
124  ds9.cmdBuffer.flush()
125  else:
126  pass
127 
128 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
129 
130 def _assignClusters(yvec, centers):
131  """Return a vector of centerIds based on their distance to the centers"""
132  assert len(centers) > 0
133 
134  minDist = numpy.nan*numpy.ones_like(yvec)
135  clusterId = numpy.empty_like(yvec)
136  clusterId.dtype = int # zeros_like(..., dtype=int) isn't in numpy 1.5
137 
138  for i, mean in enumerate(centers):
139  dist = abs(yvec - mean)
140  if i == 0:
141  update = dist == dist # True for all points
142  else:
143  update = dist < minDist
144 
145  minDist[update] = dist[update]
146  clusterId[update] = i
147 
148  return clusterId
149 
150 def _kcenters(yvec, nCluster, useMedian=False):
151  """A classic k-means algorithm, clustering yvec into nCluster clusters
152 
153  Return the set of centres, and the cluster ID for each of the points
154 
155  If useMedian is true, use the median of the cluster as its centre, rather than
156  the traditional mean
157 
158  Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means:
159  "e.g. why not use the Forgy or random partition initialization methods"
160  however, the approach adopted here seems to work well for the particular sorts of things
161  we're clustering in this application
162  """
163 
164  assert nCluster > 0
165 
166  mean0 = sorted(yvec)[len(yvec)//10] # guess
167  centers = mean0*numpy.arange(1, nCluster + 1)
168 
169  func = numpy.median if useMedian else numpy.mean
170 
171  clusterId = numpy.zeros_like(yvec) - 1 # which cluster the points are assigned to
172  clusterId.dtype = int # zeros_like(..., dtype=int) isn't in numpy 1.5
173  while True:
174  oclusterId = clusterId
175  clusterId = _assignClusters(yvec, centers)
176 
177  if numpy.all(clusterId == oclusterId):
178  break
179 
180  for i in range(nCluster):
181  centers[i] = func(yvec[clusterId == i])
182 
183  return centers, clusterId
184 
185 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0):
186  """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median"""
187 
188  nMember = sum(clusterId == clusterNum)
189  if nMember < 5: # can't compute meaningful interquartile range, so no chance of improvement
190  return clusterId
191  for iter in range(nIteration):
192  old_nMember = nMember
193 
194  inCluster0 = clusterId == clusterNum
195  yv = yvec[inCluster0]
196 
197  centers[clusterNum] = numpy.median(yv)
198  stdev = numpy.std(yv)
199 
200  syv = sorted(yv)
201  stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
202 
203  sd = stdev if stdev < stdev_iqr else stdev_iqr
204 
205  if False:
206  print "sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv))
207  newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
208  clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
209  clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
210 
211  nMember = sum(clusterId == clusterNum)
212  if nMember == old_nMember:
213  break
214 
215  return clusterId
216 
217 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
218  clear=True):
219 
220  global fig
221  if not fig:
222  fig = pyplot.figure()
223  newFig = True
224  else:
225  newFig = False
226  if clear:
227  fig.clf()
228 
229  axes = fig.add_axes((0.1, 0.1, 0.85, 0.80));
230 
231  xmin = sorted(mag)[int(0.05*len(mag))]
232  xmax = sorted(mag)[int(0.95*len(mag))]
233 
234  axes.set_xlim(-17.5, -13)
235  axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
236  axes.set_ylim(0, 10)
237 
238  colors = ["r", "g", "b", "c", "m", "k",]
239  for k, mean in enumerate(centers):
240  if k == 0:
241  axes.plot(axes.get_xlim(), (mean, mean,), "k%s" % ltype)
242 
243  l = (clusterId == k)
244  axes.plot(mag[l], width[l], marker, markersize=markersize, markeredgewidth=markeredgewidth,
245  color=colors[k%len(colors)])
246 
247  l = (clusterId == -1)
248  axes.plot(mag[l], width[l], marker, markersize=markersize, markeredgewidth=markeredgewidth,
249  color='k')
250 
251  if newFig:
252  axes.set_xlabel("model")
253  axes.set_ylabel(r"$\sqrt{I_{xx} + I_{yy}}$")
254 
255  return fig
256 
257 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
258 
260  """!
261  A measurePsfTask star selector
262  """
263  ConfigClass = ObjectSizeStarSelectorConfig
264 
265  def __init__(self, config):
266  """!
267  Construct a star selector that looks for a cluster of small objects in a size-magnitude plot.
268 
269  \param[in] config An instance of ObjectSizeStarSelectorConfig
270  """
271  self._kernelSize = config.kernelSize
272  self._borderWidth = config.borderWidth
273  self._widthMin = config.widthMin
274  self._widthMax = config.widthMax
275  self._fluxMin = config.fluxMin
276  self._fluxMax = config.fluxMax
277  self._badFlags = config.badFlags
278  self._sourceFluxField = config.sourceFluxField
279 
280  def selectStars(self, exposure, catalog, matches=None):
281  """!Return a list of PSF candidates that represent likely stars
282 
283  A list of PSF candidates may be used by a PSF fitter to construct a PSF.
284 
285  \param[in] exposure the exposure containing the sources
286  \param[in] catalog a SourceCatalog containing sources that may be stars
287  \param[in] matches astrometric matches; ignored by this star selector
288 
289  \return psfCandidateList a list of PSF candidates.
290  """
291  import lsstDebug
292  display = lsstDebug.Info(__name__).display
293  displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
294  plotMagSize = lsstDebug.Info(__name__).plotMagSize # display the magnitude-size relation
295  dumpData = lsstDebug.Info(__name__).dumpData # dump data to pickle file?
296 
297  # create a log for my application
298  logger = pexLogging.Log(pexLogging.getDefaultLog(), "meas.algorithms.objectSizeStarSelector")
299 
300  detector = exposure.getDetector()
301  pixToTanXYTransform = None
302  if detector is not None:
303  tanSys = detector.makeCameraSys(cameraGeom.TAN_PIXELS)
304  pixToTanXYTransform = detector.getTransformMap().get(tanSys)
305  #
306  # Look at the distribution of stars in the magnitude-size plane
307  #
308  flux = catalog.get(self._sourceFluxField)
309 
310  xx = numpy.empty(len(catalog))
311  xy = numpy.empty_like(xx)
312  yy = numpy.empty_like(xx)
313  for i, source in enumerate(catalog):
314  Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
315  if pixToTanXYTransform:
316  p = afwGeom.Point2D(source.getX(), source.getY())
317  linTransform = pixToTanXYTransform.linearizeForwardTransform(p).getLinear()
318  m = geomEllip.Quadrupole(Ixx, Iyy, Ixy)
319  m.transform(linTransform)
320  Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
321 
322  xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
323 
324  width = numpy.sqrt(xx + yy)
325 
326  badFlags = self._badFlags
327 
328  bad = reduce(lambda x, y: numpy.logical_or(x, catalog.get(y)), badFlags, False)
329  bad = numpy.logical_or(bad, flux < self._fluxMin)
330  bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
331  bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
332  bad = numpy.logical_or(bad, width < self._widthMin)
333  bad = numpy.logical_or(bad, width > self._widthMax)
334  if self._fluxMax > 0:
335  bad = numpy.logical_or(bad, flux > self._fluxMax)
336  good = numpy.logical_not(bad)
337 
338  if not numpy.any(good):
339  raise RuntimeError("No objects passed our cuts for consideration as psf stars")
340 
341  mag = -2.5*numpy.log10(flux[good])
342  width = width[good]
343  #
344  # Look for the maximum in the size histogram, then search upwards for the minimum that separates
345  # the initial peak (of, we presume, stars) from the galaxies
346  #
347  if dumpData:
348  import os, cPickle as pickle
349  _ii = 0
350  while True:
351  pickleFile = os.path.expanduser(os.path.join("~", "widths-%d.pkl" % _ii))
352  if not os.path.exists(pickleFile):
353  break
354  _ii += 1
355 
356  with open(pickleFile, "wb") as fd:
357  pickle.dump(mag, fd, -1)
358  pickle.dump(width, fd, -1)
359 
360  centers, clusterId = _kcenters(width, nCluster=4, useMedian=True)
361 
362  if display and plotMagSize and pyplot:
363  fig = plot(mag, width, centers, clusterId,
364  marker="+", markersize=3, markeredgewidth=None, ltype=':', clear=True)
365  else:
366  fig = None
367 
368  clusterId = _improveCluster(width, centers, clusterId)
369 
370  if display and plotMagSize and pyplot:
371  plot(mag, width, centers, clusterId, marker="x", markersize=3, markeredgewidth=None)
372 
373  stellar = (clusterId == 0)
374  #
375  # We know enough to plot, if so requested
376  #
377  frame = 0
378 
379  if fig:
380  if display and displayExposure:
381  ds9.mtv(exposure.getMaskedImage(), frame=frame, title="PSF candidates")
382 
383  global eventHandler
384  eventHandler = EventHandler(fig.get_axes()[0], mag, width,
385  catalog.getX()[good], catalog.getY()[good], frames=[frame])
386 
387  fig.show()
388 
389  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
390 
391  while True:
392  try:
393  reply = raw_input("continue? [c h(elp) q(uit) p(db)] ").strip()
394  except EOFError:
395  reply = "y"
396 
397  if reply:
398  if reply[0] == "h":
399  print """\
400  We cluster the points; red are the stellar candidates and the other colours are other clusters.
401  Points labelled + are rejects from the cluster (only for cluster 0).
402 
403  At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text
404 
405  If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in ds9.
406  """
407  elif reply[0] == "p":
408  import pdb; pdb.set_trace()
409  elif reply[0] == 'q':
410  sys.exit(1)
411  else:
412  break
413 
414  if display and displayExposure:
415  mi = exposure.getMaskedImage()
416 
417  with ds9.Buffering():
418  for i, source in enumerate(catalog):
419  if good[i]:
420  ctype = ds9.GREEN # star candidate
421  else:
422  ctype = ds9.RED # not star
423 
424  ds9.dot("+", source.getX() - mi.getX0(),
425  source.getY() - mi.getY0(), frame=frame, ctype=ctype)
426  #
427  # Time to use that stellar classification to generate psfCandidateList
428  #
429  with ds9.Buffering():
430  psfCandidateList = []
431  for isStellar, source in zip(stellar, [s for g, s in zip(good, catalog) if g]):
432  if not isStellar:
433  continue
434 
435  try:
436  psfCandidate = algorithmsLib.makePsfCandidate(source, exposure)
437  # The setXXX methods are class static, but it's convenient to call them on
438  # an instance as we don't know Exposure's pixel type
439  # (and hence psfCandidate's exact type)
440  if psfCandidate.getWidth() == 0:
441  psfCandidate.setBorderWidth(self._borderWidth)
442  psfCandidate.setWidth(self._kernelSize + 2*self._borderWidth)
443  psfCandidate.setHeight(self._kernelSize + 2*self._borderWidth)
444 
445  im = psfCandidate.getMaskedImage().getImage()
446  vmax = afwMath.makeStatistics(im, afwMath.MAX).getValue()
447  if not numpy.isfinite(vmax):
448  continue
449  psfCandidateList.append(psfCandidate)
450 
451  if display and displayExposure:
452  ds9.dot("o", source.getX() - mi.getX0(), source.getY() - mi.getY0(),
453  size=4, frame=frame, ctype=ds9.CYAN)
454  except Exception as err:
455  logger.log(pexLogging.Log.INFO,
456  "Failed to make a psfCandidate from source %d: %s" % (source.getId(), err))
457 
458  return psfCandidateList
459 
460 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