Coverage for python/lsst/meas/deblender/sourceDeblendTask.py : 18%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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/>.
22__all__ = ['SourceDeblendConfig', 'SourceDeblendTask']
24import math
25import numpy as np
27import lsst.log
28import lsst.pex.config as pexConfig
29import lsst.pipe.base as pipeBase
30import lsst.afw.math as afwMath
31import lsst.geom as geom
32import lsst.afw.geom.ellipses as afwEll
33import lsst.afw.image as afwImage
34import lsst.afw.detection as afwDet
35import lsst.afw.table as afwTable
36from lsst.utils.timer import timeMethod
38logger = lsst.log.Log.getLogger("meas.deblender.deblend")
41class SourceDeblendConfig(pexConfig.Config):
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',
46 allowed={
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.',
50 }
51 )
53 strayFluxToPointSources = pexConfig.ChoiceField(
54 doc='When the deblender should attribute stray flux to point sources',
55 dtype=str, default='necessary',
56 allowed={
57 'necessary': 'When there is not an extended object in the footprint',
58 'always': 'Always',
59 'never': ('Never; stray flux will not be attributed to any deblended child '
60 'if the deblender thinks all peaks look like point sources'),
61 }
62 )
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',
71 allowed={
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')
78 }
79 )
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 '
110 'will be ignored; minimum of 2 due to PSF gradient '
111 'calculation.'))
113 propagateAllPeaks = pexConfig.Field(dtype=bool, default=False,
114 doc=('Guarantee that all peaks produce a child source.'))
115 catchFailures = pexConfig.Field(
116 dtype=bool, default=False,
117 doc=("If True, catch exceptions thrown by the deblender, log them, "
118 "and set a flag on the parent, instead of letting them propagate up"))
119 maskPlanes = pexConfig.ListField(dtype=str, default=["SAT", "INTRP", "NO_DATA"],
120 doc="Mask planes to ignore when performing statistics")
121 maskLimits = pexConfig.DictField(
122 keytype=str,
123 itemtype=float,
124 default={},
125 doc=("Mask planes with the corresponding limit on the fraction of masked pixels. "
126 "Sources violating this limit will not be deblended."),
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")
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.")
164class SourceDeblendTask(pipeBase.Task):
165 """Split blended sources into individual sources.
167 This task has no return value; it only modifies the SourceCatalog in-place.
168 """
169 ConfigClass = SourceDeblendConfig
170 _DefaultName = "sourceDeblend"
172 def __init__(self, schema, peakSchema=None, **kwargs):
173 """Create the task, adding necessary fields to the given schema.
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)
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')
226 if self.config.catchFailures:
227 self.deblendFailedKey = schema.addField('deblend_failed', type='Flag',
228 doc="Deblending failed on source")
230 self.deblendSkippedKey = schema.addField('deblend_skipped', type='Flag',
231 doc="Deblender skipped this source")
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.'))
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.'))
243 self.hasStrayFluxKey = schema.addField(
244 'deblend_hasStrayFlux', type='Flag',
245 doc=('This source was assigned some stray flux'))
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")
266 @timeMethod
267 def run(self, exposure, sources):
268 """Get the PSF from the provided exposure and then run deblend.
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)
281 def _getPsfFwhm(self, psf, bbox):
282 # It should be easier to get a PSF's fwhm;
283 # https://dev.lsstcorp.org/trac/ticket/3030
284 return psf.computeShape().getDeterminantRadius() * 2.35
286 @timeMethod
287 def deblend(self, exposure, srcs, psf):
288 """Deblend.
290 Parameters
291 ----------
292 exposure : `lsst.afw.image.Exposure`
293 Exposure to be processed
294 srcs : `lsst.afw.table.SourceCatalog`
295 SourceCatalog containing sources detected on this exposure
296 psf : `lsst.afw.detection.Psf`
297 Point source function
299 Returns
300 -------
301 None
302 """
303 # Cull footprints if required by ci
304 if self.config.useCiLimits:
305 self.log.info(f"Using CI catalog limits, "
306 f"the original number of sources to deblend was {len(srcs)}.")
307 # Select parents with a number of children in the range
308 # config.ciDeblendChildRange
309 minChildren, maxChildren = self.config.ciDeblendChildRange
310 nPeaks = np.array([len(src.getFootprint().peaks) for src in srcs])
311 childrenInRange = np.where((nPeaks >= minChildren) & (nPeaks <= maxChildren))[0]
312 if len(childrenInRange) < self.config.ciNumParentsToDeblend:
313 raise ValueError("Fewer than ciNumParentsToDeblend children were contained in the range "
314 "indicated by ciDeblendChildRange. Adjust this range to include more "
315 "parents.")
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[:self.config.ciNumParentsToDeblend]] = 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)
328 self.log.info("Deblending %d sources" % len(srcs))
330 from lsst.meas.deblender.baseline import deblend
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)
340 n0 = len(srcs)
341 nparents = 0
342 for i, src in enumerate(srcs):
343 # t0 = time.clock()
345 fp = src.getFootprint()
346 pks = fp.getPeaks()
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)
352 if len(pks) < 2:
353 continue
355 if self.isLargeFootprint(fp):
356 src.set(self.tooBigKey, True)
357 self.skipParent(src, mi.getMask())
358 self.log.warn('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.warn('Parent %i: skipping masked footprint (area: %i)',
365 int(src.getId()), int(fp.getArea()))
366 continue
368 nparents += 1
369 bb = fp.getBBox()
370 psf_fwhm = self._getPsfFwhm(psf, bb)
372 self.log.trace('Parent %i: deblending %i peaks', int(src.getId()), len(pks))
374 self.preSingleDeblendHook(exposure, srcs, i, fp, psf, psf_fwhm, sigma1)
375 npre = len(srcs)
377 # This should really be set in deblend, but deblend doesn't have access to the src
378 src.set(self.tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks)
380 try:
381 res = deblend(
382 fp, mi, psf, psf_fwhm, sigma1=sigma1,
383 psfChisqCut1=self.config.psfChisq1,
384 psfChisqCut2=self.config.psfChisq2,
385 psfChisqCut2b=self.config.psfChisq2b,
386 maxNumberOfPeaks=self.config.maxNumberOfPeaks,
387 strayFluxToPointSources=self.config.strayFluxToPointSources,
388 assignStrayFlux=self.config.assignStrayFlux,
389 strayFluxAssignment=self.config.strayFluxRule,
390 rampFluxAtEdge=(self.config.edgeHandling == 'ramp'),
391 patchEdges=(self.config.edgeHandling == 'noclip'),
392 tinyFootprintSize=self.config.tinyFootprintSize,
393 clipStrayFluxFraction=self.config.clipStrayFluxFraction,
394 weightTemplates=self.config.weightTemplates,
395 removeDegenerateTemplates=self.config.removeDegenerateTemplates,
396 maxTempDotProd=self.config.maxTempDotProd,
397 medianSmoothTemplate=self.config.medianSmoothTemplate
398 )
399 if self.config.catchFailures:
400 src.set(self.deblendFailedKey, False)
401 except Exception as e:
402 if self.config.catchFailures:
403 self.log.warn("Unable to deblend source %d: %s" % (src.getId(), e))
404 src.set(self.deblendFailedKey, True)
405 import traceback
406 traceback.print_exc()
407 continue
408 else:
409 raise
411 kids = []
412 nchild = 0
413 for j, peak in enumerate(res.deblendedParents[0].peaks):
414 heavy = peak.getFluxPortion()
415 if heavy is None or peak.skip:
416 src.set(self.deblendSkippedKey, True)
417 if not self.config.propagateAllPeaks:
418 # Don't care
419 continue
420 # We need to preserve the peak: make sure we have enough info to create a minimal
421 # child src
422 self.log.trace("Peak at (%i,%i) failed. Using minimal default info for child.",
423 pks[j].getIx(), pks[j].getIy())
424 if heavy is None:
425 # copy the full footprint and strip out extra peaks
426 foot = afwDet.Footprint(src.getFootprint())
427 peakList = foot.getPeaks()
428 peakList.clear()
429 peakList.append(peak.peak)
430 zeroMimg = afwImage.MaskedImageF(foot.getBBox())
431 heavy = afwDet.makeHeavyFootprint(foot, zeroMimg)
432 if peak.deblendedAsPsf:
433 if peak.psfFitFlux is None:
434 peak.psfFitFlux = 0.0
435 if peak.psfFitCenter is None:
436 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy())
438 assert(len(heavy.getPeaks()) == 1)
440 src.set(self.deblendSkippedKey, False)
441 child = srcs.addNew()
442 nchild += 1
443 for key in self.toCopyFromParent:
444 child.set(key, src.get(key))
445 child.assign(heavy.getPeaks()[0], self.peakSchemaMapper)
446 child.setParent(src.getId())
447 child.setFootprint(heavy)
448 child.set(self.psfKey, peak.deblendedAsPsf)
449 child.set(self.hasStrayFluxKey, peak.strayFlux is not None)
450 if peak.deblendedAsPsf:
451 (cx, cy) = peak.psfFitCenter
452 child.set(self.psfCenterKey, geom.Point2D(cx, cy))
453 child.set(self.psfFluxKey, peak.psfFitFlux)
454 child.set(self.deblendRampedTemplateKey, peak.hasRampedTemplate)
455 child.set(self.deblendPatchedTemplateKey, peak.patched)
457 # Set the position of the peak from the parent footprint
458 # This will make it easier to match the same source across
459 # deblenders and across observations, where the peak
460 # position is unlikely to change unless enough time passes
461 # for a source to move on the sky.
462 child.set(self.peakCenter, geom.Point2I(pks[j].getIx(), pks[j].getIy()))
463 child.set(self.peakIdKey, pks[j].getId())
465 # The children have a single peak
466 child.set(self.nPeaksKey, 1)
467 # Set the number of peaks in the parent
468 child.set(self.parentNPeaksKey, len(pks))
470 kids.append(child)
472 # Child footprints may extend beyond the full extent of their parent's which
473 # results in a failure of the replace-by-noise code to reinstate these pixels
474 # to their original values. The following updates the parent footprint
475 # in-place to ensure it contains the full union of itself and all of its
476 # children's footprints.
477 spans = src.getFootprint().spans
478 for child in kids:
479 spans = spans.union(child.getFootprint().spans)
480 src.getFootprint().setSpans(spans)
482 src.set(self.nChildKey, nchild)
484 self.postSingleDeblendHook(exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res)
485 # print('Deblending parent id', src.getId(), 'took', time.clock() - t0)
487 n1 = len(srcs)
488 self.log.info('Deblended: of %i sources, %i were deblended, creating %i children, total %i sources'
489 % (n0, nparents, n1-n0, n1))
491 def preSingleDeblendHook(self, exposure, srcs, i, fp, psf, psf_fwhm, sigma1):
492 pass
494 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res):
495 pass
497 def isLargeFootprint(self, footprint):
498 """Returns whether a Footprint is large
500 'Large' is defined by thresholds on the area, size and axis ratio.
501 These may be disabled independently by configuring them to be non-positive.
503 This is principally intended to get rid of satellite streaks, which the
504 deblender or other downstream processing can have trouble dealing with
505 (e.g., multiple large HeavyFootprints can chew up memory).
506 """
507 if self.config.maxFootprintArea > 0 and footprint.getArea() > self.config.maxFootprintArea:
508 return True
509 if self.config.maxFootprintSize > 0:
510 bbox = footprint.getBBox()
511 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize:
512 return True
513 if self.config.minFootprintAxisRatio > 0:
514 axes = afwEll.Axes(footprint.getShape())
515 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA():
516 return True
517 return False
519 def isMasked(self, footprint, mask):
520 """Returns whether the footprint violates the mask limits
521 """
522 size = float(footprint.getArea())
523 for maskName, limit in self.config.maskLimits.items():
524 maskVal = mask.getPlaneBitMask(maskName)
525 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal) # spanset of unmasked pixels
526 if (size - unmaskedSpan.getArea())/size > limit:
527 return True
528 return False
530 def skipParent(self, source, mask):
531 """Indicate that the parent source is not being deblended
533 We set the appropriate flags and mask.
535 Parameters
536 ----------
537 source : `lsst.afw.table.SourceRecord`
538 The source to flag as skipped
539 mask : `lsst.afw.image.Mask`
540 The mask to update
541 """
542 fp = source.getFootprint()
543 source.set(self.deblendSkippedKey, True)
544 if self.config.notDeblendedMask:
545 mask.addMaskPlane(self.config.notDeblendedMask)
546 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask))
548 # Set the center of the parent
549 bbox = fp.getBBox()
550 centerX = int(bbox.getMinX()+bbox.getWidth()/2)
551 centerY = int(bbox.getMinY()+bbox.getHeight()/2)
552 source.set(self.peakCenter, geom.Point2I(centerX, centerY))
553 # There are no deblended children, so nChild = 0
554 source.set(self.nChildKey, 0)
555 # But we also want to know how many peaks that we would have
556 # deblended if the parent wasn't skipped.
557 source.set(self.nPeaksKey, len(fp.peaks))
558 # Top level parents are not a detected peak, so they have no peakId
559 source.set(self.peakIdKey, 0)
560 # Top level parents also have no parentNPeaks
561 source.set(self.parentNPeaksKey, 0)