35 __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=
'r-to-peak',
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.01,
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.Field(dtype=int, default=2,
107 doc=(
'Footprints smaller in width or height than this value will '
108 'be ignored; 0 to never ignore.'))
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."),
134 \anchor SourceDeblendTask_
136 \brief Split blended sources into individual sources.
138 This task has no return value; it only modifies the SourceCatalog in-place.
140 ConfigClass = SourceDeblendConfig
141 _DefaultName =
"sourceDeblend"
143 def __init__(self, schema, peakSchema=None, **kwargs):
145 Create the task, adding necessary fields to the given schema.
147 @param[in,out] schema Schema object for measurement fields; will be modified in-place.
148 @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender.
149 Any fields beyond the PeakTable minimal schema will be transferred
150 to the main source Schema. If None, no fields will be transferred
152 @param[in] **kwargs Passed to Task.__init__.
154 pipeBase.Task.__init__(self, **kwargs)
155 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
156 if peakSchema
is None:
162 for item
in peakSchema:
163 if item.key
not in peakMinimalSchema:
164 self.peakSchemaMapper.addMapping(item.key, item.field)
169 schema.addField(item.field)
170 assert schema == self.peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas"
174 self.
nChildKey = schema.addField(
'deblend_nChild', type=int,
175 doc=
'Number of children this object has (defaults to 0)')
176 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
177 doc=
'Deblender thought this source looked like a PSF')
178 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
179 'If deblended-as-psf, the PSF centroid',
"pixels")
180 self.
psfFluxKey = schema.addField(
'deblend_psfFlux', type=
'D',
181 doc=
'If deblended-as-psf, the PSF flux')
183 doc=
'Source had too many peaks; '
184 'only the brightest were included')
185 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
186 doc=
'Parent footprint covered too many pixels')
187 self.
maskedKey = schema.addField(
'deblend.masked', type=
'Flag',
188 doc=
'Parent footprint was predominantly masked')
190 if self.config.catchFailures:
192 doc=
"Deblending failed on source")
195 doc=
"Deblender skipped this source")
198 'deblend_rampedTemplate', type=
'Flag',
199 doc=(
'This source was near an image edge and the deblender used '
200 '"ramp" edge-handling.'))
203 'deblend_patchedTemplate', type=
'Flag',
204 doc=(
'This source was near an image edge and the deblender used '
205 '"patched" edge-handling.'))
208 'deblend_hasStrayFlux', type=
'Flag',
209 doc=(
'This source was assigned some stray flux'))
212 'deblend.blendedness', type=float,
213 doc=(
"A measure of how blended the source is. This is the sum of dot products between the source "
214 "and all of its deblended siblings, divided by the dot product of the deblended source with "
217 self.log.logdebug(
'Added keys to schema: %s' %
", ".join(str(x)
for x
in (
222 def run(self, exposure, sources, psf):
226 @param[in] exposure Exposure to process
227 @param[in,out] sources SourceCatalog containing sources detected on this exposure.
232 self.
deblend(exposure, sources, psf)
237 return psf.computeShape().getDeterminantRadius() * 2.35
244 @param[in] exposure Exposure to process
245 @param[in,out] srcs SourceCatalog containing sources detected on this exposure.
250 self.log.info(
"Deblending %d sources" % len(srcs))
256 mi = exposure.getMaskedImage()
258 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
260 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
261 self.log.logdebug(
'sigma1: %g' % sigma1)
263 schema = srcs.getSchema()
267 for i,src
in enumerate(srcs):
270 fp = src.getFootprint()
278 self.log.logdebug(
'Parent %i: skipping large footprint' % (int(src.getId()),))
280 if self.
isMasked(fp, exposure.getMaskedImage().getMask()):
283 self.log.logdebug(
'Parent %i: skipping masked footprint' % (int(src.getId()),))
290 self.log.logdebug(
'Parent %i: deblending %i peaks' % (int(src.getId()), len(pks)))
296 src.set(self.
tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
300 fp, mi, psf, psf_fwhm, sigma1=sigma1,
301 psfChisqCut1 = self.config.psfChisq1,
302 psfChisqCut2 = self.config.psfChisq2,
303 psfChisqCut2b= self.config.psfChisq2b,
304 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
305 strayFluxToPointSources=self.config.strayFluxToPointSources,
306 assignStrayFlux=self.config.assignStrayFlux,
307 findStrayFlux=(self.config.assignStrayFlux
or self.config.findStrayFlux),
308 strayFluxAssignment=self.config.strayFluxRule,
309 rampFluxAtEdge=(self.config.edgeHandling ==
'ramp'),
310 patchEdges=(self.config.edgeHandling ==
'noclip'),
311 tinyFootprintSize=self.config.tinyFootprintSize,
312 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
314 if self.config.catchFailures:
316 except Exception
as e:
317 if self.config.catchFailures:
318 self.log.warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
321 traceback.print_exc()
328 for j, peak
in enumerate(res.peaks):
333 msg =
'Skipping out-of-bounds peak at (%i,%i)' % (pks[j].getIx(), pks[j].getIy())
338 heavy = peak.getFluxPortion()
341 msg =
'Skipping peak at (%i,%i), child %i of %i: no flux portion' \
342 % (pks[j].getIx(), pks[j].getIy(), j+1, len(res.peaks))
348 if self.config.propagateAllPeaks:
353 peakList = foot.getPeaks()
355 peakList.append(peak.peak)
356 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
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())
363 self.log.warn(
"Peak failed. Using minimal default info for child.")
367 assert(len(heavy.getPeaks()) == 1)
370 child = srcs.addNew(); nchild += 1
372 child.setParent(src.getId())
373 child.setFootprint(heavy)
374 child.set(self.
psfKey, peak.deblendedAsPsf)
376 if peak.deblendedAsPsf:
377 (cx,cy) = peak.psfFitCenter
389 src.getFootprint().include([child.getFootprint()
for child
in kids])
399 self.log.info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources'
400 % (n0, nparents, n1-n0, n1))
405 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
409 """Returns whether a Footprint is large
411 'Large' is defined by thresholds on the area, size and axis ratio.
412 These may be disabled independently by configuring them to be non-positive.
414 This is principally intended to get rid of satellite streaks, which the
415 deblender or other downstream processing can have trouble dealing with
416 (e.g., multiple large HeavyFootprints can chew up memory).
418 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
420 if self.config.maxFootprintSize > 0:
421 bbox = footprint.getBBox()
422 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
424 if self.config.minFootprintAxisRatio > 0:
425 axes = afwEll.Axes(footprint.getShape())
426 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
431 """Returns whether the footprint violates the mask limits"""
432 size = float(footprint.getArea())
433 for maskName, limit
in self.config.maskLimits.iteritems():
434 maskVal = mask.getPlaneBitMask(maskName)
436 unmasked.intersectMask(mask, maskVal)
437 if (size - unmasked.getArea())/size > limit:
442 """Indicate that the parent source is not being deblended
444 We set the appropriate flags and mask.
446 @param source The source to flag as skipped
447 @param mask The mask to update
449 fp = source.getFootprint()
451 source.set(self.
nChildKey, len(fp.getPeaks()))
452 if self.config.notDeblendedMask:
453 mask.addMaskPlane(self.config.notDeblendedMask)
457 """Calculate the blendedness values for a blend family
459 The blendedness is defined as:
461 [heavy[i].dot(heavy[j]) for j in neighbors].sum() / heavy[i].dot(heavy[i])
463 where 'heavy' is the heavy footprint representation of the flux.
465 bbox = parent.getFootprint().getBBox()
466 if not kids
or bbox.isEmpty():
470 def getHeavyFootprint(src):
471 """Provide the HeavyFootprint for a source"""
472 fp = src.getFootprint()
473 return (afwDet.HeavyFootprintF.cast(fp)
if fp.isHeavy()
else
476 parentFoot = getHeavyFootprint(parent)
477 kidFeet = [getHeavyFootprint(src)
for src
in kids]
479 def setBlendedness(src, foot):
480 """Calculate and set the blendedness value for a source, given its image"""
481 if foot.getBBox().isEmpty()
or numpy.all(foot.getImageArray() == 0.0):
485 numerator =
sum(foot.dot(f)
for k, f
in zip(kids, kidFeet)
if k.getId() != srcId)
486 denominator = foot.dot(foot)
489 setBlendedness(parent, parentFoot)
490 for k, f
in zip(kids, kidFeet):
deblendPatchedTemplateKey
A mapping between the keys of two Schemas, used to copy data between them.
boost::enable_if< typename ExpressionTraits< Scalar >::IsScalar, Scalar >::type sum(Scalar const &scalar)
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)