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

259 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-11 01:48 -0700

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__ = ["DEFAULT_PLUGINS", "DeblenderResult", "DeblendedParent", "MultiColorPeak", 

23 "DeblendedPeak", "deblend", "newDeblend", "CachingPsf"] 

24 

25from collections import OrderedDict 

26import numpy as np 

27 

28import lsst.pex.exceptions 

29import lsst.afw.image as afwImage 

30import lsst.afw.detection as afwDet 

31import lsst.geom as geom 

32import lsst.afw.math as afwMath 

33 

34from . import plugins 

35 

36DEFAULT_PLUGINS = [ 

37 plugins.DeblenderPlugin(plugins.fitPsfs), 

38 plugins.DeblenderPlugin(plugins.buildSymmetricTemplates), 

39 plugins.DeblenderPlugin(plugins.medianSmoothTemplates), 

40 plugins.DeblenderPlugin(plugins.makeTemplatesMonotonic), 

41 plugins.DeblenderPlugin(plugins.clipFootprintsToNonzero), 

42 plugins.DeblenderPlugin(plugins.reconstructTemplates, onReset=5), 

43 plugins.DeblenderPlugin(plugins.apportionFlux), 

44] 

45 

46 

47class DeblenderResult: 

48 r"""Collection of objects in multiple bands for a single parent footprint. 

49 

50 Parameters 

51 ---------- 

52 footprint: `afw.detection.Footprint` 

53 Parent footprint to deblend. While `maskedImages`, `psfs`, 

54 and `psffwhms` are lists of objects, one for each band, 

55 `footprint` is a single parent footprint (from a `mergedDet`) 

56 this is used for all bands. 

57 mMaskedImage: `MaskedImage`\ s or `MultibandMaskedImage` 

58 Masked image containing the ``footprint`` in each band. 

59 psfs: list of `afw.detection.Psf`s 

60 Psf of the ``maskedImage`` for each band. 

61 psffwhm: list of `float`s 

62 FWHM of the ``maskedImage``'s ``psf`` in each band. 

63 maxNumberOfPeaks: `int`, optional 

64 If positive, the maximum number of peaks to deblend. 

65 If the total number of peaks is greater than ``maxNumberOfPeaks``, 

66 then only the first ``maxNumberOfPeaks`` sources are deblended. 

67 The default is 0, which deblends all of the peaks. 

68 avgNoise: `float`or list of `float`s, optional 

69 Average noise level in each ``maskedImage``. 

70 The default is ``None``, which estimates the noise from the median value of the 

71 variance plane of ``maskedImage`` for each filter. 

72 """ 

73 

74 def __init__(self, footprint, mMaskedImage, psfs, psffwhms, log, 

75 maxNumberOfPeaks=0, avgNoise=None): 

76 # Check if this is collection of footprints in multiple bands or a single footprint 

77 if not isinstance(mMaskedImage, afwImage.MultibandMaskedImage): 

78 mMaskedImage = [mMaskedImage] 

79 self.filters = [0] 

80 else: 

81 self.filters = mMaskedImage.filters 

82 try: 

83 len(psfs) 

84 except TypeError: 

85 psfs = [psfs] 

86 try: 

87 len(psffwhms) 

88 except TypeError: 

89 psffwhms = [psffwhms] 

90 try: 

91 len(avgNoise) 

92 except TypeError: 

93 if avgNoise is None: 

94 avgNoise = [None]*len(psfs) 

95 else: 

96 avgNoise = [avgNoise] 

97 # Now check that all of the parameters have the same number of entries 

98 if any([len(self.filters) != len(p) for p in [psfs, psffwhms, avgNoise]]): 

99 raise ValueError("To use the multi-color deblender, " 

100 "'maskedImage', 'psf', 'psffwhm', 'avgNoise'" 

101 "must have the same length, but instead have lengths: " 

102 "{0}".format([len(p) for p in [mMaskedImage, 

103 psfs, 

104 psffwhms, 

105 avgNoise]])) 

106 

107 self.log = log 

108 self.mMaskedImage = mMaskedImage 

109 self.footprint = footprint 

110 self.psfs = psfs 

111 

112 self.peakCount = len(footprint.getPeaks()) 

113 if maxNumberOfPeaks > 0 and maxNumberOfPeaks < self.peakCount: 

114 self.peakCount = maxNumberOfPeaks 

115 

116 # Create a DeblendedParent for the Footprint in every filter 

117 self.deblendedParents = OrderedDict([]) 

118 for n, f in enumerate(self.filters): 

119 f = self.filters[n] 

120 dp = DeblendedParent(f, footprint, mMaskedImage[f], psfs[n], 

121 psffwhms[n], avgNoise[n], maxNumberOfPeaks, self) 

122 self.deblendedParents[self.filters[n]] = dp 

123 

124 # Group the peaks in each color 

125 self.peaks = [] 

126 for idx in range(self.peakCount): 

127 peakDict = OrderedDict([(f, dp.peaks[idx]) for f, dp in self.deblendedParents.items()]) 

128 multiPeak = MultiColorPeak(peakDict, idx, self) 

129 self.peaks.append(multiPeak) 

130 

131 # Result from multiband debender (if used) 

132 self.blend = None 

133 self.failed = False 

134 

135 def getParentProperty(self, propertyName): 

136 """Get the footprint in each filter 

137 """ 

138 return [getattr(dp, propertyName) for dp in self.deblendedParents] 

139 

140 def setTemplateSums(self, templateSums, fidx=None): 

141 if fidx is not None: 

142 self.templateSums[fidx] = templateSums 

143 else: 

144 for f, templateSum in templateSums.items(): 

145 self.deblendedParents[f].templateSum = templateSum 

146 

147 

148class DeblendedParent: 

149 """Deblender result of a single parent footprint, in a single band 

150 

151 Convenience class to store useful objects used by the deblender for a single band, 

152 such as the maskedImage, psf, etc as well as the results from the deblender. 

153 

154 Parameters 

155 ---------- 

156 filterName: `str` 

157 Name of the filter used for `maskedImage` 

158 footprint: list of `afw.detection.Footprint`s 

159 Parent footprint to deblend in each band. 

160 maskedImages: list of `afw.image.MaskedImageF`s 

161 Masked image containing the ``footprint`` in each band. 

162 psf: list of `afw.detection.Psf`s 

163 Psf of the ``maskedImage`` for each band. 

164 psffwhm: list of `float`s 

165 FWHM of the ``maskedImage``'s ``psf`` in each band. 

166 avgNoise: `float`or list of `float`s, optional 

167 Average noise level in each ``maskedImage``. 

168 The default is ``None``, which estimates the noise from the median value of the 

169 variance plane of ``maskedImage`` for each filter. 

170 maxNumberOfPeaks: `int`, optional 

171 If positive, the maximum number of peaks to deblend. 

172 If the total number of peaks is greater than ``maxNumberOfPeaks``, 

173 then only the first ``maxNumberOfPeaks`` sources are deblended. 

174 The default is 0, which deblends all of the peaks. 

175 debResult: `DeblenderResult` 

176 The ``DeblenderResult`` that contains this ``DeblendedParent``. 

177 """ 

178 def __init__(self, filterName, footprint, maskedImage, psf, psffwhm, avgNoise, 

179 maxNumberOfPeaks, debResult): 

180 self.filter = filterName 

181 self.fp = footprint 

182 self.maskedImage = maskedImage 

183 self.psf = psf 

184 self.psffwhm = psffwhm 

185 self.img = maskedImage.getImage() 

186 self.imbb = self.img.getBBox() 

187 self.varimg = maskedImage.getVariance() 

188 self.mask = maskedImage.getMask() 

189 self.avgNoise = avgNoise 

190 self.updateFootprintBbox() 

191 self.debResult = debResult 

192 self.peakCount = debResult.peakCount 

193 self.templateSum = None 

194 

195 # avgNoise is an estiamte of the average noise level for the image in this filter 

196 if avgNoise is None: 

197 stats = afwMath.makeStatistics(self.varimg, self.mask, afwMath.MEDIAN) 

198 avgNoise = np.sqrt(stats.getValue(afwMath.MEDIAN)) 

199 debResult.log.trace('Estimated avgNoise for filter %s = %f', self.filter, avgNoise) 

200 self.avgNoise = avgNoise 

201 

202 # Store all of the peak information in a single object 

203 self.peaks = [] 

204 peaks = self.fp.getPeaks() 

205 for idx in range(self.peakCount): 

206 deblendedPeak = DeblendedPeak(peaks[idx], idx, self) 

207 self.peaks.append(deblendedPeak) 

208 

209 def updateFootprintBbox(self): 

210 """Update the bounding box of the parent footprint 

211 

212 If the parent Footprint is resized it will be necessary to update the bounding box of the footprint. 

213 """ 

214 # Pull out the image bounds of the parent Footprint 

215 self.bb = self.fp.getBBox() 

216 if not self.imbb.contains(self.bb): 

217 raise ValueError(('Footprint bounding-box %s extends outside image bounding-box %s') % 

218 (str(self.bb), str(self.imbb))) 

219 self.W, self.H = self.bb.getWidth(), self.bb.getHeight() 

220 self.x0, self.y0 = self.bb.getMinX(), self.bb.getMinY() 

221 self.x1, self.y1 = self.bb.getMaxX(), self.bb.getMaxY() 

222 

223 

224class MultiColorPeak: 

225 """Collection of single peak deblender results in multiple bands. 

226 

227 There is one of these objects for each Peak in the footprint. 

228 

229 Parameters 

230 ---------- 

231 peaks: `dict` of `afw.detection.PeakRecord` 

232 Each entry in ``peaks`` is a peak record for the same peak in a different band 

233 pki: int 

234 Index of the peak in `parent.peaks` 

235 parent: `DeblendedParent` 

236 DeblendedParent object that contains the peak as a child. 

237 """ 

238 def __init__(self, peaks, pki, parent): 

239 self.filters = list(peaks.keys()) 

240 self.deblendedPeaks = peaks 

241 self.parent = parent 

242 for filter, peak in self.deblendedPeaks.items(): 

243 peak.multiColorPeak = self 

244 

245 # Fields common to the peak in all bands that will be set by the deblender 

246 # In the future this is likely to contain information about the probability of the peak 

247 # being a point source, color-information about templateFootprints, etc. 

248 self.pki = pki 

249 self.skip = False 

250 self.deblendedAsPsf = False 

251 self.x = self.deblendedPeaks[self.filters[0]].peak.getFx() 

252 self.y = self.deblendedPeaks[self.filters[0]].peak.getFy() 

253 

254 

255class DeblendedPeak: 

256 """Result of deblending a single Peak within a parent Footprint. 

257 

258 There is one of these objects for each Peak in the Footprint. 

259 

260 Parameters 

261 ---------- 

262 peak: `afw.detection.PeakRecord` 

263 Peak object in a single band from a peak record 

264 pki: `int` 

265 Index of the peak in `multiColorPeak.parent.peaks` 

266 parent: `DeblendedParent` 

267 Parent in the same filter that contains the peak 

268 multiColorPeak: `MultiColorPeak` 

269 Object containing the same peak in multiple bands 

270 """ 

271 def __init__(self, peak, pki, parent, multiColorPeak=None): 

272 # Peak object 

273 self.peak = peak 

274 # int, peak index number 

275 self.pki = pki 

276 self.parent = parent 

277 self.multiColorPeak = multiColorPeak 

278 # union of all the ways of failing... 

279 self.skip = False 

280 

281 self.outOfBounds = False 

282 self.tinyFootprint = False 

283 self.noValidPixels = False 

284 self.deblendedAsPsf = False 

285 self.degenerate = False 

286 

287 # Field set during _fitPsf: 

288 self.psfFitFailed = False 

289 self.psfFitBadDof = False 

290 # (chisq, dof) for PSF fit without decenter 

291 self.psfFit1 = None 

292 # (chisq, dof) for PSF fit with decenter 

293 self.psfFit2 = None 

294 # (chisq, dof) for PSF fit after applying decenter 

295 self.psfFit3 = None 

296 # decentered PSF fit wanted to move the center too much 

297 self.psfFitBigDecenter = False 

298 # was the fit with decenter better? 

299 self.psfFitWithDecenter = False 

300 # 

301 self.psfFitR0 = None 

302 self.psfFitR1 = None 

303 self.psfFitStampExtent = None 

304 self.psfFitCenter = None 

305 self.psfFitBest = None 

306 self.psfFitParams = None 

307 self.psfFitFlux = None 

308 self.psfFitNOthers = None 

309 

310 # Things only set in _fitPsf when debugging is turned on: 

311 self.psfFitDebugPsf0Img = None 

312 self.psfFitDebugPsfImg = None 

313 self.psfFitDebugPsfDerivImg = None 

314 self.psfFitDebugPsfModel = None 

315 

316 self.failedSymmetricTemplate = False 

317 

318 # The actual template Image and Footprint 

319 self.templateImage = None 

320 self.templateFootprint = None 

321 

322 # The flux assigned to this template -- a MaskedImage 

323 self.fluxPortion = None 

324 

325 # The stray flux assigned to this template (may be None), a HeavyFootprint 

326 self.strayFlux = None 

327 

328 self.hasRampedTemplate = False 

329 

330 self.patched = False 

331 

332 # debug -- a copy of the original symmetric template 

333 self.origTemplate = None 

334 self.origFootprint = None 

335 # MaskedImage 

336 self.rampedTemplate = None 

337 # MaskedImage 

338 self.medianFilteredTemplate = None 

339 

340 # when least-squares fitting templates, the template weight. 

341 self.templateWeight = 1.0 

342 

343 def __str__(self): 

344 return (('deblend result: outOfBounds: %s, deblendedAsPsf: %s') % 

345 (self.outOfBounds, self.deblendedAsPsf)) 

346 

347 @property 

348 def psfFitChisq(self): 

349 chisq, dof = self.psfFitBest 

350 return chisq 

351 

352 @property 

353 def psfFitDof(self): 

354 chisq, dof = self.psfFitBest 

355 return dof 

356 

357 def getFluxPortion(self, strayFlux=True): 

358 """ 

359 Return a HeavyFootprint containing the flux apportioned to this peak. 

360 

361 @param[in] strayFlux include stray flux also? 

362 """ 

363 if self.templateFootprint is None or self.fluxPortion is None: 

364 return None 

365 heavy = afwDet.makeHeavyFootprint(self.templateFootprint, self.fluxPortion) 

366 if strayFlux: 

367 if self.strayFlux is not None: 

368 heavy = afwDet.mergeHeavyFootprints(heavy, self.strayFlux) 

369 

370 return heavy 

371 

372 def setStrayFlux(self, stray): 

373 self.strayFlux = stray 

374 

375 def setFluxPortion(self, mimg): 

376 self.fluxPortion = mimg 

377 

378 def setTemplateWeight(self, w): 

379 self.templateWeight = w 

380 

381 def setPatched(self): 

382 self.patched = True 

383 

384 # DEBUG 

385 def setOrigTemplate(self, t, tfoot): 

386 self.origTemplate = t.Factory(t, True) 

387 self.origFootprint = tfoot 

388 

389 def setRampedTemplate(self, t, tfoot): 

390 self.hasRampedTemplate = True 

391 self.rampedTemplate = t.Factory(t, True) 

392 

393 def setMedianFilteredTemplate(self, t, tfoot): 

394 self.medianFilteredTemplate = t.Factory(t, True) 

395 

396 def setPsfTemplate(self, tim, tfoot): 

397 self.psfFootprint = afwDet.Footprint(tfoot) 

398 self.psfTemplate = tim.Factory(tim, True) 

399 

400 def setOutOfBounds(self): 

401 self.outOfBounds = True 

402 self.skip = True 

403 

404 def setTinyFootprint(self): 

405 self.tinyFootprint = True 

406 self.skip = True 

407 

408 def setNoValidPixels(self): 

409 self.noValidPixels = True 

410 self.skip = True 

411 

412 def setPsfFitFailed(self): 

413 self.psfFitFailed = True 

414 

415 def setBadPsfDof(self): 

416 self.psfFitBadDof = True 

417 

418 def setDeblendedAsPsf(self): 

419 self.deblendedAsPsf = True 

420 

421 def setFailedSymmetricTemplate(self): 

422 self.failedSymmetricTemplate = True 

423 self.skip = True 

424 

425 def setTemplate(self, image, footprint): 

426 self.templateImage = image 

427 self.templateFootprint = footprint 

428 

429 

430def deblend(footprint, maskedImage, psf, psffwhm, 

431 psfChisqCut1=1.5, psfChisqCut2=1.5, psfChisqCut2b=1.5, fitPsfs=True, 

432 medianSmoothTemplate=True, medianFilterHalfsize=2, 

433 monotonicTemplate=True, weightTemplates=False, 

434 log=None, verbose=False, sigma1=None, maxNumberOfPeaks=0, 

435 assignStrayFlux=True, strayFluxToPointSources='necessary', strayFluxAssignment='r-to-peak', 

436 rampFluxAtEdge=False, patchEdges=False, tinyFootprintSize=2, 

437 getTemplateSum=False, clipStrayFluxFraction=0.001, clipFootprintToNonzero=True, 

438 removeDegenerateTemplates=False, maxTempDotProd=0.5 

439 ): 

440 r"""Deblend a parent ``Footprint`` in a ``MaskedImageF``. 

441 

442 Deblending assumes that ``footprint`` has multiple peaks, as it will still create a 

443 `PerFootprint` object with a list of peaks even if there is only one peak in the list. 

444 It is recommended to first check that ``footprint`` has more than one peak, similar to the 

445 execution of `lsst.meas.deblender.deblend.SourceDeblendTask`. 

446 

447 .. note:: 

448 This is the API for the old deblender, however the function builds the plugins necessary 

449 to use the new deblender to perform identically to the old deblender. 

450 To test out newer functionality use ``newDeblend`` instead. 

451 

452 Deblending involves several mandatory and optional steps: 

453 

454 # Optional: If ``fitPsfs`` is True, find all peaks that are well-fit by a PSF + background model 

455 

456 * Peaks that pass the cuts have their footprints modified to the PSF + background model 

457 and their ``deblendedAsPsf`` property set to ``True``. 

458 * Relevant parameters: ``psfChisqCut1``, ``psfChisqCut2``, ``psfChisqCut2b``, 

459 ``tinyFootprintSize``. 

460 * See the parameter descriptions for more. 

461 

462 # Build a symmetric template for each peak not well-fit by the PSF model 

463 

464 * Given ``maskedImageF``, ``footprint``, and a ``DeblendedPeak``, creates a symmetric 

465 template (``templateImage`` and ``templateFootprint``) around the peak 

466 for all peaks not flagged as ``skip`` or ``deblendedAsPsf``. 

467 * If ``patchEdges=True`` and if ``footprint`` touches pixels with the 

468 ``EDGE`` bit set, then ``footprint`` is grown to include spans whose 

469 symmetric mirror is outside of the image. 

470 * Relevant parameters: ``sigma1`` and ``patchEdges``. 

471 

472 # Optional: If ``rampFluxAtEdge`` is True, adjust flux on the edges of the template footprints 

473 

474 * Using the PSF, a peak ``Footprint`` with pixels on the edge of of ``footprint`` 

475 is grown by the psffwhm*1.5 and filled in with zeros. 

476 * The result is a new symmetric footprint template for the peaks near the edge. 

477 * Relevant parameter: ``patchEdges``. 

478 

479 # Optionally (``medianSmoothTemplate=True``) filter the template images 

480 

481 * Apply a median smoothing filter to all of the template images. 

482 * Relevant parameters: ``medianFilterHalfSize`` 

483 

484 # Optional: If ``monotonicTemplate`` is True, make the templates monotonic. 

485 

486 * The pixels in the templates are modified such that pixels 

487 further from the peak will have values smaller than those closer to the peak. 

488 

489 # Optional: If ``clipFootprintToNonzero`` is True, clip non-zero spans in the template footprints 

490 

491 * Peak ``Footprint``\ s are clipped to the region in the image containing non-zero values 

492 by dropping spans that are completely zero and moving endpoints to non-zero pixels 

493 (but does not split spans that have internal zeros). 

494 

495 # Optional: If ``weightTemplates`` is True, weight the templates to best fit the observed image 

496 

497 * Re-weight the templates so that their linear combination 

498 best represents the observed ``maskedImage`` 

499 

500 # Optional: If ``removeDegenerateTempaltes`` is True, reconstruct shredded galaxies 

501 

502 * If galaxies have substructure, such as face-on spirals, the process of identifying peaks can 

503 "shred" the galaxy into many pieces. The templates of shredded galaxies are typically quite 

504 similar because they represent the same galaxy, so we try to identify these "degenerate" peaks 

505 by looking at the inner product (in pixel space) of pairs of templates. 

506 * If they are nearly parallel, we only keep one of the peaks and reject the other. 

507 * If only one of the peaks is a PSF template, the other template is used, 

508 otherwise the one with the maximum template value is kept. 

509 * Relevant parameters: ``maxTempDotProduct`` 

510 

511 # Apportion flux to all of the peak templates 

512 

513 * Divide the ``maskedImage`` flux amongst all of the templates based on the fraction of 

514 flux assigned to each ``tempalteFootprint``. 

515 * Leftover "stray flux" is assigned to peaks based on the other parameters. 

516 * Relevant parameters: ``clipStrayFluxFraction``, ``strayFluxAssignment``, 

517 ``strayFluxToPointSources``, ``assignStrayFlux`` 

518 

519 Parameters 

520 ---------- 

521 footprint: `afw.detection.Footprint` 

522 Parent footprint to deblend 

523 maskedImage: `afw.image.MaskedImageF` 

524 Masked image containing the ``footprint`` 

525 psf: `afw.detection.Psf` 

526 Psf of the ``maskedImage`` 

527 psffwhm: `float` 

528 FWHM of the ``maskedImage``\'s ``psf`` 

529 psfChisqCut*: `float`, optional 

530 If ``fitPsfs==True``, all of the peaks are fit to the image PSF. 

531 ``psfChisqCut1`` is the maximum chi-squared-per-degree-of-freedom allowed for a peak to 

532 be considered a PSF match without recentering. 

533 A fit is also made that includes terms to recenter the PSF. 

534 ``psfChisqCut2`` is the same as ``psfChisqCut1`` except it determines the restriction on the 

535 fit that includes recentering terms. 

536 If the peak is a match for a re-centered PSF, the PSF is repositioned at the new center and 

537 the peak footprint is fit again, this time to the new PSF. 

538 If the resulting chi-squared-per-degree-of-freedom is less than ``psfChisqCut2b`` then it 

539 passes the re-centering algorithm. 

540 If the peak passes both the re-centered and fixed position cuts, the better of the two is accepted, 

541 but parameters for all three psf fits are stored in the ``DeblendedPeak``. 

542 The default for ``psfChisqCut1``, ``psfChisqCut2``, and ``psfChisqCut2b`` is ``1.5``. 

543 fitPsfs: `bool`, optional 

544 If True then all of the peaks will be compared to the image PSF to 

545 distinguish stars from galaxies. 

546 medianSmoothTemplate: ``bool``, optional 

547 If ``medianSmoothTemplate==True`` it a median smoothing filter is applied to the ``maskedImage``. 

548 The default is ``True``. 

549 medianFilterHalfSize: `int`, optional 

550 Half the box size of the median filter, ie a ``medianFilterHalfSize`` of 50 means that 

551 each output pixel will be the median of the pixels in a 101 x 101-pixel box in the input image. 

552 This parameter is only used when ``medianSmoothTemplate==True``, otherwise it is ignored. 

553 The default value is 2. 

554 monotonicTempalte: `bool`, optional 

555 If True then make the template monotonic. 

556 The default is True. 

557 weightTemplates: `bool`, optional 

558 If True, re-weight the templates so that their linear combination best represents 

559 the observed ``maskedImage``. 

560 The default is False. 

561 log: `log.Log`, optional 

562 LSST logger for logging purposes. 

563 The default is ``None`` (no logging). 

564 verbose: `bool`, optional 

565 Whether or not to show a more verbose output. 

566 The default is ``False``. 

567 sigma1: `float`, optional 

568 Average noise level in ``maskedImage``. 

569 The default is ``None``, which estimates the noise from the median value of ``maskedImage``. 

570 maxNumberOfPeaks: `int`, optional 

571 If nonzero, the maximum number of peaks to deblend. 

572 If the total number of peaks is greater than ``maxNumberOfPeaks``, 

573 then only the first ``maxNumberOfPeaks`` sources are deblended. 

574 The default is 0, which deblends all of the peaks. 

575 assignStrayFlux: `bool`, optional 

576 If True then flux in the parent footprint that is not covered by any of the 

577 template footprints is assigned to templates based on their 1/(1+r^2) distance. 

578 How the flux is apportioned is determined by ``strayFluxAssignment``. 

579 The default is True. 

580 strayFluxToPointSources: `string` 

581 Determines how stray flux is apportioned to point sources 

582 

583 * ``never``: never apportion stray flux to point sources 

584 * ``necessary`` (default): point sources are included only if there are no extended sources nearby 

585 * ``always``: point sources are always included in the 1/(1+r^2) splitting 

586 

587 strayFluxAssignment: `string`, optional 

588 Determines how stray flux is apportioned. 

589 

590 * ``trim``: Trim stray flux and do not include in any footprints 

591 * ``r-to-peak`` (default): Stray flux is assigned based on (1/(1+r^2) from the peaks 

592 * ``r-to-footprint``: Stray flux is distributed to the footprints based on 1/(1+r^2) of the 

593 minimum distance from the stray flux to footprint 

594 * ``nearest-footprint``: Stray flux is assigned to the footprint with lowest L-1 (Manhattan) 

595 distance to the stray flux 

596 

597 rampFluxAtEdge: `bool`, optional 

598 If True then extend footprints with excessive flux on the edges as described above. 

599 The default is False. 

600 patchEdges: `bool`, optional 

601 If True and if the footprint touches pixels with the ``EDGE`` bit set, 

602 then grow the footprint to include all symmetric templates. 

603 The default is ``False``. 

604 tinyFootprintSize: `float`, optional 

605 The PSF model is shrunk to the size that contains the original footprint. 

606 If the bbox of the clipped PSF model for a peak is smaller than ``max(tinyFootprintSize,2)`` 

607 then ``tinyFootprint`` for the peak is set to ``True`` and the peak is not fit. 

608 The default is 2. 

609 getTemplateSum: `bool`, optional 

610 As part of the flux calculation, the sum of the templates is calculated. 

611 If ``getTemplateSum==True`` then the sum of the templates is stored in the result (a `PerFootprint`). 

612 The default is False. 

613 clipStrayFluxFraction: `float`, optional 

614 Minimum stray-flux portion. 

615 Any stray-flux portion less than ``clipStrayFluxFraction`` is clipped to zero. 

616 The default is 0.001. 

617 clipFootprintToNonzero: `bool`, optional 

618 If True then clip non-zero spans in the template footprints. See above for more. 

619 The default is True. 

620 removeDegenerateTemplates: `bool`, optional 

621 If True then we try to identify "degenerate" peaks by looking at the inner product 

622 (in pixel space) of pairs of templates. 

623 The default is False. 

624 maxTempDotProduct: `float`, optional 

625 All dot products between templates greater than ``maxTempDotProduct`` will result in one 

626 of the templates removed. This parameter is only used when ``removeDegenerateTempaltes==True``. 

627 The default is 0.5. 

628 

629 Returns 

630 ------- 

631 res: `PerFootprint` 

632 Deblender result that contains a list of ``DeblendedPeak``\ s for each peak and (optionally) 

633 the template sum. 

634 """ 

635 avgNoise = sigma1 

636 

637 debPlugins = [] 

638 

639 # Add activated deblender plugins 

640 if fitPsfs: 

641 debPlugins.append(plugins.DeblenderPlugin(plugins.fitPsfs, 

642 psfChisqCut1=psfChisqCut1, 

643 psfChisqCut2=psfChisqCut2, 

644 psfChisqCut2b=psfChisqCut2b, 

645 tinyFootprintSize=tinyFootprintSize)) 

646 debPlugins.append(plugins.DeblenderPlugin(plugins.buildSymmetricTemplates, patchEdges=patchEdges)) 

647 if rampFluxAtEdge: 

648 debPlugins.append(plugins.DeblenderPlugin(plugins.rampFluxAtEdge, patchEdges=patchEdges)) 

649 if medianSmoothTemplate: 

650 debPlugins.append(plugins.DeblenderPlugin(plugins.medianSmoothTemplates, 

651 medianFilterHalfsize=medianFilterHalfsize)) 

652 if monotonicTemplate: 

653 debPlugins.append(plugins.DeblenderPlugin(plugins.makeTemplatesMonotonic)) 

654 if clipFootprintToNonzero: 

655 debPlugins.append(plugins.DeblenderPlugin(plugins.clipFootprintsToNonzero)) 

656 if weightTemplates: 

657 debPlugins.append(plugins.DeblenderPlugin(plugins.weightTemplates)) 

658 if removeDegenerateTemplates: 

659 if weightTemplates: 

660 onReset = len(debPlugins)-1 

661 else: 

662 onReset = len(debPlugins) 

663 debPlugins.append(plugins.DeblenderPlugin(plugins.reconstructTemplates, 

664 onReset=onReset, 

665 maxTempDotProd=maxTempDotProd)) 

666 debPlugins.append(plugins.DeblenderPlugin(plugins.apportionFlux, 

667 clipStrayFluxFraction=clipStrayFluxFraction, 

668 assignStrayFlux=assignStrayFlux, 

669 strayFluxAssignment=strayFluxAssignment, 

670 strayFluxToPointSources=strayFluxToPointSources, 

671 getTemplateSum=getTemplateSum)) 

672 

673 debResult = newDeblend(debPlugins, footprint, maskedImage, psf, psffwhm, log, verbose, avgNoise) 

674 

675 return debResult 

676 

677 

678def newDeblend(debPlugins, footprint, mMaskedImage, psfs, psfFwhms, 

679 log=None, verbose=False, avgNoise=None, maxNumberOfPeaks=0): 

680 r"""Deblend a parent ``Footprint`` in a ``MaskedImageF``. 

681 

682 Deblending assumes that ``footprint`` has multiple peaks, as it will still create a 

683 `PerFootprint` object with a list of peaks even if there is only one peak in the list. 

684 It is recommended to first check that ``footprint`` has more than one peak, similar to the 

685 execution of `lsst.meas.deblender.deblend.SourceDeblendTask`. 

686 

687 This version of the deblender accepts a list of plugins to execute, with the option to re-run parts 

688 of the deblender if templates are changed during any of the steps. 

689 

690 Parameters 

691 ---------- 

692 debPlugins: list of `meas.deblender.plugins.DeblenderPlugins` 

693 Plugins to execute (in order of execution) for the deblender. 

694 footprint: `afw.detection.Footprint` or list of Footprints 

695 Parent footprint to deblend. 

696 mMaskedImage: `MultibandMaskedImage` or `MaskedImage` 

697 Masked image in each band. 

698 psfs: `afw.detection.Psf` or list of Psfs 

699 Psf of the ``maskedImage``. 

700 psfFwhms: `float` or list of floats 

701 FWHM of the ``maskedImage``\'s ``psf``. 

702 log: `log.Log`, optional 

703 LSST logger for logging purposes. 

704 The default is ``None`` (no logging). 

705 verbose: `bool`, optional 

706 Whether or not to show a more verbose output. 

707 The default is ``False``. 

708 avgNoise: `float`or list of `float`\ s, optional 

709 Average noise level in each ``maskedImage``. 

710 The default is ``None``, which estimates the noise from the median value of the 

711 variance plane of ``maskedImage`` for each filter. 

712 maxNumberOfPeaks: `int`, optional 

713 If nonzero, the maximum number of peaks to deblend. 

714 If the total number of peaks is greater than ``maxNumberOfPeaks``, 

715 then only the first ``maxNumberOfPeaks`` sources are deblended. 

716 

717 Returns 

718 ------- 

719 debResult: `DeblendedParent` 

720 Deblender result that contains a list of ``MultiColorPeak``\ s for each peak and 

721 information that describes the footprint in all filters. 

722 """ 

723 # Import C++ routines 

724 

725 if log is None: 

726 import lsst.log as lsstLog 

727 

728 log = lsstLog.Log.getLogger(__name__) 

729 

730 if verbose: 

731 log.setLevel(lsstLog.Log.TRACE) 

732 

733 # get object that will hold our results 

734 debResult = DeblenderResult(footprint, mMaskedImage, psfs, psfFwhms, log, 

735 maxNumberOfPeaks=maxNumberOfPeaks, avgNoise=avgNoise) 

736 

737 step = 0 

738 while step < len(debPlugins): 

739 # If a failure occurs at any step, 

740 # the result is flagged as `failed` 

741 # and the remaining steps are skipped 

742 if not debResult.failed: 

743 reset = debPlugins[step].run(debResult, log) 

744 else: 

745 log.warning("Skipping steps %s", debPlugins[step:]) 

746 return debResult 

747 if reset: 

748 step = debPlugins[step].onReset 

749 else: 

750 step += 1 

751 

752 return debResult 

753 

754 

755class CachingPsf: 

756 """Cache the PSF models 

757 

758 In the PSF fitting code, we request PSF models for all peaks near 

759 the one being fit. This was turning out to be quite expensive in 

760 some cases. Here, we cache the PSF models to bring the cost down 

761 closer to O(N) rather than O(N^2). 

762 """ 

763 

764 def __init__(self, psf): 

765 self.cache = {} 

766 self.psf = psf 

767 

768 def computeImage(self, cx, cy): 

769 im = self.cache.get((cx, cy), None) 

770 if im is not None: 

771 return im 

772 try: 

773 im = self.psf.computeImage(geom.Point2D(cx, cy)) 

774 except lsst.pex.exceptions.Exception: 

775 im = self.psf.computeImage() 

776 self.cache[(cx, cy)] = im 

777 return im