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 numpy as np 

23 

24import scarlet 

25 

26import lsst.pex.exceptions 

27import lsst.afw.image as afwImage 

28import lsst.afw.detection as afwDet 

29import lsst.afw.geom as afwGeom 

30import lsst.geom as geom 

31 

32# Import C++ routines 

33from .baselineUtils import BaselineUtilsF as bUtils 

34 

35 

36def clipFootprintToNonzeroImpl(foot, image): 

37 ''' 

38 Clips the given *Footprint* to the region in the *Image* 

39 containing non-zero values. The clipping drops spans that are 

40 totally zero, and moves endpoints to non-zero; it does not 

41 split spans that have internal zeros. 

42 ''' 

43 x0 = image.getX0() 

44 y0 = image.getY0() 

45 xImMax = x0 + image.getDimensions().getX() 

46 yImMax = y0 + image.getDimensions().getY() 

47 newSpans = [] 

48 arr = image.getArray() 

49 for span in foot.spans: 

50 y = span.getY() 

51 if y < y0 or y > yImMax: 

52 continue 

53 spanX0 = span.getX0() 

54 spanX1 = span.getX1() 

55 xMin = spanX0 if spanX0 >= x0 else x0 

56 xMax = spanX1 if spanX1 <= xImMax else xImMax 

57 xarray = np.arange(xMin, xMax+1)[arr[y-y0, xMin-x0:xMax-x0+1] != 0] 

58 if len(xarray) > 0: 

59 newSpans.append(afwGeom.Span(y, xarray[0], xarray[-1])) 

60 # Time to update the SpanSet 

61 foot.setSpans(afwGeom.SpanSet(newSpans, normalize=False)) 

62 foot.removeOrphanPeaks() 

63 

64 

65class DeblenderPlugin: 

66 """Class to define plugins for the deblender. 

67 

68 The new deblender executes a series of plugins specified by the user. 

69 Each plugin defines the function to be executed, the keyword arguments required by the function, 

70 and whether or not certain portions of the deblender might need to be rerun as a result of 

71 the function. 

72 """ 

73 def __init__(self, func, onReset=None, maxIterations=50, **kwargs): 

74 """Initialize a deblender plugin 

75 

76 Parameters 

77 ---------- 

78 func: `function` 

79 Function to run when the plugin is executed. The function should always take 

80 `debResult`, a `DeblenderResult` that stores the deblender result, and 

81 `log`, an `lsst.log`, as the first two arguments, as well as any additional 

82 keyword arguments (that must be specified in ``kwargs``). 

83 The function should also return ``modified``, a `bool` that tells the deblender whether 

84 or not any templates have been modified by the function. 

85 If ``modified==True``, the deblender will go back to step ``onReset``, 

86 unless the has already been run ``maxIterations``. 

87 onReset: `int` 

88 Index of the deblender plugin to return to if ``func`` modifies any templates. 

89 The default is ``None``, which does not re-run any plugins. 

90 maxIterations: `int` 

91 Maximum number of times the deblender will reset when the current plugin 

92 returns ``True``. 

93 """ 

94 self.func = func 

95 self.kwargs = kwargs 

96 self.onReset = onReset 

97 self.maxIterations = maxIterations 

98 self.kwargs = kwargs 

99 self.iterations = 0 

100 

101 def run(self, debResult, log): 

102 """Execute the current plugin 

103 

104 Once the plugin has finished, check to see if part of the deblender must be executed again. 

105 """ 

106 log.trace("Executing %s", self.func.__name__) 

107 reset = self.func(debResult, log, **self.kwargs) 

108 if reset: 

109 self.iterations += 1 

110 if self.iterations < self.maxIterations: 

111 return self.onReset 

112 return None 

113 

114 def __str__(self): 

115 return ("<Deblender Plugin: func={0}, kwargs={1}".format(self.func.__name__, self.kwargs)) 

116 

117 def __repr__(self): 

118 return self.__str__() 

119 

120 

121def _setPeakError(debResult, log, pk, cx, cy, filters, msg, flag): 

122 """Update the peak in each band with an error 

123 

124 This function logs an error that occurs during deblending and sets the 

125 relevant flag. 

126 

127 Parameters 

128 ---------- 

129 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

130 Container for the final deblender results. 

131 log: `log.Log` 

132 LSST logger for logging purposes. 

133 pk: int 

134 Number of the peak that failed 

135 cx: float 

136 x coordinate of the peak 

137 cy: float 

138 y coordinate of the peak 

139 filters: list of str 

140 List of filter names for the exposures 

141 msg: str 

142 Message to display in log traceback 

143 flag: str 

144 Name of the flag to set 

145 

146 Returns 

147 ------- 

148 None 

149 """ 

150 log.trace("Peak {0} at ({1},{2}):{3}".format(pk, cx, cy, msg)) 

151 for fidx, f in enumerate(filters): 

152 pkResult = debResult.deblendedParents[f].peaks[pk] 

153 getattr(pkResult, flag)() 

154 

155 

156def buildMultibandTemplates(debResult, log, useWeights=False, usePsf=False, 

157 sources=None, constraints=None, config=None, maxIter=100, bgScale=0.5, 

158 relativeError=1e-2, badMask=None): 

159 """Run the Multiband Deblender to build templates 

160 

161 Parameters 

162 ---------- 

163 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

164 Container for the final deblender results. 

165 log: `log.Log` 

166 LSST logger for logging purposes. 

167 useWeights: bool, default=False 

168 Whether or not to use the variance map in each filter for the fit. 

169 usePsf: bool, default=False 

170 Whether or not to convolve the image with the PSF in each band. 

171 This is not yet implemented in an optimized algorithm, so it is recommended 

172 to leave this term off for now 

173 sources: list of `scarlet.source.Source` objects, default=None 

174 List of sources to use in the blend. By default the 

175 `scarlet.source.ExtendedSource` class is used, which initializes each 

176 source as symmetric and monotonic about a peak in the footprint peak catalog. 

177 constraints: `scarlet.constraint.Constraint`, default=None 

178 Constraint to be applied to each source. If sources require different constraints, 

179 a list of `sources` must be created instead, which ignores the `constraints` parameter. 

180 When `constraints` is `None` the default constraints are used. 

181 config: `scarlet.config.Config`, default=None 

182 Configuration for the blend. 

183 If `config` is `None` then the default `Config` is used. 

184 maxIter: int, default=100 

185 Maximum iterations for a single blend. 

186 bgScale: float 

187 Amount to scale the background RMS to set the floor for deblender model sizes 

188 relativeError: float, default=1e-2 

189 Relative error to reach for convergence 

190 badMask: list of str, default=`None` 

191 List of mask plane names to mark bad pixels. 

192 If `badPixelKeys` is `None`, the default keywords used are 

193 `["BAD", "CR", "NO_DATA", "SAT", "SUSPECT"]`. 

194 

195 Returns 

196 ------- 

197 modified: `bool` 

198 If any templates have been created then ``modified`` is ``True``, 

199 otherwise it is ``False`` (meaning all of the peaks were skipped). 

200 """ 

201 # Extract coordinates from each MultiColorPeak 

202 bbox = debResult.footprint.getBBox() 

203 peakSchema = debResult.footprint.peaks.getSchema() 

204 xmin = bbox.getMinX() 

205 ymin = bbox.getMinY() 

206 peaks = [[pk.y-ymin, pk.x-xmin] for pk in debResult.peaks] 

207 xy0 = bbox.getMin() 

208 

209 # Create the data array from the masked images 

210 mMaskedImage = debResult.mMaskedImage[:, debResult.footprint.getBBox()] 

211 data = mMaskedImage.image.array 

212 

213 # Use the inverse variance as the weights 

214 if useWeights: 

215 weights = 1/mMaskedImage.variance.array 

216 else: 

217 weights = np.ones_like(data) 

218 

219 # Use the mask plane to mask bad pixels and 

220 # the footprint to mask out pixels outside the footprint 

221 if badMask is None: 

222 badMask = ["BAD", "CR", "NO_DATA", "SAT", "SUSPECT"] 

223 fpMask = afwImage.Mask(bbox) 

224 debResult.footprint.spans.setMask(fpMask, 1) 

225 fpMask = ~fpMask.getArray().astype(bool) 

226 badPixels = mMaskedImage.mask.getPlaneBitMask(badMask) 

227 mask = (mMaskedImage.mask.array & badPixels) | fpMask[None, :] 

228 weights[mask > 0] = 0 

229 

230 # Extract the PSF from each band for PSF convolution 

231 if usePsf: 

232 psfs = [] 

233 for psf in debResult.psfs: 

234 psfs.append(psf.computeKernelImage().array) 

235 psf = np.array(psfs) 

236 else: 

237 psf = None 

238 

239 bg_rms = np.array([debResult.deblendedParents[f].avgNoise for f in debResult.filters])*bgScale 

240 if sources is None: 

241 # If only a single constraint was given, use it for all of the sources 

242 if constraints is None or isinstance(constraints[0], scarlet.constraints.Constraint): 

243 constraints = [constraints] * len(peaks) 

244 sources = [ 

245 scarlet.source.ExtendedSource(center=peak, 

246 img=data, 

247 bg_rms=bg_rms, 

248 constraints=constraints[pk], 

249 psf=psf, 

250 symmetric=True, 

251 monotonic=True, 

252 thresh=1.0, 

253 config=config) 

254 for pk, peak in enumerate(peaks) 

255 ] 

256 

257 # When a footprint includes only non-detections 

258 # (peaks in the noise too low to deblend as a source) 

259 # the deblender currently fails. 

260 try: 

261 blend = scarlet.blend.Blend(components=sources) 

262 blend.set_data(img=data, weights=weights, bg_rms=bg_rms, config=config) 

263 blend.fit(maxIter, e_rel=relativeError) 

264 except scarlet.source.SourceInitError as e: 

265 log.warn(e.args[0]) 

266 debResult.failed = True 

267 return False 

268 except np.linalg.LinAlgError: 

269 log.warn("Deblend failed catastrophically, most likely due to no signal in the footprint") 

270 debResult.failed = True 

271 return False 

272 debResult.blend = blend 

273 

274 modified = False 

275 # Create the Templates for each peak in each filter 

276 for pk, source in enumerate(blend.sources): 

277 src = source.components[0] 

278 _cx = src.Nx >> 1 

279 _cy = src.Ny >> 1 

280 

281 if debResult.peaks[pk].skip: 

282 continue 

283 modified = True 

284 cx = src.center[1]+xmin 

285 cy = src.center[0]+ymin 

286 icx = int(np.round(cx)) 

287 icy = int(np.round(cy)) 

288 imbb = debResult.deblendedParents[debResult.filters[0]].img.getBBox() 

289 

290 # Footprint must be inside the image 

291 if not imbb.contains(geom.Point2I(cx, cy)): 

292 _setPeakError(debResult, log, pk, cx, cy, debResult.filters, 

293 "peak center is not inside image", "setOutOfBounds") 

294 continue 

295 # Only save templates that have nonzero flux 

296 if np.sum(src.morph) == 0: 

297 _setPeakError(debResult, log, pk, cx, cy, debResult.filters, 

298 "had no flux", "setFailedSymmetricTemplate") 

299 continue 

300 

301 # Temporary for initial testing: combine multiple components 

302 model = blend.get_model(k=pk).astype(np.float32) 

303 

304 # The peak in each band will have the same SpanSet 

305 mask = afwImage.Mask(np.array(np.sum(model, axis=0) > 0, dtype=np.int32), xy0=xy0) 

306 ss = afwGeom.SpanSet.fromMask(mask) 

307 

308 if len(ss) == 0: 

309 log.warn("No flux in parent footprint") 

310 debResult.failed = True 

311 return False 

312 

313 # Add the template footprint and image to the deblender result for each peak 

314 for fidx, f in enumerate(debResult.filters): 

315 pkResult = debResult.deblendedParents[f].peaks[pk] 

316 tfoot = afwDet.Footprint(ss, peakSchema=peakSchema) 

317 # Add the peak with the intensity of the centered model, 

318 # which might be slightly larger than the shifted model 

319 peakFlux = np.sum(src.sed[fidx]*src.morph[_cy, _cx]) 

320 tfoot.addPeak(cx, cy, peakFlux) 

321 timg = afwImage.ImageF(model[fidx], xy0=xy0) 

322 timg = timg[tfoot.getBBox()] 

323 pkResult.setOrigTemplate(timg, tfoot) 

324 pkResult.setTemplate(timg, tfoot) 

325 pkResult.setFluxPortion(afwImage.MaskedImageF(timg)) 

326 pkResult.multiColorPeak.x = cx 

327 pkResult.multiColorPeak.y = cy 

328 pkResult.peak.setFx(cx) 

329 pkResult.peak.setFy(cy) 

330 pkResult.peak.setIx(icx) 

331 pkResult.peak.setIy(icy) 

332 return modified 

333 

334 

335def fitPsfs(debResult, log, psfChisqCut1=1.5, psfChisqCut2=1.5, psfChisqCut2b=1.5, tinyFootprintSize=2): 

336 """Fit a PSF + smooth background model (linear) to a small region around each peak 

337 

338 This function will iterate over all filters in deblender result but does not compare 

339 results across filters. 

340 DeblendedPeaks that pass the cuts have their templates modified to the PSF + background model 

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

342 

343 This will likely be replaced in the future with a function that compares the psf chi-squared cuts 

344 so that peaks flagged as point sources will be considered point sources in all bands. 

345 

346 Parameters 

347 ---------- 

348 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

349 Container for the final deblender results. 

350 log: `log.Log` 

351 LSST logger for logging purposes. 

352 psfChisqCut*: `float`, optional 

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

354 be considered a PSF match without recentering. 

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

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

357 fit that includes recentering terms. 

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

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

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

361 passes the re-centering algorithm. 

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

363 but parameters for all three psf fits are stored in the ``DebldendedPeak``. 

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

365 tinyFootprintSize: `float`, optional 

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

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

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

369 The default is 2. 

370 

371 Returns 

372 ------- 

373 modified: `bool` 

374 If any templates have been assigned to PSF point sources then ``modified`` is ``True``, 

375 otherwise it is ``False``. 

376 """ 

377 from .baseline import CachingPsf 

378 modified = False 

379 # Loop over all of the filters to build the PSF 

380 for fidx in debResult.filters: 

381 dp = debResult.deblendedParents[fidx] 

382 peaks = dp.fp.getPeaks() 

383 cpsf = CachingPsf(dp.psf) 

384 

385 # create mask image for pixels within the footprint 

386 fmask = afwImage.Mask(dp.bb) 

387 fmask.setXY0(dp.bb.getMinX(), dp.bb.getMinY()) 

388 dp.fp.spans.setMask(fmask, 1) 

389 

390 # pk.getF() -- retrieving the floating-point location of the peak 

391 # -- actually shows up in the profile if we do it in the loop, so 

392 # grab them all here. 

393 peakF = [pk.getF() for pk in peaks] 

394 

395 for pki, (pk, pkres, pkF) in enumerate(zip(peaks, dp.peaks, peakF)): 

396 log.trace('Filter %s, Peak %i', fidx, pki) 

397 ispsf = _fitPsf(dp.fp, fmask, pk, pkF, pkres, dp.bb, peaks, peakF, log, cpsf, dp.psffwhm, 

398 dp.img, dp.varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b, tinyFootprintSize) 

399 modified = modified or ispsf 

400 return modified 

401 

402 

403def _fitPsf(fp, fmask, pk, pkF, pkres, fbb, peaks, peaksF, log, psf, psffwhm, 

404 img, varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b, 

405 tinyFootprintSize=2, 

406 ): 

407 """Fit a PSF + smooth background model (linear) to a small region around a peak. 

408 

409 See fitPsfs for a more thorough description, including all parameters not described below. 

410 

411 Parameters 

412 ---------- 

413 fp: `afw.detection.Footprint` 

414 Footprint containing the Peaks to model. 

415 fmask: `afw.image.Mask` 

416 The Mask plane for pixels in the Footprint 

417 pk: `afw.detection.PeakRecord` 

418 The peak within the Footprint that we are going to fit with PSF model 

419 pkF: `afw.geom.Point2D` 

420 Floating point coordinates of the peak. 

421 pkres: `meas.deblender.DeblendedPeak` 

422 Peak results object that will hold the results. 

423 fbb: `afw.geom.Box2I` 

424 Bounding box of ``fp`` 

425 peaks: `afw.detection.PeakCatalog` 

426 Catalog of peaks contained in the parent footprint. 

427 peaksF: list of `afw.geom.Point2D` 

428 List of floating point coordinates of all of the peaks. 

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

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

431 psffwhm: list pf `float`s 

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

433 img: `afw.image.ImageF` 

434 The image that contains the footprint. 

435 varimg: `afw.image.ImageF` 

436 The variance of the image that contains the footprint. 

437 

438 Results 

439 ------- 

440 ispsf: `bool` 

441 Whether or not the peak matches a PSF model. 

442 """ 

443 import lsstDebug 

444 

445 # my __name__ is lsst.meas.deblender.baseline 

446 debugPlots = lsstDebug.Info(__name__).plots 

447 debugPsf = lsstDebug.Info(__name__).psf 

448 

449 # The small region is a disk out to R0, plus a ramp with 

450 # decreasing weight down to R1. 

451 R0 = int(np.ceil(psffwhm*1.)) 

452 # ramp down to zero weight at this radius... 

453 R1 = int(np.ceil(psffwhm*1.5)) 

454 cx, cy = pkF.getX(), pkF.getY() 

455 psfimg = psf.computeImage(cx, cy) 

456 # R2: distance to neighbouring peak in order to put it into the model 

457 R2 = R1 + min(psfimg.getWidth(), psfimg.getHeight())/2. 

458 

459 pbb = psfimg.getBBox() 

460 pbb.clip(fbb) 

461 px0, py0 = psfimg.getX0(), psfimg.getY0() 

462 

463 # Make sure we haven't been given a substitute PSF that's nowhere near where we want, as may occur if 

464 # "Cannot compute CoaddPsf at point (xx,yy); no input images at that point." 

465 if not pbb.contains(geom.Point2I(int(cx), int(cy))): 

466 pkres.setOutOfBounds() 

467 return 

468 

469 # The bounding-box of the local region we are going to fit ("stamp") 

470 xlo = int(np.floor(cx - R1)) 

471 ylo = int(np.floor(cy - R1)) 

472 xhi = int(np.ceil(cx + R1)) 

473 yhi = int(np.ceil(cy + R1)) 

474 stampbb = geom.Box2I(geom.Point2I(xlo, ylo), geom.Point2I(xhi, yhi)) 

475 stampbb.clip(fbb) 

476 xlo, xhi = stampbb.getMinX(), stampbb.getMaxX() 

477 ylo, yhi = stampbb.getMinY(), stampbb.getMaxY() 

478 if xlo > xhi or ylo > yhi: 

479 log.trace('Skipping this peak: out of bounds') 

480 pkres.setOutOfBounds() 

481 return 

482 

483 # drop tiny footprints too? 

484 if min(stampbb.getWidth(), stampbb.getHeight()) <= max(tinyFootprintSize, 2): 

485 # Minimum size limit of 2 comes from the "PSF dx" calculation, which involves shifting the PSF 

486 # by one pixel to the left and right. 

487 log.trace('Skipping this peak: tiny footprint / close to edge') 

488 pkres.setTinyFootprint() 

489 return 

490 

491 # find other peaks within range... 

492 otherpeaks = [] 

493 for pk2, pkF2 in zip(peaks, peaksF): 

494 if pk2 == pk: 

495 continue 

496 if pkF.distanceSquared(pkF2) > R2**2: 

497 continue 

498 opsfimg = psf.computeImage(pkF2.getX(), pkF2.getY()) 

499 if not opsfimg.getBBox().overlaps(stampbb): 

500 continue 

501 otherpeaks.append(opsfimg) 

502 log.trace('%i other peaks within range', len(otherpeaks)) 

503 

504 # Now we are going to do a least-squares fit for the flux in this 

505 # PSF, plus a decenter term, a linear sky, and fluxes of nearby 

506 # sources (assumed point sources). Build up the matrix... 

507 # Number of terms -- PSF flux, constant sky, X, Y, + other PSF fluxes 

508 NT1 = 4 + len(otherpeaks) 

509 # + PSF dx, dy 

510 NT2 = NT1 + 2 

511 # Number of pixels -- at most 

512 NP = (1 + yhi - ylo)*(1 + xhi - xlo) 

513 # indices of columns in the "A" matrix. 

514 I_psf = 0 

515 I_sky = 1 

516 I_sky_ramp_x = 2 

517 I_sky_ramp_y = 3 

518 # offset of other psf fluxes: 

519 I_opsf = 4 

520 I_dx = NT1 + 0 

521 I_dy = NT1 + 1 

522 

523 # Build the matrix "A", rhs "b" and weight "w". 

524 ix0, iy0 = img.getX0(), img.getY0() 

525 fx0, fy0 = fbb.getMinX(), fbb.getMinY() 

526 fslice = (slice(ylo-fy0, yhi-fy0+1), slice(xlo-fx0, xhi-fx0+1)) 

527 islice = (slice(ylo-iy0, yhi-iy0+1), slice(xlo-ix0, xhi-ix0+1)) 

528 fmask_sub = fmask .getArray()[fslice] 

529 var_sub = varimg.getArray()[islice] 

530 img_sub = img.getArray()[islice] 

531 

532 # Clip the PSF image to match its bbox 

533 psfarr = psfimg.getArray()[pbb.getMinY()-py0: 1+pbb.getMaxY()-py0, 

534 pbb.getMinX()-px0: 1+pbb.getMaxX()-px0] 

535 px0, px1 = pbb.getMinX(), pbb.getMaxX() 

536 py0, py1 = pbb.getMinY(), pbb.getMaxY() 

537 

538 # Compute the "valid" pixels within our region-of-interest 

539 valid = (fmask_sub > 0) 

540 xx, yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1) 

541 RR = ((xx - cx)**2)[np.newaxis, :] + ((yy - cy)**2)[:, np.newaxis] 

542 valid *= (RR <= R1**2) 

543 valid *= (var_sub > 0) 

544 NP = valid.sum() 

545 

546 if NP == 0: 

547 log.warn('Skipping peak at (%.1f, %.1f): no unmasked pixels nearby', cx, cy) 

548 pkres.setNoValidPixels() 

549 return 

550 

551 # pixel coords of valid pixels 

552 XX, YY = np.meshgrid(xx, yy) 

553 ipixes = np.vstack((XX[valid] - xlo, YY[valid] - ylo)).T 

554 

555 inpsfx = (xx >= px0)*(xx <= px1) 

556 inpsfy = (yy >= py0)*(yy <= py1) 

557 inpsf = np.outer(inpsfy, inpsfx) 

558 indx = np.outer(inpsfy, (xx > px0)*(xx < px1)) 

559 indy = np.outer((yy > py0)*(yy < py1), inpsfx) 

560 

561 del inpsfx 

562 del inpsfy 

563 

564 def _overlap(xlo, xhi, xmin, xmax): 

565 assert((xlo <= xmax) and (xhi >= xmin) and 

566 (xlo <= xhi) and (xmin <= xmax)) 

567 xloclamp = max(xlo, xmin) 

568 Xlo = xloclamp - xlo 

569 xhiclamp = min(xhi, xmax) 

570 Xhi = Xlo + (xhiclamp - xloclamp) 

571 assert(xloclamp >= 0) 

572 assert(Xlo >= 0) 

573 return (xloclamp, xhiclamp+1, Xlo, Xhi+1) 

574 

575 A = np.zeros((NP, NT2)) 

576 # Constant term 

577 A[:, I_sky] = 1. 

578 # Sky slope terms: dx, dy 

579 A[:, I_sky_ramp_x] = ipixes[:, 0] + (xlo-cx) 

580 A[:, I_sky_ramp_y] = ipixes[:, 1] + (ylo-cy) 

581 

582 # whew, grab the valid overlapping PSF pixels 

583 px0, px1 = pbb.getMinX(), pbb.getMaxX() 

584 py0, py1 = pbb.getMinY(), pbb.getMaxY() 

585 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0, px1) 

586 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0, py1) 

587 dpx0, dpy0 = px0 - xlo, py0 - ylo 

588 psf_y_slice = slice(sy3 - dpy0, sy4 - dpy0) 

589 psf_x_slice = slice(sx3 - dpx0, sx4 - dpx0) 

590 psfsub = psfarr[psf_y_slice, psf_x_slice] 

591 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo] 

592 A[inpsf[valid], I_psf] = psfsub[vsub] 

593 

594 # PSF dx -- by taking the half-difference of shifted-by-one and 

595 # shifted-by-minus-one. 

596 oldsx = (sx1, sx2, sx3, sx4) 

597 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0+1, px1-1) 

598 psfsub = (psfarr[psf_y_slice, sx3 - dpx0 + 1: sx4 - dpx0 + 1] - 

599 psfarr[psf_y_slice, sx3 - dpx0 - 1: sx4 - dpx0 - 1])/2. 

600 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo] 

601 A[indx[valid], I_dx] = psfsub[vsub] 

602 # revert x indices... 

603 (sx1, sx2, sx3, sx4) = oldsx 

604 

605 # PSF dy 

606 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0+1, py1-1) 

607 psfsub = (psfarr[sy3 - dpy0 + 1: sy4 - dpy0 + 1, psf_x_slice] - 

608 psfarr[sy3 - dpy0 - 1: sy4 - dpy0 - 1, psf_x_slice])/2. 

609 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo] 

610 A[indy[valid], I_dy] = psfsub[vsub] 

611 

612 # other PSFs... 

613 for j, opsf in enumerate(otherpeaks): 

614 obb = opsf.getBBox() 

615 ino = np.outer((yy >= obb.getMinY())*(yy <= obb.getMaxY()), 

616 (xx >= obb.getMinX())*(xx <= obb.getMaxX())) 

617 dpx0, dpy0 = obb.getMinX() - xlo, obb.getMinY() - ylo 

618 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, obb.getMinX(), obb.getMaxX()) 

619 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, obb.getMinY(), obb.getMaxY()) 

620 opsfarr = opsf.getArray() 

621 psfsub = opsfarr[sy3 - dpy0: sy4 - dpy0, sx3 - dpx0: sx4 - dpx0] 

622 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo] 

623 A[ino[valid], I_opsf + j] = psfsub[vsub] 

624 

625 b = img_sub[valid] 

626 

627 # Weights -- from ramp and image variance map. 

628 # Ramp weights -- from 1 at R0 down to 0 at R1. 

629 rw = np.ones_like(RR) 

630 ii = (RR > R0**2) 

631 rr = np.sqrt(RR[ii]) 

632 rw[ii] = np.maximum(0, 1. - ((rr - R0)/(R1 - R0))) 

633 w = np.sqrt(rw[valid]/var_sub[valid]) 

634 # save the effective number of pixels 

635 sumr = np.sum(rw[valid]) 

636 log.debug('sumr = %g', sumr) 

637 

638 del ii 

639 

640 Aw = A*w[:, np.newaxis] 

641 bw = b*w 

642 

643 if debugPlots: 

644 import pylab as plt 

645 plt.clf() 

646 N = NT2 + 2 

647 R, C = 2, (N+1)/2 

648 for i in range(NT2): 

649 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo)) 

650 im1[ipixes[:, 1], ipixes[:, 0]] = A[:, i] 

651 plt.subplot(R, C, i+1) 

652 plt.imshow(im1, interpolation='nearest', origin='lower') 

653 plt.subplot(R, C, NT2+1) 

654 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo)) 

655 im1[ipixes[:, 1], ipixes[:, 0]] = b 

656 plt.imshow(im1, interpolation='nearest', origin='lower') 

657 plt.subplot(R, C, NT2+2) 

658 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo)) 

659 im1[ipixes[:, 1], ipixes[:, 0]] = w 

660 plt.imshow(im1, interpolation='nearest', origin='lower') 

661 plt.savefig('A.png') 

662 

663 # We do fits with and without the decenter (dx,dy) terms. 

664 # Since the dx,dy terms are at the end of the matrix, 

665 # we can do that just by trimming off those elements. 

666 # 

667 # The SVD can fail if there are NaNs in the matrices; this should 

668 # really be handled upstream 

669 try: 

670 # NT1 is number of terms without dx,dy; 

671 # X1 is the result without decenter 

672 X1, r1, rank1, s1 = np.linalg.lstsq(Aw[:, :NT1], bw, rcond=-1) 

673 # X2 is with decenter 

674 X2, r2, rank2, s2 = np.linalg.lstsq(Aw, bw, rcond=-1) 

675 except np.linalg.LinAlgError as e: 

676 log.warn("Failed to fit PSF to child: %s", e) 

677 pkres.setPsfFitFailed() 

678 return 

679 

680 log.debug('r1 r2 %s %s', r1, r2) 

681 

682 # r is weighted chi-squared = sum over pixels: ramp * (model - 

683 # data)**2/sigma**2 

684 if len(r1) > 0: 

685 chisq1 = r1[0] 

686 else: 

687 chisq1 = 1e30 

688 if len(r2) > 0: 

689 chisq2 = r2[0] 

690 else: 

691 chisq2 = 1e30 

692 dof1 = sumr - len(X1) 

693 dof2 = sumr - len(X2) 

694 log.debug('dof1, dof2 %g %g', dof1, dof2) 

695 

696 # This can happen if we're very close to the edge (?) 

697 if dof1 <= 0 or dof2 <= 0: 

698 log.trace('Skipping this peak: bad DOF %g, %g', dof1, dof2) 

699 pkres.setBadPsfDof() 

700 return 

701 

702 q1 = chisq1/dof1 

703 q2 = chisq2/dof2 

704 log.trace('PSF fits: chisq/dof = %g, %g', q1, q2) 

705 ispsf1 = (q1 < psfChisqCut1) 

706 ispsf2 = (q2 < psfChisqCut2) 

707 

708 pkres.psfFit1 = (chisq1, dof1) 

709 pkres.psfFit2 = (chisq2, dof2) 

710 

711 # check that the fit PSF spatial derivative terms aren't too big 

712 if ispsf2: 

713 fdx, fdy = X2[I_dx], X2[I_dy] 

714 f0 = X2[I_psf] 

715 # as a fraction of the PSF flux 

716 dx = fdx/f0 

717 dy = fdy/f0 

718 ispsf2 = ispsf2 and (abs(dx) < 1. and abs(dy) < 1.) 

719 log.trace('isPSF2 -- checking derivatives: dx,dy = %g, %g -> %s', dx, dy, str(ispsf2)) 

720 if not ispsf2: 

721 pkres.psfFitBigDecenter = True 

722 

723 # Looks like a shifted PSF: try actually shifting the PSF by that amount 

724 # and re-evaluate the fit. 

725 if ispsf2: 

726 psfimg2 = psf.computeImage(cx + dx, cy + dy) 

727 # clip 

728 pbb2 = psfimg2.getBBox() 

729 pbb2.clip(fbb) 

730 

731 # Make sure we haven't been given a substitute PSF that's nowhere near where we want, as may occur if 

732 # "Cannot compute CoaddPsf at point (xx,yy); no input images at that point." 

733 if not pbb2.contains(geom.Point2I(int(cx + dx), int(cy + dy))): 

734 ispsf2 = False 

735 else: 

736 # clip image to bbox 

737 px0, py0 = psfimg2.getX0(), psfimg2.getY0() 

738 psfarr = psfimg2.getArray()[pbb2.getMinY()-py0:1+pbb2.getMaxY()-py0, 

739 pbb2.getMinX()-px0:1+pbb2.getMaxX()-px0] 

740 px0, py0 = pbb2.getMinX(), pbb2.getMinY() 

741 px1, py1 = pbb2.getMaxX(), pbb2.getMaxY() 

742 

743 # yuck! Update the PSF terms in the least-squares fit matrix. 

744 Ab = A[:, :NT1] 

745 

746 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0, px1) 

747 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0, py1) 

748 dpx0, dpy0 = px0 - xlo, py0 - ylo 

749 psfsub = psfarr[sy3-dpy0:sy4-dpy0, sx3-dpx0:sx4-dpx0] 

750 vsub = valid[sy1-ylo:sy2-ylo, sx1-xlo:sx2-xlo] 

751 xx, yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1) 

752 inpsf = np.outer((yy >= py0)*(yy <= py1), (xx >= px0)*(xx <= px1)) 

753 Ab[inpsf[valid], I_psf] = psfsub[vsub] 

754 

755 Aw = Ab*w[:, np.newaxis] 

756 # re-solve... 

757 Xb, rb, rankb, sb = np.linalg.lstsq(Aw, bw, rcond=-1) 

758 if len(rb) > 0: 

759 chisqb = rb[0] 

760 else: 

761 chisqb = 1e30 

762 dofb = sumr - len(Xb) 

763 qb = chisqb/dofb 

764 ispsf2 = (qb < psfChisqCut2b) 

765 q2 = qb 

766 X2 = Xb 

767 log.trace('shifted PSF: new chisq/dof = %g; good? %s', qb, ispsf2) 

768 pkres.psfFit3 = (chisqb, dofb) 

769 

770 # Which one do we keep? 

771 if (((ispsf1 and ispsf2) and (q2 < q1)) or 

772 (ispsf2 and not ispsf1)): 

773 Xpsf = X2 

774 chisq = chisq2 

775 dof = dof2 

776 log.debug('dof %g', dof) 

777 log.trace('Keeping shifted-PSF model') 

778 cx += dx 

779 cy += dy 

780 pkres.psfFitWithDecenter = True 

781 else: 

782 # (arbitrarily set to X1 when neither fits well) 

783 Xpsf = X1 

784 chisq = chisq1 

785 dof = dof1 

786 log.debug('dof %g', dof) 

787 log.trace('Keeping unshifted PSF model') 

788 

789 ispsf = (ispsf1 or ispsf2) 

790 

791 # Save the PSF models in images for posterity. 

792 if debugPsf: 

793 SW, SH = 1+xhi-xlo, 1+yhi-ylo 

794 psfmod = afwImage.ImageF(SW, SH) 

795 psfmod.setXY0(xlo, ylo) 

796 psfderivmodm = afwImage.MaskedImageF(SW, SH) 

797 psfderivmod = psfderivmodm.getImage() 

798 psfderivmod.setXY0(xlo, ylo) 

799 model = afwImage.ImageF(SW, SH) 

800 model.setXY0(xlo, ylo) 

801 for i in range(len(Xpsf)): 

802 for (x, y), v in zip(ipixes, A[:, i]*Xpsf[i]): 

803 ix, iy = int(x), int(y) 

804 model.set(ix, iy, model.get(ix, iy) + float(v)) 

805 if i in [I_psf, I_dx, I_dy]: 

806 psfderivmod.set(ix, iy, psfderivmod.get(ix, iy) + float(v)) 

807 for ii in range(NP): 

808 x, y = ipixes[ii, :] 

809 psfmod.set(int(x), int(y), float(A[ii, I_psf]*Xpsf[I_psf])) 

810 modelfp = afwDet.Footprint(fp.getPeaks().getSchema()) 

811 for (x, y) in ipixes: 

812 modelfp.addSpan(int(y+ylo), int(x+xlo), int(x+xlo)) 

813 modelfp.normalize() 

814 

815 pkres.psfFitDebugPsf0Img = psfimg 

816 pkres.psfFitDebugPsfImg = psfmod 

817 pkres.psfFitDebugPsfDerivImg = psfderivmod 

818 pkres.psfFitDebugPsfModel = model 

819 pkres.psfFitDebugStamp = img.Factory(img, stampbb, True) 

820 pkres.psfFitDebugValidPix = valid # numpy array 

821 pkres.psfFitDebugVar = varimg.Factory(varimg, stampbb, True) 

822 ww = np.zeros(valid.shape, np.float) 

823 ww[valid] = w 

824 pkres.psfFitDebugWeight = ww # numpy 

825 pkres.psfFitDebugRampWeight = rw 

826 

827 # Save things we learned about this peak for posterity... 

828 pkres.psfFitR0 = R0 

829 pkres.psfFitR1 = R1 

830 pkres.psfFitStampExtent = (xlo, xhi, ylo, yhi) 

831 pkres.psfFitCenter = (cx, cy) 

832 log.debug('saving chisq,dof %g %g', chisq, dof) 

833 pkres.psfFitBest = (chisq, dof) 

834 pkres.psfFitParams = Xpsf 

835 pkres.psfFitFlux = Xpsf[I_psf] 

836 pkres.psfFitNOthers = len(otherpeaks) 

837 

838 if ispsf: 

839 pkres.setDeblendedAsPsf() 

840 

841 # replace the template image by the PSF + derivatives 

842 # image. 

843 log.trace('Deblending as PSF; setting template to PSF model') 

844 

845 # Instantiate the PSF model and clip it to the footprint 

846 psfimg = psf.computeImage(cx, cy) 

847 # Scale by fit flux. 

848 psfimg *= Xpsf[I_psf] 

849 psfimg = psfimg.convertF() 

850 

851 # Clip the Footprint to the PSF model image bbox. 

852 fpcopy = afwDet.Footprint(fp) 

853 psfbb = psfimg.getBBox() 

854 fpcopy.clipTo(psfbb) 

855 bb = fpcopy.getBBox() 

856 

857 # Copy the part of the PSF model within the clipped footprint. 

858 psfmod = afwImage.ImageF(bb) 

859 fpcopy.spans.copyImage(psfimg, psfmod) 

860 # Save it as our template. 

861 clipFootprintToNonzeroImpl(fpcopy, psfmod) 

862 pkres.setTemplate(psfmod, fpcopy) 

863 

864 # DEBUG 

865 pkres.setPsfTemplate(psfmod, fpcopy) 

866 

867 return ispsf 

868 

869 

870def buildSymmetricTemplates(debResult, log, patchEdges=False, setOrigTemplate=True): 

871 """Build a symmetric template for each peak in each filter 

872 

873 Given ``maskedImageF``, ``footprint``, and a ``DebldendedPeak``, creates a symmetric template 

874 (``templateImage`` and ``templateFootprint``) around the peak for all peaks not flagged as 

875 ``skip`` or ``deblendedAsPsf``. 

876 

877 Parameters 

878 ---------- 

879 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

880 Container for the final deblender results. 

881 log: `log.Log` 

882 LSST logger for logging purposes. 

883 patchEdges: `bool`, optional 

884 If True and if the parent Footprint touches pixels with the ``EDGE`` bit set, 

885 then grow the parent Footprint to include all symmetric templates. 

886 

887 Returns 

888 ------- 

889 modified: `bool` 

890 If any peaks are not skipped or marked as point sources, ``modified`` is ``True. 

891 Otherwise ``modified`` is ``False``. 

892 """ 

893 modified = False 

894 # Create the Templates for each peak in each filter 

895 for fidx in debResult.filters: 

896 dp = debResult.deblendedParents[fidx] 

897 imbb = dp.img.getBBox() 

898 log.trace('Creating templates for footprint at x0,y0,W,H = %i, %i, %i, %i)', dp.x0, dp.y0, dp.W, dp.H) 

899 

900 for peaki, pkres in enumerate(dp.peaks): 

901 log.trace('Deblending peak %i of %i', peaki, len(dp.peaks)) 

902 # TODO: Check debResult to see if the peak is deblended as a point source 

903 # when comparing all bands, not just a single band 

904 if pkres.skip or pkres.deblendedAsPsf: 

905 continue 

906 modified = True 

907 pk = pkres.peak 

908 cx, cy = pk.getIx(), pk.getIy() 

909 if not imbb.contains(geom.Point2I(cx, cy)): 

910 log.trace('Peak center is not inside image; skipping %i', pkres.pki) 

911 pkres.setOutOfBounds() 

912 continue 

913 log.trace('computing template for peak %i at (%i, %i)', pkres.pki, cx, cy) 

914 timg, tfoot, patched = bUtils.buildSymmetricTemplate(dp.maskedImage, dp.fp, pk, dp.avgNoise, 

915 True, patchEdges) 

916 if timg is None: 

917 log.trace('Peak %i at (%i, %i): failed to build symmetric template', pkres.pki, cx, cy) 

918 pkres.setFailedSymmetricTemplate() 

919 continue 

920 

921 if patched: 

922 pkres.setPatched() 

923 

924 # possibly save the original symmetric template 

925 if setOrigTemplate: 

926 pkres.setOrigTemplate(timg, tfoot) 

927 pkres.setTemplate(timg, tfoot) 

928 return modified 

929 

930 

931def rampFluxAtEdge(debResult, log, patchEdges=False): 

932 """Adjust flux on the edges of the template footprints. 

933 

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

935 is grown by the ``psffwhm``*1.5 and filled in with ramped pixels. 

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

937 

938 Parameters 

939 ---------- 

940 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

941 Container for the final deblender results. 

942 log: `log.Log` 

943 LSST logger for logging purposes. 

944 patchEdges: `bool`, optional 

945 If True and if the parent Footprint touches pixels with the ``EDGE`` bit set, 

946 then grow the parent Footprint to include all symmetric templates. 

947 

948 Returns 

949 ------- 

950 modified: `bool` 

951 If any peaks have their templates modified to include flux at the edges, 

952 ``modified`` is ``True``. 

953 """ 

954 modified = False 

955 # Loop over all filters 

956 for fidx in debResult.filters: 

957 dp = debResult.deblendedParents[fidx] 

958 log.trace('Checking for significant flux at edge: sigma1=%g', dp.avgNoise) 

959 

960 for peaki, pkres in enumerate(dp.peaks): 

961 if pkres.skip or pkres.deblendedAsPsf: 

962 continue 

963 timg, tfoot = pkres.templateImage, pkres.templateFootprint 

964 if bUtils.hasSignificantFluxAtEdge(timg, tfoot, 3*dp.avgNoise): 

965 log.trace("Template %i has significant flux at edge: ramping", pkres.pki) 

966 try: 

967 (timg2, tfoot2, patched) = _handle_flux_at_edge(log, dp.psffwhm, timg, tfoot, dp.fp, 

968 dp.maskedImage, dp.x0, dp.x1, 

969 dp.y0, dp.y1, dp.psf, pkres.peak, 

970 dp.avgNoise, patchEdges) 

971 except lsst.pex.exceptions.Exception as exc: 

972 if (isinstance(exc, lsst.pex.exceptions.InvalidParameterError) and 

973 "CoaddPsf" in str(exc)): 

974 pkres.setOutOfBounds() 

975 continue 

976 raise 

977 pkres.setRampedTemplate(timg2, tfoot2) 

978 if patched: 

979 pkres.setPatched() 

980 pkres.setTemplate(timg2, tfoot2) 

981 modified = True 

982 return modified 

983 

984 

985def _handle_flux_at_edge(log, psffwhm, t1, tfoot, fp, maskedImage, 

986 x0, x1, y0, y1, psf, pk, sigma1, patchEdges): 

987 """Extend a template by the PSF to fill in the footprint. 

988 

989 Using the PSF, a footprint that touches the edge is passed to the function 

990 and is grown by the psffwhm*1.5 and filled in with ramped pixels. 

991 

992 Parameters 

993 ---------- 

994 log: `log.Log` 

995 LSST logger for logging purposes. 

996 psffwhm: `float` 

997 PSF FWHM in pixels. 

998 t1: `afw.image.ImageF` 

999 The image template that contains the footprint to extend. 

1000 tfoot: `afw.detection.Footprint` 

1001 Symmetric Footprint to extend. 

1002 fp: `afw.detection.Footprint` 

1003 Parent Footprint that is being deblended. 

1004 maskedImage: `afw.image.MaskedImageF` 

1005 Full MaskedImage containing the parent footprint ``fp``. 

1006 x0,y0: `init` 

1007 Minimum x,y for the bounding box of the footprint ``fp``. 

1008 x1,y1: `int` 

1009 Maximum x,y for the bounding box of the footprint ``fp``. 

1010 psf: `afw.detection.Psf` 

1011 PSF of the image. 

1012 pk: `afw.detection.PeakRecord` 

1013 The peak within the Footprint whose footprint is being extended. 

1014 sigma1: `float` 

1015 Estimated noise level in the image. 

1016 patchEdges: `bool` 

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

1018 ``EDGE`` bit set, then for spans whose symmetric mirror are outside the 

1019 image, the symmetric footprint is grown to include them and their 

1020 pixel values are stored. 

1021 

1022 Results 

1023 ------- 

1024 t2: `afw.image.ImageF` 

1025 Image of the extended footprint. 

1026 tfoot2: `afw.detection.Footprint` 

1027 Extended Footprint. 

1028 patched: `bool` 

1029 If the footprint touches an edge pixel, ``patched`` will be set to ``True``. 

1030 Otherwise ``patched`` is ``False``. 

1031 """ 

1032 log.trace('Found significant flux at template edge.') 

1033 # Compute the max of: 

1034 # -symmetric-template-clipped image * PSF 

1035 # -footprint-clipped image 

1036 # Ie, extend the template by the PSF and "fill in" the footprint. 

1037 # Then find the symmetric template of that image. 

1038 

1039 # The size we'll grow by 

1040 S = psffwhm*1.5 

1041 # make it an odd integer 

1042 S = int((S + 0.5)/2)*2 + 1 

1043 

1044 tbb = tfoot.getBBox() 

1045 tbb.grow(S) 

1046 

1047 # (footprint+margin)-clipped image; 

1048 # we need the pixels OUTSIDE the footprint to be 0. 

1049 fpcopy = afwDet.Footprint(fp) 

1050 fpcopy.dilate(S) 

1051 fpcopy.setSpans(fpcopy.spans.clippedTo(tbb)) 

1052 fpcopy.removeOrphanPeaks() 

1053 padim = maskedImage.Factory(tbb) 

1054 fpcopy.spans.clippedTo(maskedImage.getBBox()).copyMaskedImage(maskedImage, padim) 

1055 

1056 # find pixels on the edge of the template 

1057 edgepix = bUtils.getSignificantEdgePixels(t1, tfoot, -1e6) 

1058 

1059 # instantiate PSF image 

1060 xc = int((x0 + x1)/2) 

1061 yc = int((y0 + y1)/2) 

1062 psfim = psf.computeImage(geom.Point2D(xc, yc)) 

1063 pbb = psfim.getBBox() 

1064 # shift PSF image to be centered on zero 

1065 lx, ly = pbb.getMinX(), pbb.getMinY() 

1066 psfim.setXY0(lx - xc, ly - yc) 

1067 pbb = psfim.getBBox() 

1068 # clip PSF to S, if necessary 

1069 Sbox = geom.Box2I(geom.Point2I(-S, -S), geom.Extent2I(2*S+1, 2*S+1)) 

1070 if not Sbox.contains(pbb): 

1071 # clip PSF image 

1072 psfim = psfim.Factory(psfim, Sbox, afwImage.PARENT, True) 

1073 pbb = psfim.getBBox() 

1074 px0 = pbb.getMinX() 

1075 px1 = pbb.getMaxX() 

1076 py0 = pbb.getMinY() 

1077 py1 = pbb.getMaxY() 

1078 

1079 # Compute the ramped-down edge pixels 

1080 ramped = t1.Factory(tbb) 

1081 Tout = ramped.getArray() 

1082 Tin = t1.getArray() 

1083 tx0, ty0 = t1.getX0(), t1.getY0() 

1084 ox0, oy0 = ramped.getX0(), ramped.getY0() 

1085 P = psfim.getArray() 

1086 P /= P.max() 

1087 # For each edge pixel, Tout = max(Tout, edgepix * PSF) 

1088 for span in edgepix.getSpans(): 

1089 y = span.getY() 

1090 for x in range(span.getX0(), span.getX1()+1): 

1091 slc = (slice(y+py0 - oy0, y+py1+1 - oy0), 

1092 slice(x+px0 - ox0, x+px1+1 - ox0)) 

1093 Tout[slc] = np.maximum(Tout[slc], Tin[y-ty0, x-tx0]*P) 

1094 

1095 # Fill in the "padim" (which has the right variance and 

1096 # mask planes) with the ramped pixels, outside the footprint 

1097 imZeros = (padim.getImage().getArray() == 0) 

1098 padim.getImage().getArray()[imZeros] = ramped.getArray()[imZeros] 

1099 

1100 t2, tfoot2, patched = bUtils.buildSymmetricTemplate(padim, fpcopy, pk, sigma1, True, patchEdges) 

1101 

1102 # This template footprint may extend outside the parent 

1103 # footprint -- or the image. Clip it. 

1104 # NOTE that this may make it asymmetric, unlike normal templates. 

1105 imbb = maskedImage.getBBox() 

1106 tfoot2.clipTo(imbb) 

1107 tbb = tfoot2.getBBox() 

1108 # clip template image to bbox 

1109 t2 = t2.Factory(t2, tbb, afwImage.PARENT, True) 

1110 

1111 return t2, tfoot2, patched 

1112 

1113 

1114def medianSmoothTemplates(debResult, log, medianFilterHalfsize=2): 

1115 """Applying median smoothing filter to the template images for every peak in every filter. 

1116 

1117 Parameters 

1118 ---------- 

1119 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1120 Container for the final deblender results. 

1121 log: `log.Log` 

1122 LSST logger for logging purposes. 

1123 medianFilterHalfSize: `int`, optional 

1124 Half the box size of the median filter, i.e. a ``medianFilterHalfSize`` of 50 means that 

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

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

1127 

1128 Returns 

1129 ------- 

1130 modified: `bool` 

1131 Whether or not any templates were modified. 

1132 This will be ``True`` as long as there is at least one source that is not flagged as a PSF. 

1133 """ 

1134 modified = False 

1135 # Loop over all filters 

1136 for fidx in debResult.filters: 

1137 dp = debResult.deblendedParents[fidx] 

1138 for peaki, pkres in enumerate(dp.peaks): 

1139 if pkres.skip or pkres.deblendedAsPsf: 

1140 continue 

1141 modified = True 

1142 timg, tfoot = pkres.templateImage, pkres.templateFootprint 

1143 filtsize = medianFilterHalfsize*2 + 1 

1144 if timg.getWidth() >= filtsize and timg.getHeight() >= filtsize: 

1145 log.trace('Median filtering template %i', pkres.pki) 

1146 # We want the output to go in "t1", so copy it into 

1147 # "inimg" for input 

1148 inimg = timg.Factory(timg, True) 

1149 bUtils.medianFilter(inimg, timg, medianFilterHalfsize) 

1150 # possible save this median-filtered template 

1151 pkres.setMedianFilteredTemplate(timg, tfoot) 

1152 else: 

1153 log.trace('Not median-filtering template %i: size %i x %i smaller than required %i x %i', 

1154 pkres.pki, timg.getWidth(), timg.getHeight(), filtsize, filtsize) 

1155 pkres.setTemplate(timg, tfoot) 

1156 return modified 

1157 

1158 

1159def makeTemplatesMonotonic(debResult, log): 

1160 """Make the templates monotonic. 

1161 

1162 The pixels in the templates are modified such that pixels further from the peak will 

1163 have values smaller than those closer to the peak. 

1164 

1165 Parameters 

1166 ---------- 

1167 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1168 Container for the final deblender results. 

1169 log: `log.Log` 

1170 LSST logger for logging purposes. 

1171 

1172 Returns 

1173 ------- 

1174 modified: `bool` 

1175 Whether or not any templates were modified. 

1176 This will be ``True`` as long as there is at least one source that is not flagged as a PSF. 

1177 """ 

1178 modified = False 

1179 # Loop over all filters 

1180 for fidx in debResult.filters: 

1181 dp = debResult.deblendedParents[fidx] 

1182 for peaki, pkres in enumerate(dp.peaks): 

1183 if pkres.skip or pkres.deblendedAsPsf: 

1184 continue 

1185 modified = True 

1186 timg, tfoot = pkres.templateImage, pkres.templateFootprint 

1187 pk = pkres.peak 

1188 log.trace('Making template %i monotonic', pkres.pki) 

1189 bUtils.makeMonotonic(timg, pk) 

1190 pkres.setTemplate(timg, tfoot) 

1191 return modified 

1192 

1193 

1194def clipFootprintsToNonzero(debResult, log): 

1195 """Clip non-zero spans in the template footprints for every peak in each filter. 

1196 

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

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

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

1200 

1201 Parameters 

1202 ---------- 

1203 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1204 Container for the final deblender results. 

1205 log: `log.Log` 

1206 LSST logger for logging purposes. 

1207 

1208 Returns 

1209 ------- 

1210 modified: `bool` 

1211 Whether or not any templates were modified. 

1212 This will be ``True`` as long as there is at least one source that is not flagged as a PSF. 

1213 """ 

1214 # Loop over all filters 

1215 for fidx in debResult.filters: 

1216 dp = debResult.deblendedParents[fidx] 

1217 for peaki, pkres in enumerate(dp.peaks): 

1218 if pkres.skip or pkres.deblendedAsPsf: 

1219 continue 

1220 timg, tfoot = pkres.templateImage, pkres.templateFootprint 

1221 clipFootprintToNonzeroImpl(tfoot, timg) 

1222 if not tfoot.getBBox().isEmpty() and tfoot.getBBox() != timg.getBBox(afwImage.PARENT): 

1223 timg = timg.Factory(timg, tfoot.getBBox(), afwImage.PARENT, True) 

1224 pkres.setTemplate(timg, tfoot) 

1225 return False 

1226 

1227 

1228def weightTemplates(debResult, log): 

1229 """Weight the templates to best fit the observed image in each filter 

1230 

1231 This function re-weights the templates so that their linear combination best represents 

1232 the observed image in that filter. 

1233 In the future it may be useful to simultaneously weight all of the filters together. 

1234 

1235 Parameters 

1236 ---------- 

1237 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1238 Container for the final deblender results. 

1239 log: `log.Log` 

1240 LSST logger for logging purposes. 

1241 

1242 Returns 

1243 ------- 

1244 modified: `bool` 

1245 ``weightTemplates`` does not actually modify the ``Footprint`` templates other than 

1246 to add a weight to them, so ``modified`` is always ``False``. 

1247 """ 

1248 # Weight the templates by doing a least-squares fit to the image 

1249 log.trace('Weighting templates') 

1250 for fidx in debResult.filters: 

1251 _weightTemplates(debResult.deblendedParents[fidx]) 

1252 return False 

1253 

1254 

1255def _weightTemplates(dp): 

1256 """Weight the templates to best match the parent Footprint in a single filter 

1257 

1258 This includes weighting both regular templates and point source templates 

1259 

1260 Parameter 

1261 --------- 

1262 dp: `DeblendedParent` 

1263 The deblended parent to re-weight 

1264 

1265 Returns 

1266 ------- 

1267 None 

1268 """ 

1269 nchild = np.sum([pkres.skip is False for pkres in dp.peaks]) 

1270 A = np.zeros((dp.W*dp.H, nchild)) 

1271 parentImage = afwImage.ImageF(dp.bb) 

1272 afwDet.copyWithinFootprintImage(dp.fp, dp.img, parentImage) 

1273 b = parentImage.getArray().ravel() 

1274 

1275 index = 0 

1276 for pkres in dp.peaks: 

1277 if pkres.skip: 

1278 continue 

1279 childImage = afwImage.ImageF(dp.bb) 

1280 afwDet.copyWithinFootprintImage(dp.fp, pkres.templateImage, childImage) 

1281 A[:, index] = childImage.getArray().ravel() 

1282 index += 1 

1283 

1284 X1, r1, rank1, s1 = np.linalg.lstsq(A, b, rcond=-1) 

1285 del A 

1286 del b 

1287 

1288 index = 0 

1289 for pkres in dp.peaks: 

1290 if pkres.skip: 

1291 continue 

1292 pkres.templateImage *= X1[index] 

1293 pkres.setTemplateWeight(X1[index]) 

1294 index += 1 

1295 

1296 

1297def reconstructTemplates(debResult, log, maxTempDotProd=0.5): 

1298 """Remove "degenerate templates" 

1299 

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

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

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

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

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

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

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

1307 

1308 Parameters 

1309 ---------- 

1310 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1311 Container for the final deblender results. 

1312 log: `log.Log` 

1313 LSST logger for logging purposes. 

1314 maxTempDotProd: `float`, optional 

1315 All dot products between templates greater than ``maxTempDotProd`` will result in one 

1316 of the templates removed. 

1317 

1318 Returns 

1319 ------- 

1320 modified: `bool` 

1321 If any degenerate templates are found, ``modified`` is ``True``. 

1322 """ 

1323 log.trace('Looking for degnerate templates') 

1324 

1325 foundReject = False 

1326 for fidx in debResult.filters: 

1327 dp = debResult.deblendedParents[fidx] 

1328 nchild = np.sum([pkres.skip is False for pkres in dp.peaks]) 

1329 indexes = [pkres.pki for pkres in dp.peaks if pkres.skip is False] 

1330 

1331 # We build a matrix that stores the dot product between templates. 

1332 # We convert the template images to HeavyFootprints because they already have a method 

1333 # to compute the dot product. 

1334 A = np.zeros((nchild, nchild)) 

1335 maxTemplate = [] 

1336 heavies = [] 

1337 for pkres in dp.peaks: 

1338 if pkres.skip: 

1339 continue 

1340 heavies.append(afwDet.makeHeavyFootprint(pkres.templateFootprint, 

1341 afwImage.MaskedImageF(pkres.templateImage))) 

1342 maxTemplate.append(np.max(pkres.templateImage.getArray())) 

1343 

1344 for i in range(nchild): 

1345 for j in range(i + 1): 

1346 A[i, j] = heavies[i].dot(heavies[j]) 

1347 

1348 # Normalize the dot products to get the cosine of the angle between templates 

1349 for i in range(nchild): 

1350 for j in range(i): 

1351 norm = A[i, i]*A[j, j] 

1352 if norm <= 0: 

1353 A[i, j] = 0 

1354 else: 

1355 A[i, j] /= np.sqrt(norm) 

1356 

1357 # Iterate over pairs of objects and find the maximum non-diagonal element of the matrix. 

1358 # Exit the loop once we find a single degenerate pair greater than the threshold. 

1359 rejectedIndex = -1 

1360 for i in range(nchild): 

1361 currentMax = 0. 

1362 for j in range(i): 

1363 if A[i, j] > currentMax: 

1364 currentMax = A[i, j] 

1365 if currentMax > maxTempDotProd: 

1366 foundReject = True 

1367 rejectedIndex = j 

1368 

1369 if foundReject: 

1370 break 

1371 

1372 del A 

1373 

1374 # If one of the objects is identified as a PSF keep the other one, otherwise keep the one 

1375 # with the maximum template value 

1376 if foundReject: 

1377 keep = indexes[i] 

1378 reject = indexes[rejectedIndex] 

1379 if dp.peaks[keep].deblendedAsPsf and dp.peaks[reject].deblendedAsPsf is False: 

1380 keep = indexes[rejectedIndex] 

1381 reject = indexes[i] 

1382 elif dp.peaks[keep].deblendedAsPsf is False and dp.peaks[reject].deblendedAsPsf: 

1383 reject = indexes[rejectedIndex] 

1384 keep = indexes[i] 

1385 else: 

1386 if maxTemplate[rejectedIndex] > maxTemplate[i]: 

1387 keep = indexes[rejectedIndex] 

1388 reject = indexes[i] 

1389 log.trace('Removing object with index %d : %f. Degenerate with %d' % (reject, currentMax, 

1390 keep)) 

1391 dp.peaks[reject].skip = True 

1392 dp.peaks[reject].degenerate = True 

1393 

1394 return foundReject 

1395 

1396 

1397def apportionFlux(debResult, log, assignStrayFlux=True, strayFluxAssignment='r-to-peak', 

1398 strayFluxToPointSources='necessary', clipStrayFluxFraction=0.001, 

1399 getTemplateSum=False): 

1400 """Apportion flux to all of the peak templates in each filter 

1401 

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

1403 flux assigned to each ``template``. 

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

1405 

1406 Parameters 

1407 ---------- 

1408 debResult: `lsst.meas.deblender.baseline.DeblenderResult` 

1409 Container for the final deblender results. 

1410 log: `log.Log` 

1411 LSST logger for logging purposes. 

1412 assignStrayFlux: `bool`, optional 

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

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

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

1416 strayFluxAssignment: `string`, optional 

1417 Determines how stray flux is apportioned. 

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

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

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

1421 minimum distance from the stray flux to footprint 

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

1423 distance to the stray flux 

1424 strayFluxToPointSources: `string`, optional 

1425 Determines how stray flux is apportioned to point sources 

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

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

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

1429 clipStrayFluxFraction: `float`, optional 

1430 Minimum stray-flux portion. 

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

1432 getTemplateSum: `bool`, optional 

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

1434 If ``getTemplateSum==True`` then the sum of the templates is stored in the result 

1435 (a `DeblendedFootprint`). 

1436 

1437 Returns 

1438 ------- 

1439 modified: `bool` 

1440 Apportion flux always modifies the templates, so ``modified`` is always ``True``. 

1441 However, this should likely be the final step and it is unlikely that 

1442 any deblender plugins will be re-run. 

1443 """ 

1444 validStrayPtSrc = ['never', 'necessary', 'always'] 

1445 validStrayAssign = ['r-to-peak', 'r-to-footprint', 'nearest-footprint', 'trim'] 

1446 if strayFluxToPointSources not in validStrayPtSrc: 

1447 raise ValueError((('strayFluxToPointSources: value \"%s\" not in the set of allowed values: ') % 

1448 strayFluxToPointSources) + str(validStrayPtSrc)) 

1449 if strayFluxAssignment not in validStrayAssign: 

1450 raise ValueError((('strayFluxAssignment: value \"%s\" not in the set of allowed values: ') % 

1451 strayFluxAssignment) + str(validStrayAssign)) 

1452 

1453 for fidx in debResult.filters: 

1454 dp = debResult.deblendedParents[fidx] 

1455 # Prepare inputs to "apportionFlux" call. 

1456 # template maskedImages 

1457 tmimgs = [] 

1458 # template footprints 

1459 tfoots = [] 

1460 # deblended as psf 

1461 dpsf = [] 

1462 # peak x,y 

1463 pkx = [] 

1464 pky = [] 

1465 # indices of valid templates 

1466 ibi = [] 

1467 bb = dp.fp.getBBox() 

1468 

1469 for peaki, pkres in enumerate(dp.peaks): 

1470 if pkres.skip: 

1471 continue 

1472 tmimgs.append(pkres.templateImage) 

1473 tfoots.append(pkres.templateFootprint) 

1474 # for stray flux... 

1475 dpsf.append(pkres.deblendedAsPsf) 

1476 pk = pkres.peak 

1477 pkx.append(pk.getIx()) 

1478 pky.append(pk.getIy()) 

1479 ibi.append(pkres.pki) 

1480 

1481 # Now apportion flux according to the templates 

1482 log.trace('Apportioning flux among %i templates', len(tmimgs)) 

1483 sumimg = afwImage.ImageF(bb) 

1484 # .getDimensions()) 

1485 # sumimg.setXY0(bb.getMinX(), bb.getMinY()) 

1486 

1487 strayopts = 0 

1488 if strayFluxAssignment == 'trim': 

1489 assignStrayFlux = False 

1490 strayopts |= bUtils.STRAYFLUX_TRIM 

1491 if assignStrayFlux: 

1492 strayopts |= bUtils.ASSIGN_STRAYFLUX 

1493 if strayFluxToPointSources == 'necessary': 

1494 strayopts |= bUtils.STRAYFLUX_TO_POINT_SOURCES_WHEN_NECESSARY 

1495 elif strayFluxToPointSources == 'always': 

1496 strayopts |= bUtils.STRAYFLUX_TO_POINT_SOURCES_ALWAYS 

1497 

1498 if strayFluxAssignment == 'r-to-peak': 

1499 # this is the default 

1500 pass 

1501 elif strayFluxAssignment == 'r-to-footprint': 

1502 strayopts |= bUtils.STRAYFLUX_R_TO_FOOTPRINT 

1503 elif strayFluxAssignment == 'nearest-footprint': 

1504 strayopts |= bUtils.STRAYFLUX_NEAREST_FOOTPRINT 

1505 

1506 portions, strayflux = bUtils.apportionFlux(dp.maskedImage, dp.fp, tmimgs, tfoots, sumimg, dpsf, 

1507 pkx, pky, strayopts, clipStrayFluxFraction) 

1508 

1509 # Shrink parent to union of children 

1510 if strayFluxAssignment == 'trim': 

1511 finalSpanSet = afwGeom.SpanSet() 

1512 for foot in tfoots: 

1513 finalSpanSet = finalSpanSet.union(foot.spans) 

1514 dp.fp.setSpans(finalSpanSet) 

1515 

1516 # Store the template sum in the deblender result 

1517 if getTemplateSum: 

1518 debResult.setTemplateSums(sumimg, fidx) 

1519 

1520 # Save the apportioned fluxes 

1521 ii = 0 

1522 for j, (pk, pkres) in enumerate(zip(dp.fp.getPeaks(), dp.peaks)): 

1523 if pkres.skip: 

1524 continue 

1525 pkres.setFluxPortion(portions[ii]) 

1526 

1527 if assignStrayFlux: 

1528 # NOTE that due to a swig bug (https://github.com/swig/swig/issues/59) 

1529 # we CANNOT iterate over "strayflux", but must index into it. 

1530 stray = strayflux[ii] 

1531 else: 

1532 stray = None 

1533 ii += 1 

1534 

1535 pkres.setStrayFlux(stray) 

1536 

1537 # Set child footprints to contain the right number of peaks. 

1538 for j, (pk, pkres) in enumerate(zip(dp.fp.getPeaks(), dp.peaks)): 

1539 if pkres.skip: 

1540 continue 

1541 

1542 for foot, add in [(pkres.templateFootprint, True), (pkres.origFootprint, True), 

1543 (pkres.strayFlux, False)]: 

1544 if foot is None: 

1545 continue 

1546 pks = foot.getPeaks() 

1547 pks.clear() 

1548 if add: 

1549 pks.append(pk) 

1550 return True