LSSTApplications  11.0-13-gbb96280,12.1.rc1,12.1.rc1+1,12.1.rc1+2,12.1.rc1+5,12.1.rc1+8,12.1.rc1-1-g06d7636+1,12.1.rc1-1-g253890b+5,12.1.rc1-1-g3d31b68+7,12.1.rc1-1-g3db6b75+1,12.1.rc1-1-g5c1385a+3,12.1.rc1-1-g83b2247,12.1.rc1-1-g90cb4cf+6,12.1.rc1-1-g91da24b+3,12.1.rc1-2-g3521f8a,12.1.rc1-2-g39433dd+4,12.1.rc1-2-g486411b+2,12.1.rc1-2-g4c2be76,12.1.rc1-2-gc9c0491,12.1.rc1-2-gda2cd4f+6,12.1.rc1-3-g3391c73+2,12.1.rc1-3-g8c1bd6c+1,12.1.rc1-3-gcf4b6cb+2,12.1.rc1-4-g057223e+1,12.1.rc1-4-g19ed13b+2,12.1.rc1-4-g30492a7
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  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='trim',
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.001,
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.RangeField(dtype=int, default=2, min=2, inclusiveMin=True,
107  doc=('Footprints smaller in width or height than this value will '
108  'be ignored; minimum of 2 due to PSF gradient calculation.'))
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 
133 class SourceDeblendTask(pipeBase.Task):
134  """!
135  \anchor SourceDeblendTask_
136 
137  \brief Split blended sources into individual sources.
138 
139  This task has no return value; it only modifies the SourceCatalog in-place.
140  """
141  ConfigClass = SourceDeblendConfig
142  _DefaultName = "sourceDeblend"
143 
144  def __init__(self, schema, peakSchema=None, **kwargs):
145  """!
146  Create the task, adding necessary fields to the given schema.
147 
148  @param[in,out] schema Schema object for measurement fields; will be modified in-place.
149  @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender.
150  Any fields beyond the PeakTable minimal schema will be transferred
151  to the main source Schema. If None, no fields will be transferred
152  from the Peaks.
153  @param[in] **kwargs Passed to Task.__init__.
154  """
155  pipeBase.Task.__init__(self, **kwargs)
156  peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
157  if peakSchema is None:
158  # In this case, the peakSchemaMapper will transfer nothing, but we'll still have one
159  # to simplify downstream code
160  self.peakSchemaMapper = afwTable.SchemaMapper(peakMinimalSchema, schema)
161  else:
162  self.peakSchemaMapper = afwTable.SchemaMapper(peakSchema, schema)
163  for item in peakSchema:
164  if item.key not in peakMinimalSchema:
165  self.peakSchemaMapper.addMapping(item.key, item.field)
166  # Because SchemaMapper makes a copy of the output schema you give its ctor, it isn't
167  # updating this Schema in place. That's probably a design flaw, but in the meantime,
168  # we'll keep that schema in sync with the peakSchemaMapper.getOutputSchema() manually,
169  # by adding the same fields to both.
170  schema.addField(item.field)
171  assert schema == self.peakSchemaMapper.getOutputSchema(), "Logic bug mapping schemas"
172  self.addSchemaKeys(schema)
173 
174  def addSchemaKeys(self, schema):
175  self.nChildKey = schema.addField('deblend_nChild', type=int,
176  doc='Number of children this object has (defaults to 0)')
177  self.psfKey = schema.addField('deblend_deblendedAsPsf', type='Flag',
178  doc='Deblender thought this source looked like a PSF')
179  self.psfCenterKey = afwTable.Point2DKey.addFields(schema, 'deblend_psfCenter',
180  'If deblended-as-psf, the PSF centroid', "pixel")
181  self.psfFluxKey = schema.addField('deblend_psfFlux', type='D',
182  doc='If deblended-as-psf, the PSF flux')
183  self.tooManyPeaksKey = schema.addField('deblend_tooManyPeaks', type='Flag',
184  doc='Source had too many peaks; '
185  'only the brightest were included')
186  self.tooBigKey = schema.addField('deblend_parentTooBig', type='Flag',
187  doc='Parent footprint covered too many pixels')
188  self.maskedKey = schema.addField('deblend_masked', type='Flag',
189  doc='Parent footprint was predominantly masked')
190 
191  if self.config.catchFailures:
192  self.deblendFailedKey = schema.addField('deblend_failed', type='Flag',
193  doc="Deblending failed on source")
194 
195  self.deblendSkippedKey = schema.addField('deblend_skipped', type='Flag',
196  doc="Deblender skipped this source")
197 
198  self.deblendRampedTemplateKey = schema.addField(
199  'deblend_rampedTemplate', type='Flag',
200  doc=('This source was near an image edge and the deblender used '
201  '"ramp" edge-handling.'))
202 
203  self.deblendPatchedTemplateKey = schema.addField(
204  'deblend_patchedTemplate', type='Flag',
205  doc=('This source was near an image edge and the deblender used '
206  '"patched" edge-handling.'))
207 
208  self.hasStrayFluxKey = schema.addField(
209  'deblend_hasStrayFlux', type='Flag',
210  doc=('This source was assigned some stray flux'))
211 
212  self.log.trace('Added keys to schema: %s', ", ".join(str(x) for x in (
213  self.nChildKey, self.psfKey, self.psfCenterKey, self.psfFluxKey,
214  self.tooManyPeaksKey, self.tooBigKey)))
215 
216  @pipeBase.timeMethod
217  def run(self, exposure, sources):
218  """!
219  Get the psf from the provided exposure and then run deblend().
220 
221  @param[in] exposure Exposure to process
222  @param[in,out] sources SourceCatalog containing sources detected on this exposure.
223 
224  @return None
225  """
226  psf = exposure.getPsf()
227  self.deblend(exposure, sources, psf)
228 
229  def _getPsfFwhm(self, psf, bbox):
230  # It should be easier to get a PSF's fwhm;
231  # https://dev.lsstcorp.org/trac/ticket/3030
232  return psf.computeShape().getDeterminantRadius() * 2.35
233 
234  @pipeBase.timeMethod
235  def deblend(self, exposure, srcs, psf):
236  """!
237  Deblend.
238 
239  @param[in] exposure Exposure to process
240  @param[in,out] srcs SourceCatalog containing sources detected on this exposure.
241  @param[in] psf PSF
242 
243  @return None
244  """
245  self.log.info("Deblending %d sources" % len(srcs))
246 
247  from lsst.meas.deblender.baseline import deblend
248 
249  # find the median stdev in the image...
250  mi = exposure.getMaskedImage()
251  statsCtrl = afwMath.StatisticsControl()
252  statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
253  stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl)
254  sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
255  self.log.trace('sigma1: %g', sigma1)
256 
257  n0 = len(srcs)
258  nparents = 0
259  for i, src in enumerate(srcs):
260  #t0 = time.clock()
261 
262  fp = src.getFootprint()
263  pks = fp.getPeaks()
264 
265  # Since we use the first peak for the parent object, we should propagate its flags
266  # to the parent source.
267  src.assign(pks[0], self.peakSchemaMapper)
268 
269  if len(pks) < 2:
270  continue
271 
272  if self.isLargeFootprint(fp):
273  src.set(self.tooBigKey, True)
274  self.skipParent(src, mi.getMask())
275  self.log.trace('Parent %i: skipping large footprint', int(src.getId()))
276  continue
277  if self.isMasked(fp, exposure.getMaskedImage().getMask()):
278  src.set(self.maskedKey, True)
279  self.skipParent(src, mi.getMask())
280  self.log.trace('Parent %i: skipping masked footprint', int(src.getId()))
281  continue
282 
283  nparents += 1
284  bb = fp.getBBox()
285  psf_fwhm = self._getPsfFwhm(psf, bb)
286 
287  self.log.trace('Parent %i: deblending %i peaks', int(src.getId()), len(pks))
288 
289  self.preSingleDeblendHook(exposure, srcs, i, fp, psf, psf_fwhm, sigma1)
290  npre = len(srcs)
291 
292  # This should really be set in deblend, but deblend doesn't have access to the src
293  src.set(self.tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
294 
295  try:
296  res = deblend(
297  fp, mi, psf, psf_fwhm, sigma1=sigma1,
298  psfChisqCut1=self.config.psfChisq1,
299  psfChisqCut2=self.config.psfChisq2,
300  psfChisqCut2b=self.config.psfChisq2b,
301  maxNumberOfPeaks=self.config.maxNumberOfPeaks,
302  strayFluxToPointSources=self.config.strayFluxToPointSources,
303  assignStrayFlux=self.config.assignStrayFlux,
304  findStrayFlux=(self.config.assignStrayFlux or self.config.findStrayFlux),
305  strayFluxAssignment=self.config.strayFluxRule,
306  rampFluxAtEdge=(self.config.edgeHandling == 'ramp'),
307  patchEdges=(self.config.edgeHandling == 'noclip'),
308  tinyFootprintSize=self.config.tinyFootprintSize,
309  clipStrayFluxFraction=self.config.clipStrayFluxFraction,
310  )
311  if self.config.catchFailures:
312  src.set(self.deblendFailedKey, False)
313  except Exception as e:
314  if self.config.catchFailures:
315  self.log.warn("Unable to deblend source %d: %s" % (src.getId(), e))
316  src.set(self.deblendFailedKey, True)
317  import traceback
318  traceback.print_exc()
319  continue
320  else:
321  raise
322 
323  kids = []
324  nchild = 0
325  for j, peak in enumerate(res.peaks):
326  heavy = peak.getFluxPortion()
327  if heavy is None or peak.skip:
328  src.set(self.deblendSkippedKey, True)
329  if not self.config.propagateAllPeaks:
330  # Don't care
331  continue
332  # We need to preserve the peak: make sure we have enough info to create a minimal
333  # child src
334  self.log.trace("Peak at (%i,%i) failed. Using minimal default info for child.",
335  pks[j].getIx(), pks[j].getIy())
336  if heavy is None:
337  # copy the full footprint and strip out extra peaks
338  foot = afwDet.Footprint(src.getFootprint())
339  peakList = foot.getPeaks()
340  peakList.clear()
341  peakList.append(peak.peak)
342  zeroMimg = afwImage.MaskedImageF(foot.getBBox())
343  heavy = afwDet.makeHeavyFootprint(foot, zeroMimg)
344  if peak.deblendedAsPsf:
345  if peak.psfFitFlux is None:
346  peak.psfFitFlux = 0.0
347  if peak.psfFitCenter is None:
348  peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
349 
350  assert(len(heavy.getPeaks()) == 1)
351 
352  src.set(self.deblendSkippedKey, False)
353  child = srcs.addNew()
354  nchild += 1
355  child.assign(heavy.getPeaks()[0], self.peakSchemaMapper)
356  child.setParent(src.getId())
357  child.setFootprint(heavy)
358  child.set(self.psfKey, peak.deblendedAsPsf)
359  child.set(self.hasStrayFluxKey, peak.strayFlux is not None)
360  if peak.deblendedAsPsf:
361  (cx, cy) = peak.psfFitCenter
362  child.set(self.psfCenterKey, afwGeom.Point2D(cx, cy))
363  child.set(self.psfFluxKey, peak.psfFitFlux)
364  child.set(self.deblendRampedTemplateKey, peak.hasRampedTemplate)
365  child.set(self.deblendPatchedTemplateKey, peak.patched)
366  kids.append(child)
367 
368  # Child footprints may extend beyond the full extent of their parent's which
369  # results in a failure of the replace-by-noise code to reinstate these pixels
370  # to their original values. The following updates the parent footprint
371  # in-place to ensure it contains the full union of itself and all of its
372  # children's footprints.
373  src.getFootprint().include([child.getFootprint() for child in kids])
374 
375  src.set(self.nChildKey, nchild)
376 
377  self.postSingleDeblendHook(exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res)
378  #print 'Deblending parent id', src.getId(), 'took', time.clock() - t0
379 
380  n1 = len(srcs)
381  self.log.info('Deblended: of %i sources, %i were deblended, creating %i children, total %i sources'
382  % (n0, nparents, n1-n0, n1))
383 
384  def preSingleDeblendHook(self, exposure, srcs, i, fp, psf, psf_fwhm, sigma1):
385  pass
386 
387  def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
388  pass
389 
390  def isLargeFootprint(self, footprint):
391  """Returns whether a Footprint is large
392 
393  'Large' is defined by thresholds on the area, size and axis ratio.
394  These may be disabled independently by configuring them to be non-positive.
395 
396  This is principally intended to get rid of satellite streaks, which the
397  deblender or other downstream processing can have trouble dealing with
398  (e.g., multiple large HeavyFootprints can chew up memory).
399  """
400  if self.config.maxFootprintArea > 0 and footprint.getArea() > self.config.maxFootprintArea:
401  return True
402  if self.config.maxFootprintSize > 0:
403  bbox = footprint.getBBox()
404  if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
405  return True
406  if self.config.minFootprintAxisRatio > 0:
407  axes = afwEll.Axes(footprint.getShape())
408  if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
409  return True
410  return False
411 
412  def isMasked(self, footprint, mask):
413  """Returns whether the footprint violates the mask limits"""
414  size = float(footprint.getArea())
415  for maskName, limit in self.config.maskLimits.items():
416  maskVal = mask.getPlaneBitMask(maskName)
417  unmasked = afwDet.Footprint(footprint)
418  unmasked.intersectMask(mask, maskVal) # footprint of unmasked pixels
419  if (size - unmasked.getArea())/size > limit:
420  return True
421  return False
422 
423  def skipParent(self, source, mask):
424  """Indicate that the parent source is not being deblended
425 
426  We set the appropriate flags and mask.
427 
428  @param source The source to flag as skipped
429  @param mask The mask to update
430  """
431  fp = source.getFootprint()
432  source.set(self.deblendSkippedKey, True)
433  source.set(self.nChildKey, len(fp.getPeaks())) # It would have this many if we deblended them all
434  if self.config.notDeblendedMask:
435  mask.addMaskPlane(self.config.notDeblendedMask)
436  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:217
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:133
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:144
HeavyFootprint< ImagePixelT, MaskPixelT, VariancePixelT > makeHeavyFootprint(Footprint const &foot, lsst::afw::image::MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > const &img, HeavyFootprintCtrl const *ctrl=NULL)