26 import lsst.pex.config
as pexConfig
37 __all__ =
'SourceDeblendConfig',
'SourceDeblendTask'
42 edgeHandling = pexConfig.ChoiceField(
43 doc=
'What to do when a peak to be deblended is close to the edge of the image',
44 dtype=str, default=
'ramp',
46 'clip':
'Clip the template at the edge AND the mirror of the edge.',
47 'ramp':
'Ramp down flux at the image edge by the PSF',
48 'noclip':
'Ignore the edge when building the symmetric template.',
52 strayFluxToPointSources = pexConfig.ChoiceField(
53 doc=
'When the deblender should attribute stray flux to point sources',
54 dtype=str, default=
'necessary',
56 'necessary':
'When there is not an extended object in the footprint',
58 'never': (
'Never; stray flux will not be attributed to any deblended child '
59 'if the deblender thinks all peaks look like point sources'),
63 assignStrayFlux = pexConfig.Field(dtype=bool, default=
True,
64 doc=
'Assign stray flux (not claimed by any child in the deblender) '
65 'to deblend children.')
67 strayFluxRule = pexConfig.ChoiceField(
68 doc=
'How to split flux among peaks',
69 dtype=str, default=
'trim',
71 'r-to-peak':
'~ 1/(1+R^2) to the peak',
72 'r-to-footprint': (
'~ 1/(1+R^2) to the closest pixel in the footprint. '
73 'CAUTION: this can be computationally expensive on large footprints!'),
74 'nearest-footprint': (
'Assign 100% to the nearest footprint (using L-1 norm aka '
75 'Manhattan distance)'),
76 'trim': (
'Shrink the parent footprint to pixels that are not assigned to children')
80 clipStrayFluxFraction = pexConfig.Field(dtype=float, default=0.001,
81 doc=(
'When splitting stray flux, clip fractions below '
82 'this value to zero.'))
83 psfChisq1 = pexConfig.Field(dtype=float, default=1.5, optional=
False,
84 doc=(
'Chi-squared per DOF cut for deciding a source is '
85 'a PSF during deblending (un-shifted PSF model)'))
86 psfChisq2 = pexConfig.Field(dtype=float, default=1.5, optional=
False,
87 doc=(
'Chi-squared per DOF cut for deciding a source is '
88 'PSF during deblending (shifted PSF model)'))
89 psfChisq2b = pexConfig.Field(dtype=float, default=1.5, optional=
False,
90 doc=(
'Chi-squared per DOF cut for deciding a source is '
91 'a PSF during deblending (shifted PSF model #2)'))
92 maxNumberOfPeaks = pexConfig.Field(dtype=int, default=0,
93 doc=(
"Only deblend the brightest maxNumberOfPeaks peaks in the parent"
94 " (<= 0: unlimited)"))
95 maxFootprintArea = pexConfig.Field(dtype=int, default=1000000,
96 doc=(
"Maximum area for footprints before they are ignored as large; "
97 "non-positive means no threshold applied"))
98 maxFootprintSize = pexConfig.Field(dtype=int, default=0,
99 doc=(
"Maximum linear dimension for footprints before they are ignored "
100 "as large; non-positive means no threshold applied"))
101 minFootprintAxisRatio = pexConfig.Field(dtype=float, default=0.0,
102 doc=(
"Minimum axis ratio for footprints before they are ignored "
103 "as large; non-positive means no threshold applied"))
104 notDeblendedMask = pexConfig.Field(dtype=str, default=
"NOT_DEBLENDED", optional=
True,
105 doc=
"Mask name for footprints not deblended, or None")
107 tinyFootprintSize = pexConfig.RangeField(dtype=int, default=2, min=2, inclusiveMin=
True,
108 doc=(
'Footprints smaller in width or height than this value '
109 'will be ignored; minimum of 2 due to PSF gradient '
112 propagateAllPeaks = pexConfig.Field(dtype=bool, default=
False,
113 doc=(
'Guarantee that all peaks produce a child source.'))
114 catchFailures = pexConfig.Field(
115 dtype=bool, default=
False,
116 doc=(
"If True, catch exceptions thrown by the deblender, log them, "
117 "and set a flag on the parent, instead of letting them propagate up"))
118 maskPlanes = pexConfig.ListField(dtype=str, default=[
"SAT",
"INTRP",
"NO_DATA"],
119 doc=
"Mask planes to ignore when performing statistics")
120 maskLimits = pexConfig.DictField(
124 doc=(
"Mask planes with the corresponding limit on the fraction of masked pixels. "
125 "Sources violating this limit will not be deblended."),
127 weightTemplates = pexConfig.Field(
128 dtype=bool, default=
False,
129 doc=(
"If true, a least-squares fit of the templates will be done to the "
130 "full image. The templates will be re-weighted based on this fit."))
131 removeDegenerateTemplates = pexConfig.Field(dtype=bool, default=
False,
132 doc=(
"Try to remove similar templates?"))
133 maxTempDotProd = pexConfig.Field(
134 dtype=float, default=0.5,
135 doc=(
"If the dot product between two templates is larger than this value, we consider them to be "
136 "describing the same object (i.e. they are degenerate). If one of the objects has been "
137 "labeled as a PSF it will be removed, otherwise the template with the lowest value will "
139 medianSmoothTemplate = pexConfig.Field(dtype=bool, default=
True,
140 doc=
"Apply a smoothing filter to all of the template images")
152 \anchor SourceDeblendTask_
154 \brief Split blended sources into individual sources.
156 This task has no return value; it only modifies the SourceCatalog in-place.
158 ConfigClass = SourceDeblendConfig
159 _DefaultName =
"sourceDeblend"
161 def __init__(self, schema, peakSchema=None, **kwargs):
163 Create the task, adding necessary fields to the given schema.
165 @param[in,out] schema Schema object for measurement fields; will be modified in-place.
166 @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender.
167 Any fields beyond the PeakTable minimal schema will be transferred
168 to the main source Schema. If None, no fields will be transferred
170 @param[in] **kwargs Passed to Task.__init__.
172 pipeBase.Task.__init__(self, **kwargs)
175 if item.field.getName().startswith(
"merge_footprint")]
176 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
177 if peakSchema
is None:
183 for item
in peakSchema:
184 if item.key
not in peakMinimalSchema:
190 schema.addField(item.field)
191 assert schema == self.
peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas"
195 self.
nChildKey = schema.addField(
'deblend_nChild', type=np.int32,
196 doc=
'Number of children this object has (defaults to 0)')
197 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
198 doc=
'Deblender thought this source looked like a PSF')
199 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
200 'If deblended-as-psf, the PSF centroid',
"pixel")
201 self.
psfFluxKey = schema.addField(
'deblend_psf_instFlux', type=
'D',
202 doc=
'If deblended-as-psf, the instrumental PSF flux', units=
'count')
204 doc=
'Source had too many peaks; '
205 'only the brightest were included')
206 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
207 doc=
'Parent footprint covered too many pixels')
208 self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
209 doc=
'Parent footprint was predominantly masked')
211 if self.config.catchFailures:
213 doc=
"Deblending failed on source")
216 doc=
"Deblender skipped this source")
219 'deblend_rampedTemplate', type=
'Flag',
220 doc=(
'This source was near an image edge and the deblender used '
221 '"ramp" edge-handling.'))
224 'deblend_patchedTemplate', type=
'Flag',
225 doc=(
'This source was near an image edge and the deblender used '
226 '"patched" edge-handling.'))
229 'deblend_hasStrayFlux', type=
'Flag',
230 doc=(
'This source was assigned some stray flux'))
232 self.log.
trace(
'Added keys to schema: %s',
", ".join(str(x)
for x
in (
237 def run(self, exposure, sources):
239 Get the psf from the provided exposure and then run deblend().
241 @param[in] exposure Exposure to process
242 @param[in,out] sources SourceCatalog containing sources detected on this exposure.
246 psf = exposure.getPsf()
247 assert sources.getSchema() == self.
schema
248 self.
deblend(exposure, sources, psf)
250 def _getPsfFwhm(self, psf, bbox):
253 return psf.computeShape().getDeterminantRadius() * 2.35
260 @param[in] exposure Exposure to process
261 @param[in,out] srcs SourceCatalog containing sources detected on this exposure.
266 self.log.
info(
"Deblending %d sources" % len(srcs))
271 mi = exposure.getMaskedImage()
273 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
275 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
276 self.log.
trace(
'sigma1: %g', sigma1)
280 for i, src
in enumerate(srcs):
283 fp = src.getFootprint()
296 self.log.
warn(
'Parent %i: skipping large footprint (area: %i)',
297 int(src.getId()), int(fp.getArea()))
299 if self.
isMasked(fp, exposure.getMaskedImage().getMask()):
302 self.log.
warn(
'Parent %i: skipping masked footprint (area: %i)',
303 int(src.getId()), int(fp.getArea()))
310 self.log.
trace(
'Parent %i: deblending %i peaks', int(src.getId()), len(pks))
316 src.set(self.
tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
320 fp, mi, psf, psf_fwhm, sigma1=sigma1,
321 psfChisqCut1=self.config.psfChisq1,
322 psfChisqCut2=self.config.psfChisq2,
323 psfChisqCut2b=self.config.psfChisq2b,
324 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
325 strayFluxToPointSources=self.config.strayFluxToPointSources,
326 assignStrayFlux=self.config.assignStrayFlux,
327 strayFluxAssignment=self.config.strayFluxRule,
328 rampFluxAtEdge=(self.config.edgeHandling ==
'ramp'),
329 patchEdges=(self.config.edgeHandling ==
'noclip'),
330 tinyFootprintSize=self.config.tinyFootprintSize,
331 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
332 weightTemplates=self.config.weightTemplates,
333 removeDegenerateTemplates=self.config.removeDegenerateTemplates,
334 maxTempDotProd=self.config.maxTempDotProd,
335 medianSmoothTemplate=self.config.medianSmoothTemplate
337 if self.config.catchFailures:
339 except Exception
as e:
340 if self.config.catchFailures:
341 self.log.
warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
344 traceback.print_exc()
351 for j, peak
in enumerate(res.deblendedParents[0].peaks):
352 heavy = peak.getFluxPortion()
353 if heavy
is None or peak.skip:
355 if not self.config.propagateAllPeaks:
360 self.log.
trace(
"Peak at (%i,%i) failed. Using minimal default info for child.",
361 pks[j].getIx(), pks[j].getIy())
365 peakList = foot.getPeaks()
367 peakList.append(peak.peak)
368 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
370 if peak.deblendedAsPsf:
371 if peak.psfFitFlux
is None:
372 peak.psfFitFlux = 0.0
373 if peak.psfFitCenter
is None:
374 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
376 assert(len(heavy.getPeaks()) == 1)
379 child = srcs.addNew()
382 child.set(key, src.get(key))
384 child.setParent(src.getId())
385 child.setFootprint(heavy)
386 child.set(self.
psfKey, peak.deblendedAsPsf)
388 if peak.deblendedAsPsf:
389 (cx, cy) = peak.psfFitCenter
401 spans = src.getFootprint().spans
403 spans = spans.union(child.getFootprint().spans)
404 src.getFootprint().setSpans(spans)
412 self.log.
info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources'
413 % (n0, nparents, n1-n0, n1))
418 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
422 """Returns whether a Footprint is large
424 'Large' is defined by thresholds on the area, size and axis ratio.
425 These may be disabled independently by configuring them to be non-positive.
427 This is principally intended to get rid of satellite streaks, which the
428 deblender or other downstream processing can have trouble dealing with
429 (e.g., multiple large HeavyFootprints can chew up memory).
431 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
433 if self.config.maxFootprintSize > 0:
434 bbox = footprint.getBBox()
435 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
437 if self.config.minFootprintAxisRatio > 0:
438 axes = afwEll.Axes(footprint.getShape())
439 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
444 """Returns whether the footprint violates the mask limits"""
445 size = float(footprint.getArea())
446 for maskName, limit
in self.config.maskLimits.items():
447 maskVal = mask.getPlaneBitMask(maskName)
448 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal)
449 if (size - unmaskedSpan.getArea())/size > limit:
454 """Indicate that the parent source is not being deblended
456 We set the appropriate flags and mask.
458 @param source The source to flag as skipped
459 @param mask The mask to update
461 fp = source.getFootprint()
463 source.set(self.
nChildKey, len(fp.getPeaks()))
464 if self.config.notDeblendedMask:
465 mask.addMaskPlane(self.config.notDeblendedMask)
466 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))