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