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 (
235 self.
peakCenter = afwTable.Point2IKey.addFields(schema, name=
"deblend_peak_center",
236 doc=
"Center used to apply constraints in scarlet",
238 self.
peakIdKey = schema.addField(
"deblend_peakId", type=np.int32,
239 doc=
"ID of the peak in the parent footprint. "
240 "This is not unique, but the combination of 'parent'"
241 "and 'peakId' should be for all child sources. "
242 "Top level blends with no parents have 'peakId=0'")
243 self.
nPeaksKey = schema.addField(
"deblend_nPeaks", type=np.int32,
244 doc=
"Number of initial peaks in the blend. "
245 "This includes peaks that may have been culled "
246 "during deblending or failed to deblend")
248 doc=
"Same as deblend_n_peaks, but the number of peaks "
249 "in the parent footprint")
252 def run(self, exposure, sources):
254 Get the psf from the provided exposure and then run deblend().
256 @param[in] exposure Exposure to process
257 @param[in,out] sources SourceCatalog containing sources detected on this exposure.
261 psf = exposure.getPsf()
262 assert sources.getSchema() == self.
schema
263 self.
deblend(exposure, sources, psf)
265 def _getPsfFwhm(self, psf, bbox):
268 return psf.computeShape().getDeterminantRadius() * 2.35
275 @param[in] exposure Exposure to process
276 @param[in,out] srcs SourceCatalog containing sources detected on this exposure.
281 self.log.
info(
"Deblending %d sources" % len(srcs))
286 mi = exposure.getMaskedImage()
288 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
290 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
291 self.log.
trace(
'sigma1: %g', sigma1)
295 for i, src
in enumerate(srcs):
298 fp = src.getFootprint()
311 self.log.
warn(
'Parent %i: skipping large footprint (area: %i)',
312 int(src.getId()), int(fp.getArea()))
314 if self.
isMasked(fp, exposure.getMaskedImage().getMask()):
317 self.log.
warn(
'Parent %i: skipping masked footprint (area: %i)',
318 int(src.getId()), int(fp.getArea()))
325 self.log.
trace(
'Parent %i: deblending %i peaks', int(src.getId()), len(pks))
331 src.set(self.
tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
335 fp, mi, psf, psf_fwhm, sigma1=sigma1,
336 psfChisqCut1=self.config.psfChisq1,
337 psfChisqCut2=self.config.psfChisq2,
338 psfChisqCut2b=self.config.psfChisq2b,
339 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
340 strayFluxToPointSources=self.config.strayFluxToPointSources,
341 assignStrayFlux=self.config.assignStrayFlux,
342 strayFluxAssignment=self.config.strayFluxRule,
343 rampFluxAtEdge=(self.config.edgeHandling ==
'ramp'),
344 patchEdges=(self.config.edgeHandling ==
'noclip'),
345 tinyFootprintSize=self.config.tinyFootprintSize,
346 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
347 weightTemplates=self.config.weightTemplates,
348 removeDegenerateTemplates=self.config.removeDegenerateTemplates,
349 maxTempDotProd=self.config.maxTempDotProd,
350 medianSmoothTemplate=self.config.medianSmoothTemplate
352 if self.config.catchFailures:
354 except Exception
as e:
355 if self.config.catchFailures:
356 self.log.
warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
359 traceback.print_exc()
366 for j, peak
in enumerate(res.deblendedParents[0].peaks):
367 heavy = peak.getFluxPortion()
368 if heavy
is None or peak.skip:
370 if not self.config.propagateAllPeaks:
375 self.log.
trace(
"Peak at (%i,%i) failed. Using minimal default info for child.",
376 pks[j].getIx(), pks[j].getIy())
380 peakList = foot.getPeaks()
382 peakList.append(peak.peak)
383 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
385 if peak.deblendedAsPsf:
386 if peak.psfFitFlux
is None:
387 peak.psfFitFlux = 0.0
388 if peak.psfFitCenter
is None:
389 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
391 assert(len(heavy.getPeaks()) == 1)
394 child = srcs.addNew()
397 child.set(key, src.get(key))
399 child.setParent(src.getId())
400 child.setFootprint(heavy)
401 child.set(self.
psfKey, peak.deblendedAsPsf)
403 if peak.deblendedAsPsf:
404 (cx, cy) = peak.psfFitCenter
416 child.set(self.
peakIdKey, pks[j].getId())
430 spans = src.getFootprint().spans
432 spans = spans.union(child.getFootprint().spans)
433 src.getFootprint().setSpans(spans)
441 self.log.
info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources'
442 % (n0, nparents, n1-n0, n1))
447 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
451 """Returns whether a Footprint is large
453 'Large' is defined by thresholds on the area, size and axis ratio.
454 These may be disabled independently by configuring them to be non-positive.
456 This is principally intended to get rid of satellite streaks, which the
457 deblender or other downstream processing can have trouble dealing with
458 (e.g., multiple large HeavyFootprints can chew up memory).
460 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
462 if self.config.maxFootprintSize > 0:
463 bbox = footprint.getBBox()
464 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
466 if self.config.minFootprintAxisRatio > 0:
467 axes = afwEll.Axes(footprint.getShape())
468 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
473 """Returns whether the footprint violates the mask limits"""
474 size = float(footprint.getArea())
475 for maskName, limit
in self.config.maskLimits.items():
476 maskVal = mask.getPlaneBitMask(maskName)
477 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal)
478 if (size - unmaskedSpan.getArea())/size > limit:
483 """Indicate that the parent source is not being deblended
485 We set the appropriate flags and mask.
487 @param source The source to flag as skipped
488 @param mask The mask to update
490 fp = source.getFootprint()
492 if self.config.notDeblendedMask:
493 mask.addMaskPlane(self.config.notDeblendedMask)
494 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
498 centerX = int(bbox.getMinX()+bbox.getWidth()/2)
499 centerY = int(bbox.getMinY()+bbox.getHeight()/2)
505 source.set(self.
nPeaksKey, len(fp.peaks))