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

237 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-17 02:24 -0800

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=1000000, 

94 doc=("Maximum area for footprints before they are ignored as large; " 

95 "non-positive means no threshold applied")) 

96 maxFootprintSize = pexConfig.Field(dtype=int, default=0, 

97 doc=("Maximum linear dimension for footprints before they are ignored " 

98 "as large; non-positive means no threshold applied")) 

99 minFootprintAxisRatio = pexConfig.Field(dtype=float, default=0.0, 

100 doc=("Minimum axis ratio for footprints before they are ignored " 

101 "as large; non-positive means no threshold applied")) 

102 notDeblendedMask = pexConfig.Field(dtype=str, default="NOT_DEBLENDED", optional=True, 

103 doc="Mask name for footprints not deblended, or None") 

104 

105 tinyFootprintSize = pexConfig.RangeField(dtype=int, default=2, min=2, inclusiveMin=True, 

106 doc=('Footprints smaller in width or height than this value ' 

107 'will be ignored; minimum of 2 due to PSF gradient ' 

108 'calculation.')) 

109 

110 propagateAllPeaks = pexConfig.Field(dtype=bool, default=False, 

111 doc=('Guarantee that all peaks produce a child source.')) 

112 catchFailures = pexConfig.Field( 

113 dtype=bool, default=False, 

114 doc=("If True, catch exceptions thrown by the deblender, log them, " 

115 "and set a flag on the parent, instead of letting them propagate up")) 

116 maskPlanes = pexConfig.ListField(dtype=str, default=["SAT", "INTRP", "NO_DATA"], 

117 doc="Mask planes to ignore when performing statistics") 

118 maskLimits = pexConfig.DictField( 

119 keytype=str, 

120 itemtype=float, 

121 default={}, 

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

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

124 ) 

125 weightTemplates = pexConfig.Field( 

126 dtype=bool, default=False, 

127 doc=("If true, a least-squares fit of the templates will be done to the " 

128 "full image. The templates will be re-weighted based on this fit.")) 

129 removeDegenerateTemplates = pexConfig.Field(dtype=bool, default=False, 

130 doc=("Try to remove similar templates?")) 

131 maxTempDotProd = pexConfig.Field( 

132 dtype=float, default=0.5, 

133 doc=("If the dot product between two templates is larger than this value, we consider them to be " 

134 "describing the same object (i.e. they are degenerate). If one of the objects has been " 

135 "labeled as a PSF it will be removed, otherwise the template with the lowest value will " 

136 "be removed.")) 

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

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

139 

140 # Testing options 

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

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

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

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

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

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

147 useCiLimits = pexConfig.Field( 

148 dtype=bool, default=False, 

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

150 ciDeblendChildRange = pexConfig.ListField( 

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

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

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

154 ciNumParentsToDeblend = pexConfig.Field( 

155 dtype=int, default=10, 

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

157 "within `ciDebledChildRange`. " 

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

159 

160 

161class SourceDeblendTask(pipeBase.Task): 

162 """Split blended sources into individual sources. 

163 

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

165 """ 

166 ConfigClass = SourceDeblendConfig 

167 _DefaultName = "sourceDeblend" 

168 

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

170 """Create the task, adding necessary fields to the given schema. 

171 

172 Parameters 

173 ---------- 

174 schema : `lsst.afw.table.Schema` 

175 Schema object for measurement fields; will be modified in-place. 

176 peakSchema : `lsst.afw.table.peakSchema` 

177 Schema of Footprint Peaks that will be passed to the deblender. 

178 Any fields beyond the PeakTable minimal schema will be transferred 

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

180 from the Peaks 

181 **kwargs 

182 Additional keyword arguments passed to ~lsst.pipe.base.task 

183 """ 

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

185 self.schema = schema 

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

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

188 peakMinimalSchema = afwDet.PeakTable.makeMinimalSchema() 

189 if peakSchema is None: 

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

191 # to simplify downstream code 

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

193 else: 

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

195 for item in peakSchema: 

196 if item.key not in peakMinimalSchema: 

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

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

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

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

201 # by adding the same fields to both. 

202 schema.addField(item.field) 

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

204 self.addSchemaKeys(schema) 

205 

206 def addSchemaKeys(self, schema): 

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

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

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

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

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

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

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

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

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

216 doc='Source had too many peaks; ' 

217 'only the brightest were included') 

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

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

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

221 doc='Parent footprint was predominantly masked') 

222 

223 if self.config.catchFailures: 

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

225 doc="Deblending failed on source") 

226 

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

228 doc="Deblender skipped this source") 

229 

230 self.deblendRampedTemplateKey = schema.addField( 

231 'deblend_rampedTemplate', type='Flag', 

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

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

234 

235 self.deblendPatchedTemplateKey = schema.addField( 

236 'deblend_patchedTemplate', type='Flag', 

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

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

239 

240 self.hasStrayFluxKey = schema.addField( 

241 'deblend_hasStrayFlux', type='Flag', 

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

243 

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

245 self.nChildKey, self.psfKey, self.psfCenterKey, self.psfFluxKey, 

246 self.tooManyPeaksKey, self.tooBigKey))) 

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

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

249 unit="pixel") 

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

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

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

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

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

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

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

257 "This includes peaks that may have been culled " 

258 "during deblending or failed to deblend") 

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

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

261 "in the parent footprint") 

262 

263 @timeMethod 

264 def run(self, exposure, sources): 

265 """Get the PSF from the provided exposure and then run deblend. 

266 

267 Parameters 

268 ---------- 

269 exposure : `lsst.afw.image.Exposure` 

270 Exposure to be processed 

271 sources : `lsst.afw.table.SourceCatalog` 

272 SourceCatalog containing sources detected on this exposure. 

273 """ 

274 psf = exposure.getPsf() 

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

276 self.deblend(exposure, sources, psf) 

277 

278 def _getPsfFwhm(self, psf, position): 

279 return psf.computeShape(position).getDeterminantRadius() * 2.35 

280 

281 @timeMethod 

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

283 """Deblend. 

284 

285 Parameters 

286 ---------- 

287 exposure : `lsst.afw.image.Exposure` 

288 Exposure to be processed 

289 srcs : `lsst.afw.table.SourceCatalog` 

290 SourceCatalog containing sources detected on this exposure 

291 psf : `lsst.afw.detection.Psf` 

292 Point source function 

293 

294 Returns 

295 ------- 

296 None 

297 """ 

298 # Cull footprints if required by ci 

299 if self.config.useCiLimits: 

300 self.log.info(f"Using CI catalog limits, " 

301 f"the original number of sources to deblend was {len(srcs)}.") 

302 # Select parents with a number of children in the range 

303 # config.ciDeblendChildRange 

304 minChildren, maxChildren = self.config.ciDeblendChildRange 

305 nPeaks = np.array([len(src.getFootprint().peaks) for src in srcs]) 

306 childrenInRange = np.where((nPeaks >= minChildren) & (nPeaks <= maxChildren))[0] 

307 if len(childrenInRange) < self.config.ciNumParentsToDeblend: 

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

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

310 "parents.") 

311 # Keep all of the isolated parents and the first 

312 # `ciNumParentsToDeblend` children 

313 parents = nPeaks == 1 

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

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

316 srcs = srcs[parents | children] 

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

318 # will not be sequential 

319 idFactory = srcs.getIdFactory() 

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

321 idFactory.notify(maxId) 

322 

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

324 

325 from lsst.meas.deblender.baseline import deblend 

326 

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

328 mi = exposure.getMaskedImage() 

329 statsCtrl = afwMath.StatisticsControl() 

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

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

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

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

334 

335 n0 = len(srcs) 

336 nparents = 0 

337 for i, src in enumerate(srcs): 

338 # t0 = time.clock() 

339 

340 fp = src.getFootprint() 

341 pks = fp.getPeaks() 

342 

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

344 # to the parent source. 

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

346 

347 if len(pks) < 2: 

348 continue 

349 

350 if self.isLargeFootprint(fp): 

351 src.set(self.tooBigKey, True) 

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

353 self.log.warning('Parent %i: skipping large footprint (area: %i)', 

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

355 continue 

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

357 src.set(self.maskedKey, True) 

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

359 self.log.warning('Parent %i: skipping masked footprint (area: %i)', 

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

361 continue 

362 

363 nparents += 1 

364 center = fp.getCentroid() 

365 psf_fwhm = self._getPsfFwhm(psf, center) 

366 

367 if not (psf_fwhm > 0): 

368 if self.config.catchFailures: 

369 self.log.warning("Unable to deblend source %d: because PSF FWHM=%f is invalid.", 

370 src.getId(), psf_fwhm) 

371 src.set(self.deblendFailedKey, True) 

372 continue 

373 else: 

374 raise ValueError(f"PSF at {center} has an invalid FWHM value of {psf_fwhm}") 

375 

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

377 

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

379 npre = len(srcs) 

380 

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

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

383 

384 try: 

385 res = deblend( 

386 fp, mi, psf, psf_fwhm, sigma1=sigma1, 

387 psfChisqCut1=self.config.psfChisq1, 

388 psfChisqCut2=self.config.psfChisq2, 

389 psfChisqCut2b=self.config.psfChisq2b, 

390 maxNumberOfPeaks=self.config.maxNumberOfPeaks, 

391 strayFluxToPointSources=self.config.strayFluxToPointSources, 

392 assignStrayFlux=self.config.assignStrayFlux, 

393 strayFluxAssignment=self.config.strayFluxRule, 

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

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

396 tinyFootprintSize=self.config.tinyFootprintSize, 

397 clipStrayFluxFraction=self.config.clipStrayFluxFraction, 

398 weightTemplates=self.config.weightTemplates, 

399 removeDegenerateTemplates=self.config.removeDegenerateTemplates, 

400 maxTempDotProd=self.config.maxTempDotProd, 

401 medianSmoothTemplate=self.config.medianSmoothTemplate 

402 ) 

403 if self.config.catchFailures: 

404 src.set(self.deblendFailedKey, False) 

405 except Exception as e: 

406 if self.config.catchFailures: 

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

408 src.set(self.deblendFailedKey, True) 

409 import traceback 

410 traceback.print_exc() 

411 continue 

412 else: 

413 raise 

414 

415 kids = [] 

416 nchild = 0 

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

418 heavy = peak.getFluxPortion() 

419 if heavy is None or peak.skip: 

420 src.set(self.deblendSkippedKey, True) 

421 if not self.config.propagateAllPeaks: 

422 # Don't care 

423 continue 

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

425 # child src 

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

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

428 if heavy is None: 

429 # copy the full footprint and strip out extra peaks 

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

431 peakList = foot.getPeaks() 

432 peakList.clear() 

433 peakList.append(peak.peak) 

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

435 heavy = afwDet.makeHeavyFootprint(foot, zeroMimg) 

436 if peak.deblendedAsPsf: 

437 if peak.psfFitFlux is None: 

438 peak.psfFitFlux = 0.0 

439 if peak.psfFitCenter is None: 

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

441 

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

443 

444 src.set(self.deblendSkippedKey, False) 

445 child = srcs.addNew() 

446 nchild += 1 

447 for key in self.toCopyFromParent: 

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

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

450 child.setParent(src.getId()) 

451 child.setFootprint(heavy) 

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

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

454 if peak.deblendedAsPsf: 

455 (cx, cy) = peak.psfFitCenter 

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

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

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

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

460 

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

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

463 # deblenders and across observations, where the peak 

464 # position is unlikely to change unless enough time passes 

465 # for a source to move on the sky. 

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

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

468 

469 # The children have a single peak 

470 child.set(self.nPeaksKey, 1) 

471 # Set the number of peaks in the parent 

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

473 

474 kids.append(child) 

475 

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

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

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

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

480 # children's footprints. 

481 spans = src.getFootprint().spans 

482 for child in kids: 

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

484 src.getFootprint().setSpans(spans) 

485 

486 src.set(self.nChildKey, nchild) 

487 

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

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

490 

491 n1 = len(srcs) 

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

493 n0, nparents, n1-n0, n1) 

494 

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

496 pass 

497 

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

499 pass 

500 

501 def isLargeFootprint(self, footprint): 

502 """Returns whether a Footprint is large 

503 

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

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

506 

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

508 deblender or other downstream processing can have trouble dealing with 

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

510 """ 

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

512 return True 

513 if self.config.maxFootprintSize > 0: 

514 bbox = footprint.getBBox() 

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

516 return True 

517 if self.config.minFootprintAxisRatio > 0: 

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

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

520 return True 

521 return False 

522 

523 def isMasked(self, footprint, mask): 

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

525 """ 

526 size = float(footprint.getArea()) 

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

528 maskVal = mask.getPlaneBitMask(maskName) 

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

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

531 return True 

532 return False 

533 

534 def skipParent(self, source, mask): 

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

536 

537 We set the appropriate flags and mask. 

538 

539 Parameters 

540 ---------- 

541 source : `lsst.afw.table.SourceRecord` 

542 The source to flag as skipped 

543 mask : `lsst.afw.image.Mask` 

544 The mask to update 

545 """ 

546 fp = source.getFootprint() 

547 source.set(self.deblendSkippedKey, True) 

548 if self.config.notDeblendedMask: 

549 mask.addMaskPlane(self.config.notDeblendedMask) 

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

551 

552 # Set the center of the parent 

553 bbox = fp.getBBox() 

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

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

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

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

558 source.set(self.nChildKey, 0) 

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

560 # deblended if the parent wasn't skipped. 

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

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

563 source.set(self.peakIdKey, 0) 

564 # Top level parents also have no parentNPeaks 

565 source.set(self.parentNPeaksKey, 0)