Hide keyboard shortcuts

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/>. 

21 

22import math 

23import numpy as np 

24 

25import lsst.log 

26import lsst.pex.config as pexConfig 

27import lsst.pipe.base as pipeBase 

28import lsst.afw.math as afwMath 

29import lsst.geom as geom 

30import lsst.afw.geom.ellipses as afwEll 

31import lsst.afw.image as afwImage 

32import lsst.afw.detection as afwDet 

33import lsst.afw.table as afwTable 

34 

35logger = lsst.log.Log.getLogger("meas.deblender.deblend") 

36 

37__all__ = 'SourceDeblendConfig', 'SourceDeblendTask' 

38 

39 

40class SourceDeblendConfig(pexConfig.Config): 

41 

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', 

45 allowed={ 

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.', 

49 } 

50 ) 

51 

52 strayFluxToPointSources = pexConfig.ChoiceField( 

53 doc='When the deblender should attribute stray flux to point sources', 

54 dtype=str, default='necessary', 

55 allowed={ 

56 'necessary': 'When there is not an extended object in the footprint', 

57 'always': 'Always', 

58 'never': ('Never; stray flux will not be attributed to any deblended child ' 

59 'if the deblender thinks all peaks look like point sources'), 

60 } 

61 ) 

62 

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.') 

66 

67 strayFluxRule = pexConfig.ChoiceField( 

68 doc='How to split flux among peaks', 

69 dtype=str, default='trim', 

70 allowed={ 

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') 

77 } 

78 ) 

79 

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") 

106 

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 ' 

110 'calculation.')) 

111 

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( 

121 keytype=str, 

122 itemtype=float, 

123 default={}, 

124 doc=("Mask planes with the corresponding limit on the fraction of masked pixels. " 

125 "Sources violating this limit will not be deblended."), 

126 ) 

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 " 

138 "be removed.")) 

139 medianSmoothTemplate = pexConfig.Field(dtype=bool, default=True, 

140 doc="Apply a smoothing filter to all of the template images") 

141 

142 # Testing options 

143 # Some obs packages and ci packages run the full pipeline on a small 

144 # subset of data to test that the pipeline is functioning properly. 

145 # This is not meant as scientific validation, so it can be useful 

146 # to only run on a small subset of the data that is large enough to 

147 # test the desired pipeline features but not so long that the deblender 

148 # is the tall pole in terms of execution times. 

149 useCiLimits = pexConfig.Field( 

150 dtype=bool, default=False, 

151 doc="Limit the number of sources deblended for CI to prevent long build times") 

152 ciDeblendChildRange = pexConfig.ListField( 

153 dtype=int, default=[2, 10], 

154 doc="Only deblend parent Footprints with a number of peaks in the (inclusive) range indicated." 

155 "If `useCiLimits==False` then this parameter is ignored.") 

156 ciNumParentsToDeblend = pexConfig.Field( 

157 dtype=int, default=10, 

158 doc="Only use the first `ciNumParentsToDeblend` parent footprints with a total peak count " 

159 "within `ciDebledChildRange`. " 

160 "If `useCiLimits==False` then this parameter is ignored.") 

161 

162## \addtogroup LSST_task_documentation 

163## \{ 

164## \page SourceDeblendTask 

165## \ref SourceDeblendTask_ "SourceDeblendTask" 

166## \copybrief SourceDeblendTask 

167## \} 

168 

169 

170class SourceDeblendTask(pipeBase.Task): 

171 """! 

172 \anchor SourceDeblendTask_ 

173 

174 \brief Split blended sources into individual sources. 

175 

176 This task has no return value; it only modifies the SourceCatalog in-place. 

177 """ 

178 ConfigClass = SourceDeblendConfig 

179 _DefaultName = "sourceDeblend" 

180 

181 def __init__(self, schema, peakSchema=None, **kwargs): 

182 """! 

183 Create the task, adding necessary fields to the given schema. 

184 

185 @param[in,out] schema Schema object for measurement fields; will be modified in-place. 

186 @param[in] peakSchema Schema of Footprint Peaks that will be passed to the deblender. 

187 Any fields beyond the PeakTable minimal schema will be transferred 

188 to the main source Schema. If None, no fields will be transferred 

189 from the Peaks. 

190 @param[in] **kwargs Passed to Task.__init__. 

191 """ 

192 pipeBase.Task.__init__(self, **kwargs) 

193 self.schema = schema 

194 self.toCopyFromParent = [item.key for item in self.schema 

195 if item.field.getName().startswith("merge_footprint")] 

196 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema() 

197 if peakSchema is None: 

198 # In this case, the peakSchemaMapper will transfer nothing, but we'll still have one 

199 # to simplify downstream code 

200 self.peakSchemaMapper = afwTable.SchemaMapper(peakMinimalSchema, schema) 

201 else: 

202 self.peakSchemaMapper = afwTable.SchemaMapper(peakSchema, schema) 

203 for item in peakSchema: 

204 if item.key not in peakMinimalSchema: 

205 self.peakSchemaMapper.addMapping(item.key, item.field) 

206 # Because SchemaMapper makes a copy of the output schema you give its ctor, it isn't 

207 # updating this Schema in place. That's probably a design flaw, but in the meantime, 

208 # we'll keep that schema in sync with the peakSchemaMapper.getOutputSchema() manually, 

209 # by adding the same fields to both. 

210 schema.addField(item.field) 

211 assert schema == self.peakSchemaMapper.getOutputSchema(), "Logic bug mapping schemas" 

212 self.addSchemaKeys(schema) 

213 

214 def addSchemaKeys(self, schema): 

215 self.nChildKey = schema.addField('deblend_nChild', type=np.int32, 

216 doc='Number of children this object has (defaults to 0)') 

217 self.psfKey = schema.addField('deblend_deblendedAsPsf', type='Flag', 

218 doc='Deblender thought this source looked like a PSF') 

219 self.psfCenterKey = afwTable.Point2DKey.addFields(schema, 'deblend_psfCenter', 

220 'If deblended-as-psf, the PSF centroid', "pixel") 

221 self.psfFluxKey = schema.addField('deblend_psf_instFlux', type='D', 

222 doc='If deblended-as-psf, the instrumental PSF flux', units='count') 

223 self.tooManyPeaksKey = schema.addField('deblend_tooManyPeaks', type='Flag', 

224 doc='Source had too many peaks; ' 

225 'only the brightest were included') 

226 self.tooBigKey = schema.addField('deblend_parentTooBig', type='Flag', 

227 doc='Parent footprint covered too many pixels') 

228 self.maskedKey = schema.addField('deblend_masked', type='Flag', 

229 doc='Parent footprint was predominantly masked') 

230 

231 if self.config.catchFailures: 

232 self.deblendFailedKey = schema.addField('deblend_failed', type='Flag', 

233 doc="Deblending failed on source") 

234 

235 self.deblendSkippedKey = schema.addField('deblend_skipped', type='Flag', 

236 doc="Deblender skipped this source") 

237 

238 self.deblendRampedTemplateKey = schema.addField( 

239 'deblend_rampedTemplate', type='Flag', 

240 doc=('This source was near an image edge and the deblender used ' 

241 '"ramp" edge-handling.')) 

242 

243 self.deblendPatchedTemplateKey = schema.addField( 

244 'deblend_patchedTemplate', type='Flag', 

245 doc=('This source was near an image edge and the deblender used ' 

246 '"patched" edge-handling.')) 

247 

248 self.hasStrayFluxKey = schema.addField( 

249 'deblend_hasStrayFlux', type='Flag', 

250 doc=('This source was assigned some stray flux')) 

251 

252 self.log.trace('Added keys to schema: %s', ", ".join(str(x) for x in ( 

253 self.nChildKey, self.psfKey, self.psfCenterKey, self.psfFluxKey, 

254 self.tooManyPeaksKey, self.tooBigKey))) 

255 self.peakCenter = afwTable.Point2IKey.addFields(schema, name="deblend_peak_center", 

256 doc="Center used to apply constraints in scarlet", 

257 unit="pixel") 

258 self.peakIdKey = schema.addField("deblend_peakId", type=np.int32, 

259 doc="ID of the peak in the parent footprint. " 

260 "This is not unique, but the combination of 'parent'" 

261 "and 'peakId' should be for all child sources. " 

262 "Top level blends with no parents have 'peakId=0'") 

263 self.nPeaksKey = schema.addField("deblend_nPeaks", type=np.int32, 

264 doc="Number of initial peaks in the blend. " 

265 "This includes peaks that may have been culled " 

266 "during deblending or failed to deblend") 

267 self.parentNPeaksKey = schema.addField("deblend_parentNPeaks", type=np.int32, 

268 doc="Same as deblend_n_peaks, but the number of peaks " 

269 "in the parent footprint") 

270 

271 @pipeBase.timeMethod 

272 def run(self, exposure, sources): 

273 """! 

274 Get the psf from the provided exposure and then run deblend(). 

275 

276 @param[in] exposure Exposure to process 

277 @param[in,out] sources SourceCatalog containing sources detected on this exposure. 

278 

279 @return None 

280 """ 

281 psf = exposure.getPsf() 

282 assert sources.getSchema() == self.schema 

283 self.deblend(exposure, sources, psf) 

284 

285 def _getPsfFwhm(self, psf, bbox): 

286 # It should be easier to get a PSF's fwhm; 

287 # https://dev.lsstcorp.org/trac/ticket/3030 

288 return psf.computeShape().getDeterminantRadius() * 2.35 

289 

290 @pipeBase.timeMethod 

291 def deblend(self, exposure, srcs, psf): 

292 """! 

293 Deblend. 

294 

295 @param[in] exposure Exposure to process 

296 @param[in,out] srcs SourceCatalog containing sources detected on this exposure. 

297 @param[in] psf PSF 

298 

299 @return 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 if len(childrenInRange) < self.config.ciNumParentsToDeblend: 

311 raise ValueError("Fewer than ciNumParentsToDeblend children were contained in the range " 

312 "indicated by ciDeblendChildRange. Adjust this range to include more " 

313 "parents.") 

314 # Keep all of the isolated parents and the first 

315 # `ciNumParentsToDeblend` children 

316 parents = nPeaks == 1 

317 children = np.zeros((len(srcs),), dtype=bool) 

318 children[childrenInRange[:self.config.ciNumParentsToDeblend]] = True 

319 srcs = srcs[parents | children] 

320 # We need to update the IdFactory, otherwise the the source ids 

321 # will not be sequential 

322 idFactory = srcs.getIdFactory() 

323 maxId = np.max(srcs["id"]) 

324 idFactory.notify(maxId) 

325 

326 self.log.info("Deblending %d sources" % len(srcs)) 

327 

328 from lsst.meas.deblender.baseline import deblend 

329 

330 # find the median stdev in the image... 

331 mi = exposure.getMaskedImage() 

332 statsCtrl = afwMath.StatisticsControl() 

333 statsCtrl.setAndMask(mi.getMask().getPlaneBitMask(self.config.maskPlanes)) 

334 stats = afwMath.makeStatistics(mi.getVariance(), mi.getMask(), afwMath.MEDIAN, statsCtrl) 

335 sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN)) 

336 self.log.trace('sigma1: %g', sigma1) 

337 

338 n0 = len(srcs) 

339 nparents = 0 

340 for i, src in enumerate(srcs): 

341 # t0 = time.clock() 

342 

343 fp = src.getFootprint() 

344 pks = fp.getPeaks() 

345 

346 # Since we use the first peak for the parent object, we should propagate its flags 

347 # to the parent source. 

348 src.assign(pks[0], self.peakSchemaMapper) 

349 

350 if len(pks) < 2: 

351 continue 

352 

353 if self.isLargeFootprint(fp): 

354 src.set(self.tooBigKey, True) 

355 self.skipParent(src, mi.getMask()) 

356 self.log.warn('Parent %i: skipping large footprint (area: %i)', 

357 int(src.getId()), int(fp.getArea())) 

358 continue 

359 if self.isMasked(fp, exposure.getMaskedImage().getMask()): 

360 src.set(self.maskedKey, True) 

361 self.skipParent(src, mi.getMask()) 

362 self.log.warn('Parent %i: skipping masked footprint (area: %i)', 

363 int(src.getId()), int(fp.getArea())) 

364 continue 

365 

366 nparents += 1 

367 bb = fp.getBBox() 

368 psf_fwhm = self._getPsfFwhm(psf, bb) 

369 

370 self.log.trace('Parent %i: deblending %i peaks', int(src.getId()), len(pks)) 

371 

372 self.preSingleDeblendHook(exposure, srcs, i, fp, psf, psf_fwhm, sigma1) 

373 npre = len(srcs) 

374 

375 # This should really be set in deblend, but deblend doesn't have access to the src 

376 src.set(self.tooManyPeaksKey, len(fp.getPeaks()) > self.config.maxNumberOfPeaks) 

377 

378 try: 

379 res = deblend( 

380 fp, mi, psf, psf_fwhm, sigma1=sigma1, 

381 psfChisqCut1=self.config.psfChisq1, 

382 psfChisqCut2=self.config.psfChisq2, 

383 psfChisqCut2b=self.config.psfChisq2b, 

384 maxNumberOfPeaks=self.config.maxNumberOfPeaks, 

385 strayFluxToPointSources=self.config.strayFluxToPointSources, 

386 assignStrayFlux=self.config.assignStrayFlux, 

387 strayFluxAssignment=self.config.strayFluxRule, 

388 rampFluxAtEdge=(self.config.edgeHandling == 'ramp'), 

389 patchEdges=(self.config.edgeHandling == 'noclip'), 

390 tinyFootprintSize=self.config.tinyFootprintSize, 

391 clipStrayFluxFraction=self.config.clipStrayFluxFraction, 

392 weightTemplates=self.config.weightTemplates, 

393 removeDegenerateTemplates=self.config.removeDegenerateTemplates, 

394 maxTempDotProd=self.config.maxTempDotProd, 

395 medianSmoothTemplate=self.config.medianSmoothTemplate 

396 ) 

397 if self.config.catchFailures: 

398 src.set(self.deblendFailedKey, False) 

399 except Exception as e: 

400 if self.config.catchFailures: 

401 self.log.warn("Unable to deblend source %d: %s" % (src.getId(), e)) 

402 src.set(self.deblendFailedKey, True) 

403 import traceback 

404 traceback.print_exc() 

405 continue 

406 else: 

407 raise 

408 

409 kids = [] 

410 nchild = 0 

411 for j, peak in enumerate(res.deblendedParents[0].peaks): 

412 heavy = peak.getFluxPortion() 

413 if heavy is None or peak.skip: 

414 src.set(self.deblendSkippedKey, True) 

415 if not self.config.propagateAllPeaks: 

416 # Don't care 

417 continue 

418 # We need to preserve the peak: make sure we have enough info to create a minimal 

419 # child src 

420 self.log.trace("Peak at (%i,%i) failed. Using minimal default info for child.", 

421 pks[j].getIx(), pks[j].getIy()) 

422 if heavy is None: 

423 # copy the full footprint and strip out extra peaks 

424 foot = afwDet.Footprint(src.getFootprint()) 

425 peakList = foot.getPeaks() 

426 peakList.clear() 

427 peakList.append(peak.peak) 

428 zeroMimg = afwImage.MaskedImageF(foot.getBBox()) 

429 heavy = afwDet.makeHeavyFootprint(foot, zeroMimg) 

430 if peak.deblendedAsPsf: 

431 if peak.psfFitFlux is None: 

432 peak.psfFitFlux = 0.0 

433 if peak.psfFitCenter is None: 

434 peak.psfFitCenter = (peak.peak.getIx(), peak.peak.getIy()) 

435 

436 assert(len(heavy.getPeaks()) == 1) 

437 

438 src.set(self.deblendSkippedKey, False) 

439 child = srcs.addNew() 

440 nchild += 1 

441 for key in self.toCopyFromParent: 

442 child.set(key, src.get(key)) 

443 child.assign(heavy.getPeaks()[0], self.peakSchemaMapper) 

444 child.setParent(src.getId()) 

445 child.setFootprint(heavy) 

446 child.set(self.psfKey, peak.deblendedAsPsf) 

447 child.set(self.hasStrayFluxKey, peak.strayFlux is not None) 

448 if peak.deblendedAsPsf: 

449 (cx, cy) = peak.psfFitCenter 

450 child.set(self.psfCenterKey, geom.Point2D(cx, cy)) 

451 child.set(self.psfFluxKey, peak.psfFitFlux) 

452 child.set(self.deblendRampedTemplateKey, peak.hasRampedTemplate) 

453 child.set(self.deblendPatchedTemplateKey, peak.patched) 

454 

455 # Set the position of the peak from the parent footprint 

456 # This will make it easier to match the same source across 

457 # deblenders and across observations, where the peak 

458 # position is unlikely to change unless enough time passes 

459 # for a source to move on the sky. 

460 child.set(self.peakCenter, geom.Point2I(pks[j].getIx(), pks[j].getIy())) 

461 child.set(self.peakIdKey, pks[j].getId()) 

462 

463 # The children have a single peak 

464 child.set(self.nPeaksKey, 1) 

465 # Set the number of peaks in the parent 

466 child.set(self.parentNPeaksKey, len(pks)) 

467 

468 kids.append(child) 

469 

470 # Child footprints may extend beyond the full extent of their parent's which 

471 # results in a failure of the replace-by-noise code to reinstate these pixels 

472 # to their original values. The following updates the parent footprint 

473 # in-place to ensure it contains the full union of itself and all of its 

474 # children's footprints. 

475 spans = src.getFootprint().spans 

476 for child in kids: 

477 spans = spans.union(child.getFootprint().spans) 

478 src.getFootprint().setSpans(spans) 

479 

480 src.set(self.nChildKey, nchild) 

481 

482 self.postSingleDeblendHook(exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res) 

483 # print('Deblending parent id', src.getId(), 'took', time.clock() - t0) 

484 

485 n1 = len(srcs) 

486 self.log.info('Deblended: of %i sources, %i were deblended, creating %i children, total %i sources' 

487 % (n0, nparents, n1-n0, n1)) 

488 

489 def preSingleDeblendHook(self, exposure, srcs, i, fp, psf, psf_fwhm, sigma1): 

490 pass 

491 

492 def postSingleDeblendHook(self, exposure, srcs, i, npre, kids, fp, psf, psf_fwhm, sigma1, res): 

493 pass 

494 

495 def isLargeFootprint(self, footprint): 

496 """Returns whether a Footprint is large 

497 

498 'Large' is defined by thresholds on the area, size and axis ratio. 

499 These may be disabled independently by configuring them to be non-positive. 

500 

501 This is principally intended to get rid of satellite streaks, which the 

502 deblender or other downstream processing can have trouble dealing with 

503 (e.g., multiple large HeavyFootprints can chew up memory). 

504 """ 

505 if self.config.maxFootprintArea > 0 and footprint.getArea() > self.config.maxFootprintArea: 

506 return True 

507 if self.config.maxFootprintSize > 0: 

508 bbox = footprint.getBBox() 

509 if max(bbox.getWidth(), bbox.getHeight()) > self.config.maxFootprintSize: 

510 return True 

511 if self.config.minFootprintAxisRatio > 0: 

512 axes = afwEll.Axes(footprint.getShape()) 

513 if axes.getB() < self.config.minFootprintAxisRatio*axes.getA(): 

514 return True 

515 return False 

516 

517 def isMasked(self, footprint, mask): 

518 """Returns whether the footprint violates the mask limits""" 

519 size = float(footprint.getArea()) 

520 for maskName, limit in self.config.maskLimits.items(): 

521 maskVal = mask.getPlaneBitMask(maskName) 

522 unmaskedSpan = footprint.spans.intersectNot(mask, maskVal) # spanset of unmasked pixels 

523 if (size - unmaskedSpan.getArea())/size > limit: 

524 return True 

525 return False 

526 

527 def skipParent(self, source, mask): 

528 """Indicate that the parent source is not being deblended 

529 

530 We set the appropriate flags and mask. 

531 

532 @param source The source to flag as skipped 

533 @param mask The mask to update 

534 """ 

535 fp = source.getFootprint() 

536 source.set(self.deblendSkippedKey, True) 

537 if self.config.notDeblendedMask: 

538 mask.addMaskPlane(self.config.notDeblendedMask) 

539 fp.spans.setMask(mask, mask.getPlaneBitMask(self.config.notDeblendedMask)) 

540 

541 # Set the center of the parent 

542 bbox = fp.getBBox() 

543 centerX = int(bbox.getMinX()+bbox.getWidth()/2) 

544 centerY = int(bbox.getMinY()+bbox.getHeight()/2) 

545 source.set(self.peakCenter, geom.Point2I(centerX, centerY)) 

546 # There are no deblended children, so nChild = 0 

547 source.set(self.nChildKey, 0) 

548 # But we also want to know how many peaks that we would have 

549 # deblended if the parent wasn't skipped. 

550 source.set(self.nPeaksKey, len(fp.peaks)) 

551 # Top level parents are not a detected peak, so they have no peakId 

552 source.set(self.peakIdKey, 0) 

553 # Top level parents also have no parentNPeaks 

554 source.set(self.parentNPeaksKey, 0)