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