Coverage for python/lsst/ip/diffim/imagePsfMatch.py: 13%

277 statements  

« prev     ^ index     » next       coverage.py v7.2.4, created at 2023-04-29 11:38 +0000

1# This file is part of ip_diffim. 

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 lsst.daf.base as dafBase 

25import lsst.pex.config as pexConfig 

26import lsst.afw.detection as afwDetect 

27import lsst.afw.image as afwImage 

28import lsst.afw.math as afwMath 

29import lsst.afw.geom as afwGeom 

30import lsst.afw.table as afwTable 

31import lsst.geom as geom 

32import lsst.pipe.base as pipeBase 

33from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask, WarpedPsf 

34from lsst.meas.base import SingleFrameMeasurementTask 

35from .makeKernelBasisList import makeKernelBasisList 

36from .psfMatch import PsfMatchTask, PsfMatchConfigDF, PsfMatchConfigAL 

37from . import utils as diffimUtils 

38from . import diffimLib 

39from . import diffimTools 

40import lsst.afw.display as afwDisplay 

41from lsst.utils.timer import timeMethod 

42 

43__all__ = ["ImagePsfMatchConfig", "ImagePsfMatchTask", "subtractAlgorithmRegistry"] 

44 

45sigma2fwhm = 2.*np.sqrt(2.*np.log(2.)) 

46 

47 

48class ImagePsfMatchConfig(pexConfig.Config): 

49 """Configuration for image-to-image Psf matching. 

50 """ 

51 kernel = pexConfig.ConfigChoiceField( 

52 doc="kernel type", 

53 typemap=dict( 

54 AL=PsfMatchConfigAL, 

55 DF=PsfMatchConfigDF 

56 ), 

57 default="AL", 

58 ) 

59 selectDetection = pexConfig.ConfigurableField( 

60 target=SourceDetectionTask, 

61 doc="Initial detections used to feed stars to kernel fitting", 

62 ) 

63 selectMeasurement = pexConfig.ConfigurableField( 

64 target=SingleFrameMeasurementTask, 

65 doc="Initial measurements used to feed stars to kernel fitting", 

66 ) 

67 

68 def setDefaults(self): 

69 # High sigma detections only 

70 self.selectDetection.reEstimateBackground = False 

71 self.selectDetection.thresholdValue = 10.0 

72 

73 # Minimal set of measurments for star selection 

74 self.selectMeasurement.algorithms.names.clear() 

75 self.selectMeasurement.algorithms.names = ('base_SdssCentroid', 'base_PsfFlux', 'base_PixelFlags', 

76 'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord') 

77 self.selectMeasurement.slots.modelFlux = None 

78 self.selectMeasurement.slots.apFlux = None 

79 self.selectMeasurement.slots.calibFlux = None 

80 

81 

82class ImagePsfMatchTask(PsfMatchTask): 

83 """Psf-match two MaskedImages or Exposures using the sources in the images. 

84 

85 Parameters 

86 ---------- 

87 args : 

88 Arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__ 

89 kwargs : 

90 Keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__ 

91 

92 """ 

93 

94 ConfigClass = ImagePsfMatchConfig 

95 

96 def __init__(self, *args, **kwargs): 

97 """Create the ImagePsfMatchTask. 

98 """ 

99 PsfMatchTask.__init__(self, *args, **kwargs) 

100 self.kConfig = self.config.kernel.active 

101 self._warper = afwMath.Warper.fromConfig(self.kConfig.warpingConfig) 

102 # the background subtraction task uses a config from an unusual location, 

103 # so cannot easily be constructed with makeSubtask 

104 self.background = SubtractBackgroundTask(config=self.kConfig.afwBackgroundConfig, name="background", 

105 parentTask=self) 

106 self.selectSchema = afwTable.SourceTable.makeMinimalSchema() 

107 self.selectAlgMetadata = dafBase.PropertyList() 

108 self.makeSubtask("selectDetection", schema=self.selectSchema) 

109 self.makeSubtask("selectMeasurement", schema=self.selectSchema, algMetadata=self.selectAlgMetadata) 

110 

111 @timeMethod 

112 def matchExposures(self, templateExposure, scienceExposure, 

113 templateFwhmPix=None, scienceFwhmPix=None, 

114 candidateList=None, doWarping=True, convolveTemplate=True): 

115 """Warp and PSF-match an exposure to the reference. 

116 

117 Do the following, in order: 

118 

119 - Warp templateExposure to match scienceExposure, 

120 if doWarping True and their WCSs do not already match 

121 - Determine a PSF matching kernel and differential background model 

122 that matches templateExposure to scienceExposure 

123 - Convolve templateExposure by PSF matching kernel 

124 

125 Parameters 

126 ---------- 

127 templateExposure : `lsst.afw.image.Exposure` 

128 Exposure to warp and PSF-match to the reference masked image 

129 scienceExposure : `lsst.afw.image.Exposure` 

130 Exposure whose WCS and PSF are to be matched to 

131 templateFwhmPix :`float` 

132 FWHM (in pixels) of the Psf in the template image (image to convolve) 

133 scienceFwhmPix : `float` 

134 FWHM (in pixels) of the Psf in the science image 

135 candidateList : `list`, optional 

136 a list of footprints/maskedImages for kernel candidates; 

137 if `None` then source detection is run. 

138 

139 - Currently supported: list of Footprints or measAlg.PsfCandidateF 

140 

141 doWarping : `bool` 

142 what to do if ``templateExposure`` and ``scienceExposure`` WCSs do not match: 

143 

144 - if `True` then warp ``templateExposure`` to match ``scienceExposure`` 

145 - if `False` then raise an Exception 

146 

147 convolveTemplate : `bool` 

148 Whether to convolve the template image or the science image: 

149 

150 - if `True`, ``templateExposure`` is warped if doWarping, 

151 ``templateExposure`` is convolved 

152 - if `False`, ``templateExposure`` is warped if doWarping, 

153 ``scienceExposure`` is convolved 

154 

155 Returns 

156 ------- 

157 results : `lsst.pipe.base.Struct` 

158 An `lsst.pipe.base.Struct` containing these fields: 

159 

160 - ``matchedImage`` : the PSF-matched exposure = 

161 Warped ``templateExposure`` convolved by psfMatchingKernel. This has: 

162 

163 - the same parent bbox, Wcs and PhotoCalib as scienceExposure 

164 - the same filter as templateExposure 

165 - no Psf (because the PSF-matching process does not compute one) 

166 

167 - ``psfMatchingKernel`` : the PSF matching kernel 

168 - ``backgroundModel`` : differential background model 

169 - ``kernelCellSet`` : SpatialCellSet used to solve for the PSF matching kernel 

170 

171 Raises 

172 ------ 

173 RuntimeError 

174 Raised if doWarping is False and ``templateExposure`` and 

175 ``scienceExposure`` WCSs do not match 

176 """ 

177 if not self._validateWcs(templateExposure, scienceExposure): 

178 if doWarping: 

179 self.log.info("Astrometrically registering template to science image") 

180 templatePsf = templateExposure.getPsf() 

181 # Warp PSF before overwriting exposure 

182 xyTransform = afwGeom.makeWcsPairTransform(templateExposure.getWcs(), 

183 scienceExposure.getWcs()) 

184 psfWarped = WarpedPsf(templatePsf, xyTransform) 

185 templateExposure = self._warper.warpExposure(scienceExposure.getWcs(), 

186 templateExposure, 

187 destBBox=scienceExposure.getBBox()) 

188 templateExposure.setPsf(psfWarped) 

189 else: 

190 self.log.error("ERROR: Input images not registered") 

191 raise RuntimeError("Input images not registered") 

192 

193 if templateFwhmPix is None: 

194 if not templateExposure.hasPsf(): 

195 self.log.warning("No estimate of Psf FWHM for template image") 

196 else: 

197 templateFwhmPix = diffimUtils.getPsfFwhm(templateExposure.psf) 

198 self.log.info("templateFwhmPix: %s", templateFwhmPix) 

199 

200 if scienceFwhmPix is None: 

201 if not scienceExposure.hasPsf(): 

202 self.log.warning("No estimate of Psf FWHM for science image") 

203 else: 

204 scienceFwhmPix = diffimUtils.getPsfFwhm(scienceExposure.psf) 

205 self.log.info("scienceFwhmPix: %s", scienceFwhmPix) 

206 

207 if convolveTemplate: 

208 kernelSize = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix)[0].getWidth() 

209 candidateList = self.makeCandidateList( 

210 templateExposure, scienceExposure, kernelSize, candidateList) 

211 results = self.matchMaskedImages( 

212 templateExposure.getMaskedImage(), scienceExposure.getMaskedImage(), candidateList, 

213 templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix) 

214 else: 

215 kernelSize = self.makeKernelBasisList(scienceFwhmPix, templateFwhmPix)[0].getWidth() 

216 candidateList = self.makeCandidateList( 

217 templateExposure, scienceExposure, kernelSize, candidateList) 

218 results = self.matchMaskedImages( 

219 scienceExposure.getMaskedImage(), templateExposure.getMaskedImage(), candidateList, 

220 templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix) 

221 

222 psfMatchedExposure = afwImage.makeExposure(results.matchedImage, scienceExposure.getWcs()) 

223 psfMatchedExposure.setFilter(templateExposure.getFilter()) 

224 psfMatchedExposure.setPhotoCalib(scienceExposure.getPhotoCalib()) 

225 results.warpedExposure = templateExposure 

226 results.matchedExposure = psfMatchedExposure 

227 return results 

228 

229 @timeMethod 

230 def matchMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList, 

231 templateFwhmPix=None, scienceFwhmPix=None): 

232 """PSF-match a MaskedImage (templateMaskedImage) to a reference MaskedImage (scienceMaskedImage). 

233 

234 Do the following, in order: 

235 

236 - Determine a PSF matching kernel and differential background model 

237 that matches templateMaskedImage to scienceMaskedImage 

238 - Convolve templateMaskedImage by the PSF matching kernel 

239 

240 Parameters 

241 ---------- 

242 templateMaskedImage : `lsst.afw.image.MaskedImage` 

243 masked image to PSF-match to the reference masked image; 

244 must be warped to match the reference masked image 

245 scienceMaskedImage : `lsst.afw.image.MaskedImage` 

246 maskedImage whose PSF is to be matched to 

247 templateFwhmPix : `float` 

248 FWHM (in pixels) of the Psf in the template image (image to convolve) 

249 scienceFwhmPix : `float` 

250 FWHM (in pixels) of the Psf in the science image 

251 candidateList : `list`, optional 

252 A list of footprints/maskedImages for kernel candidates; 

253 if `None` then source detection is run. 

254 

255 - Currently supported: list of Footprints or measAlg.PsfCandidateF 

256 

257 Returns 

258 ------- 

259 result : `callable` 

260 An `lsst.pipe.base.Struct` containing these fields: 

261 

262 - psfMatchedMaskedImage: the PSF-matched masked image = 

263 ``templateMaskedImage`` convolved with psfMatchingKernel. 

264 This has the same xy0, dimensions and wcs as ``scienceMaskedImage``. 

265 - psfMatchingKernel: the PSF matching kernel 

266 - backgroundModel: differential background model 

267 - kernelCellSet: SpatialCellSet used to solve for the PSF matching kernel 

268 

269 Raises 

270 ------ 

271 RuntimeError 

272 Raised if input images have different dimensions 

273 """ 

274 import lsstDebug 

275 display = lsstDebug.Info(__name__).display 

276 displayTemplate = lsstDebug.Info(__name__).displayTemplate 

277 displaySciIm = lsstDebug.Info(__name__).displaySciIm 

278 displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells 

279 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

280 if not maskTransparency: 

281 maskTransparency = 0 

282 if display: 

283 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

284 

285 if not candidateList: 

286 raise RuntimeError("Candidate list must be populated by makeCandidateList") 

287 

288 if not self._validateSize(templateMaskedImage, scienceMaskedImage): 

289 self.log.error("ERROR: Input images different size") 

290 raise RuntimeError("Input images different size") 

291 

292 if display and displayTemplate: 

293 disp = afwDisplay.Display(frame=lsstDebug.frame) 

294 disp.mtv(templateMaskedImage, title="Image to convolve") 

295 lsstDebug.frame += 1 

296 

297 if display and displaySciIm: 

298 disp = afwDisplay.Display(frame=lsstDebug.frame) 

299 disp.mtv(scienceMaskedImage, title="Image to not convolve") 

300 lsstDebug.frame += 1 

301 

302 kernelCellSet = self._buildCellSet(templateMaskedImage, 

303 scienceMaskedImage, 

304 candidateList) 

305 

306 if display and displaySpatialCells: 

307 diffimUtils.showKernelSpatialCells(scienceMaskedImage, kernelCellSet, 

308 symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW, 

309 ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame, 

310 title="Image to not convolve") 

311 lsstDebug.frame += 1 

312 

313 if templateFwhmPix and scienceFwhmPix: 

314 self.log.info("Matching Psf FWHM %.2f -> %.2f pix", templateFwhmPix, scienceFwhmPix) 

315 

316 if self.kConfig.useBicForKernelBasis: 

317 tmpKernelCellSet = self._buildCellSet(templateMaskedImage, 

318 scienceMaskedImage, 

319 candidateList) 

320 nbe = diffimTools.NbasisEvaluator(self.kConfig, templateFwhmPix, scienceFwhmPix) 

321 bicDegrees = nbe(tmpKernelCellSet, self.log) 

322 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

323 basisDegGauss=bicDegrees[0], metadata=self.metadata) 

324 del tmpKernelCellSet 

325 else: 

326 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

327 metadata=self.metadata) 

328 

329 spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList) 

330 

331 psfMatchedMaskedImage = afwImage.MaskedImageF(templateMaskedImage.getBBox()) 

332 convolutionControl = afwMath.ConvolutionControl() 

333 convolutionControl.setDoNormalize(False) 

334 afwMath.convolve(psfMatchedMaskedImage, templateMaskedImage, psfMatchingKernel, convolutionControl) 

335 return pipeBase.Struct( 

336 matchedImage=psfMatchedMaskedImage, 

337 psfMatchingKernel=psfMatchingKernel, 

338 backgroundModel=backgroundModel, 

339 kernelCellSet=kernelCellSet, 

340 ) 

341 

342 @timeMethod 

343 def subtractExposures(self, templateExposure, scienceExposure, 

344 templateFwhmPix=None, scienceFwhmPix=None, 

345 candidateList=None, doWarping=True, convolveTemplate=True): 

346 """Register, Psf-match and subtract two Exposures. 

347 

348 Do the following, in order: 

349 

350 - Warp templateExposure to match scienceExposure, if their WCSs do not already match 

351 - Determine a PSF matching kernel and differential background model 

352 that matches templateExposure to scienceExposure 

353 - PSF-match templateExposure to scienceExposure 

354 - Compute subtracted exposure (see return values for equation). 

355 

356 Parameters 

357 ---------- 

358 templateExposure : `lsst.afw.image.ExposureF` 

359 Exposure to PSF-match to scienceExposure 

360 scienceExposure : `lsst.afw.image.ExposureF` 

361 Reference Exposure 

362 templateFwhmPix : `float` 

363 FWHM (in pixels) of the Psf in the template image (image to convolve) 

364 scienceFwhmPix : `float` 

365 FWHM (in pixels) of the Psf in the science image 

366 candidateList : `list`, optional 

367 A list of footprints/maskedImages for kernel candidates; 

368 if `None` then source detection is run. 

369 

370 - Currently supported: list of Footprints or measAlg.PsfCandidateF 

371 

372 doWarping : `bool` 

373 What to do if ``templateExposure``` and ``scienceExposure`` WCSs do 

374 not match: 

375 

376 - if `True` then warp ``templateExposure`` to match ``scienceExposure`` 

377 - if `False` then raise an Exception 

378 

379 convolveTemplate : `bool` 

380 Convolve the template image or the science image 

381 

382 - if `True`, ``templateExposure`` is warped if doWarping, 

383 ``templateExposure`` is convolved 

384 - if `False`, ``templateExposure`` is warped if doWarping, 

385 ``scienceExposure is`` convolved 

386 

387 Returns 

388 ------- 

389 result : `lsst.pipe.base.Struct` 

390 An `lsst.pipe.base.Struct` containing these fields: 

391 

392 - ``subtractedExposure`` : subtracted Exposure 

393 scienceExposure - (matchedImage + backgroundModel) 

394 - ``matchedImage`` : ``templateExposure`` after warping to match 

395 ``templateExposure`` (if doWarping true), 

396 and convolving with psfMatchingKernel 

397 - ``psfMatchingKernel`` : PSF matching kernel 

398 - ``backgroundModel`` : differential background model 

399 - ``kernelCellSet`` : SpatialCellSet used to determine PSF matching kernel 

400 """ 

401 results = self.matchExposures( 

402 templateExposure=templateExposure, 

403 scienceExposure=scienceExposure, 

404 templateFwhmPix=templateFwhmPix, 

405 scienceFwhmPix=scienceFwhmPix, 

406 candidateList=candidateList, 

407 doWarping=doWarping, 

408 convolveTemplate=convolveTemplate 

409 ) 

410 # Always inherit WCS and photocalib from science exposure 

411 subtractedExposure = afwImage.ExposureF(scienceExposure, deep=True) 

412 # Note, the decorrelation afterburner re-calculates the variance plane 

413 # from the variance planes of the original exposures. 

414 # That recalculation code must be in accordance with the 

415 # photometric level set here in ``subtractedMaskedImage``. 

416 if convolveTemplate: 

417 subtractedMaskedImage = subtractedExposure.maskedImage 

418 subtractedMaskedImage -= results.matchedExposure.maskedImage 

419 subtractedMaskedImage -= results.backgroundModel 

420 else: 

421 subtractedMaskedImage = subtractedExposure.maskedImage 

422 subtractedMaskedImage[:, :] = results.warpedExposure.maskedImage 

423 subtractedMaskedImage -= results.matchedExposure.maskedImage 

424 subtractedMaskedImage -= results.backgroundModel 

425 

426 # Preserve polarity of differences 

427 subtractedMaskedImage *= -1 

428 

429 # Place back on native photometric scale 

430 subtractedMaskedImage /= results.psfMatchingKernel.computeImage( 

431 afwImage.ImageD(results.psfMatchingKernel.getDimensions()), False) 

432 # We matched to the warped template 

433 subtractedExposure.setPsf(results.warpedExposure.getPsf()) 

434 

435 import lsstDebug 

436 display = lsstDebug.Info(__name__).display 

437 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm 

438 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

439 if not maskTransparency: 

440 maskTransparency = 0 

441 if display: 

442 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

443 if display and displayDiffIm: 

444 disp = afwDisplay.Display(frame=lsstDebug.frame) 

445 disp.mtv(templateExposure, title="Template") 

446 lsstDebug.frame += 1 

447 disp = afwDisplay.Display(frame=lsstDebug.frame) 

448 disp.mtv(results.matchedExposure, title="Matched template") 

449 lsstDebug.frame += 1 

450 disp = afwDisplay.Display(frame=lsstDebug.frame) 

451 disp.mtv(scienceExposure, title="Science Image") 

452 lsstDebug.frame += 1 

453 disp = afwDisplay.Display(frame=lsstDebug.frame) 

454 disp.mtv(subtractedExposure, title="Difference Image") 

455 lsstDebug.frame += 1 

456 

457 results.subtractedExposure = subtractedExposure 

458 return results 

459 

460 @timeMethod 

461 def subtractMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList, 

462 templateFwhmPix=None, scienceFwhmPix=None): 

463 """Psf-match and subtract two MaskedImages. 

464 

465 Do the following, in order: 

466 

467 - PSF-match templateMaskedImage to scienceMaskedImage 

468 - Determine the differential background 

469 - Return the difference: scienceMaskedImage 

470 ((warped templateMaskedImage convolved with psfMatchingKernel) + backgroundModel) 

471 

472 Parameters 

473 ---------- 

474 templateMaskedImage : `lsst.afw.image.MaskedImage` 

475 MaskedImage to PSF-match to ``scienceMaskedImage`` 

476 scienceMaskedImage : `lsst.afw.image.MaskedImage` 

477 Reference MaskedImage 

478 templateFwhmPix : `float` 

479 FWHM (in pixels) of the Psf in the template image (image to convolve) 

480 scienceFwhmPix : `float` 

481 FWHM (in pixels) of the Psf in the science image 

482 candidateList : `list`, optional 

483 A list of footprints/maskedImages for kernel candidates; 

484 if `None` then source detection is run. 

485 

486 - Currently supported: list of Footprints or measAlg.PsfCandidateF 

487 

488 Returns 

489 ------- 

490 results : `lsst.pipe.base.Struct` 

491 An `lsst.pipe.base.Struct` containing these fields: 

492 

493 - ``subtractedMaskedImage`` : ``scienceMaskedImage`` - (matchedImage + backgroundModel) 

494 - ``matchedImage`` : templateMaskedImage convolved with psfMatchingKernel 

495 - `psfMatchingKernel`` : PSF matching kernel 

496 - ``backgroundModel`` : differential background model 

497 - ``kernelCellSet`` : SpatialCellSet used to determine PSF matching kernel 

498 

499 """ 

500 if not candidateList: 

501 raise RuntimeError("Candidate list must be populated by makeCandidateList") 

502 

503 results = self.matchMaskedImages( 

504 templateMaskedImage=templateMaskedImage, 

505 scienceMaskedImage=scienceMaskedImage, 

506 candidateList=candidateList, 

507 templateFwhmPix=templateFwhmPix, 

508 scienceFwhmPix=scienceFwhmPix, 

509 ) 

510 

511 subtractedMaskedImage = afwImage.MaskedImageF(scienceMaskedImage, True) 

512 subtractedMaskedImage -= results.matchedImage 

513 subtractedMaskedImage -= results.backgroundModel 

514 results.subtractedMaskedImage = subtractedMaskedImage 

515 

516 import lsstDebug 

517 display = lsstDebug.Info(__name__).display 

518 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm 

519 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

520 if not maskTransparency: 

521 maskTransparency = 0 

522 if display: 

523 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

524 if display and displayDiffIm: 

525 disp = afwDisplay.Display(frame=lsstDebug.frame) 

526 disp.mtv(subtractedMaskedImage, title="Subtracted masked image") 

527 lsstDebug.frame += 1 

528 

529 return results 

530 

531 def getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None): 

532 """Get sources to use for Psf-matching. 

533 

534 This method runs detection and measurement on an exposure. 

535 The returned set of sources will be used as candidates for 

536 Psf-matching. 

537 

538 Parameters 

539 ---------- 

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

541 Exposure on which to run detection/measurement 

542 sigma : `float` 

543 Detection threshold 

544 doSmooth : `bool` 

545 Whether or not to smooth the Exposure with Psf before detection 

546 idFactory : 

547 Factory for the generation of Source ids 

548 

549 Returns 

550 ------- 

551 selectSources : 

552 source catalog containing candidates for the Psf-matching 

553 """ 

554 if idFactory: 

555 table = afwTable.SourceTable.make(self.selectSchema, idFactory) 

556 else: 

557 table = afwTable.SourceTable.make(self.selectSchema) 

558 mi = exposure.getMaskedImage() 

559 

560 imArr = mi.getImage().getArray() 

561 maskArr = mi.getMask().getArray() 

562 miArr = np.ma.masked_array(imArr, mask=maskArr) 

563 try: 

564 fitBg = self.background.fitBackground(mi) 

565 bkgd = fitBg.getImageF(self.background.config.algorithm, 

566 self.background.config.undersampleStyle) 

567 except Exception: 

568 self.log.warning("Failed to get background model. Falling back to median background estimation") 

569 bkgd = np.ma.median(miArr) 

570 

571 # Take off background for detection 

572 mi -= bkgd 

573 try: 

574 table.setMetadata(self.selectAlgMetadata) 

575 detRet = self.selectDetection.run( 

576 table=table, 

577 exposure=exposure, 

578 sigma=sigma, 

579 doSmooth=doSmooth 

580 ) 

581 selectSources = detRet.sources 

582 self.selectMeasurement.run(measCat=selectSources, exposure=exposure) 

583 finally: 

584 # Put back on the background in case it is needed down stream 

585 mi += bkgd 

586 del bkgd 

587 return selectSources 

588 

589 def makeCandidateList(self, templateExposure, scienceExposure, kernelSize, candidateList=None): 

590 """Make a list of acceptable KernelCandidates. 

591 

592 Accept or generate a list of candidate sources for 

593 Psf-matching, and examine the Mask planes in both of the 

594 images for indications of bad pixels 

595 

596 Parameters 

597 ---------- 

598 templateExposure : `lsst.afw.image.Exposure` 

599 Exposure that will be convolved 

600 scienceExposure : `lsst.afw.image.Exposure` 

601 Exposure that will be matched-to 

602 kernelSize : `float` 

603 Dimensions of the Psf-matching Kernel, used to grow detection footprints 

604 candidateList : `list`, optional 

605 List of Sources to examine. Elements must be of type afw.table.Source 

606 or a type that wraps a Source and has a getSource() method, such as 

607 meas.algorithms.PsfCandidateF. 

608 

609 Returns 

610 ------- 

611 candidateList : `list` of `dict` 

612 A list of dicts having a "source" and "footprint" 

613 field for the Sources deemed to be appropriate for Psf 

614 matching 

615 """ 

616 if candidateList is None: 

617 candidateList = self.getSelectSources(scienceExposure) 

618 

619 if len(candidateList) < 1: 

620 raise RuntimeError("No candidates in candidateList") 

621 

622 listTypes = set(type(x) for x in candidateList) 

623 if len(listTypes) > 1: 

624 raise RuntimeError("Candidate list contains mixed types: %s" % [t for t in listTypes]) 

625 

626 if not isinstance(candidateList[0], afwTable.SourceRecord): 

627 try: 

628 candidateList[0].getSource() 

629 except Exception as e: 

630 raise RuntimeError(f"Candidate List is of type: {type(candidateList[0])} " 

631 "Can only make candidate list from list of afwTable.SourceRecords, " 

632 f"measAlg.PsfCandidateF or other type with a getSource() method: {e}") 

633 candidateList = [c.getSource() for c in candidateList] 

634 

635 candidateList = diffimTools.sourceToFootprintList(candidateList, 

636 templateExposure, scienceExposure, 

637 kernelSize, 

638 self.kConfig.detectionConfig, 

639 self.log) 

640 if len(candidateList) == 0: 

641 raise RuntimeError("Cannot find any objects suitable for KernelCandidacy") 

642 

643 return candidateList 

644 

645 def makeKernelBasisList(self, targetFwhmPix=None, referenceFwhmPix=None, 

646 basisDegGauss=None, basisSigmaGauss=None, metadata=None): 

647 """Wrapper to set log messages for 

648 `lsst.ip.diffim.makeKernelBasisList`. 

649 

650 Parameters 

651 ---------- 

652 targetFwhmPix : `float`, optional 

653 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`. 

654 Not used for delta function basis sets. 

655 referenceFwhmPix : `float`, optional 

656 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`. 

657 Not used for delta function basis sets. 

658 basisDegGauss : `list` of `int`, optional 

659 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`. 

660 Not used for delta function basis sets. 

661 basisSigmaGauss : `list` of `int`, optional 

662 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`. 

663 Not used for delta function basis sets. 

664 metadata : `lsst.daf.base.PropertySet`, optional 

665 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`. 

666 Not used for delta function basis sets. 

667 

668 Returns 

669 ------- 

670 basisList: `list` of `lsst.afw.math.kernel.FixedKernel` 

671 List of basis kernels. 

672 """ 

673 basisList = makeKernelBasisList(self.kConfig, 

674 targetFwhmPix=targetFwhmPix, 

675 referenceFwhmPix=referenceFwhmPix, 

676 basisDegGauss=basisDegGauss, 

677 basisSigmaGauss=basisSigmaGauss, 

678 metadata=metadata) 

679 if targetFwhmPix == referenceFwhmPix: 

680 self.log.info("Target and reference psf fwhms are equal, falling back to config values") 

681 elif referenceFwhmPix > targetFwhmPix: 

682 self.log.info("Reference psf fwhm is the greater, normal convolution mode") 

683 else: 

684 self.log.info("Target psf fwhm is the greater, deconvolution mode") 

685 

686 return basisList 

687 

688 def _adaptCellSize(self, candidateList): 

689 """NOT IMPLEMENTED YET. 

690 """ 

691 return self.kConfig.sizeCellX, self.kConfig.sizeCellY 

692 

693 def _buildCellSet(self, templateMaskedImage, scienceMaskedImage, candidateList): 

694 """Build a SpatialCellSet for use with the solve method. 

695 

696 Parameters 

697 ---------- 

698 templateMaskedImage : `lsst.afw.image.MaskedImage` 

699 MaskedImage to PSF-matched to scienceMaskedImage 

700 scienceMaskedImage : `lsst.afw.image.MaskedImage` 

701 Reference MaskedImage 

702 candidateList : `list` 

703 A list of footprints/maskedImages for kernel candidates; 

704 

705 - Currently supported: list of Footprints or measAlg.PsfCandidateF 

706 

707 Returns 

708 ------- 

709 kernelCellSet : `lsst.afw.math.SpatialCellSet` 

710 a SpatialCellSet for use with self._solve 

711 """ 

712 if not candidateList: 

713 raise RuntimeError("Candidate list must be populated by makeCandidateList") 

714 

715 sizeCellX, sizeCellY = self._adaptCellSize(candidateList) 

716 

717 # Object to store the KernelCandidates for spatial modeling 

718 kernelCellSet = afwMath.SpatialCellSet(templateMaskedImage.getBBox(), 

719 sizeCellX, sizeCellY) 

720 

721 ps = pexConfig.makePropertySet(self.kConfig) 

722 # Place candidates within the spatial grid 

723 for cand in candidateList: 

724 if isinstance(cand, afwDetect.Footprint): 

725 bbox = cand.getBBox() 

726 else: 

727 bbox = cand['footprint'].getBBox() 

728 tmi = afwImage.MaskedImageF(templateMaskedImage, bbox) 

729 smi = afwImage.MaskedImageF(scienceMaskedImage, bbox) 

730 

731 if not isinstance(cand, afwDetect.Footprint): 

732 if 'source' in cand: 

733 cand = cand['source'] 

734 xPos = cand.getCentroid()[0] 

735 yPos = cand.getCentroid()[1] 

736 cand = diffimLib.makeKernelCandidate(xPos, yPos, tmi, smi, ps) 

737 

738 self.log.debug("Candidate %d at %f, %f", cand.getId(), cand.getXCenter(), cand.getYCenter()) 

739 kernelCellSet.insertCandidate(cand) 

740 

741 return kernelCellSet 

742 

743 def _validateSize(self, templateMaskedImage, scienceMaskedImage): 

744 """Return True if two image-like objects are the same size. 

745 """ 

746 return templateMaskedImage.getDimensions() == scienceMaskedImage.getDimensions() 

747 

748 def _validateWcs(self, templateExposure, scienceExposure): 

749 """Return True if the WCS of the two Exposures have the same origin and extent. 

750 """ 

751 templateWcs = templateExposure.getWcs() 

752 scienceWcs = scienceExposure.getWcs() 

753 templateBBox = templateExposure.getBBox() 

754 scienceBBox = scienceExposure.getBBox() 

755 

756 # LLC 

757 templateOrigin = templateWcs.pixelToSky(geom.Point2D(templateBBox.getBegin())) 

758 scienceOrigin = scienceWcs.pixelToSky(geom.Point2D(scienceBBox.getBegin())) 

759 

760 # URC 

761 templateLimit = templateWcs.pixelToSky(geom.Point2D(templateBBox.getEnd())) 

762 scienceLimit = scienceWcs.pixelToSky(geom.Point2D(scienceBBox.getEnd())) 

763 

764 self.log.info("Template Wcs : %f,%f -> %f,%f", 

765 templateOrigin[0], templateOrigin[1], 

766 templateLimit[0], templateLimit[1]) 

767 self.log.info("Science Wcs : %f,%f -> %f,%f", 

768 scienceOrigin[0], scienceOrigin[1], 

769 scienceLimit[0], scienceLimit[1]) 

770 

771 templateBBox = geom.Box2D(templateOrigin.getPosition(geom.degrees), 

772 templateLimit.getPosition(geom.degrees)) 

773 scienceBBox = geom.Box2D(scienceOrigin.getPosition(geom.degrees), 

774 scienceLimit.getPosition(geom.degrees)) 

775 if not (templateBBox.overlaps(scienceBBox)): 

776 raise RuntimeError("Input images do not overlap at all") 

777 

778 if ((templateOrigin != scienceOrigin) 

779 or (templateLimit != scienceLimit) 

780 or (templateExposure.getDimensions() != scienceExposure.getDimensions())): 

781 return False 

782 return True 

783 

784 

785subtractAlgorithmRegistry = pexConfig.makeRegistry( 

786 doc="A registry of subtraction algorithms for use as a subtask in imageDifference", 

787) 

788 

789subtractAlgorithmRegistry.register('al', ImagePsfMatchTask)