40 __all__ =
'SourceDeblendConfig',
'SourceDeblendTask',
'MultibandDeblendConfig',
'MultibandDeblendTask' 45 edgeHandling = pexConfig.ChoiceField(
46 doc=
'What to do when a peak to be deblended is close to the edge of the image',
47 dtype=str, default=
'ramp',
49 'clip':
'Clip the template at the edge AND the mirror of the edge.',
50 'ramp':
'Ramp down flux at the image edge by the PSF',
51 'noclip':
'Ignore the edge when building the symmetric template.',
55 strayFluxToPointSources = pexConfig.ChoiceField(
56 doc=
'When the deblender should attribute stray flux to point sources',
57 dtype=str, default=
'necessary',
59 'necessary':
'When there is not an extended object in the footprint',
61 'never': (
'Never; stray flux will not be attributed to any deblended child ' 62 'if the deblender thinks all peaks look like point sources'),
66 assignStrayFlux = pexConfig.Field(dtype=bool, default=
True,
67 doc=
'Assign stray flux (not claimed by any child in the deblender) ' 68 'to deblend children.')
70 strayFluxRule = pexConfig.ChoiceField(
71 doc=
'How to split flux among peaks',
72 dtype=str, default=
'trim',
74 'r-to-peak':
'~ 1/(1+R^2) to the peak',
75 'r-to-footprint': (
'~ 1/(1+R^2) to the closest pixel in the footprint. ' 76 'CAUTION: this can be computationally expensive on large footprints!'),
77 'nearest-footprint': (
'Assign 100% to the nearest footprint (using L-1 norm aka ' 78 'Manhattan distance)'),
79 'trim': (
'Shrink the parent footprint to pixels that are not assigned to children')
83 clipStrayFluxFraction = pexConfig.Field(dtype=float, default=0.001,
84 doc=(
'When splitting stray flux, clip fractions below ' 85 'this value to zero.'))
86 psfChisq1 = pexConfig.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 (un-shifted PSF model)'))
89 psfChisq2 = pexConfig.Field(dtype=float, default=1.5, optional=
False,
90 doc=(
'Chi-squared per DOF cut for deciding a source is ' 91 'PSF during deblending (shifted PSF model)'))
92 psfChisq2b = pexConfig.Field(dtype=float, default=1.5, optional=
False,
93 doc=(
'Chi-squared per DOF cut for deciding a source is ' 94 'a PSF during deblending (shifted PSF model #2)'))
95 maxNumberOfPeaks = pexConfig.Field(dtype=int, default=0,
96 doc=(
"Only deblend the brightest maxNumberOfPeaks peaks in the parent" 97 " (<= 0: unlimited)"))
98 maxFootprintArea = pexConfig.Field(dtype=int, default=1000000,
99 doc=(
"Maximum area for footprints before they are ignored as large; " 100 "non-positive means no threshold applied"))
101 maxFootprintSize = pexConfig.Field(dtype=int, default=0,
102 doc=(
"Maximum linear dimension for footprints before they are ignored " 103 "as large; non-positive means no threshold applied"))
104 minFootprintAxisRatio = pexConfig.Field(dtype=float, default=0.0,
105 doc=(
"Minimum axis ratio for footprints before they are ignored " 106 "as large; non-positive means no threshold applied"))
107 notDeblendedMask = pexConfig.Field(dtype=str, default=
"NOT_DEBLENDED", optional=
True,
108 doc=
"Mask name for footprints not deblended, or None")
110 tinyFootprintSize = pexConfig.RangeField(dtype=int, default=2, min=2, inclusiveMin=
True,
111 doc=(
'Footprints smaller in width or height than this value ' 112 'will be ignored; minimum of 2 due to PSF gradient ' 115 propagateAllPeaks = pexConfig.Field(dtype=bool, default=
False,
116 doc=(
'Guarantee that all peaks produce a child source.'))
117 catchFailures = pexConfig.Field(
118 dtype=bool, default=
False,
119 doc=(
"If True, catch exceptions thrown by the deblender, log them, " 120 "and set a flag on the parent, instead of letting them propagate up"))
121 maskPlanes = pexConfig.ListField(dtype=str, default=[
"SAT",
"INTRP",
"NO_DATA"],
122 doc=
"Mask planes to ignore when performing statistics")
123 maskLimits = pexConfig.DictField(
127 doc=(
"Mask planes with the corresponding limit on the fraction of masked pixels. " 128 "Sources violating this limit will not be deblended."),
130 weightTemplates = pexConfig.Field(
131 dtype=bool, default=
False,
132 doc=(
"If true, a least-squares fit of the templates will be done to the " 133 "full image. The templates will be re-weighted based on this fit."))
134 removeDegenerateTemplates = pexConfig.Field(dtype=bool, default=
False,
135 doc=(
"Try to remove similar templates?"))
136 maxTempDotProd = pexConfig.Field(
137 dtype=float, default=0.5,
138 doc=(
"If the dot product between two templates is larger than this value, we consider them to be " 139 "describing the same object (i.e. they are degenerate). If one of the objects has been " 140 "labeled as a PSF it will be removed, otherwise the template with the lowest value will " 142 medianSmoothTemplate = pexConfig.Field(dtype=bool, default=
True,
143 doc=
"Apply a smoothing filter to all of the template images")
155 \anchor SourceDeblendTask_ 157 \brief Split blended sources into individual sources. 159 This task has no return value; it only modifies the SourceCatalog in-place. 161 ConfigClass = SourceDeblendConfig
162 _DefaultName =
"sourceDeblend" 164 def __init__(self, schema, peakSchema=None, **kwargs):
166 Create the task, adding necessary fields to the given schema. 168 @param[in,out] schema Schema object for measurement fields; will be modified in-place. 169 @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender. 170 Any fields beyond the PeakTable minimal schema will be transferred 171 to the main source Schema. If None, no fields will be transferred 173 @param[in] **kwargs Passed to Task.__init__. 175 pipeBase.Task.__init__(self, **kwargs)
177 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
178 if peakSchema
is None:
184 for item
in peakSchema:
185 if item.key
not in peakMinimalSchema:
191 schema.addField(item.field)
192 assert schema == self.
peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas" 196 self.
nChildKey = schema.addField(
'deblend_nChild', type=np.int32,
197 doc=
'Number of children this object has (defaults to 0)')
198 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
199 doc=
'Deblender thought this source looked like a PSF')
200 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
201 'If deblended-as-psf, the PSF centroid',
"pixel")
202 self.
psfFluxKey = schema.addField(
'deblend_psf_instFlux', type=
'D',
203 doc=
'If deblended-as-psf, the instrumental PSF flux', units=
'count')
205 doc=
'Source had too many peaks; ' 206 'only the brightest were included')
207 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
208 doc=
'Parent footprint covered too many pixels')
209 self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
210 doc=
'Parent footprint was predominantly masked')
212 if self.config.catchFailures:
214 doc=
"Deblending failed on source")
217 doc=
"Deblender skipped this source")
220 'deblend_rampedTemplate', type=
'Flag',
221 doc=(
'This source was near an image edge and the deblender used ' 222 '"ramp" edge-handling.'))
225 'deblend_patchedTemplate', type=
'Flag',
226 doc=(
'This source was near an image edge and the deblender used ' 227 '"patched" edge-handling.'))
230 'deblend_hasStrayFlux', type=
'Flag',
231 doc=(
'This source was assigned some stray flux'))
233 self.log.trace(
'Added keys to schema: %s',
", ".join(str(x)
for x
in (
238 def run(self, exposure, sources):
240 Get the psf from the provided exposure and then run deblend(). 242 @param[in] exposure Exposure to process 243 @param[in,out] sources SourceCatalog containing sources detected on this exposure. 247 psf = exposure.getPsf()
248 assert sources.getSchema() == self.
schema 249 self.
deblend(exposure, sources, psf)
251 def _getPsfFwhm(self, psf, bbox):
254 return psf.computeShape().getDeterminantRadius() * 2.35
261 @param[in] exposure Exposure to process 262 @param[in,out] srcs SourceCatalog containing sources detected on this exposure. 267 self.log.info(
"Deblending %d sources" % len(srcs))
272 mi = exposure.getMaskedImage()
273 statsCtrl = afwMath.StatisticsControl()
274 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
275 stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl)
276 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
277 self.log.trace(
'sigma1: %g', sigma1)
281 for i, src
in enumerate(srcs):
284 fp = src.getFootprint()
297 self.log.trace(
'Parent %i: skipping large footprint', int(src.getId()))
299 if self.
isMasked(fp, exposure.getMaskedImage().getMask()):
302 self.log.trace(
'Parent %i: skipping masked footprint', int(src.getId()))
309 self.log.trace(
'Parent %i: deblending %i peaks', int(src.getId()), len(pks))
315 src.set(self.
tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
319 fp, mi, psf, psf_fwhm, sigma1=sigma1,
320 psfChisqCut1=self.config.psfChisq1,
321 psfChisqCut2=self.config.psfChisq2,
322 psfChisqCut2b=self.config.psfChisq2b,
323 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
324 strayFluxToPointSources=self.config.strayFluxToPointSources,
325 assignStrayFlux=self.config.assignStrayFlux,
326 strayFluxAssignment=self.config.strayFluxRule,
327 rampFluxAtEdge=(self.config.edgeHandling ==
'ramp'),
328 patchEdges=(self.config.edgeHandling ==
'noclip'),
329 tinyFootprintSize=self.config.tinyFootprintSize,
330 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
331 weightTemplates=self.config.weightTemplates,
332 removeDegenerateTemplates=self.config.removeDegenerateTemplates,
333 maxTempDotProd=self.config.maxTempDotProd,
334 medianSmoothTemplate=self.config.medianSmoothTemplate
336 if self.config.catchFailures:
338 except Exception
as e:
339 if self.config.catchFailures:
340 self.log.warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
343 traceback.print_exc()
350 for j, peak
in enumerate(res.deblendedParents[0].peaks):
351 heavy = peak.getFluxPortion()
352 if heavy
is None or peak.skip:
354 if not self.config.propagateAllPeaks:
359 self.log.trace(
"Peak at (%i,%i) failed. Using minimal default info for child.",
360 pks[j].getIx(), pks[j].getIy())
363 foot = afwDet.Footprint(src.getFootprint())
364 peakList = foot.getPeaks()
366 peakList.append(peak.peak)
367 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
368 heavy = afwDet.makeHeavyFootprint(foot, zeroMimg)
369 if peak.deblendedAsPsf:
370 if peak.psfFitFlux
is None:
371 peak.psfFitFlux = 0.0
372 if peak.psfFitCenter
is None:
373 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
375 assert(len(heavy.getPeaks()) == 1)
378 child = srcs.addNew()
381 child.setParent(src.getId())
382 child.setFootprint(heavy)
383 child.set(self.
psfKey, peak.deblendedAsPsf)
385 if peak.deblendedAsPsf:
386 (cx, cy) = peak.psfFitCenter
398 spans = src.getFootprint().spans
400 spans = spans.union(child.getFootprint().spans)
401 src.getFootprint().setSpans(spans)
409 self.log.info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources' 410 % (n0, nparents, n1-n0, n1))
415 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
419 """Returns whether a Footprint is large 421 'Large' is defined by thresholds on the area, size and axis ratio. 422 These may be disabled independently by configuring them to be non-positive. 424 This is principally intended to get rid of satellite streaks, which the 425 deblender or other downstream processing can have trouble dealing with 426 (e.g., multiple large HeavyFootprints can chew up memory). 428 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
430 if self.config.maxFootprintSize > 0:
431 bbox = footprint.getBBox()
432 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
434 if self.config.minFootprintAxisRatio > 0:
435 axes = afwEll.Axes(footprint.getShape())
436 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
441 """Returns whether the footprint violates the mask limits""" 442 size = float(footprint.getArea())
443 for maskName, limit
in self.config.maskLimits.items():
444 maskVal = mask.getPlaneBitMask(maskName)
445 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal)
446 if (size - unmaskedSpan.getArea())/size > limit:
451 """Indicate that the parent source is not being deblended 453 We set the appropriate flags and mask. 455 @param source The source to flag as skipped 456 @param mask The mask to update 458 fp = source.getFootprint()
460 source.set(self.
nChildKey, len(fp.getPeaks()))
461 if self.config.notDeblendedMask:
462 mask.addMaskPlane(self.config.notDeblendedMask)
463 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
467 """MultibandDeblendConfig 469 Configuration for the multiband deblender. 470 The parameters are organized by the parameter types, which are 471 - Stopping Criteria: Used to determine if the fit has converged 472 - Position Fitting Criteria: Used to fit the positions of the peaks 473 - Constraints: Used to apply constraints to the peaks and their components 474 - Other: Parameters that don't fit into the above categories 477 maxIter = pexConfig.Field(dtype=int, default=200,
478 doc=(
"Maximum number of iterations to deblend a single parent"))
479 relativeError = pexConfig.Field(dtype=float, default=1e-3,
480 doc=(
"Relative error to use when determining stopping criteria"))
483 minTranslation = pexConfig.Field(dtype=float, default=1e-3,
484 doc=(
"A peak must be updated by at least 'minTranslation' (pixels)" 485 "or no update is performed." 486 "This field is ignored if fitPositions is False."))
487 refinementSkip = pexConfig.Field(dtype=int, default=10,
488 doc=(
"If fitPositions is True, the positions and box sizes are" 489 "updated on every 'refinementSkip' iterations."))
490 translationMethod = pexConfig.Field(dtype=str, default=
"default",
491 doc=(
"Method to use for fitting translations." 492 "Currently 'default' is the only available option," 493 "which performs a linear fit, but it is possible that we" 494 "will use galsim or some other method as a future option"))
495 edgeFluxThresh = pexConfig.Field(dtype=float, default=1.0,
496 doc=(
"Boxes are resized when the flux at an edge is " 497 "> edgeFluxThresh * background RMS"))
498 exactLipschitz = pexConfig.Field(dtype=bool, default=
False,
499 doc=(
"Calculate exact Lipschitz constant in every step" 500 "(True) or only calculate the approximate" 501 "Lipschitz constant with significant changes in A,S" 503 stepSlack = pexConfig.Field(dtype=float, default=0.2,
504 doc=(
"A fractional measure of how much a value (like the exactLipschitz)" 505 "can change before it needs to be recalculated." 506 "This must be between 0 and 1."))
509 constraints = pexConfig.Field(dtype=str, default=
"1,+,S,M",
510 doc=(
"List of constraints to use for each object" 511 "(order does not matter)" 512 "Current options are all used by default:\n" 515 "1: normalized SED to unity" 516 "+: non-negative morphology"))
517 symmetryThresh = pexConfig.Field(dtype=float, default=1.0,
518 doc=(
"Strictness of symmetry, from" 519 "0 (no symmetry enforced) to" 520 "1 (perfect symmetry required)." 521 "If 'S' is not in `constraints`, this argument is ignored"))
522 l0Thresh = pexConfig.Field(dtype=float, default=np.nan,
523 doc=(
"L0 threshold. NaN results in no L0 penalty."))
524 l1Thresh = pexConfig.Field(dtype=float, default=np.nan,
525 doc=(
"L1 threshold. NaN results in no L1 penalty."))
526 tvxThresh = pexConfig.Field(dtype=float, default=np.nan,
527 doc=(
"Threshold for TV (total variation) constraint in the x-direction." 528 "NaN results in no TVx penalty."))
529 tvyThresh = pexConfig.Field(dtype=float, default=np.nan,
530 doc=(
"Threshold for TV (total variation) constraint in the y-direction." 531 "NaN results in no TVy penalty."))
534 useWeights = pexConfig.Field(dtype=bool, default=
False, doc=
"Use inverse variance as deblender weights")
535 bgScale = pexConfig.Field(
536 dtype=float, default=0.5,
537 doc=(
"Fraction of background RMS level to use as a" 538 "cutoff for defining the background of the image" 539 "This is used to initialize the model for each source" 540 "and to set the size of the bounding box for each source" 541 "every `refinementSkip` iteration."))
542 usePsfConvolution = pexConfig.Field(
543 dtype=bool, default=
True,
544 doc=(
"Whether or not to convolve the morphology with the" 545 "PSF in each band or use the same morphology in all bands"))
546 saveTemplates = pexConfig.Field(
547 dtype=bool, default=
True,
548 doc=
"Whether or not to save the SEDs and templates")
549 processSingles = pexConfig.Field(
550 dtype=bool, default=
False,
551 doc=
"Whether or not to process isolated sources in the deblender")
552 badMask = pexConfig.Field(
553 dtype=str, default=
"BAD,CR,NO_DATA,SAT,SUSPECT",
554 doc=
"Whether or not to process isolated sources in the deblender")
557 maxNumberOfPeaks = pexConfig.Field(
558 dtype=int, default=0,
559 doc=(
"Only deblend the brightest maxNumberOfPeaks peaks in the parent" 560 " (<= 0: unlimited)"))
561 maxFootprintArea = pexConfig.Field(
562 dtype=int, default=1000000,
563 doc=(
"Maximum area for footprints before they are ignored as large; " 564 "non-positive means no threshold applied"))
565 maxFootprintSize = pexConfig.Field(
566 dtype=int, default=0,
567 doc=(
"Maximum linear dimension for footprints before they are ignored " 568 "as large; non-positive means no threshold applied"))
569 minFootprintAxisRatio = pexConfig.Field(
570 dtype=float, default=0.0,
571 doc=(
"Minimum axis ratio for footprints before they are ignored " 572 "as large; non-positive means no threshold applied"))
573 notDeblendedMask = pexConfig.Field(
574 dtype=str, default=
"NOT_DEBLENDED", optional=
True,
575 doc=
"Mask name for footprints not deblended, or None")
577 tinyFootprintSize = pexConfig.RangeField(
578 dtype=int, default=2, min=2, inclusiveMin=
True,
579 doc=(
'Footprints smaller in width or height than this value will ' 580 'be ignored; minimum of 2 due to PSF gradient calculation.'))
581 catchFailures = pexConfig.Field(
582 dtype=bool, default=
False,
583 doc=(
"If True, catch exceptions thrown by the deblender, log them, " 584 "and set a flag on the parent, instead of letting them propagate up"))
585 propagateAllPeaks = pexConfig.Field(dtype=bool, default=
False,
586 doc=(
'Guarantee that all peaks produce a child source.'))
587 maskPlanes = pexConfig.ListField(dtype=str, default=[
"SAT",
"INTRP",
"NO_DATA"],
588 doc=
"Mask planes to ignore when performing statistics")
589 maskLimits = pexConfig.DictField(
593 doc=(
"Mask planes with the corresponding limit on the fraction of masked pixels. " 594 "Sources violating this limit will not be deblended."),
597 edgeHandling = pexConfig.ChoiceField(
598 doc=
'What to do when a peak to be deblended is close to the edge of the image',
599 dtype=str, default=
'noclip',
601 'clip':
'Clip the template at the edge AND the mirror of the edge.',
602 'ramp':
'Ramp down flux at the image edge by the PSF',
603 'noclip':
'Ignore the edge when building the symmetric template.',
607 medianSmoothTemplate = pexConfig.Field(dtype=bool, default=
False,
608 doc=
"Apply a smoothing filter to all of the template images")
609 medianFilterHalfsize = pexConfig.Field(dtype=float, default=2,
610 doc=(
'Half size of the median smoothing filter'))
611 clipFootprintToNonzero = pexConfig.Field(dtype=bool, default=
False,
612 doc=(
"Clip non-zero spans in the footprints"))
614 conserveFlux = pexConfig.Field(dtype=bool, default=
True,
615 doc=(
"Reapportion flux to the footprints so that flux is conserved"))
616 weightTemplates = pexConfig.Field(
617 dtype=bool, default=
False,
618 doc=(
"If true, a least-squares fit of the templates will be done to the " 619 "full image. The templates will be re-weighted based on this fit."))
620 strayFluxToPointSources = pexConfig.ChoiceField(
621 doc=
'When the deblender should attribute stray flux to point sources',
622 dtype=str, default=
'necessary',
624 'necessary':
'When there is not an extended object in the footprint',
626 'never': (
'Never; stray flux will not be attributed to any deblended child ' 627 'if the deblender thinks all peaks look like point sources'),
631 assignStrayFlux = pexConfig.Field(dtype=bool, default=
True,
632 doc=
'Assign stray flux (not claimed by any child in the deblender) ' 633 'to deblend children.')
635 strayFluxRule = pexConfig.ChoiceField(
636 doc=
'How to split flux among peaks',
637 dtype=str, default=
'trim',
639 'r-to-peak':
'~ 1/(1+R^2) to the peak',
640 'r-to-footprint': (
'~ 1/(1+R^2) to the closest pixel in the footprint. ' 641 'CAUTION: this can be computationally expensive on large footprints!'),
642 'nearest-footprint': (
'Assign 100% to the nearest footprint (using L-1 norm aka ' 643 'Manhattan distance)'),
644 'trim': (
'Shrink the parent footprint to pixels that are not assigned to children')
648 clipStrayFluxFraction = pexConfig.Field(dtype=float, default=0.001,
649 doc=(
'When splitting stray flux, clip fractions below ' 650 'this value to zero.'))
651 getTemplateSum = pexConfig.Field(dtype=bool, default=
False,
652 doc=(
"As part of the flux calculation, the sum of the templates is" 653 "calculated. If 'getTemplateSum==True' then the sum of the" 654 "templates is stored in the result (a 'PerFootprint')."))
658 """MultibandDeblendTask 660 Split blended sources into individual sources. 662 This task has no return value; it only modifies the SourceCatalog in-place. 664 ConfigClass = MultibandDeblendConfig
665 _DefaultName =
"multibandDeblend" 667 def __init__(self, schema, peakSchema=None, **kwargs):
668 """Create the task, adding necessary fields to the given schema. 672 schema: `lsst.afw.table.schema.schema.Schema` 673 Schema object for measurement fields; will be modified in-place. 674 peakSchema: `lsst.afw.table.schema.schema.Schema` 675 Schema of Footprint Peaks that will be passed to the deblender. 676 Any fields beyond the PeakTable minimal schema will be transferred 677 to the main source Schema. If None, no fields will be transferred 680 Names of the filters used for the eposures. This is needed to store the SED as a field 682 Passed to Task.__init__. 686 pipeBase.Task.__init__(self, **kwargs)
687 if not self.config.conserveFlux
and not self.config.saveTemplates:
688 raise ValueError(
"Either `conserveFlux` or `saveTemplates` must be True")
690 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
691 if peakSchema
is None:
697 for item
in peakSchema:
698 if item.key
not in peakMinimalSchema:
704 schema.addField(item.field)
705 assert schema == self.
peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas" 712 config = scarlet.config.Config(
713 center_min_dist=self.config.minTranslation,
714 edge_flux_thresh=self.config.edgeFluxThresh,
715 exact_lipschitz=self.config.exactLipschitz,
716 refine_skip=self.config.refinementSkip,
717 slack=self.config.stepSlack,
719 if self.config.translationMethod !=
"default":
720 err =
"Currently the only supported translationMethod is 'default', you entered '{0}'" 721 raise NotImplementedError(err.format(self.config.translationMethod))
726 _constraints = self.config.constraints.split(
",")
727 if (sorted(_constraints) != [
'+',
'1',
'M',
'S']
or 728 ~np.isnan(self.config.l0Thresh)
or 729 ~np.isnan(self.config.l1Thresh)):
731 "+": scarlet.constraint.PositivityConstraint,
732 "1": scarlet.constraint.SimpleConstraint,
733 "M": scarlet.constraint.DirectMonotonicityConstraint(use_nearest=
False),
734 "S": scarlet.constraint.DirectSymmetryConstraint(sigma=self.config.symmetryThresh)
736 for c
in _constraints:
737 if constraints
is None:
738 constraints = [constraintDict[c]]
740 constraints += [constraintDict[c]]
741 if constraints
is None:
742 constraints = scarlet.constraint.MinimalConstraint()
743 if ~np.isnan(self.config.l0Thresh):
744 constraints += [scarlet.constraint.L0Constraint(self.config.l0Thresh)]
745 if ~np.isnan(self.config.l1Thresh):
746 constraints += [scarlet.constraint.L1Constraint(self.config.l1Thresh)]
747 if ~np.isnan(self.config.tvxThresh):
748 constraints += [scarlet.constraint.TVxConstraint(self.config.tvxThresh)]
749 if ~np.isnan(self.config.tvyThresh):
750 constraints += [scarlet.constraint.TVyConstraint(self.config.tvyThresh)]
753 plugins.buildMultibandTemplates,
754 useWeights=self.config.useWeights,
755 usePsf=self.config.usePsfConvolution,
756 constraints=constraints,
758 maxIter=self.config.maxIter,
759 bgScale=self.config.bgScale,
760 relativeError=self.config.relativeError,
761 badMask=self.config.badMask.split(
","),
767 if self.config.edgeHandling ==
'ramp':
769 if self.config.medianSmoothTemplate:
771 plugins.medianSmoothTemplates,
772 medianFilterHalfsize=self.config.medianFilterHalfsize))
773 if self.config.clipFootprintToNonzero:
775 if self.config.conserveFlux:
776 if self.config.weightTemplates:
779 plugins.apportionFlux,
780 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
781 assignStrayFlux=self.config.assignStrayFlux,
782 strayFluxAssignment=self.config.strayFluxRule,
783 strayFluxToPointSources=self.config.strayFluxToPointSources,
784 getTemplateSum=self.config.getTemplateSum))
786 def _addSchemaKeys(self, schema):
787 """Add deblender specific keys to the schema 789 self.
runtimeKey = schema.addField(
'runtime', type=np.float32, doc=
'runtime in ms')
791 self.
nChildKey = schema.addField(
'deblend_nChild', type=np.int32,
792 doc=
'Number of children this object has (defaults to 0)')
793 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
794 doc=
'Deblender thought this source looked like a PSF')
796 doc=
'Source had too many peaks; ' 797 'only the brightest were included')
798 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
799 doc=
'Parent footprint covered too many pixels')
800 self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
801 doc=
'Parent footprint was predominantly masked')
803 doc=
"Deblending failed on source")
806 doc=
"Deblender skipped this source")
810 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
811 'If deblended-as-psf, the PSF centroid',
"pixel")
812 self.
psfFluxKey = schema.addField(
'deblend_psf_instFlux', type=
'D',
813 doc=
'If deblended-as-psf, the instrumental PSF flux', units=
'count')
815 'deblend_rampedTemplate', type=
'Flag',
816 doc=(
'This source was near an image edge and the deblender used ' 817 '"ramp" edge-handling.'))
820 'deblend_patchedTemplate', type=
'Flag',
821 doc=(
'This source was near an image edge and the deblender used ' 822 '"patched" edge-handling.'))
825 'deblend_hasStrayFlux', type=
'Flag',
826 doc=(
'This source was assigned some stray flux'))
828 self.log.trace(
'Added keys to schema: %s',
", ".join(str(x)
for x
in (
833 def run(self, mExposure, mergedSources):
834 """Get the psf from each exposure and then run deblend(). 838 mExposure: `MultibandExposure` 839 The exposures should be co-added images of the same 840 shape and region of the sky. 841 mergedSources: `SourceCatalog` 842 The merged `SourceCatalog` that contains parent footprints 843 to (potentially) deblend. 847 fluxCatalogs: dict or None 848 Keys are the names of the filters and the values are 849 `lsst.afw.table.source.source.SourceCatalog`'s. 850 These are the flux-conserved catalogs with heavy footprints with 851 the image data weighted by the multiband templates. 852 If `self.config.conserveFlux` is `False`, then this item will be None 853 templateCatalogs: dict or None 854 Keys are the names of the filters and the values are 855 `lsst.afw.table.source.source.SourceCatalog`'s. 856 These are catalogs with heavy footprints that are the templates 857 created by the multiband templates. 858 If `self.config.saveTemplates` is `False`, then this item will be None 860 psfs = {f: mExposure[f].getPsf()
for f
in mExposure.filters}
861 return self.
deblend(mExposure, mergedSources, psfs)
863 def _getPsfFwhm(self, psf, bbox):
864 return psf.computeShape().getDeterminantRadius() * 2.35
866 def _addChild(self, parentId, peak, sources, heavy):
867 """Add a child to a catalog 869 This creates a new child in the source catalog, 870 assigning it a parent id, adding a footprint, 871 and setting all appropriate flags based on the 874 assert len(heavy.getPeaks()) == 1
875 src = sources.addNew()
877 src.setParent(parentId)
878 src.setFootprint(heavy)
879 src.set(self.
psfKey, peak.deblendedAsPsf)
888 """Deblend a data cube of multiband images 892 mExposure: `MultibandExposure` 893 The exposures should be co-added images of the same 894 shape and region of the sky. 895 sources: `SourceCatalog` 896 The merged `SourceCatalog` that contains parent footprints 897 to (potentially) deblend. 899 Keys are the names of the filters 900 (should be the same as `mExposure.filters`) 901 and the values are the PSFs in each band. 905 fluxCatalogs: dict or None 906 Keys are the names of the filters and the values are 907 `lsst.afw.table.source.source.SourceCatalog`'s. 908 These are the flux-conserved catalogs with heavy footprints with 909 the image data weighted by the multiband templates. 910 If `self.config.conserveFlux` is `False`, then this item will be None 911 templateCatalogs: dict or None 912 Keys are the names of the filters and the values are 913 `lsst.afw.table.source.source.SourceCatalog`'s. 914 These are catalogs with heavy footprints that are the templates 915 created by the multiband templates. 916 If `self.config.saveTemplates` is `False`, then this item will be None 920 if tuple(psfs.keys()) != mExposure.filters:
921 msg =
"PSF keys must be the same as mExposure.filters ({0}), got {1}" 922 raise ValueError(msg.format(mExposure.filters, psfs.keys()))
924 filters = mExposure.filters
925 mMaskedImage = afwImage.MultibandMaskedImage(filters=mExposure.filters, image=mExposure.image,
926 mask=mExposure.mask, variance=mExposure.variance)
927 self.log.info(
"Deblending {0} sources in {1} exposures".format(len(sources), len(mExposure)))
932 exposure = mExposure[f]
933 mi = exposure.getMaskedImage()
934 statsCtrl = afwMath.StatisticsControl()
935 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
936 stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl)
937 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
938 self.log.trace(
'Exposure {0}, sigma1: {1}'.format(f, sigma1))
942 if self.config.conserveFlux:
945 _catalog = afwTable.SourceCatalog(sources.table.clone())
946 _catalog.extend(sources)
947 fluxCatalogs[f] = _catalog
950 if self.config.saveTemplates:
951 templateCatalogs = {}
953 _catalog = afwTable.SourceCatalog(sources.table.clone())
954 _catalog.extend(sources)
955 templateCatalogs[f] = _catalog
957 templateCatalogs =
None 961 for pk, src
in enumerate(sources):
962 foot = src.getFootprint()
963 logger.info(
"id: {0}".format(src[
"id"]))
964 peaks = foot.getPeaks()
971 if len(peaks) < 2
and not self.config.processSingles:
973 if self.config.saveTemplates:
974 templateCatalogs[f][pk].set(self.
runtimeKey, 0)
975 if self.config.conserveFlux:
980 self.
skipParent(src, [mi.getMask()
for mi
in mMaskedImage])
981 self.log.trace(
'Parent %i: skipping large footprint', int(src.getId()))
983 if self.
isMasked(foot, exposure.getMaskedImage().getMask()):
986 self.log.trace(
'Parent %i: skipping masked footprint', int(src.getId()))
988 if len(peaks) > self.config.maxNumberOfPeaks:
990 msg =
'Parent {0}: Too many peaks, using the first {1} peaks' 991 self.log.trace(msg.format(int(src.getId()), self.config.maxNumberOfPeaks))
994 bbox = foot.getBBox()
995 psf_fwhms = {f: self.
_getPsfFwhm(psf, bbox)
for f, psf
in psfs.items()}
996 self.log.trace(
'Parent %i: deblending %i peaks', int(src.getId()), len(peaks))
1003 images = mMaskedImage[:, bbox]
1004 psf_list = [psfs[f]
for f
in filters]
1005 fwhm_list = [psf_fwhms[f]
for f
in filters]
1006 avgNoise = [sigmas[f]
for f
in filters]
1010 mMaskedImage=images,
1014 maxNumberOfPeaks=self.config.maxNumberOfPeaks)
1016 runtime = (tf-t0)*1000
1021 except Exception
as e:
1022 if self.config.catchFailures:
1023 self.log.warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
1027 traceback.print_exc()
1033 templateParents = {}
1035 parentId = src.getId()
1037 if self.config.saveTemplates:
1038 templateParents[f] = templateCatalogs[f][pk]
1039 templateParents[f].set(self.
runtimeKey, runtime)
1040 if self.config.conserveFlux:
1041 fluxParents[f] = fluxCatalogs[f][pk]
1048 for j, multiPeak
in enumerate(result.peaks):
1049 heavy = {f: peak.getFluxPortion()
for f, peak
in multiPeak.deblendedPeaks.items()}
1050 no_flux = all([v
is None for v
in heavy.values()])
1051 skip_peak = all([peak.skip
for peak
in multiPeak.deblendedPeaks.values()])
1052 if no_flux
or skip_peak:
1054 if not self.config.propagateAllPeaks:
1059 msg =
"Peak at {0} failed deblending. Using minimal default info for child." 1060 self.log.trace(msg.format(multiPeak.x, multiPeak.y))
1063 pfoot = afwDet.Footprint(foot)
1064 peakList = pfoot.getPeaks()
1066 pfoot.addPeak(multiPeak.x, multiPeak.y, 0)
1067 zeroMimg = afwImage.MaskedImageF(pfoot.getBBox())
1069 heavy[f] = afwDet.makeHeavyFootprint(pfoot, zeroMimg)
1075 if len(heavy[f].getPeaks()) != 1:
1076 err =
"Heavy footprint should have a single peak, got {0}" 1077 raise ValueError(err.format(len(heavy[f].getPeaks())))
1078 peak = multiPeak.deblendedPeaks[f]
1079 if self.config.saveTemplates:
1080 cat = templateCatalogs[f]
1081 tfoot = peak.templateFootprint
1082 timg = afwImage.MaskedImageF(peak.templateImage)
1083 tHeavy = afwDet.makeHeavyFootprint(tfoot, timg)
1084 child = self.
_addChild(parentId, peak, cat, tHeavy)
1086 child.setId(src.getId())
1089 templateSpans[f] = templateSpans[f].union(tHeavy.getSpans())
1090 if self.config.conserveFlux:
1091 cat = fluxCatalogs[f]
1092 child = self.
_addChild(parentId, peak, cat, heavy[f])
1094 child.setId(src.getId())
1097 fluxSpans[f] = fluxSpans[f].union(heavy[f].getSpans())
1106 if self.config.saveTemplates:
1107 templateParents[f].set(self.
nChildKey, nchild)
1108 templateParents[f].getFootprint().setSpans(templateSpans[f])
1109 if self.config.conserveFlux:
1110 fluxParents[f].set(self.
nChildKey, nchild)
1111 fluxParents[f].getFootprint().setSpans(fluxSpans[f])
1114 pk, npre, foot, psfs, psf_fwhms, sigmas, result)
1116 if fluxCatalogs
is not None:
1117 n1 = len(list(fluxCatalogs.values())[0])
1119 n1 = len(list(templateCatalogs.values())[0])
1120 self.log.info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources' 1121 % (n0, nparents, n1-n0, n1))
1122 return fluxCatalogs, templateCatalogs
1128 pk, npre, fp, psfs, psf_fwhms, sigmas, result):
1132 """Returns whether a Footprint is large 1134 'Large' is defined by thresholds on the area, size and axis ratio. 1135 These may be disabled independently by configuring them to be non-positive. 1137 This is principally intended to get rid of satellite streaks, which the 1138 deblender or other downstream processing can have trouble dealing with 1139 (e.g., multiple large HeavyFootprints can chew up memory). 1141 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
1143 if self.config.maxFootprintSize > 0:
1144 bbox = footprint.getBBox()
1145 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
1147 if self.config.minFootprintAxisRatio > 0:
1148 axes = afwEll.Axes(footprint.getShape())
1149 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
1154 """Returns whether the footprint violates the mask limits""" 1155 size = float(footprint.getArea())
1156 for maskName, limit
in self.config.maskLimits.items():
1157 maskVal = mask.getPlaneBitMask(maskName)
1158 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal)
1159 if (size - unmaskedSpan.getArea())/size > limit:
1164 """Indicate that the parent source is not being deblended 1166 We set the appropriate flags and masks for each exposure. 1170 source: `lsst.afw.table.source.source.SourceRecord` 1171 The source to flag as skipped 1172 masks: list of `lsst.afw.image.MaskX` 1173 The mask in each band to update with the non-detection 1175 fp = source.getFootprint()
1177 source.set(self.
nChildKey, len(fp.getPeaks()))
1178 if self.config.notDeblendedMask:
1180 mask.addMaskPlane(self.config.notDeblendedMask)
1181 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
def newDeblend(debPlugins, footprint, mMaskedImage, psfs, psfFwhms, log=None, verbose=False, avgNoise=None, maxNumberOfPeaks=0)
def _addChild(self, parentId, peak, sources, heavy)
def isLargeFootprint(self, footprint)
def isLargeFootprint(self, footprint)
deblendPatchedTemplateKey
def postSingleDeblendHook(self, exposures, fluxCatalogs, templateCatalogs, pk, npre, fp, psfs, psf_fwhms, sigmas, result)
def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res)
def _getPsfFwhm(self, psf, bbox)
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 __init__(self, schema, peakSchema=None, kwargs)
def _getPsfFwhm(self, psf, bbox)
def _addSchemaKeys(self, schema)
def preSingleDeblendHook(self, exposures, sources, pk, fp, psfs, psf_fwhms, sigmas)
def addSchemaKeys(self, schema)
Split blended sources into individual sources.
def skipParent(self, source, mask)
def deblend(self, exposure, srcs, psf)
Deblend.
def skipParent(self, source, masks)
def __init__(self, schema, peakSchema=None, kwargs)
Create the task, adding necessary fields to the given schema.
def deblend(self, mExposure, sources, psfs)
def isMasked(self, footprint, mask)
deblendPatchedTemplateKey
def isMasked(self, footprint, mask)
static Log getLogger(std::string const &loggername)
def run(self, mExposure, mergedSources)