lsst.meas.deblender  15.0-2-g35685a8+1
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 as np
24 import time
25 
26 import lsst.log
27 import lsst.pex.config as pexConfig
28 import lsst.pipe.base as pipeBase
29 import lsst.afw.math as afwMath
30 import lsst.afw.geom as afwGeom
31 import lsst.afw.geom.ellipses as afwEll
32 import lsst.afw.image as afwImage
33 import lsst.afw.detection as afwDet
34 import lsst.afw.table as afwTable
35 
36 logger = lsst.log.Log.getLogger("meas.deblender.deblend")
37 
38 __all__ = 'SourceDeblendConfig', 'SourceDeblendTask', 'MultibandDeblendConfig', 'MultibandDeblendTask'
39 
40 
41 class SourceDeblendConfig(pexConfig.Config):
42 
43  edgeHandling = pexConfig.ChoiceField(
44  doc='What to do when a peak to be deblended is close to the edge of the image',
45  dtype=str, default='ramp',
46  allowed={
47  'clip': 'Clip the template at the edge AND the mirror of the edge.',
48  'ramp': 'Ramp down flux at the image edge by the PSF',
49  'noclip': 'Ignore the edge when building the symmetric template.',
50  }
51  )
52 
53  strayFluxToPointSources = pexConfig.ChoiceField(
54  doc='When the deblender should attribute stray flux to point sources',
55  dtype=str, default='necessary',
56  allowed={
57  'necessary': 'When there is not an extended object in the footprint',
58  'always': 'Always',
59  'never': ('Never; stray flux will not be attributed to any deblended child '
60  'if the deblender thinks all peaks look like point sources'),
61  }
62  )
63 
64  assignStrayFlux = pexConfig.Field(dtype=bool, default=True,
65  doc='Assign stray flux (not claimed by any child in the deblender) '
66  'to deblend children.')
67 
68  strayFluxRule = pexConfig.ChoiceField(
69  doc='How to split flux among peaks',
70  dtype=str, default='trim',
71  allowed={
72  'r-to-peak': '~ 1/(1+R^2) to the peak',
73  'r-to-footprint': ('~ 1/(1+R^2) to the closest pixel in the footprint. '
74  'CAUTION: this can be computationally expensive on large footprints!'),
75  'nearest-footprint': ('Assign 100% to the nearest footprint (using L-1 norm aka '
76  'Manhattan distance)'),
77  'trim': ('Shrink the parent footprint to pixels that are not assigned to children')
78  }
79  )
80 
81  clipStrayFluxFraction = pexConfig.Field(dtype=float, default=0.001,
82  doc=('When splitting stray flux, clip fractions below '
83  'this value to zero.'))
84  psfChisq1 = pexConfig.Field(dtype=float, default=1.5, optional=False,
85  doc=('Chi-squared per DOF cut for deciding a source is '
86  'a PSF during deblending (un-shifted PSF model)'))
87  psfChisq2 = pexConfig.Field(dtype=float, default=1.5, optional=False,
88  doc=('Chi-squared per DOF cut for deciding a source is '
89  'PSF during deblending (shifted PSF model)'))
90  psfChisq2b = pexConfig.Field(dtype=float, default=1.5, optional=False,
91  doc=('Chi-squared per DOF cut for deciding a source is '
92  'a PSF during deblending (shifted PSF model #2)'))
93  maxNumberOfPeaks = pexConfig.Field(dtype=int, default=0,
94  doc=("Only deblend the brightest maxNumberOfPeaks peaks in the parent"
95  " (<= 0: unlimited)"))
96  maxFootprintArea = pexConfig.Field(dtype=int, default=1000000,
97  doc=("Maximum area for footprints before they are ignored as large; "
98  "non-positive means no threshold applied"))
99  maxFootprintSize = pexConfig.Field(dtype=int, default=0,
100  doc=("Maximum linear dimension for footprints before they are ignored "
101  "as large; non-positive means no threshold applied"))
102  minFootprintAxisRatio = pexConfig.Field(dtype=float, default=0.0,
103  doc=("Minimum axis ratio for footprints before they are ignored "
104  "as large; non-positive means no threshold applied"))
105  notDeblendedMask = pexConfig.Field(dtype=str, default="NOT_DEBLENDED", optional=True,
106  doc="Mask name for footprints not deblended, or None")
107 
108  tinyFootprintSize = pexConfig.RangeField(dtype=int, default=2, min=2, inclusiveMin=True,
109  doc=('Footprints smaller in width or height than this value will '
110  'be ignored; minimum of 2 due to PSF gradient calculation.'))
111 
112  propagateAllPeaks = pexConfig.Field(dtype=bool, default=False,
113  doc=('Guarantee that all peaks produce a child source.'))
114  catchFailures = pexConfig.Field(dtype=bool, default=False,
115  doc=("If True, catch exceptions thrown by the deblender, log them, "
116  "and set a flag on the parent, instead of letting them propagate up"))
117  maskPlanes = pexConfig.ListField(dtype=str, default=["SAT", "INTRP", "NO_DATA"],
118  doc="Mask planes to ignore when performing statistics")
119  maskLimits = pexConfig.DictField(
120  keytype=str,
121  itemtype=float,
122  default={},
123  doc=("Mask planes with the corresponding limit on the fraction of masked pixels. "
124  "Sources violating this limit will not be deblended."),
125  )
126  weightTemplates = pexConfig.Field(dtype=bool, default=False,
127  doc=("If true, a least-squares fit of the templates will be done to the "
128  "full image. The templates will be re-weighted based on this fit."))
129  removeDegenerateTemplates = pexConfig.Field(dtype=bool, default=False,
130  doc=("Try to remove similar templates?"))
131  maxTempDotProd = pexConfig.Field(dtype=float, default=0.5,
132  doc=("If the dot product between two templates is larger than this value"
133  ", we consider them to be describing the same object (i.e. they are "
134  "degenerate). If one of the objects has been labeled as a PSF it "
135  "will be removed, otherwise the template with the lowest value will "
136  "be removed."))
137  medianSmoothTemplate = pexConfig.Field(dtype=bool, default=True,
138  doc="Apply a smoothing filter to all of the template images")
139 
140 
146 
147 
148 class SourceDeblendTask(pipeBase.Task):
149  """!
150  \anchor SourceDeblendTask_
151 
152  \brief Split blended sources into individual sources.
153 
154  This task has no return value; it only modifies the SourceCatalog in-place.
155  """
156  ConfigClass = SourceDeblendConfig
157  _DefaultName = "sourceDeblend"
158 
159  def __init__(self, schema, peakSchema=None, **kwargs):
160  """!
161  Create the task, adding necessary fields to the given schema.
162 
163  @param[in,out] schema Schema object for measurement fields; will be modified in-place.
164  @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender.
165  Any fields beyond the PeakTable minimal schema will be transferred
166  to the main source Schema. If None, no fields will be transferred
167  from the Peaks.
168  @param[in] **kwargs Passed to Task.__init__.
169  """
170  pipeBase.Task.__init__(self, **kwargs)
171  peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
172  if peakSchema is None:
173  # In this case, the peakSchemaMapper will transfer nothing, but we'll still have one
174  # to simplify downstream code
175  self.peakSchemaMapper = afwTable.SchemaMapper(peakMinimalSchema, schema)
176  else:
177  self.peakSchemaMapper = afwTable.SchemaMapper(peakSchema, schema)
178  for item in peakSchema:
179  if item.key not in peakMinimalSchema:
180  self.peakSchemaMapper.addMapping(item.key, item.field)
181  # Because SchemaMapper makes a copy of the output schema you give its ctor, it isn't
182  # updating this Schema in place. That's probably a design flaw, but in the meantime,
183  # we'll keep that schema in sync with the peakSchemaMapper.getOutputSchema() manually,
184  # by adding the same fields to both.
185  schema.addField(item.field)
186  assert schema == self.peakSchemaMapper.getOutputSchema(), "Logic bug mapping schemas"
187  self.addSchemaKeys(schema)
188 
189  def addSchemaKeys(self, schema):
190  self.nChildKey = schema.addField('deblend_nChild', type=np.int32,
191  doc='Number of children this object has (defaults to 0)')
192  self.psfKey = schema.addField('deblend_deblendedAsPsf', type='Flag',
193  doc='Deblender thought this source looked like a PSF')
194  self.psfCenterKey = afwTable.Point2DKey.addFields(schema, 'deblend_psfCenter',
195  'If deblended-as-psf, the PSF centroid', "pixel")
196  self.psfFluxKey = schema.addField('deblend_psfFlux', type='D',
197  doc='If deblended-as-psf, the PSF flux')
198  self.tooManyPeaksKey = schema.addField('deblend_tooManyPeaks', type='Flag',
199  doc='Source had too many peaks; '
200  'only the brightest were included')
201  self.tooBigKey = schema.addField('deblend_parentTooBig', type='Flag',
202  doc='Parent footprint covered too many pixels')
203  self.maskedKey = schema.addField('deblend_masked', type='Flag',
204  doc='Parent footprint was predominantly masked')
205 
206  if self.config.catchFailures:
207  self.deblendFailedKey = schema.addField('deblend_failed', type='Flag',
208  doc="Deblending failed on source")
209 
210  self.deblendSkippedKey = schema.addField('deblend_skipped', type='Flag',
211  doc="Deblender skipped this source")
212 
213  self.deblendRampedTemplateKey = schema.addField(
214  'deblend_rampedTemplate', type='Flag',
215  doc=('This source was near an image edge and the deblender used '
216  '"ramp" edge-handling.'))
217 
218  self.deblendPatchedTemplateKey = schema.addField(
219  'deblend_patchedTemplate', type='Flag',
220  doc=('This source was near an image edge and the deblender used '
221  '"patched" edge-handling.'))
222 
223  self.hasStrayFluxKey = schema.addField(
224  'deblend_hasStrayFlux', type='Flag',
225  doc=('This source was assigned some stray flux'))
226 
227  self.log.trace('Added keys to schema: %s', ", ".join(str(x) for x in (
228  self.nChildKey, self.psfKey, self.psfCenterKey, self.psfFluxKey,
229  self.tooManyPeaksKey, self.tooBigKey)))
230 
231  @pipeBase.timeMethod
232  def run(self, exposure, sources):
233  """!
234  Get the psf from the provided exposure and then run deblend().
235 
236  @param[in] exposure Exposure to process
237  @param[in,out] sources SourceCatalog containing sources detected on this exposure.
238 
239  @return None
240  """
241  psf = exposure.getPsf()
242  self.deblend(exposure, sources, psf)
243 
244  def _getPsfFwhm(self, psf, bbox):
245  # It should be easier to get a PSF's fwhm;
246  # https://dev.lsstcorp.org/trac/ticket/3030
247  return psf.computeShape().getDeterminantRadius() * 2.35
248 
249  @pipeBase.timeMethod
250  def deblend(self, exposure, srcs, psf):
251  """!
252  Deblend.
253 
254  @param[in] exposure Exposure to process
255  @param[in,out] srcs SourceCatalog containing sources detected on this exposure.
256  @param[in] psf PSF
257 
258  @return None
259  """
260  self.log.info("Deblending %d sources" % len(srcs))
261 
262  from lsst.meas.deblender.baseline import deblend
263 
264  # find the median stdev in the image...
265  mi = exposure.getMaskedImage()
266  statsCtrl = afwMath.StatisticsControl()
267  statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
268  stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl)
269  sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
270  self.log.trace('sigma1: %g', sigma1)
271 
272  n0 = len(srcs)
273  nparents = 0
274  for i, src in enumerate(srcs):
275  #t0 = time.clock()
276 
277  fp = src.getFootprint()
278  pks = fp.getPeaks()
279 
280  # Since we use the first peak for the parent object, we should propagate its flags
281  # to the parent source.
282  src.assign(pks[0], self.peakSchemaMapper)
283 
284  if len(pks) < 2:
285  continue
286 
287  if self.isLargeFootprint(fp):
288  src.set(self.tooBigKey, True)
289  self.skipParent(src, mi.getMask())
290  self.log.trace('Parent %i: skipping large footprint', int(src.getId()))
291  continue
292  if self.isMasked(fp, exposure.getMaskedImage().getMask()):
293  src.set(self.maskedKey, True)
294  self.skipParent(src, mi.getMask())
295  self.log.trace('Parent %i: skipping masked footprint', int(src.getId()))
296  continue
297 
298  nparents += 1
299  bb = fp.getBBox()
300  psf_fwhm = self._getPsfFwhm(psf, bb)
301 
302  self.log.trace('Parent %i: deblending %i peaks', int(src.getId()), len(pks))
303 
304  self.preSingleDeblendHook(exposure, srcs, i, fp, psf, psf_fwhm, sigma1)
305  npre = len(srcs)
306 
307  # This should really be set in deblend, but deblend doesn't have access to the src
308  src.set(self.tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
309 
310  try:
311  res = deblend(
312  fp, mi, psf, psf_fwhm, sigma1=sigma1,
313  psfChisqCut1=self.config.psfChisq1,
314  psfChisqCut2=self.config.psfChisq2,
315  psfChisqCut2b=self.config.psfChisq2b,
316  maxNumberOfPeaks=self.config.maxNumberOfPeaks,
317  strayFluxToPointSources=self.config.strayFluxToPointSources,
318  assignStrayFlux=self.config.assignStrayFlux,
319  strayFluxAssignment=self.config.strayFluxRule,
320  rampFluxAtEdge=(self.config.edgeHandling == 'ramp'),
321  patchEdges=(self.config.edgeHandling == 'noclip'),
322  tinyFootprintSize=self.config.tinyFootprintSize,
323  clipStrayFluxFraction=self.config.clipStrayFluxFraction,
324  weightTemplates=self.config.weightTemplates,
325  removeDegenerateTemplates=self.config.removeDegenerateTemplates,
326  maxTempDotProd=self.config.maxTempDotProd,
327  medianSmoothTemplate=self.config.medianSmoothTemplate
328  )
329  if self.config.catchFailures:
330  src.set(self.deblendFailedKey, False)
331  except Exception as e:
332  if self.config.catchFailures:
333  self.log.warn("Unable to deblend source %d: %s" % (src.getId(), e))
334  src.set(self.deblendFailedKey, True)
335  import traceback
336  traceback.print_exc()
337  continue
338  else:
339  raise
340 
341  kids = []
342  nchild = 0
343  for j, peak in enumerate(res.deblendedParents[0].peaks):
344  heavy = peak.getFluxPortion()
345  if heavy is None or peak.skip:
346  src.set(self.deblendSkippedKey, True)
347  if not self.config.propagateAllPeaks:
348  # Don't care
349  continue
350  # We need to preserve the peak: make sure we have enough info to create a minimal
351  # child src
352  self.log.trace("Peak at (%i,%i) failed. Using minimal default info for child.",
353  pks[j].getIx(), pks[j].getIy())
354  if heavy is None:
355  # copy the full footprint and strip out extra peaks
356  foot = afwDet.Footprint(src.getFootprint())
357  peakList = foot.getPeaks()
358  peakList.clear()
359  peakList.append(peak.peak)
360  zeroMimg = afwImage.MaskedImageF(foot.getBBox())
361  heavy = afwDet.makeHeavyFootprint(foot, zeroMimg)
362  if peak.deblendedAsPsf:
363  if peak.psfFitFlux is None:
364  peak.psfFitFlux = 0.0
365  if peak.psfFitCenter is None:
366  peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
367 
368  assert(len(heavy.getPeaks()) == 1)
369 
370  src.set(self.deblendSkippedKey, False)
371  child = srcs.addNew()
372  nchild += 1
373  child.assign(heavy.getPeaks()[0], self.peakSchemaMapper)
374  child.setParent(src.getId())
375  child.setFootprint(heavy)
376  child.set(self.psfKey, peak.deblendedAsPsf)
377  child.set(self.hasStrayFluxKey, peak.strayFlux is not None)
378  if peak.deblendedAsPsf:
379  (cx, cy) = peak.psfFitCenter
380  child.set(self.psfCenterKey, afwGeom.Point2D(cx, cy))
381  child.set(self.psfFluxKey, peak.psfFitFlux)
382  child.set(self.deblendRampedTemplateKey, peak.hasRampedTemplate)
383  child.set(self.deblendPatchedTemplateKey, peak.patched)
384  kids.append(child)
385 
386  # Child footprints may extend beyond the full extent of their parent's which
387  # results in a failure of the replace-by-noise code to reinstate these pixels
388  # to their original values. The following updates the parent footprint
389  # in-place to ensure it contains the full union of itself and all of its
390  # children's footprints.
391  spans = src.getFootprint().spans
392  for child in kids:
393  spans = spans.union(child.getFootprint().spans)
394  src.getFootprint().setSpans(spans)
395 
396  src.set(self.nChildKey, nchild)
397 
398  self.postSingleDeblendHook(exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res)
399  #print 'Deblending parent id', src.getId(), 'took', time.clock() - t0
400 
401  n1 = len(srcs)
402  self.log.info('Deblended: of %i sources, %i were deblended, creating %i children, total %i sources'
403  % (n0, nparents, n1-n0, n1))
404 
405  def preSingleDeblendHook(self, exposure, srcs, i, fp, psf, psf_fwhm, sigma1):
406  pass
407 
408  def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
409  pass
410 
411  def isLargeFootprint(self, footprint):
412  """Returns whether a Footprint is large
413 
414  'Large' is defined by thresholds on the area, size and axis ratio.
415  These may be disabled independently by configuring them to be non-positive.
416 
417  This is principally intended to get rid of satellite streaks, which the
418  deblender or other downstream processing can have trouble dealing with
419  (e.g., multiple large HeavyFootprints can chew up memory).
420  """
421  if self.config.maxFootprintArea > 0 and footprint.getArea() > self.config.maxFootprintArea:
422  return True
423  if self.config.maxFootprintSize > 0:
424  bbox = footprint.getBBox()
425  if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
426  return True
427  if self.config.minFootprintAxisRatio > 0:
428  axes = afwEll.Axes(footprint.getShape())
429  if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
430  return True
431  return False
432 
433  def isMasked(self, footprint, mask):
434  """Returns whether the footprint violates the mask limits"""
435  size = float(footprint.getArea())
436  for maskName, limit in self.config.maskLimits.items():
437  maskVal = mask.getPlaneBitMask(maskName)
438  unmaskedSpan = footprint.spans.intersectNot(mask, maskVal) # spanset of unmasked pixels
439  if (size - unmaskedSpan.getArea())/size > limit:
440  return True
441  return False
442 
443  def skipParent(self, source, mask):
444  """Indicate that the parent source is not being deblended
445 
446  We set the appropriate flags and mask.
447 
448  @param source The source to flag as skipped
449  @param mask The mask to update
450  """
451  fp = source.getFootprint()
452  source.set(self.deblendSkippedKey, True)
453  source.set(self.nChildKey, len(fp.getPeaks())) # It would have this many if we deblended them all
454  if self.config.notDeblendedMask:
455  mask.addMaskPlane(self.config.notDeblendedMask)
456  fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
457 
458 class MultibandDeblendConfig(pexConfig.Config):
459  """MultibandDeblendConfig
460 
461  Configuration for the multiband deblender.
462  The parameters are organized by the parameter types, which are
463  - Stopping Criteria: Used to determine if the fit has converged
464  - Position Fitting Criteria: Used to fit the positions of the peaks
465  - Constraints: Used to apply constraints to the peaks and their components
466  - Other: Parameters that don't fit into the above categories
467  """
468  # Stopping Criteria
469  maxIter = pexConfig.Field(dtype=int, default=200,
470  doc=("Maximum number of iterations to deblend a single parent"))
471  relativeError = pexConfig.Field(dtype=float, default=1e-3,
472  doc=("Relative error to use when determining stopping criteria"))
473 
474  # Blend Configuration options
475  minTranslation = pexConfig.Field(dtype=float, default=1e-3,
476  doc=("A peak must be updated by at least 'minTranslation' (pixels)"
477  "or no update is performed."
478  "This field is ignored if fitPositions is False."))
479  refinementSkip = pexConfig.Field(dtype=int, default=10,
480  doc=("If fitPositions is True, the positions and box sizes are"
481  "updated on every 'refinementSkip' iterations."))
482  translationMethod = pexConfig.Field(dtype=str, default="default",
483  doc=("Method to use for fitting translations."
484  "Currently 'default' is the only available option,"
485  "which performs a linear fit, but it is possible that we"
486  "will use galsim or some other method as a future option"))
487  edgeFluxThresh = pexConfig.Field(dtype=float, default=1.0,
488  doc=("Boxes are resized when the flux at an edge is "
489  "> edgeFluxThresh * background RMS"))
490  exactLipschitz = pexConfig.Field(dtype=bool, default=False,
491  doc=("Calculate exact Lipschitz constant in every step"
492  "(True) or only calculate the approximate"
493  "Lipschitz constant with significant changes in A,S"
494  "(False)"))
495  stepSlack = pexConfig.Field(dtype=float, default=0.2,
496  doc=("A fractional measure of how much a value (like the exactLipschitz)"
497  "can change before it needs to be recalculated."
498  "This must be between 0 and 1."))
499 
500  # Constraints
501  constraints = pexConfig.Field(dtype=str, default="1,+,S,M",
502  doc=("List of constraints to use for each object"
503  "(order does not matter)"
504  "Current options are all used by default:\n"
505  "S: symmetry\n"
506  "M: monotonicity\n"
507  "1: normalized SED to unity"
508  "+: non-negative morphology"))
509  symmetryThresh = pexConfig.Field(dtype=float, default=1.0,
510  doc=("Strictness of symmetry, from"
511  "0 (no symmetry enforced) to"
512  "1 (perfect symmetry required)."
513  "If 'S' is not in `constraints`, this argument is ignored"))
514  l0Thresh = pexConfig.Field(dtype=float, default=np.nan,
515  doc=("L0 threshold. NaN results in no L0 penalty."))
516  l1Thresh = pexConfig.Field(dtype=float, default=np.nan,
517  doc=("L1 threshold. NaN results in no L1 penalty."))
518  tvxThresh = pexConfig.Field(dtype=float, default=np.nan,
519  doc=("Threshold for TV (total variation) constraint in the x-direction."
520  "NaN results in no TVx penalty."))
521  tvyThresh = pexConfig.Field(dtype=float, default=np.nan,
522  doc=("Threshold for TV (total variation) constraint in the y-direction."
523  "NaN results in no TVy penalty."))
524 
525  # Other scarlet paremeters
526  useWeights = pexConfig.Field(dtype=bool, default=False, doc="Use inverse variance as deblender weights")
527  bgScale = pexConfig.Field(dtype=float, default=0.5,
528  doc=("Fraction of background RMS level to use as a"
529  "cutoff for defining the background of the image"
530  "This is used to initialize the model for each source"
531  "and to set the size of the bounding box for each source"
532  "every `refinementSkip` iteration."))
533  usePsfConvolution = pexConfig.Field(dtype=bool, default=True,
534  doc=("Whether or not to convolve the morphology with the"
535  "PSF in each band or use the same morphology"
536  "in all bands"))
537  saveTemplates = pexConfig.Field(dtype=bool, default=True,
538  doc="Whether or not to save the SEDs and templates")
539  processSingles = pexConfig.Field(dtype=bool, default=False,
540  doc="Whether or not to process isolated sources in the deblender")
541  badMask = pexConfig.Field(dtype=str, default="BAD,CR,NO_DATA,SAT,SUSPECT",
542  doc="Whether or not to process isolated sources in the deblender")
543  # Old deblender parameters used in this implementation (some of which might be removed later)
544 
545  maxNumberOfPeaks = pexConfig.Field(dtype=int, default=0,
546  doc=("Only deblend the brightest maxNumberOfPeaks peaks in the parent"
547  " (<= 0: unlimited)"))
548  maxFootprintArea = pexConfig.Field(dtype=int, default=1000000,
549  doc=("Maximum area for footprints before they are ignored as large; "
550  "non-positive means no threshold applied"))
551  maxFootprintSize = pexConfig.Field(dtype=int, default=0,
552  doc=("Maximum linear dimension for footprints before they are ignored "
553  "as large; non-positive means no threshold applied"))
554  minFootprintAxisRatio = pexConfig.Field(dtype=float, default=0.0,
555  doc=("Minimum axis ratio for footprints before they are ignored "
556  "as large; non-positive means no threshold applied"))
557  notDeblendedMask = pexConfig.Field(dtype=str, default="NOT_DEBLENDED", optional=True,
558  doc="Mask name for footprints not deblended, or None")
559 
560  tinyFootprintSize = pexConfig.RangeField(dtype=int, default=2, min=2, inclusiveMin=True,
561  doc=('Footprints smaller in width or height than this value will '
562  'be ignored; minimum of 2 due to PSF gradient calculation.'))
563  catchFailures = pexConfig.Field(dtype=bool, default=False,
564  doc=("If True, catch exceptions thrown by the deblender, log them, "
565  "and set a flag on the parent, instead of letting them propagate up"))
566  propagateAllPeaks = pexConfig.Field(dtype=bool, default=False,
567  doc=('Guarantee that all peaks produce a child source.'))
568  maskPlanes = pexConfig.ListField(dtype=str, default=["SAT", "INTRP", "NO_DATA"],
569  doc="Mask planes to ignore when performing statistics")
570  maskLimits = pexConfig.DictField(
571  keytype=str,
572  itemtype=float,
573  default={},
574  doc=("Mask planes with the corresponding limit on the fraction of masked pixels. "
575  "Sources violating this limit will not be deblended."),
576  )
577 
578  edgeHandling = pexConfig.ChoiceField(
579  doc='What to do when a peak to be deblended is close to the edge of the image',
580  dtype=str, default='ramp',
581  allowed={
582  'clip': 'Clip the template at the edge AND the mirror of the edge.',
583  'ramp': 'Ramp down flux at the image edge by the PSF',
584  'noclip': 'Ignore the edge when building the symmetric template.',
585  }
586  )
587 
588  medianSmoothTemplate = pexConfig.Field(dtype=bool, default=False,
589  doc="Apply a smoothing filter to all of the template images")
590  medianFilterHalfsize = pexConfig.Field(dtype=float, default=2,
591  doc=('Half size of the median smoothing filter'))
592  clipFootprintToNonzero = pexConfig.Field(dtype=bool, default=True,
593  doc=("Clip non-zero spans in the footprints"))
594 
595  conserveFlux = pexConfig.Field(dtype=bool, default=False,
596  doc=("Reapportion flux to the footprints so that flux is conserved"))
597  weightTemplates = pexConfig.Field(dtype=bool, default=False,
598  doc=("If true, a least-squares fit of the templates will be done to the "
599  "full image. The templates will be re-weighted based on this fit."))
600  strayFluxToPointSources = pexConfig.ChoiceField(
601  doc='When the deblender should attribute stray flux to point sources',
602  dtype=str, default='necessary',
603  allowed={
604  'necessary': 'When there is not an extended object in the footprint',
605  'always': 'Always',
606  'never': ('Never; stray flux will not be attributed to any deblended child '
607  'if the deblender thinks all peaks look like point sources'),
608  }
609  )
610 
611  assignStrayFlux = pexConfig.Field(dtype=bool, default=True,
612  doc='Assign stray flux (not claimed by any child in the deblender) '
613  'to deblend children.')
614 
615  strayFluxRule = pexConfig.ChoiceField(
616  doc='How to split flux among peaks',
617  dtype=str, default='trim',
618  allowed={
619  'r-to-peak': '~ 1/(1+R^2) to the peak',
620  'r-to-footprint': ('~ 1/(1+R^2) to the closest pixel in the footprint. '
621  'CAUTION: this can be computationally expensive on large footprints!'),
622  'nearest-footprint': ('Assign 100% to the nearest footprint (using L-1 norm aka '
623  'Manhattan distance)'),
624  'trim': ('Shrink the parent footprint to pixels that are not assigned to children')
625  }
626  )
627 
628  clipStrayFluxFraction = pexConfig.Field(dtype=float, default=0.001,
629  doc=('When splitting stray flux, clip fractions below '
630  'this value to zero.'))
631  getTemplateSum = pexConfig.Field(dtype=bool, default=False,
632  doc=("As part of the flux calculation, the sum of the templates is"
633  "calculated. If 'getTemplateSum==True' then the sum of the"
634  "templates is stored in the result (a 'PerFootprint')."))
635 
636 class MultibandDeblendTask(pipeBase.Task):
637  """MultibandDeblendTask
638 
639  Split blended sources into individual sources.
640 
641  This task has no return value; it only modifies the SourceCatalog in-place.
642  """
643  ConfigClass = MultibandDeblendConfig
644  _DefaultName = "multibandDeblend"
645 
646  def __init__(self, schema, peakSchema=None, **kwargs):
647  """Create the task, adding necessary fields to the given schema.
648 
649  Parameters
650  ----------
651  schema: `lsst.afw.table.schema.schema.Schema`
652  Schema object for measurement fields; will be modified in-place.
653  peakSchema: `lsst.afw.table.schema.schema.Schema`
654  Schema of Footprint Peaks that will be passed to the deblender.
655  Any fields beyond the PeakTable minimal schema will be transferred
656  to the main source Schema. If None, no fields will be transferred
657  from the Peaks.
658  bands: list of str
659  Names of the filters used for the eposures. This is needed to store the SED as a field
660  **kwargs
661  Passed to Task.__init__.
662  """
663  from lsst.meas.deblender import plugins
664  import scarlet
665 
666  pipeBase.Task.__init__(self, **kwargs)
667  if not self.config.conserveFlux and not self.config.saveTemplates:
668  raise ValueError("Either `conserveFlux` or `saveTemplates` must be True")
669 
670  peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
671  if peakSchema is None:
672  # In this case, the peakSchemaMapper will transfer nothing, but we'll still have one
673  # to simplify downstream code
674  self.peakSchemaMapper = afwTable.SchemaMapper(peakMinimalSchema, schema)
675  else:
676  self.peakSchemaMapper = afwTable.SchemaMapper(peakSchema, schema)
677  for item in peakSchema:
678  if item.key not in peakMinimalSchema:
679  self.peakSchemaMapper.addMapping(item.key, item.field)
680  # Because SchemaMapper makes a copy of the output schema you give its ctor, it isn't
681  # updating this Schema in place. That's probably a design flaw, but in the meantime,
682  # we'll keep that schema in sync with the peakSchemaMapper.getOutputSchema() manually,
683  # by adding the same fields to both.
684  schema.addField(item.field)
685  assert schema == self.peakSchemaMapper.getOutputSchema(), "Logic bug mapping schemas"
686  self._addSchemaKeys(schema)
687 
688  # Create the plugins for multiband deblending using the Config options
689 
690  # Basic deblender configuration
691  config = scarlet.config.Config(
692  center_min_dist=self.config.minTranslation,
693  edge_flux_thresh=self.config.edgeFluxThresh,
694  exact_lipschitz=self.config.exactLipschitz,
695  refine_skip=self.config.refinementSkip,
696  slack=self.config.stepSlack,
697  )
698  if self.config.translationMethod != "default":
699  err = "Currently the only supported translationMethod is 'default', you entered '{0}'"
700  raise NotImplementedError(err.format(self.config.translationMethod))
701 
702  # If the default constraints are not used, set the constraints for
703  # all of the sources
704  constraints = None
705  _constraints = self.config.constraints.split(",")
706  if (sorted(_constraints) != ['+', '1', 'M', 'S']
707  or ~np.isnan(self.config.l0Thresh)
708  or ~np.isnan(self.config.l1Thresh)
709  ):
710  constraintDict = {
711  "+": scarlet.constraints.PositivityConstraint,
712  "1": scarlet.constraints.SimpleConstraint,
713  "M": scarlet.constraints.DirectMonotonicityConstraint(use_nearest=False),
714  "S": scarlet.constraints.DirectSymmetryConstraint(sigma=self.config.symmetryThresh)
715  }
716  for c in _constraints:
717  if constraints is None:
718  constraints = constraintDict[c]
719  else:
720  constraints = constraints & constraintDict[c]
721  if constraints is None:
722  constraints = scarlet.constraints.MinimalConstraint()
723  if ~np.isnan(self.config.l0Thresh):
724  constraints = constraints & scarlet.constraints.L0Constraint(self.config.l0Thresh)
725  if ~np.isnan(self.config.l1Thresh):
726  constraints = constraints & scarlet.constraints.L1Constraint(self.config.l1Thresh)
727  if ~np.isnan(self.config.tvxThresh):
728  constraints = constraints & scarlet.constraints.TVxConstraint(self.config.tvxThresh)
729  if ~np.isnan(self.config.tvyThresh):
730  constraints = constraints & scarlet.constraints.TVyConstraint(self.config.tvyThresh)
731 
732  multiband_plugin = plugins.DeblenderPlugin(
733  plugins.buildMultibandTemplates,
734  useWeights=self.config.useWeights,
735  usePsf=self.config.usePsfConvolution,
736  constraints=constraints,
737  config=config,
738  maxIter=self.config.maxIter,
739  bgScale=self.config.bgScale,
740  relativeError=self.config.relativeError,
741  badMask=self.config.badMask.split(","),
742  )
743  self.plugins = [multiband_plugin]
744 
745  # Plugins from the old deblender for post-template processing
746  # (see lsst.meas_deblender.baseline.deblend)
747  patchEdges = self.config.edgeHandling == 'noclip'
748  if self.config.edgeHandling == 'ramp':
749  self.plugins.append(plugins.DeblenderPlugin(plugins.rampFluxAtEdge, patchEdges=False))
750  if self.config.medianSmoothTemplate:
751  self.plugins.append(plugins.DeblenderPlugin(plugins.medianSmoothTemplates,
752  medianFilterHalfsize=self.config.medianFilterHalfsize))
753  if self.config.clipFootprintToNonzero:
754  self.plugins.append(plugins.DeblenderPlugin(plugins.clipFootprintsToNonzero))
755  if self.config.conserveFlux:
756  if self.config.weightTemplates:
757  self.plugins.append(plugins.DeblenderPlugin(plugins.weightTemplates))
758  self.plugins.append(plugins.DeblenderPlugin(plugins.apportionFlux,
759  clipStrayFluxFraction=self.config.clipStrayFluxFraction,
760  assignStrayFlux=self.config.assignStrayFlux,
761  strayFluxAssignment=self.config.strayFluxRule,
762  strayFluxToPointSources=self.config.strayFluxToPointSources,
763  getTemplateSum=self.config.getTemplateSum))
764 
765 
766  def _addSchemaKeys(self, schema):
767  """Add deblender specific keys to the schema
768  """
769  self.runtimeKey = schema.addField('runtime', type=np.float32, doc='runtime in ms')
770  # Keys from old Deblender that might be kept in the new deblender
771  self.nChildKey = schema.addField('deblend_nChild', type=np.int32,
772  doc='Number of children this object has (defaults to 0)')
773  self.psfKey = schema.addField('deblend_deblendedAsPsf', type='Flag',
774  doc='Deblender thought this source looked like a PSF')
775  self.tooManyPeaksKey = schema.addField('deblend_tooManyPeaks', type='Flag',
776  doc='Source had too many peaks; '
777  'only the brightest were included')
778  self.tooBigKey = schema.addField('deblend_parentTooBig', type='Flag',
779  doc='Parent footprint covered too many pixels')
780  self.maskedKey = schema.addField('deblend_masked', type='Flag',
781  doc='Parent footprint was predominantly masked')
782  self.deblendFailedKey = schema.addField('deblend_failed', type='Flag',
783  doc="Deblending failed on source")
784 
785  self.deblendSkippedKey = schema.addField('deblend_skipped', type='Flag',
786  doc="Deblender skipped this source")
787 
788  # Keys from the old Deblender that are likely to be removed for the new deblender
789  # TODO: Remove these if they remain unused
790  self.psfCenterKey = afwTable.Point2DKey.addFields(schema, 'deblend_psfCenter',
791  'If deblended-as-psf, the PSF centroid', "pixel")
792  self.psfFluxKey = schema.addField('deblend_psfFlux', type='D',
793  doc='If deblended-as-psf, the PSF flux')
794  self.deblendRampedTemplateKey = schema.addField(
795  'deblend_rampedTemplate', type='Flag',
796  doc=('This source was near an image edge and the deblender used '
797  '"ramp" edge-handling.'))
798 
799  self.deblendPatchedTemplateKey = schema.addField(
800  'deblend_patchedTemplate', type='Flag',
801  doc=('This source was near an image edge and the deblender used '
802  '"patched" edge-handling.'))
803 
804  self.hasStrayFluxKey = schema.addField(
805  'deblend_hasStrayFlux', type='Flag',
806  doc=('This source was assigned some stray flux'))
807 
808  self.log.trace('Added keys to schema: %s', ", ".join(str(x) for x in (
809  self.nChildKey, self.psfKey, self.psfCenterKey, self.psfFluxKey,
810  self.tooManyPeaksKey, self.tooBigKey)))
811 
812  @pipeBase.timeMethod
813  def run(self, exposures, sources):
814  """Get the psf from each exposure and then run deblend().
815 
816  Parameters
817  ----------
818  exposures: dict
819  Keys of the dict are the names of the filters and values are
820  `lsst.afw.image.exposure.exposure.ExposureF`'s.
821  The exposures should be co-added images of the same
822  shape and region of the sky.
823  sources: dict
824  Keys are the names of the filters and the values are
825  `lsst.afw.table.source.source.SourceCatalog`'s, which
826  should be a merged catalog of the sources in each band.
827 
828  Returns
829  -------
830  flux_catalogs: dict or None
831  Keys are the names of the filters and the values are
832  `lsst.afw.table.source.source.SourceCatalog`'s.
833  These are the flux-conserved catalogs with heavy footprints with
834  the image data weighted by the multiband templates.
835  If `self.config.conserveFlux` is `False`, then this item will be None
836  template_catalogs: dict or None
837  Keys are the names of the filters and the values are
838  `lsst.afw.table.source.source.SourceCatalog`'s.
839  These are catalogs with heavy footprints that are the templates
840  created by the multiband templates.
841  If `self.config.saveTemplates` is `False`, then this item will be None
842  """
843  psfs = {B:exp.getPsf() for B, exp in exposures.items()}
844  return self.deblend(exposures, sources, psfs)
845 
846  def _getPsfFwhm(self, psf, bbox):
847  return psf.computeShape().getDeterminantRadius() * 2.35
848 
849  def _addChild(self, parentId, peak, sources, heavy):
850  """Add a child to a catalog
851 
852  This creates a new child in the source catalog,
853  assigning it a parent id, adding a footprint,
854  and setting all appropriate flags based on the
855  deblender result.
856  """
857  assert len(heavy.getPeaks())==1
858  src = sources.addNew()
859  src.assign(heavy.getPeaks()[0], self.peakSchemaMapper)
860  src.setParent(parentId)
861  src.setFootprint(heavy)
862  src.set(self.psfKey, peak.deblendedAsPsf)
863  src.set(self.hasStrayFluxKey, peak.strayFlux is not None)
864  src.set(self.deblendRampedTemplateKey, peak.hasRampedTemplate)
865  src.set(self.deblendPatchedTemplateKey, peak.patched)
866  src.set(self.runtimeKey, 0)
867  return src
868 
869  @pipeBase.timeMethod
870  def deblend(self, exposures, sources, psfs, bands=None):
871  """Deblend a data cube of multiband images
872 
873  Parameters
874  ----------
875  exposures: dict
876  Keys of the dict are the names of the filters and values are
877  `lsst.afw.image.exposure.exposure.ExposureF`'s.
878  The exposures should be co-added images of the same
879  shape and region of the sky.
880  sources: dict
881  Keys are the names of the filters and the values are
882  `lsst.afw.table.source.source.SourceCatalog`'s, which
883  should be a merged catalog of the sources in each band ('deepCoadd_mergeDet').
884  psfs: dict
885  bands: list, default=None
886  Names of the bands in the deblender.
887  If `bands` is `None`, the keys of `exposures` are used.
888  Either `bands` should be specified or `exposures` should be an
889  `OrderedDict` to set the preferential order of the filters.
890 
891  Returns
892  -------
893  flux_catalogs: dict or None
894  Keys are the names of the filters and the values are
895  `lsst.afw.table.source.source.SourceCatalog`'s.
896  These are the flux-conserved catalogs with heavy footprints with
897  the image data weighted by the multiband templates.
898  If `self.config.conserveFlux` is `False`, then this item will be None
899  template_catalogs: dict or None
900  Keys are the names of the filters and the values are
901  `lsst.afw.table.source.source.SourceCatalog`'s.
902  These are catalogs with heavy footprints that are the templates
903  created by the multiband templates.
904  If `self.config.saveTemplates` is `False`, then this item will be None
905  """
906  from lsst.meas.deblender.baseline import newDeblend
907  import deblender
908 
909  if bands is None:
910  bands = list(exposures.keys())
911  maskedImages = {band:exp.getMaskedImage() for band, exp in exposures.items()}
912  self.log.info("Deblending {0} sources in {1} exposures".format(len(sources), len(bands)))
913 
914  # find the median stdev in each image
915  sigmas = {}
916  for f, exposure in exposures.items():
917  mi = exposure.getMaskedImage()
918  statsCtrl = afwMath.StatisticsControl()
919  statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
920  stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl)
921  sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
922  self.log.trace('Exposure {0}, sigma1: {1}'.format(f, sigma1))
923  sigmas[f] = sigma1
924 
925  # Create the output catalogs
926  if self.config.conserveFlux:
927  flux_catalogs = {band:afwTable.SourceCatalog(sources.clone()) for band in bands}
928  else:
929  flux_catalogs = None
930  if self.config.saveTemplates:
931  template_catalogs = {band:afwTable.SourceCatalog(sources.clone()) for band in bands}
932  else:
933  template_catalogs = None
934 
935  n0 = len(sources)
936  nparents = 0
937  maskedImages = {band: exp.getMaskedImage() for band, exp in exposures.items()}
938  for pk, src in enumerate(sources):
939  foot = src.getFootprint()
940  logger.info("id: {0}".format(src["id"]))
941  peaks = foot.getPeaks()
942 
943  # Since we use the first peak for the parent object, we should propagate its flags
944  # to the parent source.
945  src.assign(peaks[0], self.peakSchemaMapper)
946 
947  # Block of Skipping conditions
948  if len(peaks) < 2 and not self.config.processSingles:
949  for band in bands:
950  if self.config.saveTemplates:
951  tsrc = template_catalogs[band].addNew()
952  tsrc.assign(src)
953  tsrc.set(self.runtimeKey, 0)
954  templateParents[band] = tsrc
955  if self.config.conserveFlux:
956  tsrc = flux_catalogs[band].addNew()
957  tsrc.assign(src)
958  tsrc.set(self.runtimeKey, 0)
959  fluxParents[band] = tsrc
960  continue
961  if self.isLargeFootprint(foot):
962  src.set(self.tooBigKey, True)
963  self.skipParent(src, [mi.getMask() for mi in maskedImages])
964  self.log.trace('Parent %i: skipping large footprint', int(src.getId()))
965  continue
966  if self.isMasked(foot, exposure.getMaskedImage().getMask()):
967  src.set(self.maskedKey, True)
968  self.skipParent(src, mi.getMask())
969  self.log.trace('Parent %i: skipping masked footprint', int(src.getId()))
970  continue
971  if len(peaks) > self.config.maxNumberOfPeaks:
972  src.set(self.tooManyPeaksKey, True)
973  msg = 'Parent {0}: Too many peaks, using the first {1} peaks'
974  self.log.trace(msg.format(int(src.getId()), self.config.maxNumberOfPeaks))
975 
976  nparents += 1
977  bbox = foot.getBBox()
978  psf_fwhms = {band:self._getPsfFwhm(psf, bbox) for band, psf in psfs.items()}
979  self.log.trace('Parent %i: deblending %i peaks', int(src.getId()), len(peaks))
980  self.preSingleDeblendHook(exposures, sources, pk, foot, psfs, psf_fwhms, sigmas)
981  npre = len(sources)
982  # Run the deblender
983  try:
984  t0=time.time()
985  PARENT = afwImage.PARENT
986  # Build the parameter lists with the same ordering
987  images = [maskedImages[band].Factory(maskedImages[band], bbox, PARENT)
988  for band in bands]
989  psf_list = [psfs[band] for band in bands]
990  fwhm_list = [psf_fwhms[band] for band in bands]
991  avgNoise = [sigmas[band] for band in bands]
992 
993  result = newDeblend(debPlugins=self.plugins,
994  footprint=foot,
995  maskedImages=images,
996  psfs=psf_list,
997  psfFwhms=fwhm_list,
998  filters=bands,
999  avgNoise=avgNoise,
1000  maxNumberOfPeaks=self.config.maxNumberOfPeaks
1001  )
1002  tf=time.time()
1003  runtime = (tf-t0)*1000
1004  if result.failed:
1005  src.set(self.deblendFailedKey, False)
1006  src.set(self.runtimeKey, 0)
1007  continue
1008  except Exception as e:
1009  if self.config.catchFailures:
1010  self.log.warn("Unable to deblend source %d: %s" % (src.getId(), e))
1011  src.set(self.deblendFailedKey, True)
1012  src.set(self.runtimeKey, 0)
1013  import traceback
1014  traceback.print_exc()
1015  continue
1016  else:
1017  raise
1018 
1019  # Add the merged source as a parent in the catalog for each band
1020  templateParents = {}
1021  fluxParents = {}
1022  parentId = src.getId()
1023  for band in bands:
1024  if self.config.saveTemplates:
1025  tsrc = template_catalogs[band].addNew()
1026  tsrc.assign(src)
1027  tsrc.set("id", parentId)
1028  tsrc.set(self.runtimeKey, runtime)
1029  _fp = afwDet.Footprint()
1030  _fp.setPeakSchema(src.getFootprint().getPeaks().getSchema())
1031  tsrc.setFootprint(_fp)
1032  templateParents[band] = tsrc
1033  if self.config.conserveFlux:
1034  tsrc = flux_catalogs[band].addNew()
1035  tsrc.assign(src)
1036  tsrc.set(self.runtimeKey, runtime)
1037  tsrc.set("id", parentId)
1038  _fp = afwDet.Footprint()
1039  _fp.setPeakSchema(src.getFootprint().getPeaks().getSchema())
1040  tsrc.setFootprint(_fp)
1041  fluxParents[band] = tsrc
1042 
1043  # Add each source to the catalogs in each band
1044  templateSpans = {band:afwGeom.SpanSet() for band in bands}
1045  fluxSpans = {band:afwGeom.SpanSet() for band in bands}
1046  nchild = 0
1047  for j, multiPeak in enumerate(result.peaks):
1048  heavy = {band:peak.getFluxPortion() for band, peak in multiPeak.deblendedPeaks.items()}
1049  no_flux = all([v is None for v in heavy.values()])
1050  skip_peak = all([peak.skip for peak in multiPeak.deblendedPeaks.values()])
1051  if no_flux or skip_peak:
1052  src.set(self.deblendSkippedKey, True)
1053  if not self.config.propagateAllPeaks:
1054  # We don't care
1055  continue
1056  # We need to preserve the peak: make sure we have enough info to create a minimal
1057  # child src
1058  msg = "Peak at {0} failed deblending. Using minimal default info for child."
1059  self.log.trace(msg.format(multiPeak.x, multiPeak.y))
1060 
1061  # copy the full footprint and strip out extra peaks
1062  pfoot = afwDet.Footprint(foot)
1063  peakList = pfoot.getPeaks()
1064  peakList.clear()
1065  pfoot.addPeak(multiPeak.x, multiPeak.y, 0)
1066  zeroMimg = afwImage.MaskedImageF(pfoot.getBBox())
1067  for band in bands:
1068  heavy[band] = afwDet.makeHeavyFootprint(pfoot, zeroMimg)
1069  else:
1070  src.set(self.deblendSkippedKey, False)
1071 
1072  # Add the peak to the source catalog in each band
1073  for band in bands:
1074  if len(heavy[band].getPeaks()) != 1:
1075  raise ValueError("Heavy footprint has multiple peaks, expected 1")
1076  peak = multiPeak.deblendedPeaks[band]
1077  if self.config.saveTemplates:
1078  cat = template_catalogs[band]
1079  tfoot = peak.templateFootprint
1080  timg = afwImage.MaskedImageF(peak.templateImage)
1081  tHeavy = afwDet.makeHeavyFootprint(tfoot, timg)
1082  child = self._addChild(parentId, peak, cat, tHeavy)
1083  if parentId==0:
1084  child.setId(src.getId())
1085  child.set(self.runtimeKey, runtime)
1086  else:
1087  _peak = tHeavy.getPeaks()[0]
1088  templateParents[band].getFootprint().addPeak(_peak.getFx(), _peak.getFy(),
1089  _peak.getPeakValue())
1090  templateSpans[band] = templateSpans[band].union(tHeavy.getSpans())
1091  if self.config.conserveFlux:
1092  cat = flux_catalogs[band]
1093  child = self._addChild(parentId, peak, cat, heavy[band])
1094  if parentId==0:
1095  child.setId(src.getId())
1096  child.set(self.runtimeKey, runtime)
1097  else:
1098  _peak = heavy[band].getPeaks()[0]
1099  fluxParents[band].getFootprint().addPeak(_peak.getFx(), _peak.getFy(),
1100  _peak.getPeakValue())
1101  fluxSpans[band] = fluxSpans[band].union(heavy[band].getSpans())
1102  nchild += 1
1103 
1104  # Child footprints may extend beyond the full extent of their parent's which
1105  # results in a failure of the replace-by-noise code to reinstate these pixels
1106  # to their original values. The following updates the parent footprint
1107  # in-place to ensure it contains the full union of itself and all of its
1108  # children's footprints.
1109  for band in bands:
1110  if self.config.saveTemplates:
1111  templateParents[band].set(self.nChildKey, nchild)
1112  templateParents[band].getFootprint().setSpans(templateSpans[band])
1113  if self.config.conserveFlux:
1114  fluxParents[band].set(self.nChildKey, nchild)
1115  fluxParents[band].getFootprint().setSpans(fluxSpans[band])
1116 
1117  self.postSingleDeblendHook(exposure, flux_catalogs, template_catalogs,
1118  pk, npre, foot, psfs, psf_fwhms, sigmas, result)
1119 
1120  if flux_catalogs is not None:
1121  n1 = len(list(flux_catalogs.values())[0])
1122  else:
1123  n1 = len(list(template_catalogs.values())[0])
1124  self.log.info('Deblended: of %i sources, %i were deblended, creating %i children, total %i sources'
1125  % (n0, nparents, n1-n0, n1))
1126  return flux_catalogs, template_catalogs
1127 
1128  def preSingleDeblendHook(self, exposures, sources, pk, fp, psfs, psf_fwhms, sigmas):
1129  pass
1130 
1131  def postSingleDeblendHook(self, exposures, flux_catalogs, template_catalogs,
1132  pk, npre, fp, psfs, psf_fwhms, sigmas, result):
1133  pass
1134 
1135  def isLargeFootprint(self, footprint):
1136  """Returns whether a Footprint is large
1137 
1138  'Large' is defined by thresholds on the area, size and axis ratio.
1139  These may be disabled independently by configuring them to be non-positive.
1140 
1141  This is principally intended to get rid of satellite streaks, which the
1142  deblender or other downstream processing can have trouble dealing with
1143  (e.g., multiple large HeavyFootprints can chew up memory).
1144  """
1145  if self.config.maxFootprintArea > 0 and footprint.getArea() > self.config.maxFootprintArea:
1146  return True
1147  if self.config.maxFootprintSize > 0:
1148  bbox = footprint.getBBox()
1149  if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
1150  return True
1151  if self.config.minFootprintAxisRatio > 0:
1152  axes = afwEll.Axes(footprint.getShape())
1153  if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
1154  return True
1155  return False
1156 
1157  def isMasked(self, footprint, mask):
1158  """Returns whether the footprint violates the mask limits"""
1159  size = float(footprint.getArea())
1160  for maskName, limit in self.config.maskLimits.items():
1161  maskVal = mask.getPlaneBitMask(maskName)
1162  unmaskedSpan = footprint.spans.intersectNot(mask, maskVal) # spanset of unmasked pixels
1163  if (size - unmaskedSpan.getArea())/size > limit:
1164  return True
1165  return False
1166 
1167  def skipParent(self, source, masks):
1168  """Indicate that the parent source is not being deblended
1169 
1170  We set the appropriate flags and masks for each exposure.
1171 
1172  Parameters
1173  ----------
1174  source: `lsst.afw.table.source.source.SourceRecord`
1175  The source to flag as skipped
1176  masks: list of `lsst.afw.image.mask.mask.MaskX`
1177  The mask in each band to update with the non-detection
1178  """
1179  fp = source.getFootprint()
1180  source.set(self.deblendSkippedKey, True)
1181  source.set(self.nChildKey, len(fp.getPeaks())) # It would have this many if we deblended them all
1182  if self.config.notDeblendedMask:
1183  for mask in masks:
1184  mask.addMaskPlane(self.config.notDeblendedMask)
1185  fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
def _addChild(self, parentId, peak, sources, heavy)
Definition: deblend.py:849
def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res)
Definition: deblend.py:408
def preSingleDeblendHook(self, exposure, srcs, i, fp, psf, psf_fwhm, sigma1)
Definition: deblend.py:405
def run(self, exposure, sources)
Get the psf from the provided exposure and then run deblend().
Definition: deblend.py:232
def postSingleDeblendHook(self, exposures, flux_catalogs, template_catalogs, pk, npre, fp, psfs, psf_fwhms, sigmas, result)
Definition: deblend.py:1132
def __init__(self, schema, peakSchema=None, kwargs)
Definition: deblend.py:646
def preSingleDeblendHook(self, exposures, sources, pk, fp, psfs, psf_fwhms, sigmas)
Definition: deblend.py:1128
static Log getLogger(std::string const &loggername)
Split blended sources into individual sources.
Definition: deblend.py:148
def deblend(self, exposure, srcs, psf)
Deblend.
Definition: deblend.py:250
def newDeblend(debPlugins, footprint, maskedImages, psfs, psfFwhms, filters=None, log=None, verbose=False, avgNoise=None, maxNumberOfPeaks=0)
Definition: baseline.py:680
def deblend(self, exposures, sources, psfs, bands=None)
Definition: deblend.py:870
def __init__(self, schema, peakSchema=None, kwargs)
Create the task, adding necessary fields to the given schema.
Definition: deblend.py:159
def isMasked(self, footprint, mask)
Definition: deblend.py:433