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 findStrayFlux = pexConf.Field(dtype=bool, default=
True,
61 doc=
'Find stray flux---flux not claimed by any child in the deblender.')
63 assignStrayFlux = pexConf.Field(dtype=bool, default=
True,
64 doc=
'Assign stray flux to deblend children. Implies findStrayFlux.')
66 strayFluxRule = pexConf.ChoiceField(
67 doc=
'How to split flux among peaks',
68 dtype=str, default=
'trim',
70 'r-to-peak':
'~ 1/(1+R^2) to the peak',
71 'r-to-footprint': (
'~ 1/(1+R^2) to the closest pixel in the footprint. '
72 'CAUTION: this can be computationally expensive on large footprints!'),
73 'nearest-footprint': (
'Assign 100% to the nearest footprint (using L-1 norm aka '
74 'Manhattan distance)'),
75 'trim': (
'Shrink the parent footprint to pixels that are not assigned to children')
79 clipStrayFluxFraction = pexConf.Field(dtype=float, default=0.001,
80 doc=(
'When splitting stray flux, clip fractions below '
81 'this value to zero.'))
82 psfChisq1 = pexConf.Field(dtype=float, default=1.5, optional=
False,
83 doc=(
'Chi-squared per DOF cut for deciding a source is '
84 'a PSF during deblending (un-shifted PSF model)'))
85 psfChisq2 = pexConf.Field(dtype=float, default=1.5, optional=
False,
86 doc=(
'Chi-squared per DOF cut for deciding a source is '
87 'PSF during deblending (shifted PSF model)'))
88 psfChisq2b = pexConf.Field(dtype=float, default=1.5, optional=
False,
89 doc=(
'Chi-squared per DOF cut for deciding a source is '
90 'a PSF during deblending (shifted PSF model #2)'))
91 maxNumberOfPeaks = pexConf.Field(dtype=int, default=0,
92 doc=(
"Only deblend the brightest maxNumberOfPeaks peaks in the parent"
93 " (<= 0: unlimited)"))
94 maxFootprintArea = pexConf.Field(dtype=int, default=1000000,
95 doc=(
"Maximum area for footprints before they are ignored as large; "
96 "non-positive means no threshold applied"))
97 maxFootprintSize = pexConf.Field(dtype=int, default=0,
98 doc=(
"Maximum linear dimension for footprints before they are ignored "
99 "as large; non-positive means no threshold applied"))
100 minFootprintAxisRatio = pexConf.Field(dtype=float, default=0.0,
101 doc=(
"Minimum axis ratio for footprints before they are ignored "
102 "as large; non-positive means no threshold applied"))
103 notDeblendedMask = pexConf.Field(dtype=str, default=
"NOT_DEBLENDED", optional=
True,
104 doc=
"Mask name for footprints not deblended, or None")
106 tinyFootprintSize = pexConf.RangeField(dtype=int, default=2, min=2, inclusiveMin=
True,
107 doc=(
'Footprints smaller in width or height than this value will '
108 'be ignored; minimum of 2 due to PSF gradient calculation.'))
110 propagateAllPeaks = pexConf.Field(dtype=bool, default=
False,
111 doc=(
'Guarantee that all peaks produce a child source.'))
112 catchFailures = pexConf.Field(dtype=bool, default=
False,
113 doc=(
"If True, catch exceptions thrown by the deblender, log them, "
114 "and set a flag on the parent, instead of letting them propagate up"))
115 maskPlanes = pexConf.ListField(dtype=str, default=[
"SAT",
"INTRP",
"NO_DATA"],
116 doc=
"Mask planes to ignore when performing statistics")
117 maskLimits = pexConf.DictField(
121 doc=(
"Mask planes with the corresponding limit on the fraction of masked pixels. "
122 "Sources violating this limit will not be deblended."),
135 \anchor SourceDeblendTask_
137 \brief Split blended sources into individual sources.
139 This task has no return value; it only modifies the SourceCatalog in-place.
141 ConfigClass = SourceDeblendConfig
142 _DefaultName =
"sourceDeblend"
144 def __init__(self, schema, peakSchema=None, **kwargs):
146 Create the task, adding necessary fields to the given schema.
148 @param[in,out] schema Schema object for measurement fields; will be modified in-place.
149 @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender.
150 Any fields beyond the PeakTable minimal schema will be transferred
151 to the main source Schema. If None, no fields will be transferred
153 @param[in] **kwargs Passed to Task.__init__.
155 pipeBase.Task.__init__(self, **kwargs)
156 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
157 if peakSchema
is None:
163 for item
in peakSchema:
164 if item.key
not in peakMinimalSchema:
165 self.peakSchemaMapper.addMapping(item.key, item.field)
170 schema.addField(item.field)
171 assert schema == self.peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas"
175 self.
nChildKey = schema.addField(
'deblend_nChild', type=int,
176 doc=
'Number of children this object has (defaults to 0)')
177 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
178 doc=
'Deblender thought this source looked like a PSF')
179 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
180 'If deblended-as-psf, the PSF centroid',
"pixel")
181 self.
psfFluxKey = schema.addField(
'deblend_psfFlux', type=
'D',
182 doc=
'If deblended-as-psf, the PSF flux')
184 doc=
'Source had too many peaks; '
185 'only the brightest were included')
186 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
187 doc=
'Parent footprint covered too many pixels')
188 self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
189 doc=
'Parent footprint was predominantly masked')
191 if self.config.catchFailures:
193 doc=
"Deblending failed on source")
196 doc=
"Deblender skipped this source")
199 'deblend_rampedTemplate', type=
'Flag',
200 doc=(
'This source was near an image edge and the deblender used '
201 '"ramp" edge-handling.'))
204 'deblend_patchedTemplate', type=
'Flag',
205 doc=(
'This source was near an image edge and the deblender used '
206 '"patched" edge-handling.'))
209 'deblend_hasStrayFlux', type=
'Flag',
210 doc=(
'This source was assigned some stray flux'))
212 self.log.trace(
'Added keys to schema: %s',
", ".join(str(x)
for x
in (
217 def run(self, exposure, sources):
219 Get the psf from the provided exposure and then run deblend().
221 @param[in] exposure Exposure to process
222 @param[in,out] sources SourceCatalog containing sources detected on this exposure.
226 psf = exposure.getPsf()
227 self.
deblend(exposure, sources, psf)
232 return psf.computeShape().getDeterminantRadius() * 2.35
239 @param[in] exposure Exposure to process
240 @param[in,out] srcs SourceCatalog containing sources detected on this exposure.
245 self.log.info(
"Deblending %d sources" % len(srcs))
250 mi = exposure.getMaskedImage()
252 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
254 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
255 self.log.trace(
'sigma1: %g', sigma1)
259 for i, src
in enumerate(srcs):
262 fp = src.getFootprint()
275 self.log.trace(
'Parent %i: skipping large footprint', int(src.getId()))
277 if self.
isMasked(fp, exposure.getMaskedImage().getMask()):
280 self.log.trace(
'Parent %i: skipping masked footprint', int(src.getId()))
287 self.log.trace(
'Parent %i: deblending %i peaks', int(src.getId()), len(pks))
293 src.set(self.
tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
297 fp, mi, psf, psf_fwhm, sigma1=sigma1,
298 psfChisqCut1=self.config.psfChisq1,
299 psfChisqCut2=self.config.psfChisq2,
300 psfChisqCut2b=self.config.psfChisq2b,
301 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
302 strayFluxToPointSources=self.config.strayFluxToPointSources,
303 assignStrayFlux=self.config.assignStrayFlux,
304 findStrayFlux=(self.config.assignStrayFlux
or self.config.findStrayFlux),
305 strayFluxAssignment=self.config.strayFluxRule,
306 rampFluxAtEdge=(self.config.edgeHandling ==
'ramp'),
307 patchEdges=(self.config.edgeHandling ==
'noclip'),
308 tinyFootprintSize=self.config.tinyFootprintSize,
309 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
311 if self.config.catchFailures:
313 except Exception
as e:
314 if self.config.catchFailures:
315 self.log.warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
318 traceback.print_exc()
325 for j, peak
in enumerate(res.peaks):
326 heavy = peak.getFluxPortion()
327 if heavy
is None or peak.skip:
329 if not self.config.propagateAllPeaks:
334 self.log.trace(
"Peak at (%i,%i) failed. Using minimal default info for child.",
335 pks[j].getIx(), pks[j].getIy())
339 peakList = foot.getPeaks()
341 peakList.append(peak.peak)
342 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
344 if peak.deblendedAsPsf:
345 if peak.psfFitFlux
is None:
346 peak.psfFitFlux = 0.0
347 if peak.psfFitCenter
is None:
348 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
350 assert(len(heavy.getPeaks()) == 1)
353 child = srcs.addNew()
356 child.setParent(src.getId())
357 child.setFootprint(heavy)
358 child.set(self.
psfKey, peak.deblendedAsPsf)
360 if peak.deblendedAsPsf:
361 (cx, cy) = peak.psfFitCenter
373 src.getFootprint().include([child.getFootprint()
for child
in kids])
381 self.log.info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources'
382 % (n0, nparents, n1-n0, n1))
387 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
391 """Returns whether a Footprint is large
393 'Large' is defined by thresholds on the area, size and axis ratio.
394 These may be disabled independently by configuring them to be non-positive.
396 This is principally intended to get rid of satellite streaks, which the
397 deblender or other downstream processing can have trouble dealing with
398 (e.g., multiple large HeavyFootprints can chew up memory).
400 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
402 if self.config.maxFootprintSize > 0:
403 bbox = footprint.getBBox()
404 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
406 if self.config.minFootprintAxisRatio > 0:
407 axes = afwEll.Axes(footprint.getShape())
408 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
413 """Returns whether the footprint violates the mask limits"""
414 size = float(footprint.getArea())
415 for maskName, limit
in self.config.maskLimits.items():
416 maskVal = mask.getPlaneBitMask(maskName)
418 unmasked.intersectMask(mask, maskVal)
419 if (size - unmasked.getArea())/size > limit:
424 """Indicate that the parent source is not being deblended
426 We set the appropriate flags and mask.
428 @param source The source to flag as skipped
429 @param mask The mask to update
431 fp = source.getFootprint()
433 source.set(self.
nChildKey, len(fp.getPeaks()))
434 if self.config.notDeblendedMask:
435 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)