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
secondMomentStarSelector.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 collections
23 import math
24 
25 import numpy
26 
27 import lsst.pex.config as pexConfig
28 import lsst.afw.detection as afwDetection
29 import lsst.afw.display.ds9 as ds9
30 import lsst.afw.image as afwImage
31 import lsst.afw.math as afwMath
32 import lsst.afw.table as afwTable
33 import lsst.afw.geom as afwGeom
34 import lsst.afw.geom.ellipses as geomEllip
35 import lsst.afw.cameraGeom as cameraGeom
36 from . import algorithmsLib
37 from . import measurement
38 from .measurement import SourceMeasurementTask, SourceMeasurementConfig
39 from lsst.meas.base.sfm import SingleFrameMeasurementTask, SingleFrameMeasurementConfig
40 from lsst.meas.base.base import Version0FlagMapper
41 
42 class SecondMomentStarSelectorConfig(pexConfig.Config):
43  fluxLim = pexConfig.Field(
44  doc = "specify the minimum psfFlux for good Psf Candidates",
45  dtype = float,
46  default = 12500.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  clumpNSigma = pexConfig.Field(
56  doc = "candidate PSF's shapes must lie within this many sigma of the average shape",
57  dtype = float,
58  default = 2.0,
59  check = lambda x: x >= 0.0,
60  )
61  kernelSize = pexConfig.Field(
62  doc = "size of the kernel to create",
63  dtype = int,
64  default = 21,
65  )
66  borderWidth = pexConfig.Field(
67  doc = "number of pixels to ignore around the edge of PSF candidate postage stamps",
68  dtype = int,
69  default = 0,
70  )
71  badFlags = pexConfig.ListField(
72  doc = "List of flags which cause a source to be rejected as bad",
73  dtype = str,
74  default = ["base_PixelFlags_flag_edge",
75  "base_PixelFlags_flag_interpolatedCenter",
76  "base_PixelFlags_flag_saturatedCenter",
77  "base_PixelFlags_flag_crCenter",
78  ]
79  )
80  histSize = pexConfig.Field(
81  doc = "Number of bins in moment histogram",
82  dtype = int,
83  default = 64,
84  check = lambda x: x > 0,
85  )
86  histMomentMax = pexConfig.Field(
87  doc = "Maximum moment to consider",
88  dtype = float,
89  default = 100.0,
90  check = lambda x: x > 0,
91  )
92  histMomentMaxMultiplier = pexConfig.Field(
93  doc = "Multiplier of mean for maximum moments histogram range",
94  dtype = float,
95  default = 5.0,
96  check = lambda x: x > 0,
97  )
98  histMomentClip = pexConfig.Field(
99  doc = "Clipping threshold for moments histogram range",
100  dtype = float,
101  default = 5.0,
102  check = lambda x: x > 0,
103  )
104  histMomentMinMultiplier = pexConfig.Field(
105  doc = "Multiplier of mean for minimum moments histogram range",
106  dtype = float,
107  default = 2.0,
108  check = lambda x: x > 0,
109  )
110 
111 Clump = collections.namedtuple('Clump', ['peak', 'x', 'y', 'ixx', 'ixy', 'iyy', 'a', 'b', 'c'])
112 
113 class CheckSource(object):
114  """A functor to check whether a source has any flags set that should cause it to be labeled bad."""
115 
116  def __init__(self, table, badFlags, fluxLim, fluxMax):
117  if table.getVersion() == 0:
118  badFlags = Version0FlagMapper(badFlags)
119  self.keys = [table.getSchema().find(name).key for name in badFlags]
120  self.keys.append(table.getCentroidFlagKey())
121  self.fluxLim = fluxLim
122  self.fluxMax = fluxMax
123 
124  def __call__(self, source):
125  for k in self.keys:
126  if source.get(k):
127  return False
128  if self.fluxLim != None and source.getPsfFlux() < self.fluxLim: # ignore faint objects
129  return False
130  if self.fluxMax != 0.0 and source.getPsfFlux() > self.fluxMax: # ignore bright objects
131  return False
132  return True
133 
135  ConfigClass = SecondMomentStarSelectorConfig
136 
137  def __init__(self, config):
138  """Construct a star selector that uses second moments
139 
140  This is a naive algorithm and should be used with caution.
141 
142  @param[in] config: An instance of SecondMomentStarSelectorConfig
143  """
144  self.config = config
145 
146  def selectStars(self, exposure, catalog, matches=None):
147  """Return a list of PSF candidates that represent likely stars
148 
149  A list of PSF candidates may be used by a PSF fitter to construct a PSF.
150 
151  @param[in] exposure: the exposure containing the sources
152  @param[in] catalog: a SourceCatalog containing sources that may be stars
153  @param[in] matches: astrometric matches; ignored by this star selector
154 
155  @return psfCandidateList: a list of PSF candidates.
156  """
157  import lsstDebug
158  display = lsstDebug.Info(__name__).display
159 
160  displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
161 
162  isGoodSource = CheckSource(catalog.getTable(), self.config.badFlags, self.config.fluxLim,
163  self.config.fluxMax)
164 
165  detector = exposure.getDetector()
166 
167  mi = exposure.getMaskedImage()
168  #
169  # Create an Image of Ixx v. Iyy, i.e. a 2-D histogram
170  #
171 
172  # Use stats on our Ixx/yy values to determine the xMax/yMax range for clump image
173  iqqList = []
174  for s in catalog:
175  ixx, iyy = s.getIxx(), s.getIyy()
176  # ignore NaN and unrealistically large values
177  if (ixx == ixx and ixx < self.config.histMomentMax and
178  iyy == iyy and iyy < self.config.histMomentMax and
179  isGoodSource(s)):
180  iqqList.append(s.getIxx())
181  iqqList.append(s.getIyy())
182  stat = afwMath.makeStatistics(iqqList, afwMath.MEANCLIP | afwMath.STDEVCLIP | afwMath.MAX)
183  iqqMean = stat.getValue(afwMath.MEANCLIP)
184  iqqStd = stat.getValue(afwMath.STDEVCLIP)
185  iqqMax = stat.getValue(afwMath.MAX)
186 
187  iqqLimit = max(iqqMean + self.config.histMomentClip*iqqStd,
188  self.config.histMomentMaxMultiplier*iqqMean)
189  # if the max value is smaller than our range, use max as the limit, but don't go below N*mean
190  if iqqLimit > iqqMax:
191  iqqLimit = max(self.config.histMomentMinMultiplier*iqqMean, iqqMax)
192 
193  psfHist = _PsfShapeHistogram(detector=detector, xSize=self.config.histSize, ySize=self.config.histSize,
194  ixxMax=iqqLimit, iyyMax=iqqLimit)
195 
196  if display and displayExposure:
197  frame = 0
198  ds9.mtv(mi, frame=frame, title="PSF candidates")
199 
200  with ds9.Buffering():
201  for source in catalog:
202  if isGoodSource(source):
203  if psfHist.insert(source): # n.b. this call has the side effect of inserting
204  ctype = ds9.GREEN # good
205  else:
206  ctype = ds9.MAGENTA # rejected
207  else:
208  ctype = ds9.RED # bad
209 
210  if display and displayExposure:
211  ds9.dot("o", source.getX() - mi.getX0(),
212  source.getY() - mi.getY0(), frame=frame, ctype=ctype)
213 
214  clumps = psfHist.getClumps(catalog.getTable().getVersion(), display=display)
215 
216  #
217  # Go through and find all the PSF-like objects
218  #
219  # We'll split the image into a number of cells, each of which contributes only
220  # one PSF candidate star
221  #
222  psfCandidateList = []
223 
224  pixToTanXYTransform = None
225  if detector is not None:
226  tanSys = detector.makeCameraSys(cameraGeom.TAN_PIXELS)
227  pixToTanXYTransform = detector.getTransformMap().get(tanSys)
228 
229  # psf candidate shapes must lie within this many RMS of the average shape
230  # N.b. if Ixx == Iyy, Ixy = 0 the criterion is
231  # dx^2 + dy^2 < self.config.clumpNSigma*(Ixx + Iyy) == 2*self.config.clumpNSigma*Ixx
232  for source in catalog:
233  if not isGoodSource(source): continue
234  Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
235  if pixToTanXYTransform:
236  p = afwGeom.Point2D(source.getX(), source.getY())
237  linTransform = pixToTanXYTransform.linearizeForwardTransform(p).getLinear()
238  m = geomEllip.Quadrupole(Ixx, Iyy, Ixy)
239  m.transform(linTransform)
240  Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
241 
242  x, y = psfHist.momentsToPixel(Ixx, Iyy)
243  for clump in clumps:
244  dx, dy = (x - clump.x), (y - clump.y)
245 
246  if math.sqrt(clump.a*dx*dx + 2*clump.b*dx*dy + clump.c*dy*dy) < 2*self.config.clumpNSigma:
247  # A test for > would be confused by NaN
248  if not isGoodSource(source):
249  continue
250  try:
251  psfCandidate = algorithmsLib.makePsfCandidate(source, exposure)
252 
253  # The setXXX methods are class static, but it's convenient to call them on
254  # an instance as we don't know Exposure's pixel type
255  # (and hence psfCandidate's exact type)
256  if psfCandidate.getWidth() == 0:
257  psfCandidate.setBorderWidth(self.config.borderWidth)
258  psfCandidate.setWidth(self.config.kernelSize + 2*self.config.borderWidth)
259  psfCandidate.setHeight(self.config.kernelSize + 2*self.config.borderWidth)
260 
261  im = psfCandidate.getMaskedImage().getImage()
262  if not numpy.isfinite(afwMath.makeStatistics(im, afwMath.MAX).getValue()):
263  continue
264  psfCandidateList.append(psfCandidate)
265 
266  if display and displayExposure:
267  ds9.dot("o", source.getX() - mi.getX0(), source.getY() - mi.getY0(),
268  size=4, frame=frame, ctype=ds9.CYAN)
269  except Exception as err:
270  pass # FIXME: should log this!
271  break
272 
273  return psfCandidateList
274 
275 class _PsfShapeHistogram(object):
276  """A class to represent a histogram of (Ixx, Iyy)
277  """
278  def __init__(self, xSize=32, ySize=32, ixxMax=30, iyyMax=30, detector=None, xy0=afwGeom.Point2D(0,0)):
279  """Construct a _PsfShapeHistogram
280 
281  The maximum seeing FWHM that can be tolerated is [xy]Max/2.35 pixels.
282  The 'resolution' of stars vs galaxies/CRs is provided by [xy]Size/[xy]Max.
283  A larger (better) resolution may thresh the peaks, but a smaller (worse)
284  resolution will allow stars and galaxies/CRs to mix. The disadvantages of
285  a larger (better) resolution can be compensated (some) by using multiple
286  histogram peaks.
287 
288  @input[in] [xy]Size: the size of the psfImage (in pixels)
289  @input[in] ixxMax, iyyMax: the maximum values for I[xy][xy]
290  """
291  self._xSize, self._ySize = xSize, ySize
292  self._xMax, self._yMax = ixxMax, iyyMax
293  self._psfImage = afwImage.ImageF(afwGeom.ExtentI(xSize, ySize), 0)
294  self._num = 0
295  self.detector = detector
296  self.xy0 = xy0
297 
298  def getImage(self):
299  return self._psfImage
300 
301  def insert(self, source):
302  """Insert source into the histogram."""
303 
304  ixx, iyy, ixy = source.getIxx(), source.getIyy(), source.getIxy()
305  if self.detector:
306  tanSys = self.detector.makeCameraSys(cameraGeom.TAN_PIXELS)
307  if tanSys in self.detector.getTransformMap():
308  pixToTanXYTransform = self.detector.getTransformMap()[tanSys]
309  p = afwGeom.Point2D(source.getX(), source.getY())
310  linTransform = pixToTanXYTransform.linearizeForwardTransform(p).getLinear()
311  m = geomEllip.Quadrupole(ixx, iyy, ixy)
312  m.transform(linTransform)
313  ixx, iyy, ixy = m.getIxx(), m.getIyy(), m.getIxy()
314 
315  try:
316  pixel = self.momentsToPixel(ixx, iyy)
317  i = int(pixel[0])
318  j = int(pixel[1])
319  except:
320  return 0
321 
322  if i in range(0, self._xSize) and j in range(0, self._ySize):
323  if i != 0 or j != 0:
324  self._psfImage.set(i, j, self._psfImage.get(i, j) + 1)
325  self._num += 1
326  return 1 # success
327 
328  return 0 # failure
329 
330  def momentsToPixel(self, ixx, iyy):
331  #x = math.sqrt(ixx) * self._xSize / self._xMax
332  #y = math.sqrt(iyy) * self._ySize / self._yMax
333  x = ixx * self._xSize / self._xMax
334  y = iyy * self._ySize / self._yMax
335  return x, y
336 
337  def pixelToMoments(self, x, y):
338  """Given a peak position in self._psfImage, return the corresponding (Ixx, Iyy)"""
339 
340  #ixx = (x*self._xMax/self._xSize)**2
341  #iyy = (y*self._yMax/self._ySize)**2
342  ixx = x*self._xMax/self._xSize
343  iyy = y*self._yMax/self._ySize
344  return ixx, iyy
345 
346  def getClumps(self, tableVersion, sigma=1.0, display=False):
347  if self._num <= 0:
348  raise RuntimeError("No candidate PSF sources")
349 
350  psfImage = self.getImage()
351  #
352  # Embed psfImage into a larger image so we can smooth when measuring it
353  #
354  width, height = psfImage.getWidth(), psfImage.getHeight()
355  largeImg = psfImage.Factory(afwGeom.ExtentI(2*width, 2*height))
356  largeImg.set(0)
357 
358  bbox = afwGeom.BoxI(afwGeom.PointI(width, height), afwGeom.ExtentI(width, height))
359  subLargeImg = psfImage.Factory(largeImg, bbox, afwImage.LOCAL)
360  subLargeImg <<= psfImage
361  del subLargeImg
362  #
363  # Now measure that image, looking for the highest peak. Start by building an Exposure
364  #
365  msk = afwImage.MaskU(largeImg.getDimensions())
366  msk.set(0)
367  var = afwImage.ImageF(largeImg.getDimensions())
368  var.set(1)
369  mpsfImage = afwImage.MaskedImageF(largeImg, msk, var)
370  mpsfImage.setXY0(afwGeom.PointI(-width, -height))
371  del msk
372  del var
373  exposure = afwImage.makeExposure(mpsfImage)
374 
375  #
376  # Next run an object detector
377  #
378  maxVal = afwMath.makeStatistics(psfImage, afwMath.MAX).getValue()
379  threshold = maxVal - sigma*math.sqrt(maxVal)
380  if threshold <= 0.0:
381  threshold = maxVal
382 
383  threshold = afwDetection.Threshold(threshold)
384 
385  ds = afwDetection.FootprintSet(mpsfImage, threshold, "DETECTED")
386  #
387  # And measure it. This policy isn't the one we use to measure
388  # Sources, it's only used to characterize this PSF histogram
389  #
390  schema = afwTable.SourceTable.makeMinimalSchema()
391  schema.setVersion(tableVersion)
392  if tableVersion == 0:
393  psfImageConfig = SourceMeasurementConfig()
394  psfImageConfig.slots.centroid = "centroid.sdss"
395  psfImageConfig.slots.psfFlux = "flux.psf"
396  psfImageConfig.slots.apFlux = "flux.naive"
397  psfImageConfig.slots.modelFlux = None
398  psfImageConfig.slots.instFlux = None
399  psfImageConfig.slots.shape = "shape.sdss"
400  psfImageConfig.algorithms.names = ["flags.pixel", "shape.sdss",
401  "flux.psf", "flux.naive"]
402  psfImageConfig.centroider.name = "centroid.sdss"
403  psfImageConfig.algorithms["flux.naive"].radius = 3.0
404  psfImageConfig.validate()
405  task = SourceMeasurementTask(schema, config=psfImageConfig)
406  else:
407  psfImageConfig = SingleFrameMeasurementConfig()
408  psfImageConfig.slots.centroid = "base_SdssCentroid"
409  psfImageConfig.slots.psfFlux = "base_PsfFlux"
410  psfImageConfig.slots.apFlux = "base_NaiveFlux"
411  psfImageConfig.slots.modelFlux = None
412  psfImageConfig.slots.instFlux = None
413  psfImageConfig.slots.shape = "base_SdssShape"
414  psfImageConfig.algorithms.names = ["base_SdssCentroid", "base_PsfFlux", "base_NaiveFlux",
415  "base_PixelFlags", "base_SdssShape"]
416  psfImageConfig.algorithms["base_NaiveFlux"].radius = 3.0
417  psfImageConfig.validate()
418  task = SingleFrameMeasurementTask(schema, config=psfImageConfig)
419 
420  catalog = afwTable.SourceCatalog(schema)
421 
422  gaussianWidth = 1.5 # Gaussian sigma for detection convolution
423  exposure.setPsf(algorithmsLib.DoubleGaussianPsf(11, 11, gaussianWidth))
424 
425  ds.makeSources(catalog)
426  #
427  # Show us the Histogram
428  #
429  if display:
430  frame = 1
431  dispImage = mpsfImage.Factory(mpsfImage, afwGeom.BoxI(afwGeom.PointI(width, height),
432  afwGeom.ExtentI(width, height)),
433  afwImage.LOCAL)
434  ds9.mtv(dispImage,title="PSF Selection Image", frame=frame)
435 
436 
437  clumps = list() # List of clumps, to return
438  e = None # thrown exception
439  IzzMin = 1.0 # Minimum value for second moments
440  IzzMax = (self._xSize/8.0)**2 # Max value ... clump r < clumpImgSize/8
441  # diameter should be < 1/4 clumpImgSize
442  apFluxes = []
443  task.run(exposure, catalog) # notes that this is backwards for the new framework
444  for i, source in enumerate(catalog):
445  if source.getCentroidFlag():
446  continue
447  x, y = source.getX(), source.getY()
448 
449  apFluxes.append(source.getApFlux())
450 
451  val = mpsfImage.getImage().get(int(x) + width, int(y) + height)
452 
453  psfClumpIxx = source.getIxx()
454  psfClumpIxy = source.getIxy()
455  psfClumpIyy = source.getIyy()
456 
457  if display:
458  if i == 0:
459  ds9.pan(x, y, frame=frame)
460 
461  ds9.dot("+", x, y, ctype=ds9.YELLOW, frame=frame)
462  ds9.dot("@:%g,%g,%g" % (psfClumpIxx, psfClumpIxy, psfClumpIyy), x, y,
463  ctype=ds9.YELLOW, frame=frame)
464 
465  if psfClumpIxx < IzzMin or psfClumpIyy < IzzMin:
466  psfClumpIxx = max(psfClumpIxx, IzzMin)
467  psfClumpIyy = max(psfClumpIyy, IzzMin)
468  if display:
469  ds9.dot("@:%g,%g,%g" % (psfClumpIxx, psfClumpIxy, psfClumpIyy), x, y,
470  ctype=ds9.RED, frame=frame)
471 
472  det = psfClumpIxx*psfClumpIyy - psfClumpIxy*psfClumpIxy
473  try:
474  a, b, c = psfClumpIyy/det, -psfClumpIxy/det, psfClumpIxx/det
475  except ZeroDivisionError:
476  a, b, c = 1e4, 0, 1e4
477 
478  clumps.append(Clump(peak=val, x=x, y=y, a=a, b=b, c=c,
479  ixx=psfClumpIxx, ixy=psfClumpIxy, iyy=psfClumpIyy))
480 
481  if len(clumps) == 0:
482  msg = "Failed to determine center of PSF clump"
483  if e:
484  msg += ": %s" % e
485  raise RuntimeError(msg)
486 
487  # if it's all we got return it
488  if len(clumps) == 1:
489  return clumps
490 
491  # which clump is the best?
492  # if we've undistorted the moments, stars should only have 1 clump
493  # use the apFlux from the clump measurement, and take the highest
494  # ... this clump has more psf star candidate neighbours than the others.
495 
496  # get rid of any that are huge, and thus poorly defined
497  goodClumps = []
498  for clump in clumps:
499  if clump.ixx < IzzMax and clump.iyy < IzzMax:
500  goodClumps.append(clump)
501 
502  # if culling > IzzMax cost us all clumps, we'll have to take what we have
503  if len(goodClumps) == 0:
504  goodClumps = clumps
505 
506  # use the 'brightest' clump
507  iBestClump = numpy.argsort(apFluxes)[0]
508  clumps = [clumps[iBestClump]]
509  return clumps
A subtask for measuring the properties of sources on a single exposure.
Definition: sfm.py:143
A Threshold is used to pass a threshold value to detection algorithms.
Definition: Threshold.h:44
An integer coordinate rectangle.
Definition: Box.h:53
double max
Definition: attributes.cc:218
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Definition: fwd.h:55
Config class for single frame measurement driver task.
Definition: sfm.py:113
def Version0FlagMapper
Definition: base.py:75
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
Definition: Statistics.cc:1023
Exposure< ImagePixelT, MaskPixelT, VariancePixelT >::Ptr makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, boost::shared_ptr< Wcs const > wcs=boost::shared_ptr< Wcs const >())
Definition: Exposure.h:308