26 import lsst.pex.config 
as pexConfig
 
   37 __all__ = 
'SourceDeblendConfig', 
'SourceDeblendTask' 
   42     edgeHandling = pexConfig.ChoiceField(
 
   43         doc=
'What to do when a peak to be deblended is close to the edge of the image',
 
   44         dtype=str, default=
'ramp',
 
   46             'clip': 
'Clip the template at the edge AND the mirror of the edge.',
 
   47             'ramp': 
'Ramp down flux at the image edge by the PSF',
 
   48             'noclip': 
'Ignore the edge when building the symmetric template.',
 
   52     strayFluxToPointSources = pexConfig.ChoiceField(
 
   53         doc=
'When the deblender should attribute stray flux to point sources',
 
   54         dtype=str, default=
'necessary',
 
   56             'necessary': 
'When there is not an extended object in the footprint',
 
   58             'never': (
'Never; stray flux will not be attributed to any deblended child ' 
   59                       'if the deblender thinks all peaks look like point sources'),
 
   63     assignStrayFlux = pexConfig.Field(dtype=bool, default=
True,
 
   64                                       doc=
'Assign stray flux (not claimed by any child in the deblender) ' 
   65                                           'to deblend children.')
 
   67     strayFluxRule = pexConfig.ChoiceField(
 
   68         doc=
'How to split flux among peaks',
 
   69         dtype=str, default=
'trim',
 
   71             'r-to-peak': 
'~ 1/(1+R^2) to the peak',
 
   72             'r-to-footprint': (
'~ 1/(1+R^2) to the closest pixel in the footprint.  ' 
   73                                'CAUTION: this can be computationally expensive on large footprints!'),
 
   74             'nearest-footprint': (
'Assign 100% to the nearest footprint (using L-1 norm aka ' 
   75                                   'Manhattan distance)'),
 
   76             'trim': (
'Shrink the parent footprint to pixels that are not assigned to children')
 
   80     clipStrayFluxFraction = pexConfig.Field(dtype=float, default=0.001,
 
   81                                             doc=(
'When splitting stray flux, clip fractions below ' 
   82                                                  'this value to zero.'))
 
   83     psfChisq1 = pexConfig.Field(dtype=float, default=1.5, optional=
False,
 
   84                                 doc=(
'Chi-squared per DOF cut for deciding a source is ' 
   85                                      'a PSF during deblending (un-shifted PSF model)'))
 
   86     psfChisq2 = pexConfig.Field(dtype=float, default=1.5, optional=
False,
 
   87                                 doc=(
'Chi-squared per DOF cut for deciding a source is ' 
   88                                      'PSF during deblending (shifted PSF model)'))
 
   89     psfChisq2b = pexConfig.Field(dtype=float, default=1.5, optional=
False,
 
   90                                  doc=(
'Chi-squared per DOF cut for deciding a source is ' 
   91                                       'a PSF during deblending (shifted PSF model #2)'))
 
   92     maxNumberOfPeaks = pexConfig.Field(dtype=int, default=0,
 
   93                                        doc=(
"Only deblend the brightest maxNumberOfPeaks peaks in the parent" 
   94                                             " (<= 0: unlimited)"))
 
   95     maxFootprintArea = pexConfig.Field(dtype=int, default=1000000,
 
   96                                        doc=(
"Maximum area for footprints before they are ignored as large; " 
   97                                             "non-positive means no threshold applied"))
 
   98     maxFootprintSize = pexConfig.Field(dtype=int, default=0,
 
   99                                        doc=(
"Maximum linear dimension for footprints before they are ignored " 
  100                                             "as large; non-positive means no threshold applied"))
 
  101     minFootprintAxisRatio = pexConfig.Field(dtype=float, default=0.0,
 
  102                                             doc=(
"Minimum axis ratio for footprints before they are ignored " 
  103                                                  "as large; non-positive means no threshold applied"))
 
  104     notDeblendedMask = pexConfig.Field(dtype=str, default=
"NOT_DEBLENDED", optional=
True,
 
  105                                        doc=
"Mask name for footprints not deblended, or None")
 
  107     tinyFootprintSize = pexConfig.RangeField(dtype=int, default=2, min=2, inclusiveMin=
True,
 
  108                                              doc=(
'Footprints smaller in width or height than this value ' 
  109                                                   'will be ignored; minimum of 2 due to PSF gradient ' 
  112     propagateAllPeaks = pexConfig.Field(dtype=bool, default=
False,
 
  113                                         doc=(
'Guarantee that all peaks produce a child source.'))
 
  114     catchFailures = pexConfig.Field(
 
  115         dtype=bool, default=
False,
 
  116         doc=(
"If True, catch exceptions thrown by the deblender, log them, " 
  117              "and set a flag on the parent, instead of letting them propagate up"))
 
  118     maskPlanes = pexConfig.ListField(dtype=str, default=[
"SAT", 
"INTRP", 
"NO_DATA"],
 
  119                                      doc=
"Mask planes to ignore when performing statistics")
 
  120     maskLimits = pexConfig.DictField(
 
  124         doc=(
"Mask planes with the corresponding limit on the fraction of masked pixels. " 
  125              "Sources violating this limit will not be deblended."),
 
  127     weightTemplates = pexConfig.Field(
 
  128         dtype=bool, default=
False,
 
  129         doc=(
"If true, a least-squares fit of the templates will be done to the " 
  130              "full image. The templates will be re-weighted based on this fit."))
 
  131     removeDegenerateTemplates = pexConfig.Field(dtype=bool, default=
False,
 
  132                                                 doc=(
"Try to remove similar templates?"))
 
  133     maxTempDotProd = pexConfig.Field(
 
  134         dtype=float, default=0.5,
 
  135         doc=(
"If the dot product between two templates is larger than this value, we consider them to be " 
  136              "describing the same object (i.e. they are degenerate).  If one of the objects has been " 
  137              "labeled as a PSF it will be removed, otherwise the template with the lowest value will " 
  139     medianSmoothTemplate = pexConfig.Field(dtype=bool, default=
True,
 
  140                                            doc=
"Apply a smoothing filter to all of the template images")
 
  152     \anchor SourceDeblendTask_ 
  154     \brief Split blended sources into individual sources. 
  156     This task has no return value; it only modifies the SourceCatalog in-place. 
  158     ConfigClass = SourceDeblendConfig
 
  159     _DefaultName = 
"sourceDeblend" 
  161     def __init__(self, schema, peakSchema=None, **kwargs):
 
  163         Create the task, adding necessary fields to the given schema. 
  165         @param[in,out] schema        Schema object for measurement fields; will be modified in-place. 
  166         @param[in]     peakSchema    Schema of Footprint Peaks that will be passed to the deblender. 
  167                                      Any fields beyond the PeakTable minimal schema will be transferred 
  168                                      to the main source Schema.  If None, no fields will be transferred 
  170         @param[in]     **kwargs      Passed to Task.__init__. 
  172         pipeBase.Task.__init__(self, **kwargs)
 
  175                                  if item.field.getName().startswith(
"merge_footprint")]
 
  176         peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
 
  177         if peakSchema 
is None:
 
  183             for item 
in peakSchema:
 
  184                 if item.key 
not in peakMinimalSchema:
 
  190                     schema.addField(item.field)
 
  191             assert schema == self.
peakSchemaMapper.getOutputSchema(), 
"Logic bug mapping schemas" 
  195         self.
nChildKey = schema.addField(
'deblend_nChild', type=np.int32,
 
  196                                          doc=
'Number of children this object has (defaults to 0)')
 
  197         self.
psfKey = schema.addField(
'deblend_deblendedAsPsf', type=
'Flag',
 
  198                                       doc=
'Deblender thought this source looked like a PSF')
 
  199         self.
psfCenterKey = afwTable.Point2DKey.addFields(schema, 
'deblend_psfCenter',
 
  200                                                           'If deblended-as-psf, the PSF centroid', 
"pixel")
 
  201         self.
psfFluxKey = schema.addField(
'deblend_psf_instFlux', type=
'D',
 
  202                                           doc=
'If deblended-as-psf, the instrumental PSF flux', units=
'count')
 
  204                                                doc=
'Source had too many peaks; ' 
  205                                                'only the brightest were included')
 
  206         self.
tooBigKey = schema.addField(
'deblend_parentTooBig', type=
'Flag',
 
  207                                          doc=
'Parent footprint covered too many pixels')
 
  208         self.
maskedKey = schema.addField(
'deblend_masked', type=
'Flag',
 
  209                                          doc=
'Parent footprint was predominantly masked')
 
  211         if self.config.catchFailures:
 
  213                                                     doc=
"Deblending failed on source")
 
  216                                                  doc=
"Deblender skipped this source")
 
  219             'deblend_rampedTemplate', type=
'Flag',
 
  220             doc=(
'This source was near an image edge and the deblender used ' 
  221                  '"ramp" edge-handling.'))
 
  224             'deblend_patchedTemplate', type=
'Flag',
 
  225             doc=(
'This source was near an image edge and the deblender used ' 
  226                  '"patched" edge-handling.'))
 
  229             'deblend_hasStrayFlux', type=
'Flag',
 
  230             doc=(
'This source was assigned some stray flux'))
 
  232         self.log.
trace(
'Added keys to schema: %s', 
", ".join(str(x) 
for x 
in (
 
  237     def run(self, exposure, sources):
 
  239         Get the psf from the provided exposure and then run deblend(). 
  241         @param[in]     exposure Exposure to process 
  242         @param[in,out] sources  SourceCatalog containing sources detected on this exposure. 
  246         psf = exposure.getPsf()
 
  247         assert sources.getSchema() == self.
schema 
  248         self.
deblend(exposure, sources, psf)
 
  250     def _getPsfFwhm(self, psf, bbox):
 
  253         return psf.computeShape().getDeterminantRadius() * 2.35
 
  260         @param[in]     exposure Exposure to process 
  261         @param[in,out] srcs     SourceCatalog containing sources detected on this exposure. 
  266         self.log.
info(
"Deblending %d sources" % len(srcs))
 
  271         mi = exposure.getMaskedImage()
 
  273         statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
 
  275         sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
 
  276         self.log.
trace(
'sigma1: %g', sigma1)
 
  280         for i, src 
in enumerate(srcs):
 
  283             fp = src.getFootprint()
 
  296                 self.log.
warn(
'Parent %i: skipping large footprint (area: %i)',
 
  297                               int(src.getId()), int(fp.getArea()))
 
  299             if self.
isMasked(fp, exposure.getMaskedImage().getMask()):
 
  302                 self.log.
warn(
'Parent %i: skipping masked footprint (area: %i)',
 
  303                               int(src.getId()), int(fp.getArea()))
 
  310             self.log.
trace(
'Parent %i: deblending %i peaks', int(src.getId()), len(pks))
 
  316             src.set(self.
tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
 
  320                     fp, mi, psf, psf_fwhm, sigma1=sigma1,
 
  321                     psfChisqCut1=self.config.psfChisq1,
 
  322                     psfChisqCut2=self.config.psfChisq2,
 
  323                     psfChisqCut2b=self.config.psfChisq2b,
 
  324                     maxNumberOfPeaks=self.config.maxNumberOfPeaks,
 
  325                     strayFluxToPointSources=self.config.strayFluxToPointSources,
 
  326                     assignStrayFlux=self.config.assignStrayFlux,
 
  327                     strayFluxAssignment=self.config.strayFluxRule,
 
  328                     rampFluxAtEdge=(self.config.edgeHandling == 
'ramp'),
 
  329                     patchEdges=(self.config.edgeHandling == 
'noclip'),
 
  330                     tinyFootprintSize=self.config.tinyFootprintSize,
 
  331                     clipStrayFluxFraction=self.config.clipStrayFluxFraction,
 
  332                     weightTemplates=self.config.weightTemplates,
 
  333                     removeDegenerateTemplates=self.config.removeDegenerateTemplates,
 
  334                     maxTempDotProd=self.config.maxTempDotProd,
 
  335                     medianSmoothTemplate=self.config.medianSmoothTemplate
 
  337                 if self.config.catchFailures:
 
  339             except Exception 
as e:
 
  340                 if self.config.catchFailures:
 
  341                     self.log.
warn(
"Unable to deblend source %d: %s" % (src.getId(), e))
 
  344                     traceback.print_exc()
 
  351             for j, peak 
in enumerate(res.deblendedParents[0].peaks):
 
  352                 heavy = peak.getFluxPortion()
 
  353                 if heavy 
is None or peak.skip:
 
  355                     if not self.config.propagateAllPeaks:
 
  360                     self.log.
trace(
"Peak at (%i,%i) failed.  Using minimal default info for child.",
 
  361                                    pks[j].getIx(), pks[j].getIy())
 
  365                         peakList = foot.getPeaks()
 
  367                         peakList.append(peak.peak)
 
  368                         zeroMimg = afwImage.MaskedImageF(foot.getBBox())
 
  370                     if peak.deblendedAsPsf:
 
  371                         if peak.psfFitFlux 
is None:
 
  372                             peak.psfFitFlux = 0.0
 
  373                         if peak.psfFitCenter 
is None:
 
  374                             peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
 
  376                 assert(len(heavy.getPeaks()) == 1)
 
  379                 child = srcs.addNew()
 
  382                     child.set(key, src.get(key))
 
  384                 child.setParent(src.getId())
 
  385                 child.setFootprint(heavy)
 
  386                 child.set(self.
psfKey, peak.deblendedAsPsf)
 
  388                 if peak.deblendedAsPsf:
 
  389                     (cx, cy) = peak.psfFitCenter
 
  401             spans = src.getFootprint().spans
 
  403                 spans = spans.union(child.getFootprint().spans)
 
  404             src.getFootprint().setSpans(spans)
 
  412         self.log.
info(
'Deblended: of %i sources, %i were deblended, creating %i children, total %i sources' 
  413                       % (n0, nparents, n1-n0, n1))
 
  418     def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
 
  422         """Returns whether a Footprint is large 
  424         'Large' is defined by thresholds on the area, size and axis ratio. 
  425         These may be disabled independently by configuring them to be non-positive. 
  427         This is principally intended to get rid of satellite streaks, which the 
  428         deblender or other downstream processing can have trouble dealing with 
  429         (e.g., multiple large HeavyFootprints can chew up memory). 
  431         if self.config.maxFootprintArea > 0 
and footprint.getArea() > self.config.maxFootprintArea:
 
  433         if self.config.maxFootprintSize > 0:
 
  434             bbox = footprint.getBBox()
 
  435             if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
 
  437         if self.config.minFootprintAxisRatio > 0:
 
  438             axes = afwEll.Axes(footprint.getShape())
 
  439             if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
 
  444         """Returns whether the footprint violates the mask limits""" 
  445         size = float(footprint.getArea())
 
  446         for maskName, limit 
in self.config.maskLimits.items():
 
  447             maskVal = mask.getPlaneBitMask(maskName)
 
  448             unmaskedSpan = footprint.spans.intersectNot(mask, maskVal)  
 
  449             if (size - unmaskedSpan.getArea())/size > limit:
 
  454         """Indicate that the parent source is not being deblended 
  456         We set the appropriate flags and mask. 
  458         @param source  The source to flag as skipped 
  459         @param mask  The mask to update 
  461         fp = source.getFootprint()
 
  463         source.set(self.
nChildKey, len(fp.getPeaks()))  
 
  464         if self.config.notDeblendedMask:
 
  465             mask.addMaskPlane(self.config.notDeblendedMask)
 
  466             fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))