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)
178 if item.field.getName().startswith(
"merge_footprint")]
179 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
180 if peakSchema
is None:
186 for item
in peakSchema:
187 if item.key
not in peakMinimalSchema:
193 schema.addField(item.field)
194 assert schema == self.
peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas" 198 self.
nChildKey = schema.addField(
'deblend_nChild', type=np.int32,
199 doc=
'Number of children this object has (defaults to 0)')
200 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
201 doc=
'Deblender thought this source looked like a PSF')
202 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
203 'If deblended-as-psf, the PSF centroid',
"pixel")
204 self.
psfFluxKey = schema.addField(
'deblend_psf_instFlux', type=
'D',
205 doc=
'If deblended-as-psf, the instrumental PSF flux', units=
'count')
207 doc=
'Source had too many peaks; ' 208 'only the brightest were included')
209 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
210 doc=
'Parent footprint covered too many pixels')
211 self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
212 doc=
'Parent footprint was predominantly masked')
214 if self.config.catchFailures:
216 doc=
"Deblending failed on source")
219 doc=
"Deblender skipped this source")
222 'deblend_rampedTemplate', type=
'Flag',
223 doc=(
'This source was near an image edge and the deblender used ' 224 '"ramp" edge-handling.'))
227 'deblend_patchedTemplate', type=
'Flag',
228 doc=(
'This source was near an image edge and the deblender used ' 229 '"patched" edge-handling.'))
232 'deblend_hasStrayFlux', type=
'Flag',
233 doc=(
'This source was assigned some stray flux'))
235 self.log.
trace(
'Added keys to schema: %s',
", ".join(
str(x)
for x
in (
240 def run(self, exposure, sources):
242 Get the psf from the provided exposure and then run deblend(). 244 @param[in] exposure Exposure to process 245 @param[in,out] sources SourceCatalog containing sources detected on this exposure. 249 psf = exposure.getPsf()
250 assert sources.getSchema() == self.
schema 251 self.
deblend(exposure, sources, psf)
253 def _getPsfFwhm(self, psf, bbox):
256 return psf.computeShape().getDeterminantRadius() * 2.35
263 @param[in] exposure Exposure to process 264 @param[in,out] srcs SourceCatalog containing sources detected on this exposure. 269 self.log.
info(
"Deblending %d sources" % len(srcs))
274 mi = exposure.getMaskedImage()
276 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
278 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
279 self.log.
trace(
'sigma1: %g', sigma1)
283 for i, src
in enumerate(srcs):
286 fp = src.getFootprint()
299 self.log.
trace(
'Parent %i: skipping large footprint',
int(src.getId()))
301 if self.
isMasked(fp, exposure.getMaskedImage().getMask()):
304 self.log.
trace(
'Parent %i: skipping masked footprint',
int(src.getId()))
311 self.log.
trace(
'Parent %i: deblending %i peaks',
int(src.getId()), len(pks))
317 src.set(self.
tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
321 fp, mi, psf, psf_fwhm, sigma1=sigma1,
322 psfChisqCut1=self.config.psfChisq1,
323 psfChisqCut2=self.config.psfChisq2,
324 psfChisqCut2b=self.config.psfChisq2b,
325 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
326 strayFluxToPointSources=self.config.strayFluxToPointSources,
327 assignStrayFlux=self.config.assignStrayFlux,
328 strayFluxAssignment=self.config.strayFluxRule,
329 rampFluxAtEdge=(self.config.edgeHandling ==
'ramp'),
330 patchEdges=(self.config.edgeHandling ==
'noclip'),
331 tinyFootprintSize=self.config.tinyFootprintSize,
332 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
333 weightTemplates=self.config.weightTemplates,
334 removeDegenerateTemplates=self.config.removeDegenerateTemplates,
335 maxTempDotProd=self.config.maxTempDotProd,
336 medianSmoothTemplate=self.config.medianSmoothTemplate
338 if self.config.catchFailures:
340 except Exception
as e:
341 if self.config.catchFailures:
342 self.log.
warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
345 traceback.print_exc()
352 for j, peak
in enumerate(res.deblendedParents[0].peaks):
353 heavy = peak.getFluxPortion()
354 if heavy
is None or peak.skip:
356 if not self.config.propagateAllPeaks:
361 self.log.
trace(
"Peak at (%i,%i) failed. Using minimal default info for child.",
362 pks[j].getIx(), pks[j].getIy())
366 peakList = foot.getPeaks()
368 peakList.append(peak.peak)
369 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
371 if peak.deblendedAsPsf:
372 if peak.psfFitFlux
is None:
373 peak.psfFitFlux = 0.0
374 if peak.psfFitCenter
is None:
375 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
377 assert(len(heavy.getPeaks()) == 1)
380 child = srcs.addNew()
383 child.set(key, src.get(key))
385 child.setParent(src.getId())
386 child.setFootprint(heavy)
387 child.set(self.
psfKey, peak.deblendedAsPsf)
389 if peak.deblendedAsPsf:
390 (cx, cy) = peak.psfFitCenter
402 spans = src.getFootprint().spans
404 spans = spans.union(child.getFootprint().spans)
405 src.getFootprint().setSpans(spans)
413 self.log.
info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources' 414 % (n0, nparents, n1-n0, n1))
419 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
423 """Returns whether a Footprint is large 425 'Large' is defined by thresholds on the area, size and axis ratio. 426 These may be disabled independently by configuring them to be non-positive. 428 This is principally intended to get rid of satellite streaks, which the 429 deblender or other downstream processing can have trouble dealing with 430 (e.g., multiple large HeavyFootprints can chew up memory). 432 if self.config.maxFootprintArea > 0
and footprint.getArea() > self.config.maxFootprintArea:
434 if self.config.maxFootprintSize > 0:
435 bbox = footprint.getBBox()
436 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
438 if self.config.minFootprintAxisRatio > 0:
439 axes = afwEll.Axes(footprint.getShape())
440 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
445 """Returns whether the footprint violates the mask limits""" 446 size =
float(footprint.getArea())
447 for maskName, limit
in self.config.maskLimits.items():
448 maskVal = mask.getPlaneBitMask(maskName)
449 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal)
450 if (size - unmaskedSpan.getArea())/size > limit:
455 """Indicate that the parent source is not being deblended 457 We set the appropriate flags and mask. 459 @param source The source to flag as skipped 460 @param mask The mask to update 462 fp = source.getFootprint()
464 source.set(self.
nChildKey, len(fp.getPeaks()))
465 if self.config.notDeblendedMask:
466 mask.addMaskPlane(self.config.notDeblendedMask)
467 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
471 """MultibandDeblendConfig 473 Configuration for the multiband deblender. 474 The parameters are organized by the parameter types, which are 475 - Stopping Criteria: Used to determine if the fit has converged 476 - Position Fitting Criteria: Used to fit the positions of the peaks 477 - Constraints: Used to apply constraints to the peaks and their components 478 - Other: Parameters that don't fit into the above categories 481 maxIter = pexConfig.Field(dtype=int, default=200,
482 doc=(
"Maximum number of iterations to deblend a single parent"))
483 relativeError = pexConfig.Field(dtype=float, default=1e-3,
484 doc=(
"Relative error to use when determining stopping criteria"))
487 minTranslation = pexConfig.Field(dtype=float, default=1e-3,
488 doc=(
"A peak must be updated by at least 'minTranslation' (pixels)" 489 "or no update is performed." 490 "This field is ignored if fitPositions is False."))
491 refinementSkip = pexConfig.Field(dtype=int, default=10,
492 doc=(
"If fitPositions is True, the positions and box sizes are" 493 "updated on every 'refinementSkip' iterations."))
494 translationMethod = pexConfig.Field(dtype=str, default=
"default",
495 doc=(
"Method to use for fitting translations." 496 "Currently 'default' is the only available option," 497 "which performs a linear fit, but it is possible that we" 498 "will use galsim or some other method as a future option"))
499 edgeFluxThresh = pexConfig.Field(dtype=float, default=1.0,
500 doc=(
"Boxes are resized when the flux at an edge is " 501 "> edgeFluxThresh * background RMS"))
502 exactLipschitz = pexConfig.Field(dtype=bool, default=
False,
503 doc=(
"Calculate exact Lipschitz constant in every step" 504 "(True) or only calculate the approximate" 505 "Lipschitz constant with significant changes in A,S" 507 stepSlack = pexConfig.Field(dtype=float, default=0.2,
508 doc=(
"A fractional measure of how much a value (like the exactLipschitz)" 509 "can change before it needs to be recalculated." 510 "This must be between 0 and 1."))
513 constraints = pexConfig.Field(dtype=str, default=
"1,+,S,M",
514 doc=(
"List of constraints to use for each object" 515 "(order does not matter)" 516 "Current options are all used by default:\n" 519 "1: normalized SED to unity" 520 "+: non-negative morphology"))
521 symmetryThresh = pexConfig.Field(dtype=float, default=1.0,
522 doc=(
"Strictness of symmetry, from" 523 "0 (no symmetry enforced) to" 524 "1 (perfect symmetry required)." 525 "If 'S' is not in `constraints`, this argument is ignored"))
526 l0Thresh = pexConfig.Field(dtype=float, default=np.nan,
527 doc=(
"L0 threshold. NaN results in no L0 penalty."))
528 l1Thresh = pexConfig.Field(dtype=float, default=np.nan,
529 doc=(
"L1 threshold. NaN results in no L1 penalty."))
530 tvxThresh = pexConfig.Field(dtype=float, default=np.nan,
531 doc=(
"Threshold for TV (total variation) constraint in the x-direction." 532 "NaN results in no TVx penalty."))
533 tvyThresh = pexConfig.Field(dtype=float, default=np.nan,
534 doc=(
"Threshold for TV (total variation) constraint in the y-direction." 535 "NaN results in no TVy penalty."))
538 useWeights = pexConfig.Field(dtype=bool, default=
False, doc=
"Use inverse variance as deblender weights")
539 bgScale = pexConfig.Field(
540 dtype=float, default=0.5,
541 doc=(
"Fraction of background RMS level to use as a" 542 "cutoff for defining the background of the image" 543 "This is used to initialize the model for each source" 544 "and to set the size of the bounding box for each source" 545 "every `refinementSkip` iteration."))
546 usePsfConvolution = pexConfig.Field(
547 dtype=bool, default=
True,
548 doc=(
"Whether or not to convolve the morphology with the" 549 "PSF in each band or use the same morphology in all bands"))
550 saveTemplates = pexConfig.Field(
551 dtype=bool, default=
True,
552 doc=
"Whether or not to save the SEDs and templates")
553 processSingles = pexConfig.Field(
554 dtype=bool, default=
False,
555 doc=
"Whether or not to process isolated sources in the deblender")
556 badMask = pexConfig.Field(
557 dtype=str, default=
"BAD,CR,NO_DATA,SAT,SUSPECT",
558 doc=
"Whether or not to process isolated sources in the deblender")
561 maxNumberOfPeaks = pexConfig.Field(
562 dtype=int, default=0,
563 doc=(
"Only deblend the brightest maxNumberOfPeaks peaks in the parent" 564 " (<= 0: unlimited)"))
565 maxFootprintArea = pexConfig.Field(
566 dtype=int, default=1000000,
567 doc=(
"Maximum area for footprints before they are ignored as large; " 568 "non-positive means no threshold applied"))
569 maxFootprintSize = pexConfig.Field(
570 dtype=int, default=0,
571 doc=(
"Maximum linear dimension for footprints before they are ignored " 572 "as large; non-positive means no threshold applied"))
573 minFootprintAxisRatio = pexConfig.Field(
574 dtype=float, default=0.0,
575 doc=(
"Minimum axis ratio for footprints before they are ignored " 576 "as large; non-positive means no threshold applied"))
577 notDeblendedMask = pexConfig.Field(
578 dtype=str, default=
"NOT_DEBLENDED", optional=
True,
579 doc=
"Mask name for footprints not deblended, or None")
581 tinyFootprintSize = pexConfig.RangeField(
582 dtype=int, default=2, min=2, inclusiveMin=
True,
583 doc=(
'Footprints smaller in width or height than this value will ' 584 'be ignored; minimum of 2 due to PSF gradient calculation.'))
585 catchFailures = pexConfig.Field(
586 dtype=bool, default=
False,
587 doc=(
"If True, catch exceptions thrown by the deblender, log them, " 588 "and set a flag on the parent, instead of letting them propagate up"))
589 propagateAllPeaks = pexConfig.Field(dtype=bool, default=
False,
590 doc=(
'Guarantee that all peaks produce a child source.'))
591 maskPlanes = pexConfig.ListField(dtype=str, default=[
"SAT",
"INTRP",
"NO_DATA"],
592 doc=
"Mask planes to ignore when performing statistics")
593 maskLimits = pexConfig.DictField(
597 doc=(
"Mask planes with the corresponding limit on the fraction of masked pixels. " 598 "Sources violating this limit will not be deblended."),
601 edgeHandling = pexConfig.ChoiceField(
602 doc=
'What to do when a peak to be deblended is close to the edge of the image',
603 dtype=str, default=
'noclip',
605 'clip':
'Clip the template at the edge AND the mirror of the edge.',
606 'ramp':
'Ramp down flux at the image edge by the PSF',
607 'noclip':
'Ignore the edge when building the symmetric template.',
611 medianSmoothTemplate = pexConfig.Field(dtype=bool, default=
False,
612 doc=
"Apply a smoothing filter to all of the template images")
613 medianFilterHalfsize = pexConfig.Field(dtype=float, default=2,
614 doc=(
'Half size of the median smoothing filter'))
615 clipFootprintToNonzero = pexConfig.Field(dtype=bool, default=
False,
616 doc=(
"Clip non-zero spans in the footprints"))
618 conserveFlux = pexConfig.Field(dtype=bool, default=
True,
619 doc=(
"Reapportion flux to the footprints so that flux is conserved"))
620 weightTemplates = pexConfig.Field(
621 dtype=bool, default=
False,
622 doc=(
"If true, a least-squares fit of the templates will be done to the " 623 "full image. The templates will be re-weighted based on this fit."))
624 strayFluxToPointSources = pexConfig.ChoiceField(
625 doc=
'When the deblender should attribute stray flux to point sources',
626 dtype=str, default=
'necessary',
628 'necessary':
'When there is not an extended object in the footprint',
630 'never': (
'Never; stray flux will not be attributed to any deblended child ' 631 'if the deblender thinks all peaks look like point sources'),
635 assignStrayFlux = pexConfig.Field(dtype=bool, default=
True,
636 doc=
'Assign stray flux (not claimed by any child in the deblender) ' 637 'to deblend children.')
639 strayFluxRule = pexConfig.ChoiceField(
640 doc=
'How to split flux among peaks',
641 dtype=str, default=
'trim',
643 'r-to-peak':
'~ 1/(1+R^2) to the peak',
644 'r-to-footprint': (
'~ 1/(1+R^2) to the closest pixel in the footprint. ' 645 'CAUTION: this can be computationally expensive on large footprints!'),
646 'nearest-footprint': (
'Assign 100% to the nearest footprint (using L-1 norm aka ' 647 'Manhattan distance)'),
648 'trim': (
'Shrink the parent footprint to pixels that are not assigned to children')
652 clipStrayFluxFraction = pexConfig.Field(dtype=float, default=0.001,
653 doc=(
'When splitting stray flux, clip fractions below ' 654 'this value to zero.'))
655 getTemplateSum = pexConfig.Field(dtype=bool, default=
False,
656 doc=(
"As part of the flux calculation, the sum of the templates is" 657 "calculated. If 'getTemplateSum==True' then the sum of the" 658 "templates is stored in the result (a 'PerFootprint')."))
662 """MultibandDeblendTask 664 Split blended sources into individual sources. 666 This task has no return value; it only modifies the SourceCatalog in-place. 668 ConfigClass = MultibandDeblendConfig
669 _DefaultName =
"multibandDeblend" 671 def __init__(self, schema, peakSchema=None, **kwargs):
672 """Create the task, adding necessary fields to the given schema. 676 schema: `lsst.afw.table.schema.schema.Schema` 677 Schema object for measurement fields; will be modified in-place. 678 peakSchema: `lsst.afw.table.schema.schema.Schema` 679 Schema of Footprint Peaks that will be passed to the deblender. 680 Any fields beyond the PeakTable minimal schema will be transferred 681 to the main source Schema. If None, no fields will be transferred 684 Names of the filters used for the eposures. This is needed to store the SED as a field 686 Passed to Task.__init__. 690 pipeBase.Task.__init__(self, **kwargs)
691 if not self.config.conserveFlux
and not self.config.saveTemplates:
692 raise ValueError(
"Either `conserveFlux` or `saveTemplates` must be True")
694 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
695 if peakSchema
is None:
701 for item
in peakSchema:
702 if item.key
not in peakMinimalSchema:
708 schema.addField(item.field)
709 assert schema == self.
peakSchemaMapper.getOutputSchema(),
"Logic bug mapping schemas" 716 config = scarlet.config.Config(
717 center_min_dist=self.config.minTranslation,
718 edge_flux_thresh=self.config.edgeFluxThresh,
719 exact_lipschitz=self.config.exactLipschitz,
720 refine_skip=self.config.refinementSkip,
721 slack=self.config.stepSlack,
723 if self.config.translationMethod !=
"default":
724 err =
"Currently the only supported translationMethod is 'default', you entered '{0}'" 725 raise NotImplementedError(err.format(self.config.translationMethod))
730 _constraints = self.config.constraints.split(
",")
731 if (sorted(_constraints) != [
'+',
'1',
'M',
'S']
or 732 ~np.isnan(self.config.l0Thresh)
or 733 ~np.isnan(self.config.l1Thresh)):
735 "+": scarlet.constraint.PositivityConstraint,
736 "1": scarlet.constraint.SimpleConstraint,
737 "M": scarlet.constraint.DirectMonotonicityConstraint(use_nearest=
False),
738 "S": scarlet.constraint.DirectSymmetryConstraint(sigma=self.config.symmetryThresh)
740 for c
in _constraints:
741 if constraints
is None:
742 constraints = [constraintDict[c]]
744 constraints += [constraintDict[c]]
745 if constraints
is None:
746 constraints = scarlet.constraint.MinimalConstraint()
747 if ~np.isnan(self.config.l0Thresh):
748 constraints += [scarlet.constraint.L0Constraint(self.config.l0Thresh)]
749 if ~np.isnan(self.config.l1Thresh):
750 constraints += [scarlet.constraint.L1Constraint(self.config.l1Thresh)]
751 if ~np.isnan(self.config.tvxThresh):
752 constraints += [scarlet.constraint.TVxConstraint(self.config.tvxThresh)]
753 if ~np.isnan(self.config.tvyThresh):
754 constraints += [scarlet.constraint.TVyConstraint(self.config.tvyThresh)]
757 plugins.buildMultibandTemplates,
758 useWeights=self.config.useWeights,
759 usePsf=self.config.usePsfConvolution,
760 constraints=constraints,
762 maxIter=self.config.maxIter,
763 bgScale=self.config.bgScale,
764 relativeError=self.config.relativeError,
765 badMask=self.config.badMask.split(
","),
771 if self.config.edgeHandling ==
'ramp':
773 if self.config.medianSmoothTemplate:
775 plugins.medianSmoothTemplates,
776 medianFilterHalfsize=self.config.medianFilterHalfsize))
777 if self.config.clipFootprintToNonzero:
779 if self.config.conserveFlux:
780 if self.config.weightTemplates:
783 plugins.apportionFlux,
784 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
785 assignStrayFlux=self.config.assignStrayFlux,
786 strayFluxAssignment=self.config.strayFluxRule,
787 strayFluxToPointSources=self.config.strayFluxToPointSources,
788 getTemplateSum=self.config.getTemplateSum))
790 def _addSchemaKeys(self, schema):
791 """Add deblender specific keys to the schema 793 self.
runtimeKey = schema.addField(
'runtime', type=np.float32, doc=
'runtime in ms')
795 self.
nChildKey = schema.addField(
'deblend_nChild', type=np.int32,
796 doc=
'Number of children this object has (defaults to 0)')
797 self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
798 doc=
'Deblender thought this source looked like a PSF')
800 doc=
'Source had too many peaks; ' 801 'only the brightest were included')
802 self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
803 doc=
'Parent footprint covered too many pixels')
804 self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
805 doc=
'Parent footprint was predominantly masked')
807 doc=
"Deblending failed on source")
810 doc=
"Deblender skipped this source")
814 self.
psfCenterKey = afwTable.Point2DKey.addFields(schema,
'deblend_psfCenter',
815 'If deblended-as-psf, the PSF centroid',
"pixel")
816 self.
psfFluxKey = schema.addField(
'deblend_psf_instFlux', type=
'D',
817 doc=
'If deblended-as-psf, the instrumental PSF flux', units=
'count')
819 'deblend_rampedTemplate', type=
'Flag',
820 doc=(
'This source was near an image edge and the deblender used ' 821 '"ramp" edge-handling.'))
824 'deblend_patchedTemplate', type=
'Flag',
825 doc=(
'This source was near an image edge and the deblender used ' 826 '"patched" edge-handling.'))
829 'deblend_hasStrayFlux', type=
'Flag',
830 doc=(
'This source was assigned some stray flux'))
832 self.log.
trace(
'Added keys to schema: %s',
", ".join(
str(x)
for x
in (
837 def run(self, mExposure, mergedSources):
838 """Get the psf from each exposure and then run deblend(). 842 mExposure: `MultibandExposure` 843 The exposures should be co-added images of the same 844 shape and region of the sky. 845 mergedSources: `SourceCatalog` 846 The merged `SourceCatalog` that contains parent footprints 847 to (potentially) deblend. 851 fluxCatalogs: dict or None 852 Keys are the names of the filters and the values are 853 `lsst.afw.table.source.source.SourceCatalog`'s. 854 These are the flux-conserved catalogs with heavy footprints with 855 the image data weighted by the multiband templates. 856 If `self.config.conserveFlux` is `False`, then this item will be None 857 templateCatalogs: dict or None 858 Keys are the names of the filters and the values are 859 `lsst.afw.table.source.source.SourceCatalog`'s. 860 These are catalogs with heavy footprints that are the templates 861 created by the multiband templates. 862 If `self.config.saveTemplates` is `False`, then this item will be None 864 psfs = {f: mExposure[f].getPsf()
for f
in mExposure.filters}
865 return self.
deblend(mExposure, mergedSources, psfs)
867 def _getPsfFwhm(self, psf, bbox):
868 return psf.computeShape().getDeterminantRadius() * 2.35
870 def _addChild(self, parentId, peak, sources, heavy):
871 """Add a child to a catalog 873 This creates a new child in the source catalog, 874 assigning it a parent id, adding a footprint, 875 and setting all appropriate flags based on the 878 assert len(heavy.getPeaks()) == 1
879 src = sources.addNew()
881 src.setParent(parentId)
882 src.setFootprint(heavy)
883 src.set(self.
psfKey, peak.deblendedAsPsf)
892 """Deblend a data cube of multiband images 896 mExposure: `MultibandExposure` 897 The exposures should be co-added images of the same 898 shape and region of the sky. 899 sources: `SourceCatalog` 900 The merged `SourceCatalog` that contains parent footprints 901 to (potentially) deblend. 903 Keys are the names of the filters 904 (should be the same as `mExposure.filters`) 905 and the values are the PSFs in each band. 909 fluxCatalogs: dict or None 910 Keys are the names of the filters and the values are 911 `lsst.afw.table.source.source.SourceCatalog`'s. 912 These are the flux-conserved catalogs with heavy footprints with 913 the image data weighted by the multiband templates. 914 If `self.config.conserveFlux` is `False`, then this item will be None 915 templateCatalogs: dict or None 916 Keys are the names of the filters and the values are 917 `lsst.afw.table.source.source.SourceCatalog`'s. 918 These are catalogs with heavy footprints that are the templates 919 created by the multiband templates. 920 If `self.config.saveTemplates` is `False`, then this item will be None 924 if tuple(psfs.keys()) != mExposure.filters:
925 msg =
"PSF keys must be the same as mExposure.filters ({0}), got {1}" 926 raise ValueError(msg.format(mExposure.filters, psfs.keys()))
928 filters = mExposure.filters
930 mask=mExposure.mask, variance=mExposure.variance)
931 self.log.
info(
"Deblending {0} sources in {1} exposures".
format(len(sources), len(mExposure)))
936 exposure = mExposure[f]
937 mi = exposure.getMaskedImage()
939 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
941 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
942 self.log.
trace(
'Exposure {0}, sigma1: {1}'.
format(f, sigma1))
946 if self.config.conserveFlux:
950 _catalog.extend(sources)
951 fluxCatalogs[f] = _catalog
954 if self.config.saveTemplates:
955 templateCatalogs = {}
958 _catalog.extend(sources)
959 templateCatalogs[f] = _catalog
961 templateCatalogs =
None 965 for pk, src
in enumerate(sources):
966 foot = src.getFootprint()
967 logger.info(
"id: {0}".
format(src[
"id"]))
968 peaks = foot.getPeaks()
975 if len(peaks) < 2
and not self.config.processSingles:
977 if self.config.saveTemplates:
979 if self.config.conserveFlux:
984 self.
skipParent(src, [mi.getMask()
for mi
in mMaskedImage])
985 self.log.
trace(
'Parent %i: skipping large footprint',
int(src.getId()))
987 if self.
isMasked(foot, exposure.getMaskedImage().getMask()):
990 self.log.
trace(
'Parent %i: skipping masked footprint',
int(src.getId()))
992 if len(peaks) > self.config.maxNumberOfPeaks:
994 msg =
'Parent {0}: Too many peaks, using the first {1} peaks' 995 self.log.
trace(msg.format(
int(src.getId()), self.config.maxNumberOfPeaks))
998 bbox = foot.getBBox()
999 psf_fwhms = {f: self.
_getPsfFwhm(psf, bbox)
for f, psf
in psfs.items()}
1000 self.log.
trace(
'Parent %i: deblending %i peaks',
int(src.getId()), len(peaks))
1007 images = mMaskedImage[:, bbox]
1008 psf_list = [psfs[f]
for f
in filters]
1009 fwhm_list = [psf_fwhms[f]
for f
in filters]
1010 avgNoise = [sigmas[f]
for f
in filters]
1014 mMaskedImage=images,
1018 maxNumberOfPeaks=self.config.maxNumberOfPeaks)
1020 runtime = (tf-t0)*1000
1025 except Exception
as e:
1026 if self.config.catchFailures:
1027 self.log.
warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
1031 traceback.print_exc()
1037 templateParents = {}
1039 parentId = src.getId()
1041 if self.config.saveTemplates:
1042 templateParents[f] = templateCatalogs[f][pk]
1044 if self.config.conserveFlux:
1045 fluxParents[f] = fluxCatalogs[f][pk]
1052 for j, multiPeak
in enumerate(result.peaks):
1053 heavy = {f: peak.getFluxPortion()
for f, peak
in multiPeak.deblendedPeaks.items()}
1054 no_flux =
all([v
is None for v
in heavy.values()])
1055 skip_peak =
all([peak.skip
for peak
in multiPeak.deblendedPeaks.values()])
1056 if no_flux
or skip_peak:
1058 if not self.config.propagateAllPeaks:
1063 msg =
"Peak at {0} failed deblending. Using minimal default info for child." 1064 self.log.
trace(msg.format(multiPeak.x, multiPeak.y))
1068 peakList = pfoot.getPeaks()
1070 pfoot.addPeak(multiPeak.x, multiPeak.y, 0)
1071 zeroMimg = afwImage.MaskedImageF(pfoot.getBBox())
1079 if len(heavy[f].getPeaks()) != 1:
1080 err =
"Heavy footprint should have a single peak, got {0}" 1081 raise ValueError(err.format(len(heavy[f].getPeaks())))
1082 peak = multiPeak.deblendedPeaks[f]
1083 if self.config.saveTemplates:
1084 cat = templateCatalogs[f]
1085 tfoot = peak.templateFootprint
1086 timg = afwImage.MaskedImageF(peak.templateImage)
1088 child = self.
_addChild(parentId, peak, cat, tHeavy)
1090 child.setId(src.getId())
1093 templateSpans[f] = templateSpans[f].union(tHeavy.getSpans())
1094 if self.config.conserveFlux:
1095 cat = fluxCatalogs[f]
1096 child = self.
_addChild(parentId, peak, cat, heavy[f])
1098 child.setId(src.getId())
1101 fluxSpans[f] = fluxSpans[f].union(heavy[f].getSpans())
1110 if self.config.saveTemplates:
1112 templateParents[f].getFootprint().setSpans(templateSpans[f])
1113 if self.config.conserveFlux:
1115 fluxParents[f].getFootprint().setSpans(fluxSpans[f])
1118 pk, npre, foot, psfs, psf_fwhms, sigmas, result)
1120 if fluxCatalogs
is not None:
1121 n1 = len(
list(fluxCatalogs.values())[0])
1123 n1 = len(
list(templateCatalogs.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 fluxCatalogs, templateCatalogs
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.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 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)
A compact representation of a collection of pixels.
A mapping between the keys of two Schemas, used to copy data between them.
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().
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
daf::base::PropertySet * set
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
def __init__(self, schema, peakSchema=None, kwargs)
def _getPsfFwhm(self, psf, bbox)
bool all(CoordinateExpr< N > const &expr) noexcept
Return true if all elements are true.
def _addSchemaKeys(self, schema)
Pass parameters to a Statistics object.
def preSingleDeblendHook(self, exposures, sources, pk, fp, psfs, psf_fwhms, sigmas)
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
def addSchemaKeys(self, schema)
Split blended sources into individual sources.
def skipParent(self, source, mask)
def deblend(self, exposure, srcs, psf)
Deblend.
static Log getLogger(Log const &logger)
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)
daf::base::PropertyList * list
def run(self, mExposure, mergedSources)
HeavyFootprint< ImagePixelT, MaskPixelT, VariancePixelT > makeHeavyFootprint(Footprint const &foot, lsst::afw::image::MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > const &img, HeavyFootprintCtrl const *ctrl=NULL)
Create a HeavyFootprint with footprint defined by the given Footprint and pixel values from the given...