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 

22__all__ = ['SourceDeblendConfig', 'SourceDeblendTask'] 

23 

24import math 

25import numpy as np 

26 

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 

37 

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

39 

40 

41class SourceDeblendConfig(pexConfig.Config): 

42 

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 ) 

52 

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 ) 

63 

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

67 

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 ) 

80 

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

107 

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

112 

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

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

285 

286 @timeMethod 

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

288 """Deblend. 

289 

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 

298 

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) 

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

367 

368 nparents += 1 

369 bb = fp.getBBox() 

370 psf_fwhm = self._getPsfFwhm(psf, bb) 

371 

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

373 

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

375 npre = len(srcs) 

376 

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) 

379 

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 

410 

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

437 

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

439 

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) 

456 

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

464 

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

469 

470 kids.append(child) 

471 

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) 

481 

482 src.set(self.nChildKey, nchild) 

483 

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) 

486 

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

490 

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

492 pass 

493 

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

495 pass 

496 

497 def isLargeFootprint(self, footprint): 

498 """Returns whether a Footprint is large 

499 

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. 

502 

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 

518 

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 

529 

530 def skipParent(self, source, mask): 

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

532 

533 We set the appropriate flags and mask. 

534 

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

547 

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)