LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
sourceDeblendTask.py
Go to the documentation of this file.
1# This file is part of meas_deblender.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22__all__ = ['SourceDeblendConfig', 'SourceDeblendTask']
23
24import math
25import numpy as np
26
27import lsst.pex.config as pexConfig
28import lsst.pipe.base as pipeBase
29import lsst.afw.math as afwMath
30import lsst.geom as geom
31import lsst.afw.geom.ellipses as afwEll
32import lsst.afw.image as afwImage
33import lsst.afw.detection as afwDet
34import lsst.afw.table as afwTable
35from lsst.utils.timer import timeMethod
36
37
38class SourceDeblendConfig(pexConfig.Config):
39
40 edgeHandling = pexConfig.ChoiceField(
41 doc='What to do when a peak to be deblended is close to the edge of the image',
42 dtype=str, default='ramp',
43 allowed={
44 'clip': 'Clip the template at the edge AND the mirror of the edge.',
45 'ramp': 'Ramp down flux at the image edge by the PSF',
46 'noclip': 'Ignore the edge when building the symmetric template.',
47 }
48 )
49
50 strayFluxToPointSources = pexConfig.ChoiceField(
51 doc='When the deblender should attribute stray flux to point sources',
52 dtype=str, default='necessary',
53 allowed={
54 'necessary': 'When there is not an extended object in the footprint',
55 'always': 'Always',
56 'never': ('Never; stray flux will not be attributed to any deblended child '
57 'if the deblender thinks all peaks look like point sources'),
58 }
59 )
60
61 assignStrayFlux = pexConfig.Field(dtype=bool, default=True,
62 doc='Assign stray flux (not claimed by any child in the deblender) '
63 'to deblend children.')
64
65 strayFluxRule = pexConfig.ChoiceField(
66 doc='How to split flux among peaks',
67 dtype=str, default='trim',
68 allowed={
69 'r-to-peak': '~ 1/(1+R^2) to the peak',
70 'r-to-footprint': ('~ 1/(1+R^2) to the closest pixel in the footprint. '
71 'CAUTION: this can be computationally expensive on large footprints!'),
72 'nearest-footprint': ('Assign 100% to the nearest footprint (using L-1 norm aka '
73 'Manhattan distance)'),
74 'trim': ('Shrink the parent footprint to pixels that are not assigned to children')
75 }
76 )
77
78 clipStrayFluxFraction = pexConfig.Field(dtype=float, default=0.001,
79 doc=('When splitting stray flux, clip fractions below '
80 'this value to zero.'))
81 psfChisq1 = pexConfig.Field(dtype=float, default=1.5, optional=False,
82 doc=('Chi-squared per DOF cut for deciding a source is '
83 'a PSF during deblending (un-shifted PSF model)'))
84 psfChisq2 = pexConfig.Field(dtype=float, default=1.5, optional=False,
85 doc=('Chi-squared per DOF cut for deciding a source is '
86 'PSF during deblending (shifted PSF model)'))
87 psfChisq2b = pexConfig.Field(dtype=float, default=1.5, optional=False,
88 doc=('Chi-squared per DOF cut for deciding a source is '
89 'a PSF during deblending (shifted PSF model #2)'))
90 maxNumberOfPeaks = pexConfig.Field(dtype=int, default=0,
91 doc=("Only deblend the brightest maxNumberOfPeaks peaks in the parent"
92 " (<= 0: unlimited)"))
93 maxFootprintArea = pexConfig.Field(dtype=int, default=10000,
94 doc=("Maximum area for footprints before they are ignored as large; "
95 "non-positive means no threshold applied."
96 " Default value is to prevent excessive memory usage."))
97 maxFootprintSize = pexConfig.Field(dtype=int, default=0,
98 doc=("Maximum linear dimension for footprints before they are ignored "
99 "as large; non-positive means no threshold applied"))
100 minFootprintAxisRatio = pexConfig.Field(dtype=float, default=0.0,
101 doc=("Minimum axis ratio for footprints before they are ignored "
102 "as large; non-positive means no threshold applied"))
103 notDeblendedMask = pexConfig.Field(dtype=str, default="NOT_DEBLENDED", optional=True,
104 doc="Mask name for footprints not deblended, or None")
105
106 tinyFootprintSize = pexConfig.RangeField(dtype=int, default=2, min=2, inclusiveMin=True,
107 doc=('Footprints smaller in width or height than this value '
108 'will be ignored; minimum of 2 due to PSF gradient '
109 'calculation.'))
110
111 propagateAllPeaks = pexConfig.Field(dtype=bool, default=False,
112 doc=('Guarantee that all peaks produce a child source.'))
113 catchFailures = pexConfig.Field(
114 dtype=bool,
115 default=True,
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(
121 keytype=str,
122 itemtype=float,
123 default={"NO_DATA": 0.25},
124 doc=("Mask planes with the corresponding limit on the fraction of masked pixels. "
125 "Sources violating this limit will not be deblended. "
126 "Default rejects sources in vignetted regions."),
127 )
128 weightTemplates = pexConfig.Field(
129 dtype=bool, default=False,
130 doc=("If true, a least-squares fit of the templates will be done to the "
131 "full image. The templates will be re-weighted based on this fit."))
132 removeDegenerateTemplates = pexConfig.Field(dtype=bool, default=False,
133 doc=("Try to remove similar templates?"))
134 maxTempDotProd = pexConfig.Field(
135 dtype=float, default=0.5,
136 doc=("If the dot product between two templates is larger than this value, we consider them to be "
137 "describing the same object (i.e. they are degenerate). If one of the objects has been "
138 "labeled as a PSF it will be removed, otherwise the template with the lowest value will "
139 "be removed."))
140 medianSmoothTemplate = pexConfig.Field(dtype=bool, default=True,
141 doc="Apply a smoothing filter to all of the template images")
142
143 # Testing options
144 # Some obs packages and ci packages run the full pipeline on a small
145 # subset of data to test that the pipeline is functioning properly.
146 # This is not meant as scientific validation, so it can be useful
147 # to only run on a small subset of the data that is large enough to
148 # test the desired pipeline features but not so long that the deblender
149 # is the tall pole in terms of execution times.
150 useCiLimits = pexConfig.Field(
151 dtype=bool, default=False,
152 doc="Limit the number of sources deblended for CI to prevent long build times")
153 ciDeblendChildRange = pexConfig.ListField(
154 dtype=int, default=[2, 10],
155 doc="Only deblend parent Footprints with a number of peaks in the (inclusive) range indicated."
156 "If `useCiLimits==False` then this parameter is ignored.")
157 ciNumParentsToDeblend = pexConfig.Field(
158 dtype=int, default=10,
159 doc="Only use the first `ciNumParentsToDeblend` parent footprints with a total peak count "
160 "within `ciDebledChildRange`. "
161 "If `useCiLimits==False` then this parameter is ignored.")
162
163
164class SourceDeblendTask(pipeBase.Task):
165 """Split blended sources into individual sources.
166
167 This task has no return value; it only modifies the SourceCatalog in-place.
168 """
169 ConfigClass = SourceDeblendConfig
170 _DefaultName = "sourceDeblend"
171
172 def __init__(self, schema, peakSchema=None, **kwargs):
173 """Create the task, adding necessary fields to the given schema.
174
175 Parameters
176 ----------
177 schema : `lsst.afw.table.Schema`
178 Schema object for measurement fields; will be modified in-place.
179 peakSchema : `lsst.afw.table.peakSchema`
180 Schema of Footprint Peaks that will be passed to the deblender.
181 Any fields beyond the PeakTable minimal schema will be transferred
182 to the main source Schema. If None, no fields will be transferred
183 from the Peaks
184 **kwargs
185 Additional keyword arguments passed to ~lsst.pipe.base.task
186 """
187 pipeBase.Task.__init__(self, **kwargs)
188 self.schema = schema
189 self.toCopyFromParent = [item.key for item in self.schema
190 if item.field.getName().startswith("merge_footprint")]
191 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema()
192 if peakSchema is None:
193 # In this case, the peakSchemaMapper will transfer nothing, but we'll still have one
194 # to simplify downstream code
195 self.peakSchemaMapper = afwTable.SchemaMapper(peakMinimalSchema, schema)
196 else:
197 self.peakSchemaMapper = afwTable.SchemaMapper(peakSchema, schema)
198 for item in peakSchema:
199 if item.key not in peakMinimalSchema:
200 self.peakSchemaMapper.addMapping(item.key, item.field)
201 # Because SchemaMapper makes a copy of the output schema you give its ctor, it isn't
202 # updating this Schema in place. That's probably a design flaw, but in the meantime,
203 # we'll keep that schema in sync with the peakSchemaMapper.getOutputSchema() manually,
204 # by adding the same fields to both.
205 schema.addField(item.field)
206 assert schema == self.peakSchemaMapper.getOutputSchema(), "Logic bug mapping schemas"
207 self.addSchemaKeys(schema)
208
209 def addSchemaKeys(self, schema):
210 self.nChildKey = schema.addField('deblend_nChild', type=np.int32,
211 doc='Number of children this object has (defaults to 0)')
212 self.psfKey = schema.addField('deblend_deblendedAsPsf', type='Flag',
213 doc='Deblender thought this source looked like a PSF')
214 self.psfCenterKey = afwTable.Point2DKey.addFields(schema, 'deblend_psfCenter',
215 'If deblended-as-psf, the PSF centroid', "pixel")
216 self.psfFluxKey = schema.addField('deblend_psf_instFlux', type='D',
217 doc='If deblended-as-psf, the instrumental PSF flux', units='count')
218 self.tooManyPeaksKey = schema.addField('deblend_tooManyPeaks', type='Flag',
219 doc='Source had too many peaks; '
220 'only the brightest were included')
221 self.tooBigKey = schema.addField('deblend_parentTooBig', type='Flag',
222 doc='Parent footprint covered too many pixels')
223 self.maskedKey = schema.addField('deblend_masked', type='Flag',
224 doc='Parent footprint was predominantly masked')
225
226 if self.config.catchFailures:
227 self.deblendFailedKey = schema.addField('deblend_failed', type='Flag',
228 doc="Deblending failed on source")
229
230 self.deblendSkippedKey = schema.addField('deblend_skipped', type='Flag',
231 doc="Deblender skipped this source")
232
233 self.deblendRampedTemplateKey = schema.addField(
234 'deblend_rampedTemplate', type='Flag',
235 doc=('This source was near an image edge and the deblender used '
236 '"ramp" edge-handling.'))
237
238 self.deblendPatchedTemplateKey = schema.addField(
239 'deblend_patchedTemplate', type='Flag',
240 doc=('This source was near an image edge and the deblender used '
241 '"patched" edge-handling.'))
242
243 self.hasStrayFluxKey = schema.addField(
244 'deblend_hasStrayFlux', type='Flag',
245 doc=('This source was assigned some stray flux'))
246
247 self.log.trace('Added keys to schema: %s', ", ".join(str(x) for x in (
248 self.nChildKey, self.psfKey, self.psfCenterKey, self.psfFluxKey,
249 self.tooManyPeaksKey, self.tooBigKey)))
250 self.peakCenter = afwTable.Point2IKey.addFields(schema, name="deblend_peak_center",
251 doc="Center used to apply constraints in scarlet",
252 unit="pixel")
253 self.peakIdKey = schema.addField("deblend_peakId", type=np.int32,
254 doc="ID of the peak in the parent footprint. "
255 "This is not unique, but the combination of 'parent'"
256 "and 'peakId' should be for all child sources. "
257 "Top level blends with no parents have 'peakId=0'")
258 self.nPeaksKey = schema.addField("deblend_nPeaks", type=np.int32,
259 doc="Number of initial peaks in the blend. "
260 "This includes peaks that may have been culled "
261 "during deblending or failed to deblend")
262 self.parentNPeaksKey = schema.addField("deblend_parentNPeaks", type=np.int32,
263 doc="Same as deblend_n_peaks, but the number of peaks "
264 "in the parent footprint")
265
266 @timeMethod
267 def run(self, exposure, sources):
268 """Get the PSF from the provided exposure and then run deblend.
269
270 Parameters
271 ----------
272 exposure : `lsst.afw.image.Exposure`
273 Exposure to be processed
274 sources : `lsst.afw.table.SourceCatalog`
275 SourceCatalog containing sources detected on this exposure.
276 """
277 psf = exposure.getPsf()
278 assert sources.getSchema() == self.schema
279 self.deblend(exposure, sources, psf)
280
281 def _getPsfFwhm(self, psf, position):
282 return psf.computeShape(position).getDeterminantRadius() * 2.35
283
284 @timeMethod
285 def deblend(self, exposure, srcs, psf):
286 """Deblend.
287
288 Parameters
289 ----------
290 exposure : `lsst.afw.image.Exposure`
291 Exposure to be processed
292 srcs : `lsst.afw.table.SourceCatalog`
293 SourceCatalog containing sources detected on this exposure
294 psf : `lsst.afw.detection.Psf`
295 Point source function
296
297 Returns
298 -------
299 None
300 """
301 # Cull footprints if required by ci
302 if self.config.useCiLimits:
303 self.log.info(f"Using CI catalog limits, "
304 f"the original number of sources to deblend was {len(srcs)}.")
305 # Select parents with a number of children in the range
306 # config.ciDeblendChildRange
307 minChildren, maxChildren = self.config.ciDeblendChildRange
308 nPeaks = np.array([len(src.getFootprint().peaks) for src in srcs])
309 childrenInRange = np.where((nPeaks >= minChildren) & (nPeaks <= maxChildren))[0]
310 numParentsToDeblend = self.config.ciNumParentsToDeblend
311 if len(childrenInRange) < numParentsToDeblend:
312 # Only use the top N instead of the full requested list.
313 if len(childrenInRange) == 0:
314 raise ValueError("No children to deblend; cannot continue with CI testing.")
315 numParentsToDeblend = len(childrenInRange)
316 # Keep all of the isolated parents and the first
317 # `ciNumParentsToDeblend` children
318 parents = nPeaks == 1
319 children = np.zeros((len(srcs),), dtype=bool)
320 children[childrenInRange[:numParentsToDeblend]] = True
321 srcs = srcs[parents | children]
322 # We need to update the IdFactory, otherwise the the source ids
323 # will not be sequential
324 idFactory = srcs.getIdFactory()
325 maxId = np.max(srcs["id"])
326 idFactory.notify(maxId)
327
328 self.log.info("Deblending %d sources", len(srcs))
329
330 from lsst.meas.deblender.baseline import deblend
331
332 # find the median stdev in the image...
333 mi = exposure.getMaskedImage()
334 statsCtrl = afwMath.StatisticsControl()
335 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes))
336 stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl)
337 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
338 self.log.trace('sigma1: %g', sigma1)
339
340 n0 = len(srcs)
341 nparents = 0
342 for i, src in enumerate(srcs):
343 # t0 = time.clock()
344
345 fp = src.getFootprint()
346 pks = fp.getPeaks()
347
348 # Since we use the first peak for the parent object, we should propagate its flags
349 # to the parent source.
350 src.assign(pks[0], self.peakSchemaMapper)
351
352 if len(pks) < 2:
353 continue
354
355 if self.isLargeFootprint(fp):
356 src.set(self.tooBigKey, True)
357 self.skipParent(src, mi.getMask())
358 self.log.debug('Parent %i: skipping large footprint (area: %i)',
359 int(src.getId()), int(fp.getArea()))
360 continue
361 if self.isMasked(fp, exposure.getMaskedImage().getMask()):
362 src.set(self.maskedKey, True)
363 self.skipParent(src, mi.getMask())
364 self.log.debug('Parent %i: skipping masked footprint (area: %i)',
365 int(src.getId()), int(fp.getArea()))
366 continue
367
368 nparents += 1
369 center = fp.getCentroid()
370 psf_fwhm = self._getPsfFwhm(psf, center)
371
372 if not (psf_fwhm > 0):
373 if self.config.catchFailures:
374 self.log.warning("Unable to deblend source %d: because PSF FWHM=%f is invalid.",
375 src.getId(), psf_fwhm)
376 src.set(self.deblendFailedKey, True)
377 continue
378 else:
379 raise ValueError(f"PSF at {center} has an invalid FWHM value of {psf_fwhm}")
380
381 self.log.trace('Parent %i: deblending %i peaks', int(src.getId()), len(pks))
382
383 self.preSingleDeblendHook(exposure, srcs, i, fp, psf, psf_fwhm, sigma1)
384 npre = len(srcs)
385
386 # This should really be set in deblend, but deblend doesn't have access to the src
387 src.set(self.tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
388
389 try:
390 res = deblend(
391 fp, mi, psf, psf_fwhm, sigma1=sigma1,
392 psfChisqCut1=self.config.psfChisq1,
393 psfChisqCut2=self.config.psfChisq2,
394 psfChisqCut2b=self.config.psfChisq2b,
395 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
396 strayFluxToPointSources=self.config.strayFluxToPointSources,
397 assignStrayFlux=self.config.assignStrayFlux,
398 strayFluxAssignment=self.config.strayFluxRule,
399 rampFluxAtEdge=(self.config.edgeHandling == 'ramp'),
400 patchEdges=(self.config.edgeHandling == 'noclip'),
401 tinyFootprintSize=self.config.tinyFootprintSize,
402 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
403 weightTemplates=self.config.weightTemplates,
404 removeDegenerateTemplates=self.config.removeDegenerateTemplates,
405 maxTempDotProd=self.config.maxTempDotProd,
406 medianSmoothTemplate=self.config.medianSmoothTemplate
407 )
408 if self.config.catchFailures:
409 src.set(self.deblendFailedKey, False)
410 except Exception as e:
411 if self.config.catchFailures:
412 self.log.warning("Unable to deblend source %d: %s", src.getId(), e)
413 src.set(self.deblendFailedKey, True)
414 import traceback
415 traceback.print_exc()
416 continue
417 else:
418 raise
419
420 kids = []
421 nchild = 0
422 for j, peak in enumerate(res.deblendedParents[0].peaks):
423 heavy = peak.getFluxPortion()
424 if heavy is None or peak.skip:
425 src.set(self.deblendSkippedKey, True)
426 if not self.config.propagateAllPeaks:
427 # Don't care
428 continue
429 # We need to preserve the peak: make sure we have enough info to create a minimal
430 # child src
431 self.log.trace("Peak at (%i,%i) failed. Using minimal default info for child.",
432 pks[j].getIx(), pks[j].getIy())
433 if heavy is None:
434 # copy the full footprint and strip out extra peaks
435 foot = afwDet.Footprint(src.getFootprint())
436 peakList = foot.getPeaks()
437 peakList.clear()
438 peakList.append(peak.peak)
439 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
440 heavy = afwDet.makeHeavyFootprint(foot, zeroMimg)
441 if peak.deblendedAsPsf:
442 if peak.psfFitFlux is None:
443 peak.psfFitFlux = 0.0
444 if peak.psfFitCenter is None:
445 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
446
447 assert len(heavy.getPeaks()) == 1
448
449 src.set(self.deblendSkippedKey, False)
450 child = srcs.addNew()
451 nchild += 1
452 for key in self.toCopyFromParent:
453 child.set(key, src.get(key))
454 child.assign(heavy.getPeaks()[0], self.peakSchemaMapper)
455 child.setParent(src.getId())
456 child.setFootprint(heavy)
457 child.set(self.psfKey, peak.deblendedAsPsf)
458 child.set(self.hasStrayFluxKey, peak.strayFlux is not None)
459 if peak.deblendedAsPsf:
460 (cx, cy) = peak.psfFitCenter
461 child.set(self.psfCenterKey, geom.Point2D(cx, cy))
462 child.set(self.psfFluxKey, peak.psfFitFlux)
463 child.set(self.deblendRampedTemplateKey, peak.hasRampedTemplate)
464 child.set(self.deblendPatchedTemplateKey, peak.patched)
465
466 # Set the position of the peak from the parent footprint
467 # This will make it easier to match the same source across
468 # deblenders and across observations, where the peak
469 # position is unlikely to change unless enough time passes
470 # for a source to move on the sky.
471 child.set(self.peakCenter, geom.Point2I(pks[j].getIx(), pks[j].getIy()))
472 child.set(self.peakIdKey, pks[j].getId())
473
474 # The children have a single peak
475 child.set(self.nPeaksKey, 1)
476 # Set the number of peaks in the parent
477 child.set(self.parentNPeaksKey, len(pks))
478
479 kids.append(child)
480
481 # Child footprints may extend beyond the full extent of their parent's which
482 # results in a failure of the replace-by-noise code to reinstate these pixels
483 # to their original values. The following updates the parent footprint
484 # in-place to ensure it contains the full union of itself and all of its
485 # children's footprints.
486 spans = src.getFootprint().spans
487 for child in kids:
488 spans = spans.union(child.getFootprint().spans)
489 src.getFootprint().setSpans(spans)
490
491 src.set(self.nChildKey, nchild)
492
493 self.postSingleDeblendHook(exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res)
494 # print('Deblending parent id', src.getId(), 'took', time.clock() - t0)
495
496 n1 = len(srcs)
497 self.log.info('Deblended: of %i sources, %i were deblended, creating %i children, total %i sources',
498 n0, nparents, n1-n0, n1)
499
500 def preSingleDeblendHook(self, exposure, srcs, i, fp, psf, psf_fwhm, sigma1):
501 pass
502
503 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
504 pass
505
506 def isLargeFootprint(self, footprint):
507 """Returns whether a Footprint is large
508
509 'Large' is defined by thresholds on the area, size and axis ratio.
510 These may be disabled independently by configuring them to be non-positive.
511
512 This is principally intended to get rid of satellite streaks, which the
513 deblender or other downstream processing can have trouble dealing with
514 (e.g., multiple large HeavyFootprints can chew up memory).
515 """
516 if self.config.maxFootprintArea > 0 and footprint.getArea() > self.config.maxFootprintArea:
517 return True
518 if self.config.maxFootprintSize > 0:
519 bbox = footprint.getBBox()
520 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
521 return True
522 if self.config.minFootprintAxisRatio > 0:
523 axes = afwEll.Axes(footprint.getShape())
524 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
525 return True
526 return False
527
528 def isMasked(self, footprint, mask):
529 """Returns whether the footprint violates the mask limits
530 """
531 size = float(footprint.getArea())
532 for maskName, limit in self.config.maskLimits.items():
533 maskVal = mask.getPlaneBitMask(maskName)
534 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal) # spanset of unmasked pixels
535 if (size - unmaskedSpan.getArea())/size > limit:
536 return True
537 return False
538
539 def skipParent(self, source, mask):
540 """Indicate that the parent source is not being deblended
541
542 We set the appropriate flags and mask.
543
544 Parameters
545 ----------
546 source : `lsst.afw.table.SourceRecord`
547 The source to flag as skipped
548 mask : `lsst.afw.image.Mask`
549 The mask to update
550 """
551 fp = source.getFootprint()
552 source.set(self.deblendSkippedKey, True)
553 if self.config.notDeblendedMask:
554 mask.addMaskPlane(self.config.notDeblendedMask)
555 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
556
557 # Set the center of the parent
558 bbox = fp.getBBox()
559 centerX = int(bbox.getMinX()+bbox.getWidth()/2)
560 centerY = int(bbox.getMinY()+bbox.getHeight()/2)
561 source.set(self.peakCenter, geom.Point2I(centerX, centerY))
562 # There are no deblended children, so nChild = 0
563 source.set(self.nChildKey, 0)
564 # But we also want to know how many peaks that we would have
565 # deblended if the parent wasn't skipped.
566 source.set(self.nPeaksKey, len(fp.peaks))
567 # Top level parents are not a detected peak, so they have no peakId
568 source.set(self.peakIdKey, 0)
569 # Top level parents also have no parentNPeaks
570 source.set(self.parentNPeaksKey, 0)
int max
Class to describe the properties of a detected object from an image.
Definition Footprint.h:63
Pass parameters to a Statistics object.
Definition Statistics.h:83
A mapping between the keys of two Schemas, used to copy data between them.
preSingleDeblendHook(self, exposure, srcs, i, fp, psf, psf_fwhm, sigma1)
__init__(self, schema, peakSchema=None, **kwargs)
postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res)
HeavyFootprint< ImagePixelT, MaskPixelT, VariancePixelT > makeHeavyFootprint(Footprint const &foot, lsst::afw::image::MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > const &img, HeavyFootprintCtrl const *ctrl=nullptr)
Create a HeavyFootprint with footprint defined by the given Footprint and pixel values from the given...
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361