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 "
144 \anchor SourceDeblendTask_
146 \brief Split blended sources into individual sources.
148 This task has no return value; it only modifies the SourceCatalog in-place.
150 ConfigClass = SourceDeblendConfig
151 _DefaultName =
"sourceDeblend"
153 def __init__(self, schema, peakSchema=None, **kwargs):
155 Create the task, adding necessary fields to the given schema.
157 @param[in,out] schema Schema object for measurement fields; will be modified in-place.
158 @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender.
159 Any fields beyond the PeakTable minimal schema will be transferred
160 to the main source Schema. If None, no fields will be transferred
162 @param[in] **kwargs Passed to Task.__init__.
164 pipeBase.Task.__init__(self, **kwargs)
165 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
166 if peakSchema
is None:
172 for item
in peakSchema:
173 if item.key
not in peakMinimalSchema:
174 self.peakSchemaMapper.addMapping(item.key, item.field)
179 schema.addField(item.field)
180 assert schema == self.peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas"
184 self.
nChildKey = schema.addField(
'deblend_nChild', type=int,
185 doc=
'Number of children this object has (defaults to 0)')
186 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
187 doc=
'Deblender thought this source looked like a PSF')
188 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
189 'If deblended-as-psf, the PSF centroid',
"pixel")
190 self.
psfFluxKey = schema.addField(
'deblend_psfFlux', type=
'D',
191 doc=
'If deblended-as-psf, the PSF flux')
193 doc=
'Source had too many peaks; '
194 'only the brightest were included')
195 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
196 doc=
'Parent footprint covered too many pixels')
197 self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
198 doc=
'Parent footprint was predominantly masked')
200 if self.config.catchFailures:
202 doc=
"Deblending failed on source")
205 doc=
"Deblender skipped this source")
208 'deblend_rampedTemplate', type=
'Flag',
209 doc=(
'This source was near an image edge and the deblender used '
210 '"ramp" edge-handling.'))
213 'deblend_patchedTemplate', type=
'Flag',
214 doc=(
'This source was near an image edge and the deblender used '
215 '"patched" edge-handling.'))
218 'deblend_hasStrayFlux', type=
'Flag',
219 doc=(
'This source was assigned some stray flux'))
221 self.log.trace(
'Added keys to schema: %s',
", ".join(str(x)
for x
in (
226 def run(self, exposure, sources):
228 Get the psf from the provided exposure and then run deblend().
230 @param[in] exposure Exposure to process
231 @param[in,out] sources SourceCatalog containing sources detected on this exposure.
235 psf = exposure.getPsf()
236 self.
deblend(exposure, sources, psf)
241 return psf.computeShape().getDeterminantRadius() * 2.35
248 @param[in] exposure Exposure to process
249 @param[in,out] srcs SourceCatalog containing sources detected on this exposure.
254 self.log.info(
"Deblending %d sources" % len(srcs))
259 mi = exposure.getMaskedImage()
261 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
263 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
264 self.log.trace(
'sigma1: %g', sigma1)
268 for i, src
in enumerate(srcs):
271 fp = src.getFootprint()
284 self.log.trace(
'Parent %i: skipping large footprint', int(src.getId()))
286 if self.
isMasked(fp, exposure.getMaskedImage().getMask()):
289 self.log.trace(
'Parent %i: skipping masked footprint', int(src.getId()))
296 self.log.trace(
'Parent %i: deblending %i peaks', int(src.getId()), len(pks))
302 src.set(self.
tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
306 fp, mi, psf, psf_fwhm, sigma1=sigma1,
307 psfChisqCut1=self.config.psfChisq1,
308 psfChisqCut2=self.config.psfChisq2,
309 psfChisqCut2b=self.config.psfChisq2b,
310 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
311 strayFluxToPointSources=self.config.strayFluxToPointSources,
312 assignStrayFlux=self.config.assignStrayFlux,
313 strayFluxAssignment=self.config.strayFluxRule,
314 rampFluxAtEdge=(self.config.edgeHandling ==
'ramp'),
315 patchEdges=(self.config.edgeHandling ==
'noclip'),
316 tinyFootprintSize=self.config.tinyFootprintSize,
317 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
318 weightTemplates=self.config.weightTemplates,
319 removeDegenerateTemplates=self.config.removeDegenerateTemplates,
320 maxTempDotProd=self.config.maxTempDotProd
322 if self.config.catchFailures:
324 except Exception
as e:
325 if self.config.catchFailures:
326 self.log.warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
329 traceback.print_exc()
336 for j, peak
in enumerate(res.deblendedParents[0].peaks):
337 heavy = peak.getFluxPortion()
338 if heavy
is None or peak.skip:
340 if not self.config.propagateAllPeaks:
345 self.log.trace(
"Peak at (%i,%i) failed. Using minimal default info for child.",
346 pks[j].getIx(), pks[j].getIy())
350 peakList = foot.getPeaks()
352 peakList.append(peak.peak)
353 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
355 if peak.deblendedAsPsf:
356 if peak.psfFitFlux
is None:
357 peak.psfFitFlux = 0.0
358 if peak.psfFitCenter
is None:
359 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
361 assert(len(heavy.getPeaks()) == 1)
364 child = srcs.addNew()
367 child.setParent(src.getId())
368 child.setFootprint(heavy)
369 child.set(self.
psfKey, peak.deblendedAsPsf)
371 if peak.deblendedAsPsf:
372 (cx, cy) = peak.psfFitCenter
384 src.getFootprint().include([child.getFootprint()
for child
in kids])
392 self.log.info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources'
393 % (n0, nparents, n1-n0, n1))
398 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
402 """Returns whether a Footprint is large
404 'Large' is defined by thresholds on the area, size and axis ratio.
405 These may be disabled independently by configuring them to be non-positive.
407 This is principally intended to get rid of satellite streaks, which the
408 deblender or other downstream processing can have trouble dealing with
409 (e.g., multiple large HeavyFootprints can chew up memory).
411 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
413 if self.config.maxFootprintSize > 0:
414 bbox = footprint.getBBox()
415 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
417 if self.config.minFootprintAxisRatio > 0:
418 axes = afwEll.Axes(footprint.getShape())
419 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
424 """Returns whether the footprint violates the mask limits"""
425 size = float(footprint.getArea())
426 for maskName, limit
in self.config.maskLimits.items():
427 maskVal = mask.getPlaneBitMask(maskName)
429 unmasked.intersectMask(mask, maskVal)
430 if (size - unmasked.getArea())/size > limit:
435 """Indicate that the parent source is not being deblended
437 We set the appropriate flags and mask.
439 @param source The source to flag as skipped
440 @param mask The mask to update
442 fp = source.getFootprint()
444 source.set(self.
nChildKey, len(fp.getPeaks()))
445 if self.config.notDeblendedMask:
446 mask.addMaskPlane(self.config.notDeblendedMask)
deblendPatchedTemplateKey
A mapping between the keys of two Schemas, used to copy data between them.
def run
Get the psf from the provided exposure and then run deblend().
Pass parameters to a Statistics objectA class to pass parameters which control how the stats are calc...
Split blended sources into individual sources.
def postSingleDeblendHook
MaskT setMaskFromFootprint(lsst::afw::image::Mask< MaskT > *mask, Footprint const &footprint, MaskT const bitmask)
OR bitmask into all the Mask's pixels that are in the Footprint.
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
def __init__
Create the task, adding necessary fields to the given schema.
HeavyFootprint< ImagePixelT, MaskPixelT, VariancePixelT > makeHeavyFootprint(Footprint const &foot, lsst::afw::image::MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > const &img, HeavyFootprintCtrl const *ctrl=NULL)
Create a HeavyFootprint with footprint defined by the given Footprint and pixel values from the given...