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
deblend.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 <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 import math
23 import numpy
24 import time
25 
26 import lsst.pex.config as pexConf
27 import lsst.pipe.base as pipeBase
28 import lsst.afw.math as afwMath
29 import lsst.afw.geom as afwGeom
30 import lsst.afw.geom.ellipses as afwEll
31 import lsst.afw.image as afwImage
32 import lsst.afw.detection as afwDet
33 import lsst.afw.table as afwTable
34 
35 __all__ = 'SourceDeblendConfig', 'SourceDeblendTask'
36 
37 class SourceDeblendConfig(pexConf.Config):
38 
39  edgeHandling = pexConf.ChoiceField(
40  doc='What to do when a peak to be deblended is close to the edge of the image',
41  dtype=str, default='ramp',
42  allowed={
43  'clip': 'Clip the template at the edge AND the mirror of the edge.',
44  'ramp': 'Ramp down flux at the image edge by the PSF',
45  'noclip': 'Ignore the edge when building the symmetric template.',
46  }
47  )
48 
49  strayFluxToPointSources = pexConf.ChoiceField(
50  doc='When the deblender should attribute stray flux to point sources',
51  dtype=str, default='necessary',
52  allowed={
53  'necessary': 'When there is not an extended object in the footprint',
54  'always': 'Always',
55  'never': ('Never; stray flux will not be attributed to any deblended child '
56  'if the deblender thinks all peaks look like point sources'),
57  }
58  )
59 
60  findStrayFlux = pexConf.Field(dtype=bool, default=True,
61  doc='Find stray flux---flux not claimed by any child in the deblender.')
62 
63  assignStrayFlux = pexConf.Field(dtype=bool, default=True,
64  doc='Assign stray flux to deblend children. Implies findStrayFlux.')
65 
66  strayFluxRule = pexConf.ChoiceField(
67  doc='How to split flux among peaks',
68  dtype=str, default='r-to-peak',
69  allowed={
70  'r-to-peak': '~ 1/(1+R^2) to the peak',
71  'r-to-footprint': ('~ 1/(1+R^2) to the closest pixel in the footprint. '
72  'CAUTION: this can be computationally expensive on large footprints!'),
73  'nearest-footprint': ('Assign 100% to the nearest footprint (using L-1 norm aka '
74  'Manhattan distance)'),
75  'trim': ('Shrink the parent footprint to pixels that are not assigned to children')
76  }
77  )
78 
79  clipStrayFluxFraction = pexConf.Field(dtype=float, default=0.01,
80  doc=('When splitting stray flux, clip fractions below '
81  'this value to zero.'))
82  psfChisq1 = pexConf.Field(dtype=float, default=1.5, optional=False,
83  doc=('Chi-squared per DOF cut for deciding a source is '
84  'a PSF during deblending (un-shifted PSF model)'))
85  psfChisq2 = pexConf.Field(dtype=float, default=1.5, optional=False,
86  doc=('Chi-squared per DOF cut for deciding a source is '
87  'PSF during deblending (shifted PSF model)'))
88  psfChisq2b = pexConf.Field(dtype=float, default=1.5, optional=False,
89  doc=('Chi-squared per DOF cut for deciding a source is '
90  'a PSF during deblending (shifted PSF model #2)'))
91  maxNumberOfPeaks = pexConf.Field(dtype=int, default=0,
92  doc=("Only deblend the brightest maxNumberOfPeaks peaks in the parent"
93  " (<= 0: unlimited)"))
94  maxFootprintArea = pexConf.Field(dtype=int, default=1000000,
95  doc=("Maximum area for footprints before they are ignored as large; "
96  "non-positive means no threshold applied"))
97  maxFootprintSize = pexConf.Field(dtype=int, default=0,
98  doc=("Maximum linear dimension for footprints before they are ignored "
99  "as large; non-positive means no threshold applied"))
100  minFootprintAxisRatio = pexConf.Field(dtype=float, default=0.0,
101  doc=("Minimum axis ratio for footprints before they are ignored "
102  "as large; non-positive means no threshold applied"))
103  notDeblendedMask = pexConf.Field(dtype=str, default="NOT_DEBLENDED", optional=True,
104  doc="Mask name for footprints not deblended, or None")
105 
106  tinyFootprintSize = pexConf.Field(dtype=int, default=2,
107  doc=('Footprints smaller in width or height than this value will '
108  'be ignored; 0 to never ignore.'))
109 
110  propagateAllPeaks = pexConf.Field(dtype=bool, default=False,
111  doc=('Guarantee that all peaks produce a child source.'))
112  catchFailures = pexConf.Field(dtype=bool, default=False,
113  doc=("If True, catch exceptions thrown by the deblender, log them, "
114  "and set a flag on the parent, instead of letting them propagate up"))
115  maskPlanes = pexConf.ListField(dtype=str, default=["SAT", "INTRP", "NO_DATA"],
116  doc="Mask planes to ignore when performing statistics")
117  maskLimits = pexConf.DictField(
118  keytype = str,
119  itemtype = float,
120  default = {},
121  doc = ("Mask planes with the corresponding limit on the fraction of masked pixels. "
122  "Sources violating this limit will not be deblended."),
123  )
124 
125 ## \addtogroup LSST_task_documentation
126 ## \{
127 ## \page SourceDeblendTask
128 ## \ref SourceDeblendTask_ "SourceDeblendTask"
129 ## \copybrief SourceDeblendTask
130 ## \}
131 
132 class SourceDeblendTask(pipeBase.Task):
133  """!
134  \anchor SourceDeblendTask_
135 
136  \brief Split blended sources into individual sources.
137 
138  This task has no return value; it only modifies the SourceCatalog in-place.
139  """
140  ConfigClass = SourceDeblendConfig
141  _DefaultName = "sourceDeblend"
142 
143  def __init__(self, schema, peakSchema=None, **kwargs):
144  """!
145  Create the task, adding necessary fields to the given schema.
146 
147  @param[in,out] schema Schema object for measurement fields; will be modified in-place.
148  @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender.
149  Any fields beyond the PeakTable minimal schema will be transferred
150  to the main source Schema. If None, no fields will be transferred
151  from the Peaks.
152  @param[in] **kwargs Passed to Task.__init__.
153  """
154  pipeBase.Task.__init__(self, **kwargs)
155  peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
156  if peakSchema is None:
157  # In this case, the peakSchemaMapper will transfer nothing, but we'll still have one
158  # to simplify downstream code
159  self.peakSchemaMapper = afwTable.SchemaMapper(peakMinimalSchema, schema)
160  else:
161  self.peakSchemaMapper = afwTable.SchemaMapper(peakSchema, schema)
162  for item in peakSchema:
163  if item.key not in peakMinimalSchema:
164  self.peakSchemaMapper.addMapping(item.key, item.field)
165  # Because SchemaMapper makes a copy of the output schema you give its ctor, it isn't
166  # updating this Schema in place. That's probably a design flaw, but in the meantime,
167  # we'll keep that schema in sync with the peakSchemaMapper.getOutputSchema() manually,
168  # by adding the same fields to both.
169  schema.addField(item.field)
170  assert schema == self.peakSchemaMapper.getOutputSchema(), "Logic bug mapping schemas"
171  self.addSchemaKeys(schema)
172 
173  def addSchemaKeys(self, schema):
174  self.nChildKey = schema.addField('deblend_nChild', type=int,
175  doc='Number of children this object has (defaults to 0)')
176  self.psfKey = schema.addField('deblend_deblendedAsPsf', type='Flag',
177  doc='Deblender thought this source looked like a PSF')
178  self.psfCenterKey = afwTable.Point2DKey.addFields(schema, 'deblend_psfCenter',
179  'If deblended-as-psf, the PSF centroid', "pixels")
180  self.psfFluxKey = schema.addField('deblend_psfFlux', type='D',
181  doc='If deblended-as-psf, the PSF flux')
182  self.tooManyPeaksKey = schema.addField('deblend_tooManyPeaks', type='Flag',
183  doc='Source had too many peaks; '
184  'only the brightest were included')
185  self.tooBigKey = schema.addField('deblend_parentTooBig', type='Flag',
186  doc='Parent footprint covered too many pixels')
187  self.maskedKey = schema.addField('deblend.masked', type='Flag',
188  doc='Parent footprint was predominantly masked')
189 
190  if self.config.catchFailures:
191  self.deblendFailedKey = schema.addField('deblend_failed', type='Flag',
192  doc="Deblending failed on source")
193 
194  self.deblendSkippedKey = schema.addField('deblend_skipped', type='Flag',
195  doc="Deblender skipped this source")
196 
197  self.deblendRampedTemplateKey = schema.addField(
198  'deblend_rampedTemplate', type='Flag',
199  doc=('This source was near an image edge and the deblender used '
200  '"ramp" edge-handling.'))
201 
202  self.deblendPatchedTemplateKey = schema.addField(
203  'deblend_patchedTemplate', type='Flag',
204  doc=('This source was near an image edge and the deblender used '
205  '"patched" edge-handling.'))
206 
207  self.hasStrayFluxKey = schema.addField(
208  'deblend_hasStrayFlux', type='Flag',
209  doc=('This source was assigned some stray flux'))
210 
211  self.blendednessKey = schema.addField(
212  'deblend.blendedness', type=float,
213  doc=("A measure of how blended the source is. This is the sum of dot products between the source "
214  "and all of its deblended siblings, divided by the dot product of the deblended source with "
215  "itself"))
216 
217  self.log.logdebug('Added keys to schema: %s' % ", ".join(str(x) for x in (
218  self.nChildKey, self.psfKey, self.psfCenterKey, self.psfFluxKey,
219  self.tooManyPeaksKey, self.tooBigKey)))
220 
221  @pipeBase.timeMethod
222  def run(self, exposure, sources, psf):
223  """!
224  Run deblend().
225 
226  @param[in] exposure Exposure to process
227  @param[in,out] sources SourceCatalog containing sources detected on this exposure.
228  @param[in] psf PSF
229 
230  @return None
231  """
232  self.deblend(exposure, sources, psf)
233 
234  def _getPsfFwhm(self, psf, bbox):
235  # It should be easier to get a PSF's fwhm;
236  # https://dev.lsstcorp.org/trac/ticket/3030
237  return psf.computeShape().getDeterminantRadius() * 2.35
238 
239  @pipeBase.timeMethod
240  def deblend(self, exposure, srcs, psf):
241  """!
242  Deblend.
243 
244  @param[in] exposure Exposure to process
245  @param[in,out] srcs SourceCatalog containing sources detected on this exposure.
246  @param[in] psf PSF
247 
248  @return None
249  """
250  self.log.info("Deblending %d sources" % len(srcs))
251 
252  from lsst.meas.deblender.baseline import deblend
253  import lsst.meas.algorithms as measAlg
254 
255  # find the median stdev in the image...
256  mi = exposure.getMaskedImage()
257  statsCtrl = afwMath.StatisticsControl()
258  statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
259  stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl)
260  sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
261  self.log.logdebug('sigma1: %g' % sigma1)
262 
263  schema = srcs.getSchema()
264 
265  n0 = len(srcs)
266  nparents = 0
267  for i,src in enumerate(srcs):
268  #t0 = time.clock()
269 
270  fp = src.getFootprint()
271  pks = fp.getPeaks()
272  if len(pks) < 2:
273  continue
274 
275  if self.isLargeFootprint(fp):
276  src.set(self.tooBigKey, True)
277  self.skipParent(src, mi.getMask())
278  self.log.logdebug('Parent %i: skipping large footprint' % (int(src.getId()),))
279  continue
280  if self.isMasked(fp, exposure.getMaskedImage().getMask()):
281  src.set(self.maskedKey, True)
282  self.skipParent(src, mi.getMask())
283  self.log.logdebug('Parent %i: skipping masked footprint' % (int(src.getId()),))
284  continue
285 
286  nparents += 1
287  bb = fp.getBBox()
288  psf_fwhm = self._getPsfFwhm(psf, bb)
289 
290  self.log.logdebug('Parent %i: deblending %i peaks' % (int(src.getId()), len(pks)))
291 
292  self.preSingleDeblendHook(exposure, srcs, i, fp, psf, psf_fwhm, sigma1)
293  npre = len(srcs)
294 
295  # This should really be set in deblend, but deblend doesn't have access to the src
296  src.set(self.tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
297 
298  try:
299  res = deblend(
300  fp, mi, psf, psf_fwhm, sigma1=sigma1,
301  psfChisqCut1 = self.config.psfChisq1,
302  psfChisqCut2 = self.config.psfChisq2,
303  psfChisqCut2b= self.config.psfChisq2b,
304  maxNumberOfPeaks=self.config.maxNumberOfPeaks,
305  strayFluxToPointSources=self.config.strayFluxToPointSources,
306  assignStrayFlux=self.config.assignStrayFlux,
307  findStrayFlux=(self.config.assignStrayFlux or self.config.findStrayFlux),
308  strayFluxAssignment=self.config.strayFluxRule,
309  rampFluxAtEdge=(self.config.edgeHandling == 'ramp'),
310  patchEdges=(self.config.edgeHandling == 'noclip'),
311  tinyFootprintSize=self.config.tinyFootprintSize,
312  clipStrayFluxFraction=self.config.clipStrayFluxFraction,
313  )
314  if self.config.catchFailures:
315  src.set(self.deblendFailedKey, False)
316  except Exception as e:
317  if self.config.catchFailures:
318  self.log.warn("Unable to deblend source %d: %s" % (src.getId(), e))
319  src.set(self.deblendFailedKey, True)
320  import traceback
321  traceback.print_exc()
322  continue
323  else:
324  raise
325 
326  kids = []
327  nchild = 0
328  for j, peak in enumerate(res.peaks):
329 
330  failed = False
331  if peak.skip:
332  # skip this source?
333  msg = 'Skipping out-of-bounds peak at (%i,%i)' % (pks[j].getIx(), pks[j].getIy())
334  self.log.warn(msg)
335  src.set(self.deblendSkippedKey, True)
336  failed = True
337 
338  heavy = peak.getFluxPortion()
339  if heavy is None:
340  # This can happen for children >= maxNumberOfPeaks
341  msg = 'Skipping peak at (%i,%i), child %i of %i: no flux portion' \
342  % (pks[j].getIx(), pks[j].getIy(), j+1, len(res.peaks))
343  self.log.warn(msg)
344  src.set(self.deblendSkippedKey, True)
345  failed = True
346 
347  if failed:
348  if self.config.propagateAllPeaks:
349  # make sure we have enough info to create a minimal child src
350  if heavy is None:
351  # copy the full footprint and strip out extra peaks
352  foot = afwDet.Footprint(src.getFootprint())
353  peakList = foot.getPeaks()
354  peakList.clear()
355  peakList.append(peak.peak)
356  zeroMimg = afwImage.MaskedImageF(foot.getBBox())
357  heavy = afwDet.makeHeavyFootprint(foot, zeroMimg)
358  if peak.deblendedAsPsf:
359  if peak.psfFitFlux is None:
360  peak.psfFitFlux = 0.0
361  if peak.psfFitCenter is None:
362  peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
363  self.log.warn("Peak failed. Using minimal default info for child.")
364  else:
365  continue
366 
367  assert(len(heavy.getPeaks()) == 1)
368 
369  src.set(self.deblendSkippedKey, False)
370  child = srcs.addNew(); nchild += 1
371  child.assign(heavy.getPeaks()[0], self.peakSchemaMapper)
372  child.setParent(src.getId())
373  child.setFootprint(heavy)
374  child.set(self.psfKey, peak.deblendedAsPsf)
375  child.set(self.hasStrayFluxKey, peak.strayFlux is not None)
376  if peak.deblendedAsPsf:
377  (cx,cy) = peak.psfFitCenter
378  child.set(self.psfCenterKey, afwGeom.Point2D(cx, cy))
379  child.set(self.psfFluxKey, peak.psfFitFlux)
380  child.set(self.deblendRampedTemplateKey, peak.hasRampedTemplate)
381  child.set(self.deblendPatchedTemplateKey, peak.patched)
382  kids.append(child)
383 
384  # Child footprints may extend beyond the full extent of their parent's which
385  # results in a failure of the replace-by-noise code to reinstate these pixels
386  # to their original values. The following updates the parent footprint
387  # in-place to ensure it contains the full union of itself and all of its
388  # children's footprints.
389  src.getFootprint().include([child.getFootprint() for child in kids])
390 
391  src.set(self.nChildKey, nchild)
392  self.calculateBlendedness(exposure.getMaskedImage(), src, kids)
393 
394  self.postSingleDeblendHook(exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res)
395  #print 'Deblending parent id', src.getId(), 'took', time.clock() - t0
396 
397 
398  n1 = len(srcs)
399  self.log.info('Deblended: of %i sources, %i were deblended, creating %i children, total %i sources'
400  % (n0, nparents, n1-n0, n1))
401 
402  def preSingleDeblendHook(self, exposure, srcs, i, fp, psf, psf_fwhm, sigma1):
403  pass
404 
405  def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
406  pass
407 
408  def isLargeFootprint(self, footprint):
409  """Returns whether a Footprint is large
410 
411  'Large' is defined by thresholds on the area, size and axis ratio.
412  These may be disabled independently by configuring them to be non-positive.
413 
414  This is principally intended to get rid of satellite streaks, which the
415  deblender or other downstream processing can have trouble dealing with
416  (e.g., multiple large HeavyFootprints can chew up memory).
417  """
418  if self.config.maxFootprintArea > 0 and footprint.getArea() > self.config.maxFootprintArea:
419  return True
420  if self.config.maxFootprintSize > 0:
421  bbox = footprint.getBBox()
422  if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
423  return True
424  if self.config.minFootprintAxisRatio > 0:
425  axes = afwEll.Axes(footprint.getShape())
426  if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
427  return True
428  return False
429 
430  def isMasked(self, footprint, mask):
431  """Returns whether the footprint violates the mask limits"""
432  size = float(footprint.getArea())
433  for maskName, limit in self.config.maskLimits.iteritems():
434  maskVal = mask.getPlaneBitMask(maskName)
435  unmasked = afwDet.Footprint(footprint)
436  unmasked.intersectMask(mask, maskVal) # footprint of unmasked pixels
437  if (size - unmasked.getArea())/size > limit:
438  return True
439  return False
440 
441  def skipParent(self, source, mask):
442  """Indicate that the parent source is not being deblended
443 
444  We set the appropriate flags and mask.
445 
446  @param source The source to flag as skipped
447  @param mask The mask to update
448  """
449  fp = source.getFootprint()
450  source.set(self.deblendSkippedKey, True)
451  source.set(self.nChildKey, len(fp.getPeaks())) # It would have this many if we deblended them all
452  if self.config.notDeblendedMask:
453  mask.addMaskPlane(self.config.notDeblendedMask)
454  afwDet.setMaskFromFootprint(mask, fp, mask.getPlaneBitMask(self.config.notDeblendedMask))
455 
456  def calculateBlendedness(self, maskedImage, parent, kids):
457  """Calculate the blendedness values for a blend family
458 
459  The blendedness is defined as:
460 
461  [heavy[i].dot(heavy[j]) for j in neighbors].sum() / heavy[i].dot(heavy[i])
462 
463  where 'heavy' is the heavy footprint representation of the flux.
464  """
465  bbox = parent.getFootprint().getBBox()
466  if not kids or bbox.isEmpty():
467  parent.set(self.blendednessKey, 0.0)
468  return
469 
470  def getHeavyFootprint(src):
471  """Provide the HeavyFootprint for a source"""
472  fp = src.getFootprint()
473  return (afwDet.HeavyFootprintF.cast(fp) if fp.isHeavy() else
474  afwDet.makeHeavyFootprint(fp, maskedImage))
475 
476  parentFoot = getHeavyFootprint(parent)
477  kidFeet = [getHeavyFootprint(src) for src in kids]
478 
479  def setBlendedness(src, foot):
480  """Calculate and set the blendedness value for a source, given its image"""
481  if foot.getBBox().isEmpty() or numpy.all(foot.getImageArray() == 0.0):
482  src.set(self.blendednessKey, 0.0)
483  return
484  srcId = src.getId()
485  numerator = sum(foot.dot(f) for k, f in zip(kids, kidFeet) if k.getId() != srcId)
486  denominator = foot.dot(foot)
487  src.set(self.blendednessKey, numerator/denominator)
488 
489  setBlendedness(parent, parentFoot)
490  for k, f in zip(kids, kidFeet):
491  setBlendedness(k, f)
A mapping between the keys of two Schemas, used to copy data between them.
Definition: SchemaMapper.h:19
boost::enable_if< typename ExpressionTraits< Scalar >::IsScalar, Scalar >::type sum(Scalar const &scalar)
Definition: operators.h:1250
Pass parameters to a Statistics objectA class to pass parameters which control how the stats are calc...
Definition: Statistics.h:92
A set of pixels in an Image.
Definition: Footprint.h:62
Split blended sources into individual sources.
Definition: deblend.py:132
MaskT setMaskFromFootprint(lsst::afw::image::Mask< MaskT > *mask, Footprint const &footprint, MaskT const bitmask)
OR bitmask into all the Mask&#39;s pixels that are in the Footprint.
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
Definition: Statistics.cc:1082
def __init__
Create the task, adding necessary fields to the given schema.
Definition: deblend.py:143
HeavyFootprint< ImagePixelT, MaskPixelT, VariancePixelT > makeHeavyFootprint(Footprint const &foot, lsst::afw::image::MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > const &img, HeavyFootprintCtrl const *ctrl=NULL)