38 __all__ =
'SourceDeblendConfig',
'SourceDeblendTask',
'MultibandDeblendConfig',
'MultibandDeblendTask' 43 edgeHandling = pexConfig.ChoiceField(
44 doc=
'What to do when a peak to be deblended is close to the edge of the image',
45 dtype=str, default=
'ramp',
47 'clip':
'Clip the template at the edge AND the mirror of the edge.',
48 'ramp':
'Ramp down flux at the image edge by the PSF',
49 'noclip':
'Ignore the edge when building the symmetric template.',
53 strayFluxToPointSources = pexConfig.ChoiceField(
54 doc=
'When the deblender should attribute stray flux to point sources',
55 dtype=str, default=
'necessary',
57 'necessary':
'When there is not an extended object in the footprint',
59 'never': (
'Never; stray flux will not be attributed to any deblended child ' 60 'if the deblender thinks all peaks look like point sources'),
64 assignStrayFlux = pexConfig.Field(dtype=bool, default=
True,
65 doc=
'Assign stray flux (not claimed by any child in the deblender) ' 66 'to deblend children.')
68 strayFluxRule = pexConfig.ChoiceField(
69 doc=
'How to split flux among peaks',
70 dtype=str, default=
'trim',
72 'r-to-peak':
'~ 1/(1+R^2) to the peak',
73 'r-to-footprint': (
'~ 1/(1+R^2) to the closest pixel in the footprint. ' 74 'CAUTION: this can be computationally expensive on large footprints!'),
75 'nearest-footprint': (
'Assign 100% to the nearest footprint (using L-1 norm aka ' 76 'Manhattan distance)'),
77 'trim': (
'Shrink the parent footprint to pixels that are not assigned to children')
81 clipStrayFluxFraction = pexConfig.Field(dtype=float, default=0.001,
82 doc=(
'When splitting stray flux, clip fractions below ' 83 'this value to zero.'))
84 psfChisq1 = pexConfig.Field(dtype=float, default=1.5, optional=
False,
85 doc=(
'Chi-squared per DOF cut for deciding a source is ' 86 'a PSF during deblending (un-shifted PSF model)'))
87 psfChisq2 = pexConfig.Field(dtype=float, default=1.5, optional=
False,
88 doc=(
'Chi-squared per DOF cut for deciding a source is ' 89 'PSF during deblending (shifted PSF model)'))
90 psfChisq2b = pexConfig.Field(dtype=float, default=1.5, optional=
False,
91 doc=(
'Chi-squared per DOF cut for deciding a source is ' 92 'a PSF during deblending (shifted PSF model #2)'))
93 maxNumberOfPeaks = pexConfig.Field(dtype=int, default=0,
94 doc=(
"Only deblend the brightest maxNumberOfPeaks peaks in the parent" 95 " (<= 0: unlimited)"))
96 maxFootprintArea = pexConfig.Field(dtype=int, default=1000000,
97 doc=(
"Maximum area for footprints before they are ignored as large; " 98 "non-positive means no threshold applied"))
99 maxFootprintSize = pexConfig.Field(dtype=int, default=0,
100 doc=(
"Maximum linear dimension for footprints before they are ignored " 101 "as large; non-positive means no threshold applied"))
102 minFootprintAxisRatio = pexConfig.Field(dtype=float, default=0.0,
103 doc=(
"Minimum axis ratio for footprints before they are ignored " 104 "as large; non-positive means no threshold applied"))
105 notDeblendedMask = pexConfig.Field(dtype=str, default=
"NOT_DEBLENDED", optional=
True,
106 doc=
"Mask name for footprints not deblended, or None")
108 tinyFootprintSize = pexConfig.RangeField(dtype=int, default=2, min=2, inclusiveMin=
True,
109 doc=(
'Footprints smaller in width or height than this value will ' 110 'be ignored; minimum of 2 due to PSF gradient calculation.'))
112 propagateAllPeaks = pexConfig.Field(dtype=bool, default=
False,
113 doc=(
'Guarantee that all peaks produce a child source.'))
114 catchFailures = pexConfig.Field(dtype=bool, default=
False,
115 doc=(
"If True, catch exceptions thrown by the deblender, log them, " 116 "and set a flag on the parent, instead of letting them propagate up"))
117 maskPlanes = pexConfig.ListField(dtype=str, default=[
"SAT",
"INTRP",
"NO_DATA"],
118 doc=
"Mask planes to ignore when performing statistics")
119 maskLimits = pexConfig.DictField(
123 doc=(
"Mask planes with the corresponding limit on the fraction of masked pixels. " 124 "Sources violating this limit will not be deblended."),
126 weightTemplates = pexConfig.Field(dtype=bool, default=
False,
127 doc=(
"If true, a least-squares fit of the templates will be done to the " 128 "full image. The templates will be re-weighted based on this fit."))
129 removeDegenerateTemplates = pexConfig.Field(dtype=bool, default=
False,
130 doc=(
"Try to remove similar templates?"))
131 maxTempDotProd = pexConfig.Field(dtype=float, default=0.5,
132 doc=(
"If the dot product between two templates is larger than this value" 133 ", we consider them to be describing the same object (i.e. they are " 134 "degenerate). If one of the objects has been labeled as a PSF it " 135 "will be removed, otherwise the template with the lowest value will " 137 medianSmoothTemplate = pexConfig.Field(dtype=bool, default=
True,
138 doc=
"Apply a smoothing filter to all of the template images")
150 \anchor SourceDeblendTask_ 152 \brief Split blended sources into individual sources. 154 This task has no return value; it only modifies the SourceCatalog in-place. 156 ConfigClass = SourceDeblendConfig
157 _DefaultName =
"sourceDeblend" 159 def __init__(self, schema, peakSchema=None, **kwargs):
161 Create the task, adding necessary fields to the given schema. 163 @param[in,out] schema Schema object for measurement fields; will be modified in-place. 164 @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender. 165 Any fields beyond the PeakTable minimal schema will be transferred 166 to the main source Schema. If None, no fields will be transferred 168 @param[in] **kwargs Passed to Task.__init__. 170 pipeBase.Task.__init__(self, **kwargs)
171 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
172 if peakSchema
is None:
178 for item
in peakSchema:
179 if item.key
not in peakMinimalSchema:
185 schema.addField(item.field)
186 assert schema == self.
peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas" 190 self.
nChildKey = schema.addField(
'deblend_nChild', type=np.int32,
191 doc=
'Number of children this object has (defaults to 0)')
192 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
193 doc=
'Deblender thought this source looked like a PSF')
194 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
195 'If deblended-as-psf, the PSF centroid',
"pixel")
196 self.
psfFluxKey = schema.addField(
'deblend_psfFlux', type=
'D',
197 doc=
'If deblended-as-psf, the PSF flux')
199 doc=
'Source had too many peaks; ' 200 'only the brightest were included')
201 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
202 doc=
'Parent footprint covered too many pixels')
203 self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
204 doc=
'Parent footprint was predominantly masked')
206 if self.config.catchFailures:
208 doc=
"Deblending failed on source")
211 doc=
"Deblender skipped this source")
214 'deblend_rampedTemplate', type=
'Flag',
215 doc=(
'This source was near an image edge and the deblender used ' 216 '"ramp" edge-handling.'))
219 'deblend_patchedTemplate', type=
'Flag',
220 doc=(
'This source was near an image edge and the deblender used ' 221 '"patched" edge-handling.'))
224 'deblend_hasStrayFlux', type=
'Flag',
225 doc=(
'This source was assigned some stray flux'))
227 self.log.trace(
'Added keys to schema: %s',
", ".join(str(x)
for x
in (
232 def run(self, exposure, sources):
234 Get the psf from the provided exposure and then run deblend(). 236 @param[in] exposure Exposure to process 237 @param[in,out] sources SourceCatalog containing sources detected on this exposure. 241 psf = exposure.getPsf()
242 self.
deblend(exposure, sources, psf)
244 def _getPsfFwhm(self, psf, bbox):
247 return psf.computeShape().getDeterminantRadius() * 2.35
254 @param[in] exposure Exposure to process 255 @param[in,out] srcs SourceCatalog containing sources detected on this exposure. 260 self.log.info(
"Deblending %d sources" % len(srcs))
265 mi = exposure.getMaskedImage()
266 statsCtrl = afwMath.StatisticsControl()
267 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
268 stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl)
269 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
270 self.log.trace(
'sigma1: %g', sigma1)
274 for i, src
in enumerate(srcs):
277 fp = src.getFootprint()
290 self.log.trace(
'Parent %i: skipping large footprint', int(src.getId()))
292 if self.
isMasked(fp, exposure.getMaskedImage().getMask()):
295 self.log.trace(
'Parent %i: skipping masked footprint', int(src.getId()))
302 self.log.trace(
'Parent %i: deblending %i peaks', int(src.getId()), len(pks))
308 src.set(self.
tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
312 fp, mi, psf, psf_fwhm, sigma1=sigma1,
313 psfChisqCut1=self.config.psfChisq1,
314 psfChisqCut2=self.config.psfChisq2,
315 psfChisqCut2b=self.config.psfChisq2b,
316 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
317 strayFluxToPointSources=self.config.strayFluxToPointSources,
318 assignStrayFlux=self.config.assignStrayFlux,
319 strayFluxAssignment=self.config.strayFluxRule,
320 rampFluxAtEdge=(self.config.edgeHandling ==
'ramp'),
321 patchEdges=(self.config.edgeHandling ==
'noclip'),
322 tinyFootprintSize=self.config.tinyFootprintSize,
323 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
324 weightTemplates=self.config.weightTemplates,
325 removeDegenerateTemplates=self.config.removeDegenerateTemplates,
326 maxTempDotProd=self.config.maxTempDotProd,
327 medianSmoothTemplate=self.config.medianSmoothTemplate
329 if self.config.catchFailures:
331 except Exception
as e:
332 if self.config.catchFailures:
333 self.log.warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
336 traceback.print_exc()
343 for j, peak
in enumerate(res.deblendedParents[0].peaks):
344 heavy = peak.getFluxPortion()
345 if heavy
is None or peak.skip:
347 if not self.config.propagateAllPeaks:
352 self.log.trace(
"Peak at (%i,%i) failed. Using minimal default info for child.",
353 pks[j].getIx(), pks[j].getIy())
356 foot = afwDet.Footprint(src.getFootprint())
357 peakList = foot.getPeaks()
359 peakList.append(peak.peak)
360 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
361 heavy = afwDet.makeHeavyFootprint(foot, zeroMimg)
362 if peak.deblendedAsPsf:
363 if peak.psfFitFlux
is None:
364 peak.psfFitFlux = 0.0
365 if peak.psfFitCenter
is None:
366 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
368 assert(len(heavy.getPeaks()) == 1)
371 child = srcs.addNew()
374 child.setParent(src.getId())
375 child.setFootprint(heavy)
376 child.set(self.
psfKey, peak.deblendedAsPsf)
378 if peak.deblendedAsPsf:
379 (cx, cy) = peak.psfFitCenter
391 spans = src.getFootprint().spans
393 spans = spans.union(child.getFootprint().spans)
394 src.getFootprint().setSpans(spans)
402 self.log.info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources' 403 % (n0, nparents, n1-n0, n1))
408 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
412 """Returns whether a Footprint is large 414 'Large' is defined by thresholds on the area, size and axis ratio. 415 These may be disabled independently by configuring them to be non-positive. 417 This is principally intended to get rid of satellite streaks, which the 418 deblender or other downstream processing can have trouble dealing with 419 (e.g., multiple large HeavyFootprints can chew up memory). 421 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
423 if self.config.maxFootprintSize > 0:
424 bbox = footprint.getBBox()
425 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
427 if self.config.minFootprintAxisRatio > 0:
428 axes = afwEll.Axes(footprint.getShape())
429 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
434 """Returns whether the footprint violates the mask limits""" 435 size = float(footprint.getArea())
436 for maskName, limit
in self.config.maskLimits.items():
437 maskVal = mask.getPlaneBitMask(maskName)
438 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal)
439 if (size - unmaskedSpan.getArea())/size > limit:
444 """Indicate that the parent source is not being deblended 446 We set the appropriate flags and mask. 448 @param source The source to flag as skipped 449 @param mask The mask to update 451 fp = source.getFootprint()
453 source.set(self.
nChildKey, len(fp.getPeaks()))
454 if self.config.notDeblendedMask:
455 mask.addMaskPlane(self.config.notDeblendedMask)
456 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
459 """MultibandDeblendConfig 461 Configuration for the multiband deblender. 462 The parameters are organized by the parameter types, which are 463 - Stopping Criteria: Used to determine if the fit has converged 464 - Position Fitting Criteria: Used to fit the positions of the peaks 465 - Constraints: Used to apply constraints to the peaks and their components 466 - Other: Parameters that don't fit into the above categories 469 maxIter = pexConfig.Field(dtype=int, default=200,
470 doc=(
"Maximum number of iterations to deblend a single parent"))
471 relativeError = pexConfig.Field(dtype=float, default=1e-3,
472 doc=(
"Relative error to use when determining stopping criteria"))
475 minTranslation = pexConfig.Field(dtype=float, default=1e-3,
476 doc=(
"A peak must be updated by at least 'minTranslation' (pixels)" 477 "or no update is performed." 478 "This field is ignored if fitPositions is False."))
479 refinementSkip = pexConfig.Field(dtype=int, default=10,
480 doc=(
"If fitPositions is True, the positions and box sizes are" 481 "updated on every 'refinementSkip' iterations."))
482 translationMethod = pexConfig.Field(dtype=str, default=
"default",
483 doc=(
"Method to use for fitting translations." 484 "Currently 'default' is the only available option," 485 "which performs a linear fit, but it is possible that we" 486 "will use galsim or some other method as a future option"))
487 edgeFluxThresh = pexConfig.Field(dtype=float, default=1.0,
488 doc=(
"Boxes are resized when the flux at an edge is " 489 "> edgeFluxThresh * background RMS"))
490 exactLipschitz = pexConfig.Field(dtype=bool, default=
False,
491 doc=(
"Calculate exact Lipschitz constant in every step" 492 "(True) or only calculate the approximate" 493 "Lipschitz constant with significant changes in A,S" 495 stepSlack = pexConfig.Field(dtype=float, default=0.2,
496 doc=(
"A fractional measure of how much a value (like the exactLipschitz)" 497 "can change before it needs to be recalculated." 498 "This must be between 0 and 1."))
501 constraints = pexConfig.Field(dtype=str, default=
"1,+,S,M",
502 doc=(
"List of constraints to use for each object" 503 "(order does not matter)" 504 "Current options are all used by default:\n" 507 "1: normalized SED to unity" 508 "+: non-negative morphology"))
509 symmetryThresh = pexConfig.Field(dtype=float, default=1.0,
510 doc=(
"Strictness of symmetry, from" 511 "0 (no symmetry enforced) to" 512 "1 (perfect symmetry required)." 513 "If 'S' is not in `constraints`, this argument is ignored"))
514 l0Thresh = pexConfig.Field(dtype=float, default=np.nan,
515 doc=(
"L0 threshold. NaN results in no L0 penalty."))
516 l1Thresh = pexConfig.Field(dtype=float, default=np.nan,
517 doc=(
"L1 threshold. NaN results in no L1 penalty."))
518 tvxThresh = pexConfig.Field(dtype=float, default=np.nan,
519 doc=(
"Threshold for TV (total variation) constraint in the x-direction." 520 "NaN results in no TVx penalty."))
521 tvyThresh = pexConfig.Field(dtype=float, default=np.nan,
522 doc=(
"Threshold for TV (total variation) constraint in the y-direction." 523 "NaN results in no TVy penalty."))
526 useWeights = pexConfig.Field(dtype=bool, default=
False, doc=
"Use inverse variance as deblender weights")
527 bgScale = pexConfig.Field(dtype=float, default=0.5,
528 doc=(
"Fraction of background RMS level to use as a" 529 "cutoff for defining the background of the image" 530 "This is used to initialize the model for each source" 531 "and to set the size of the bounding box for each source" 532 "every `refinementSkip` iteration."))
533 usePsfConvolution = pexConfig.Field(dtype=bool, default=
True,
534 doc=(
"Whether or not to convolve the morphology with the" 535 "PSF in each band or use the same morphology" 537 saveTemplates = pexConfig.Field(dtype=bool, default=
True,
538 doc=
"Whether or not to save the SEDs and templates")
539 processSingles = pexConfig.Field(dtype=bool, default=
False,
540 doc=
"Whether or not to process isolated sources in the deblender")
541 badMask = pexConfig.Field(dtype=str, default=
"BAD,CR,NO_DATA,SAT,SUSPECT",
542 doc=
"Whether or not to process isolated sources in the deblender")
545 maxNumberOfPeaks = pexConfig.Field(dtype=int, default=0,
546 doc=(
"Only deblend the brightest maxNumberOfPeaks peaks in the parent" 547 " (<= 0: unlimited)"))
548 maxFootprintArea = pexConfig.Field(dtype=int, default=1000000,
549 doc=(
"Maximum area for footprints before they are ignored as large; " 550 "non-positive means no threshold applied"))
551 maxFootprintSize = pexConfig.Field(dtype=int, default=0,
552 doc=(
"Maximum linear dimension for footprints before they are ignored " 553 "as large; non-positive means no threshold applied"))
554 minFootprintAxisRatio = pexConfig.Field(dtype=float, default=0.0,
555 doc=(
"Minimum axis ratio for footprints before they are ignored " 556 "as large; non-positive means no threshold applied"))
557 notDeblendedMask = pexConfig.Field(dtype=str, default=
"NOT_DEBLENDED", optional=
True,
558 doc=
"Mask name for footprints not deblended, or None")
560 tinyFootprintSize = pexConfig.RangeField(dtype=int, default=2, min=2, inclusiveMin=
True,
561 doc=(
'Footprints smaller in width or height than this value will ' 562 'be ignored; minimum of 2 due to PSF gradient calculation.'))
563 catchFailures = pexConfig.Field(dtype=bool, default=
False,
564 doc=(
"If True, catch exceptions thrown by the deblender, log them, " 565 "and set a flag on the parent, instead of letting them propagate up"))
566 propagateAllPeaks = pexConfig.Field(dtype=bool, default=
False,
567 doc=(
'Guarantee that all peaks produce a child source.'))
568 maskPlanes = pexConfig.ListField(dtype=str, default=[
"SAT",
"INTRP",
"NO_DATA"],
569 doc=
"Mask planes to ignore when performing statistics")
570 maskLimits = pexConfig.DictField(
574 doc=(
"Mask planes with the corresponding limit on the fraction of masked pixels. " 575 "Sources violating this limit will not be deblended."),
578 edgeHandling = pexConfig.ChoiceField(
579 doc=
'What to do when a peak to be deblended is close to the edge of the image',
580 dtype=str, default=
'ramp',
582 'clip':
'Clip the template at the edge AND the mirror of the edge.',
583 'ramp':
'Ramp down flux at the image edge by the PSF',
584 'noclip':
'Ignore the edge when building the symmetric template.',
588 medianSmoothTemplate = pexConfig.Field(dtype=bool, default=
False,
589 doc=
"Apply a smoothing filter to all of the template images")
590 medianFilterHalfsize = pexConfig.Field(dtype=float, default=2,
591 doc=(
'Half size of the median smoothing filter'))
592 clipFootprintToNonzero = pexConfig.Field(dtype=bool, default=
True,
593 doc=(
"Clip non-zero spans in the footprints"))
595 conserveFlux = pexConfig.Field(dtype=bool, default=
False,
596 doc=(
"Reapportion flux to the footprints so that flux is conserved"))
597 weightTemplates = pexConfig.Field(dtype=bool, default=
False,
598 doc=(
"If true, a least-squares fit of the templates will be done to the " 599 "full image. The templates will be re-weighted based on this fit."))
600 strayFluxToPointSources = pexConfig.ChoiceField(
601 doc=
'When the deblender should attribute stray flux to point sources',
602 dtype=str, default=
'necessary',
604 'necessary':
'When there is not an extended object in the footprint',
606 'never': (
'Never; stray flux will not be attributed to any deblended child ' 607 'if the deblender thinks all peaks look like point sources'),
611 assignStrayFlux = pexConfig.Field(dtype=bool, default=
True,
612 doc=
'Assign stray flux (not claimed by any child in the deblender) ' 613 'to deblend children.')
615 strayFluxRule = pexConfig.ChoiceField(
616 doc=
'How to split flux among peaks',
617 dtype=str, default=
'trim',
619 'r-to-peak':
'~ 1/(1+R^2) to the peak',
620 'r-to-footprint': (
'~ 1/(1+R^2) to the closest pixel in the footprint. ' 621 'CAUTION: this can be computationally expensive on large footprints!'),
622 'nearest-footprint': (
'Assign 100% to the nearest footprint (using L-1 norm aka ' 623 'Manhattan distance)'),
624 'trim': (
'Shrink the parent footprint to pixels that are not assigned to children')
628 clipStrayFluxFraction = pexConfig.Field(dtype=float, default=0.001,
629 doc=(
'When splitting stray flux, clip fractions below ' 630 'this value to zero.'))
631 getTemplateSum = pexConfig.Field(dtype=bool, default=
False,
632 doc=(
"As part of the flux calculation, the sum of the templates is" 633 "calculated. If 'getTemplateSum==True' then the sum of the" 634 "templates is stored in the result (a 'PerFootprint')."))
637 """MultibandDeblendTask 639 Split blended sources into individual sources. 641 This task has no return value; it only modifies the SourceCatalog in-place. 643 ConfigClass = MultibandDeblendConfig
644 _DefaultName =
"multibandDeblend" 646 def __init__(self, schema, peakSchema=None, **kwargs):
647 """Create the task, adding necessary fields to the given schema. 651 schema: `lsst.afw.table.schema.schema.Schema` 652 Schema object for measurement fields; will be modified in-place. 653 peakSchema: `lsst.afw.table.schema.schema.Schema` 654 Schema of Footprint Peaks that will be passed to the deblender. 655 Any fields beyond the PeakTable minimal schema will be transferred 656 to the main source Schema. If None, no fields will be transferred 659 Names of the filters used for the eposures. This is needed to store the SED as a field 661 Passed to Task.__init__. 666 pipeBase.Task.__init__(self, **kwargs)
667 if not self.config.conserveFlux
and not self.config.saveTemplates:
668 raise ValueError(
"Either `conserveFlux` or `saveTemplates` must be True")
670 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
671 if peakSchema
is None:
677 for item
in peakSchema:
678 if item.key
not in peakMinimalSchema:
684 schema.addField(item.field)
685 assert schema == self.
peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas" 691 config = scarlet.config.Config(
692 center_min_dist=self.config.minTranslation,
693 edge_flux_thresh=self.config.edgeFluxThresh,
694 exact_lipschitz=self.config.exactLipschitz,
695 refine_skip=self.config.refinementSkip,
696 slack=self.config.stepSlack,
698 if self.config.translationMethod !=
"default":
699 err =
"Currently the only supported translationMethod is 'default', you entered '{0}'" 700 raise NotImplementedError(err.format(self.config.translationMethod))
705 _constraints = self.config.constraints.split(
",")
706 if (sorted(_constraints) != [
'+',
'1',
'M',
'S']
707 or ~np.isnan(self.config.l0Thresh)
708 or ~np.isnan(self.config.l1Thresh)
711 "+": scarlet.constraints.PositivityConstraint,
712 "1": scarlet.constraints.SimpleConstraint,
713 "M": scarlet.constraints.DirectMonotonicityConstraint(use_nearest=
False),
714 "S": scarlet.constraints.DirectSymmetryConstraint(sigma=self.config.symmetryThresh)
716 for c
in _constraints:
717 if constraints
is None:
718 constraints = constraintDict[c]
720 constraints = constraints & constraintDict[c]
721 if constraints
is None:
722 constraints = scarlet.constraints.MinimalConstraint()
723 if ~np.isnan(self.config.l0Thresh):
724 constraints = constraints & scarlet.constraints.L0Constraint(self.config.l0Thresh)
725 if ~np.isnan(self.config.l1Thresh):
726 constraints = constraints & scarlet.constraints.L1Constraint(self.config.l1Thresh)
727 if ~np.isnan(self.config.tvxThresh):
728 constraints = constraints & scarlet.constraints.TVxConstraint(self.config.tvxThresh)
729 if ~np.isnan(self.config.tvyThresh):
730 constraints = constraints & scarlet.constraints.TVyConstraint(self.config.tvyThresh)
733 plugins.buildMultibandTemplates,
734 useWeights=self.config.useWeights,
735 usePsf=self.config.usePsfConvolution,
736 constraints=constraints,
738 maxIter=self.config.maxIter,
739 bgScale=self.config.bgScale,
740 relativeError=self.config.relativeError,
741 badMask=self.config.badMask.split(
","),
747 patchEdges = self.config.edgeHandling ==
'noclip' 748 if self.config.edgeHandling ==
'ramp':
750 if self.config.medianSmoothTemplate:
752 medianFilterHalfsize=self.config.medianFilterHalfsize))
753 if self.config.clipFootprintToNonzero:
755 if self.config.conserveFlux:
756 if self.config.weightTemplates:
759 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
760 assignStrayFlux=self.config.assignStrayFlux,
761 strayFluxAssignment=self.config.strayFluxRule,
762 strayFluxToPointSources=self.config.strayFluxToPointSources,
763 getTemplateSum=self.config.getTemplateSum))
766 def _addSchemaKeys(self, schema):
767 """Add deblender specific keys to the schema 769 self.
runtimeKey = schema.addField(
'runtime', type=np.float32, doc=
'runtime in ms')
771 self.
nChildKey = schema.addField(
'deblend_nChild', type=np.int32,
772 doc=
'Number of children this object has (defaults to 0)')
773 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
774 doc=
'Deblender thought this source looked like a PSF')
776 doc=
'Source had too many peaks; ' 777 'only the brightest were included')
778 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
779 doc=
'Parent footprint covered too many pixels')
780 self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
781 doc=
'Parent footprint was predominantly masked')
783 doc=
"Deblending failed on source")
786 doc=
"Deblender skipped this source")
790 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
791 'If deblended-as-psf, the PSF centroid',
"pixel")
792 self.
psfFluxKey = schema.addField(
'deblend_psfFlux', type=
'D',
793 doc=
'If deblended-as-psf, the PSF flux')
795 'deblend_rampedTemplate', type=
'Flag',
796 doc=(
'This source was near an image edge and the deblender used ' 797 '"ramp" edge-handling.'))
800 'deblend_patchedTemplate', type=
'Flag',
801 doc=(
'This source was near an image edge and the deblender used ' 802 '"patched" edge-handling.'))
805 'deblend_hasStrayFlux', type=
'Flag',
806 doc=(
'This source was assigned some stray flux'))
808 self.log.trace(
'Added keys to schema: %s',
", ".join(str(x)
for x
in (
813 def run(self, exposures, sources):
814 """Get the psf from each exposure and then run deblend(). 819 Keys of the dict are the names of the filters and values are 820 `lsst.afw.image.exposure.exposure.ExposureF`'s. 821 The exposures should be co-added images of the same 822 shape and region of the sky. 824 Keys are the names of the filters and the values are 825 `lsst.afw.table.source.source.SourceCatalog`'s, which 826 should be a merged catalog of the sources in each band. 830 flux_catalogs: dict or None 831 Keys are the names of the filters and the values are 832 `lsst.afw.table.source.source.SourceCatalog`'s. 833 These are the flux-conserved catalogs with heavy footprints with 834 the image data weighted by the multiband templates. 835 If `self.config.conserveFlux` is `False`, then this item will be None 836 template_catalogs: dict or None 837 Keys are the names of the filters and the values are 838 `lsst.afw.table.source.source.SourceCatalog`'s. 839 These are catalogs with heavy footprints that are the templates 840 created by the multiband templates. 841 If `self.config.saveTemplates` is `False`, then this item will be None 843 psfs = {B:exp.getPsf()
for B, exp
in exposures.items()}
844 return self.
deblend(exposures, sources, psfs)
846 def _getPsfFwhm(self, psf, bbox):
847 return psf.computeShape().getDeterminantRadius() * 2.35
849 def _addChild(self, parentId, peak, sources, heavy):
850 """Add a child to a catalog 852 This creates a new child in the source catalog, 853 assigning it a parent id, adding a footprint, 854 and setting all appropriate flags based on the 857 assert len(heavy.getPeaks())==1
858 src = sources.addNew()
860 src.setParent(parentId)
861 src.setFootprint(heavy)
862 src.set(self.
psfKey, peak.deblendedAsPsf)
870 def deblend(self, exposures, sources, psfs, bands=None):
871 """Deblend a data cube of multiband images 876 Keys of the dict are the names of the filters and values are 877 `lsst.afw.image.exposure.exposure.ExposureF`'s. 878 The exposures should be co-added images of the same 879 shape and region of the sky. 881 Keys are the names of the filters and the values are 882 `lsst.afw.table.source.source.SourceCatalog`'s, which 883 should be a merged catalog of the sources in each band ('deepCoadd_mergeDet'). 885 bands: list, default=None 886 Names of the bands in the deblender. 887 If `bands` is `None`, the keys of `exposures` are used. 888 Either `bands` should be specified or `exposures` should be an 889 `OrderedDict` to set the preferential order of the filters. 893 flux_catalogs: dict or None 894 Keys are the names of the filters and the values are 895 `lsst.afw.table.source.source.SourceCatalog`'s. 896 These are the flux-conserved catalogs with heavy footprints with 897 the image data weighted by the multiband templates. 898 If `self.config.conserveFlux` is `False`, then this item will be None 899 template_catalogs: dict or None 900 Keys are the names of the filters and the values are 901 `lsst.afw.table.source.source.SourceCatalog`'s. 902 These are catalogs with heavy footprints that are the templates 903 created by the multiband templates. 904 If `self.config.saveTemplates` is `False`, then this item will be None 910 bands = list(exposures.keys())
911 maskedImages = {band:exp.getMaskedImage()
for band, exp
in exposures.items()}
912 self.log.info(
"Deblending {0} sources in {1} exposures".format(len(sources), len(bands)))
916 for f, exposure
in exposures.items():
917 mi = exposure.getMaskedImage()
918 statsCtrl = afwMath.StatisticsControl()
919 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
920 stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl)
921 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
922 self.log.trace(
'Exposure {0}, sigma1: {1}'.format(f, sigma1))
926 if self.config.conserveFlux:
927 flux_catalogs = {band:afwTable.SourceCatalog(sources.clone())
for band
in bands}
930 if self.config.saveTemplates:
931 template_catalogs = {band:afwTable.SourceCatalog(sources.clone())
for band
in bands}
933 template_catalogs =
None 937 maskedImages = {band: exp.getMaskedImage()
for band, exp
in exposures.items()}
938 for pk, src
in enumerate(sources):
939 foot = src.getFootprint()
940 logger.info(
"id: {0}".format(src[
"id"]))
941 peaks = foot.getPeaks()
948 if len(peaks) < 2
and not self.config.processSingles:
950 if self.config.saveTemplates:
951 tsrc = template_catalogs[band].addNew()
954 templateParents[band] = tsrc
955 if self.config.conserveFlux:
956 tsrc = flux_catalogs[band].addNew()
959 fluxParents[band] = tsrc
963 self.
skipParent(src, [mi.getMask()
for mi
in maskedImages])
964 self.log.trace(
'Parent %i: skipping large footprint', int(src.getId()))
966 if self.
isMasked(foot, exposure.getMaskedImage().getMask()):
969 self.log.trace(
'Parent %i: skipping masked footprint', int(src.getId()))
971 if len(peaks) > self.config.maxNumberOfPeaks:
973 msg =
'Parent {0}: Too many peaks, using the first {1} peaks' 974 self.log.trace(msg.format(int(src.getId()), self.config.maxNumberOfPeaks))
977 bbox = foot.getBBox()
978 psf_fwhms = {band:self.
_getPsfFwhm(psf, bbox)
for band, psf
in psfs.items()}
979 self.log.trace(
'Parent %i: deblending %i peaks', int(src.getId()), len(peaks))
985 PARENT = afwImage.PARENT
987 images = [maskedImages[band].Factory(maskedImages[band], bbox, PARENT)
989 psf_list = [psfs[band]
for band
in bands]
990 fwhm_list = [psf_fwhms[band]
for band
in bands]
991 avgNoise = [sigmas[band]
for band
in bands]
1000 maxNumberOfPeaks=self.config.maxNumberOfPeaks
1003 runtime = (tf-t0)*1000
1008 except Exception
as e:
1009 if self.config.catchFailures:
1010 self.log.warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
1014 traceback.print_exc()
1020 templateParents = {}
1022 parentId = src.getId()
1024 if self.config.saveTemplates:
1025 tsrc = template_catalogs[band].addNew()
1027 tsrc.set(
"id", parentId)
1029 _fp = afwDet.Footprint()
1030 _fp.setPeakSchema(src.getFootprint().getPeaks().getSchema())
1031 tsrc.setFootprint(_fp)
1032 templateParents[band] = tsrc
1033 if self.config.conserveFlux:
1034 tsrc = flux_catalogs[band].addNew()
1037 tsrc.set(
"id", parentId)
1038 _fp = afwDet.Footprint()
1039 _fp.setPeakSchema(src.getFootprint().getPeaks().getSchema())
1040 tsrc.setFootprint(_fp)
1041 fluxParents[band] = tsrc
1044 templateSpans = {band:afwGeom.SpanSet()
for band
in bands}
1045 fluxSpans = {band:afwGeom.SpanSet()
for band
in bands}
1047 for j, multiPeak
in enumerate(result.peaks):
1048 heavy = {band:peak.getFluxPortion()
for band, peak
in multiPeak.deblendedPeaks.items()}
1049 no_flux = all([v
is None for v
in heavy.values()])
1050 skip_peak = all([peak.skip
for peak
in multiPeak.deblendedPeaks.values()])
1051 if no_flux
or skip_peak:
1053 if not self.config.propagateAllPeaks:
1058 msg =
"Peak at {0} failed deblending. Using minimal default info for child." 1059 self.log.trace(msg.format(multiPeak.x, multiPeak.y))
1062 pfoot = afwDet.Footprint(foot)
1063 peakList = pfoot.getPeaks()
1065 pfoot.addPeak(multiPeak.x, multiPeak.y, 0)
1066 zeroMimg = afwImage.MaskedImageF(pfoot.getBBox())
1068 heavy[band] = afwDet.makeHeavyFootprint(pfoot, zeroMimg)
1074 if len(heavy[band].getPeaks()) != 1:
1075 raise ValueError(
"Heavy footprint has multiple peaks, expected 1")
1076 peak = multiPeak.deblendedPeaks[band]
1077 if self.config.saveTemplates:
1078 cat = template_catalogs[band]
1079 tfoot = peak.templateFootprint
1080 timg = afwImage.MaskedImageF(peak.templateImage)
1081 tHeavy = afwDet.makeHeavyFootprint(tfoot, timg)
1082 child = self.
_addChild(parentId, peak, cat, tHeavy)
1084 child.setId(src.getId())
1087 _peak = tHeavy.getPeaks()[0]
1088 templateParents[band].getFootprint().addPeak(_peak.getFx(), _peak.getFy(),
1089 _peak.getPeakValue())
1090 templateSpans[band] = templateSpans[band].union(tHeavy.getSpans())
1091 if self.config.conserveFlux:
1092 cat = flux_catalogs[band]
1093 child = self.
_addChild(parentId, peak, cat, heavy[band])
1095 child.setId(src.getId())
1098 _peak = heavy[band].getPeaks()[0]
1099 fluxParents[band].getFootprint().addPeak(_peak.getFx(), _peak.getFy(),
1100 _peak.getPeakValue())
1101 fluxSpans[band] = fluxSpans[band].union(heavy[band].getSpans())
1110 if self.config.saveTemplates:
1111 templateParents[band].set(self.
nChildKey, nchild)
1112 templateParents[band].getFootprint().setSpans(templateSpans[band])
1113 if self.config.conserveFlux:
1114 fluxParents[band].set(self.
nChildKey, nchild)
1115 fluxParents[band].getFootprint().setSpans(fluxSpans[band])
1118 pk, npre, foot, psfs, psf_fwhms, sigmas, result)
1120 if flux_catalogs
is not None:
1121 n1 = len(list(flux_catalogs.values())[0])
1123 n1 = len(list(template_catalogs.values())[0])
1124 self.log.info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources' 1125 % (n0, nparents, n1-n0, n1))
1126 return flux_catalogs, template_catalogs
1132 pk, npre, fp, psfs, psf_fwhms, sigmas, result):
1136 """Returns whether a Footprint is large 1138 'Large' is defined by thresholds on the area, size and axis ratio. 1139 These may be disabled independently by configuring them to be non-positive. 1141 This is principally intended to get rid of satellite streaks, which the 1142 deblender or other downstream processing can have trouble dealing with 1143 (e.g., multiple large HeavyFootprints can chew up memory). 1145 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
1147 if self.config.maxFootprintSize > 0:
1148 bbox = footprint.getBBox()
1149 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
1151 if self.config.minFootprintAxisRatio > 0:
1152 axes = afwEll.Axes(footprint.getShape())
1153 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
1158 """Returns whether the footprint violates the mask limits""" 1159 size = float(footprint.getArea())
1160 for maskName, limit
in self.config.maskLimits.items():
1161 maskVal = mask.getPlaneBitMask(maskName)
1162 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal)
1163 if (size - unmaskedSpan.getArea())/size > limit:
1168 """Indicate that the parent source is not being deblended 1170 We set the appropriate flags and masks for each exposure. 1174 source: `lsst.afw.table.source.source.SourceRecord` 1175 The source to flag as skipped 1176 masks: list of `lsst.afw.image.mask.mask.MaskX` 1177 The mask in each band to update with the non-detection 1179 fp = source.getFootprint()
1181 source.set(self.
nChildKey, len(fp.getPeaks()))
1182 if self.config.notDeblendedMask:
1184 mask.addMaskPlane(self.config.notDeblendedMask)
1185 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
def _addChild(self, parentId, peak, sources, heavy)
def isLargeFootprint(self, footprint)
def isLargeFootprint(self, footprint)
deblendPatchedTemplateKey
def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res)
def run(self, exposures, sources)
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 postSingleDeblendHook(self, exposures, flux_catalogs, template_catalogs, pk, npre, fp, psfs, psf_fwhms, sigmas, result)
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)
static Log getLogger(std::string const &loggername)
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 newDeblend(debPlugins, footprint, maskedImages, psfs, psfFwhms, filters=None, log=None, verbose=False, avgNoise=None, maxNumberOfPeaks=0)
def deblend(self, exposures, sources, psfs, bands=None)
def __init__(self, schema, peakSchema=None, kwargs)
Create the task, adding necessary fields to the given schema.
def isMasked(self, footprint, mask)
deblendPatchedTemplateKey
def isMasked(self, footprint, mask)