34 __all__ =
'SourceDeblendConfig',
'SourceDeblendTask' 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',
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.',
49 strayFluxToPointSources = pexConf.ChoiceField(
50 doc=
'When the deblender should attribute stray flux to point sources',
51 dtype=str, default=
'necessary',
53 'necessary':
'When there is not an extended object in the footprint',
55 'never': (
'Never; stray flux will not be attributed to any deblended child ' 56 'if the deblender thinks all peaks look like point sources'),
60 assignStrayFlux = pexConf.Field(dtype=bool, default=
True,
61 doc=
'Assign stray flux (not claimed by any child in the deblender) ' 62 'to deblend children.')
64 strayFluxRule = pexConf.ChoiceField(
65 doc=
'How to split flux among peaks',
66 dtype=str, default=
'trim',
68 'r-to-peak':
'~ 1/(1+R^2) to the peak',
69 'r-to-footprint': (
'~ 1/(1+R^2) to the closest pixel in the footprint. ' 70 'CAUTION: this can be computationally expensive on large footprints!'),
71 'nearest-footprint': (
'Assign 100% to the nearest footprint (using L-1 norm aka ' 72 'Manhattan distance)'),
73 'trim': (
'Shrink the parent footprint to pixels that are not assigned to children')
77 clipStrayFluxFraction = pexConf.Field(dtype=float, default=0.001,
78 doc=(
'When splitting stray flux, clip fractions below ' 79 'this value to zero.'))
80 psfChisq1 = pexConf.Field(dtype=float, default=1.5, optional=
False,
81 doc=(
'Chi-squared per DOF cut for deciding a source is ' 82 'a PSF during deblending (un-shifted PSF model)'))
83 psfChisq2 = pexConf.Field(dtype=float, default=1.5, optional=
False,
84 doc=(
'Chi-squared per DOF cut for deciding a source is ' 85 'PSF during deblending (shifted PSF model)'))
86 psfChisq2b = pexConf.Field(dtype=float, default=1.5, optional=
False,
87 doc=(
'Chi-squared per DOF cut for deciding a source is ' 88 'a PSF during deblending (shifted PSF model #2)'))
89 maxNumberOfPeaks = pexConf.Field(dtype=int, default=0,
90 doc=(
"Only deblend the brightest maxNumberOfPeaks peaks in the parent" 91 " (<= 0: unlimited)"))
92 maxFootprintArea = pexConf.Field(dtype=int, default=1000000,
93 doc=(
"Maximum area for footprints before they are ignored as large; " 94 "non-positive means no threshold applied"))
95 maxFootprintSize = pexConf.Field(dtype=int, default=0,
96 doc=(
"Maximum linear dimension for footprints before they are ignored " 97 "as large; non-positive means no threshold applied"))
98 minFootprintAxisRatio = pexConf.Field(dtype=float, default=0.0,
99 doc=(
"Minimum axis ratio for footprints before they are ignored " 100 "as large; non-positive means no threshold applied"))
101 notDeblendedMask = pexConf.Field(dtype=str, default=
"NOT_DEBLENDED", optional=
True,
102 doc=
"Mask name for footprints not deblended, or None")
104 tinyFootprintSize = pexConf.RangeField(dtype=int, default=2, min=2, inclusiveMin=
True,
105 doc=(
'Footprints smaller in width or height than this value will ' 106 'be ignored; minimum of 2 due to PSF gradient calculation.'))
108 propagateAllPeaks = pexConf.Field(dtype=bool, default=
False,
109 doc=(
'Guarantee that all peaks produce a child source.'))
110 catchFailures = pexConf.Field(dtype=bool, default=
False,
111 doc=(
"If True, catch exceptions thrown by the deblender, log them, " 112 "and set a flag on the parent, instead of letting them propagate up"))
113 maskPlanes = pexConf.ListField(dtype=str, default=[
"SAT",
"INTRP",
"NO_DATA"],
114 doc=
"Mask planes to ignore when performing statistics")
115 maskLimits = pexConf.DictField(
119 doc=(
"Mask planes with the corresponding limit on the fraction of masked pixels. " 120 "Sources violating this limit will not be deblended."),
122 weightTemplates = pexConf.Field(dtype=bool, default=
False,
123 doc=(
"If true, a least-squares fit of the templates will be done to the " 124 "full image. The templates will be re-weighted based on this fit."))
125 removeDegenerateTemplates = pexConf.Field(dtype=bool, default=
False,
126 doc=(
"Try to remove similar templates?"))
127 maxTempDotProd = pexConf.Field(dtype=float, default=0.5,
128 doc=(
"If the dot product between two templates is larger than this value" 129 ", we consider them to be describing the same object (i.e. they are " 130 "degenerate). If one of the objects has been labeled as a PSF it " 131 "will be removed, otherwise the template with the lowest value will " 133 medianSmoothTemplate = pexConf.Field(dtype=bool, default=
True,
134 doc=
"Apply a smoothing filter to all of the template images")
146 \anchor SourceDeblendTask_ 148 \brief Split blended sources into individual sources. 150 This task has no return value; it only modifies the SourceCatalog in-place. 152 ConfigClass = SourceDeblendConfig
153 _DefaultName =
"sourceDeblend" 155 def __init__(self, schema, peakSchema=None, **kwargs):
157 Create the task, adding necessary fields to the given schema. 159 @param[in,out] schema Schema object for measurement fields; will be modified in-place. 160 @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender. 161 Any fields beyond the PeakTable minimal schema will be transferred 162 to the main source Schema. If None, no fields will be transferred 164 @param[in] **kwargs Passed to Task.__init__. 166 pipeBase.Task.__init__(self, **kwargs)
167 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
168 if peakSchema
is None:
174 for item
in peakSchema:
175 if item.key
not in peakMinimalSchema:
181 schema.addField(item.field)
182 assert schema == self.
peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas" 186 self.
nChildKey = schema.addField(
'deblend_nChild', type=np.int32,
187 doc=
'Number of children this object has (defaults to 0)')
188 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
189 doc=
'Deblender thought this source looked like a PSF')
190 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
191 'If deblended-as-psf, the PSF centroid',
"pixel")
192 self.
psfFluxKey = schema.addField(
'deblend_psfFlux', type=
'D',
193 doc=
'If deblended-as-psf, the PSF flux')
195 doc=
'Source had too many peaks; ' 196 'only the brightest were included')
197 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
198 doc=
'Parent footprint covered too many pixels')
199 self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
200 doc=
'Parent footprint was predominantly masked')
202 if self.config.catchFailures:
204 doc=
"Deblending failed on source")
207 doc=
"Deblender skipped this source")
210 'deblend_rampedTemplate', type=
'Flag',
211 doc=(
'This source was near an image edge and the deblender used ' 212 '"ramp" edge-handling.'))
215 'deblend_patchedTemplate', type=
'Flag',
216 doc=(
'This source was near an image edge and the deblender used ' 217 '"patched" edge-handling.'))
220 'deblend_hasStrayFlux', type=
'Flag',
221 doc=(
'This source was assigned some stray flux'))
223 self.log.trace(
'Added keys to schema: %s',
", ".join(str(x)
for x
in (
228 def run(self, exposure, sources):
230 Get the psf from the provided exposure and then run deblend(). 232 @param[in] exposure Exposure to process 233 @param[in,out] sources SourceCatalog containing sources detected on this exposure. 237 psf = exposure.getPsf()
238 self.
deblend(exposure, sources, psf)
240 def _getPsfFwhm(self, psf, bbox):
243 return psf.computeShape().getDeterminantRadius() * 2.35
250 @param[in] exposure Exposure to process 251 @param[in,out] srcs SourceCatalog containing sources detected on this exposure. 256 self.log.info(
"Deblending %d sources" % len(srcs))
261 mi = exposure.getMaskedImage()
262 statsCtrl = afwMath.StatisticsControl()
263 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
264 stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl)
265 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
266 self.log.trace(
'sigma1: %g', sigma1)
270 for i, src
in enumerate(srcs):
273 fp = src.getFootprint()
286 self.log.trace(
'Parent %i: skipping large footprint', int(src.getId()))
288 if self.
isMasked(fp, exposure.getMaskedImage().getMask()):
291 self.log.trace(
'Parent %i: skipping masked footprint', int(src.getId()))
298 self.log.trace(
'Parent %i: deblending %i peaks', int(src.getId()), len(pks))
304 src.set(self.
tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
308 fp, mi, psf, psf_fwhm, sigma1=sigma1,
309 psfChisqCut1=self.config.psfChisq1,
310 psfChisqCut2=self.config.psfChisq2,
311 psfChisqCut2b=self.config.psfChisq2b,
312 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
313 strayFluxToPointSources=self.config.strayFluxToPointSources,
314 assignStrayFlux=self.config.assignStrayFlux,
315 strayFluxAssignment=self.config.strayFluxRule,
316 rampFluxAtEdge=(self.config.edgeHandling ==
'ramp'),
317 patchEdges=(self.config.edgeHandling ==
'noclip'),
318 tinyFootprintSize=self.config.tinyFootprintSize,
319 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
320 weightTemplates=self.config.weightTemplates,
321 removeDegenerateTemplates=self.config.removeDegenerateTemplates,
322 maxTempDotProd=self.config.maxTempDotProd,
323 medianSmoothTemplate=self.config.medianSmoothTemplate
325 if self.config.catchFailures:
327 except Exception
as e:
328 if self.config.catchFailures:
329 self.log.warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
332 traceback.print_exc()
339 for j, peak
in enumerate(res.deblendedParents[0].peaks):
340 heavy = peak.getFluxPortion()
341 if heavy
is None or peak.skip:
343 if not self.config.propagateAllPeaks:
348 self.log.trace(
"Peak at (%i,%i) failed. Using minimal default info for child.",
349 pks[j].getIx(), pks[j].getIy())
352 foot = afwDet.Footprint(src.getFootprint())
353 peakList = foot.getPeaks()
355 peakList.append(peak.peak)
356 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
357 heavy = afwDet.makeHeavyFootprint(foot, zeroMimg)
358 if peak.deblendedAsPsf:
359 if peak.psfFitFlux
is None:
360 peak.psfFitFlux = 0.0
361 if peak.psfFitCenter
is None:
362 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
364 assert(len(heavy.getPeaks()) == 1)
367 child = srcs.addNew()
370 child.setParent(src.getId())
371 child.setFootprint(heavy)
372 child.set(self.
psfKey, peak.deblendedAsPsf)
374 if peak.deblendedAsPsf:
375 (cx, cy) = peak.psfFitCenter
387 spans = src.getFootprint().spans
389 spans = spans.union(child.getFootprint().spans)
390 src.getFootprint().setSpans(spans)
398 self.log.info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources' 399 % (n0, nparents, n1-n0, n1))
404 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
408 """Returns whether a Footprint is large 410 'Large' is defined by thresholds on the area, size and axis ratio. 411 These may be disabled independently by configuring them to be non-positive. 413 This is principally intended to get rid of satellite streaks, which the 414 deblender or other downstream processing can have trouble dealing with 415 (e.g., multiple large HeavyFootprints can chew up memory). 417 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
419 if self.config.maxFootprintSize > 0:
420 bbox = footprint.getBBox()
421 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
423 if self.config.minFootprintAxisRatio > 0:
424 axes = afwEll.Axes(footprint.getShape())
425 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
430 """Returns whether the footprint violates the mask limits""" 431 size = float(footprint.getArea())
432 for maskName, limit
in self.config.maskLimits.items():
433 maskVal = mask.getPlaneBitMask(maskName)
434 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal)
435 if (size - unmaskedSpan.getArea())/size > limit:
440 """Indicate that the parent source is not being deblended 442 We set the appropriate flags and mask. 444 @param source The source to flag as skipped 445 @param mask The mask to update 447 fp = source.getFootprint()
449 source.set(self.
nChildKey, len(fp.getPeaks()))
450 if self.config.notDeblendedMask:
451 mask.addMaskPlane(self.config.notDeblendedMask)
452 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
def isLargeFootprint(self, footprint)
deblendPatchedTemplateKey
def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res)
def preSingleDeblendHook(self, exposure, srcs, i, fp, psf, psf_fwhm, sigma1)
def run(self, exposure, sources)
Get the psf from the provided exposure and then run deblend().
def _getPsfFwhm(self, psf, bbox)
def addSchemaKeys(self, schema)
Split blended sources into individual sources.
def skipParent(self, source, mask)
def deblend(self, exposure, srcs, psf)
Deblend.
def __init__(self, schema, peakSchema=None, kwargs)
Create the task, adding necessary fields to the given schema.
def isMasked(self, footprint, mask)