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

282 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-06-02 04:23 -0700

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 Notes 

93 ----- 

94 Upon initialization, the kernel configuration is defined by self.config.kernel.active. 

95 The task creates an lsst.afw.math.Warper from the subConfig self.config.kernel.active.warpingConfig. 

96 A schema for the selection and measurement of candidate lsst.ip.diffim.KernelCandidates is 

97 defined, and used to initize subTasks selectDetection (for candidate detection) and selectMeasurement 

98 (for candidate measurement). 

99 

100 Description 

101 

102 Build a Psf-matching kernel using two input images, either as MaskedImages (in which case they need 

103 to be astrometrically aligned) or Exposures (in which case astrometric alignment will happen by 

104 default but may be turned off). This requires a list of input Sources which may be provided 

105 by the calling Task; if not, the Task will perform a coarse source detection 

106 and selection for this purpose. Sources are vetted for signal-to-noise and masked pixels 

107 (in both the template and science image), and substamps around each acceptable 

108 source are extracted and used to create an instance of KernelCandidate. 

109 Each KernelCandidate is then placed within a lsst.afw.math.SpatialCellSet, which is used by an ensemble of 

110 lsst.afw.math.CandidateVisitor instances to build the Psf-matching kernel. These visitors include, in 

111 the order that they are called: BuildSingleKernelVisitor, KernelSumVisitor, BuildSpatialKernelVisitor, 

112 and AssessSpatialKernelVisitor. 

113 

114 Sigma clipping of KernelCandidates is performed as follows: 

115 

116 - BuildSingleKernelVisitor, using the substamp diffim residuals from the per-source kernel fit, 

117 if PsfMatchConfig.singleKernelClipping is True 

118 - KernelSumVisitor, using the mean and standard deviation of the kernel sum from all candidates, 

119 if PsfMatchConfig.kernelSumClipping is True 

120 - AssessSpatialKernelVisitor, using the substamp diffim ressiduals from the spatial kernel fit, 

121 if PsfMatchConfig.spatialKernelClipping is True 

122 

123 The actual solving for the kernel (and differential background model) happens in 

124 lsst.ip.diffim.PsfMatchTask._solve. This involves a loop over the SpatialCellSet that first builds the 

125 per-candidate matching kernel for the requested number of KernelCandidates per cell 

126 (PsfMatchConfig.nStarPerCell). The quality of this initial per-candidate difference image is examined, 

127 using moments of the pixel residuals in the difference image normalized by the square root of the variance 

128 (i.e. sigma); ideally this should follow a normal (0, 1) distribution, 

129 but the rejection thresholds are set 

130 by the config (PsfMatchConfig.candidateResidualMeanMax and PsfMatchConfig.candidateResidualStdMax). 

131 All candidates that pass this initial build are then examined en masse to find the 

132 mean/stdev of the kernel sums across all candidates. 

133 Objects that are significantly above or below the mean, 

134 typically due to variability or sources that are saturated in one image but not the other, 

135 are also rejected.This threshold is defined by PsfMatchConfig.maxKsumSigma. 

136 Finally, a spatial model is built using all currently-acceptable candidates, 

137 and the spatial model used to derive a second set of (spatial) residuals 

138 which are again used to reject bad candidates, using the same thresholds as above. 

139 

140 Invoking the Task 

141 

142 There is no run() method for this Task. Instead there are 4 methods that 

143 may be used to invoke the Psf-matching. These are 

144 `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.matchMaskedImages`, 

145 `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.subtractMaskedImages`, 

146 `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.matchExposures`, and 

147 `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.subtractExposures`. 

148 

149 The methods that operate on lsst.afw.image.MaskedImage require that the images already be astrometrically 

150 aligned, and are the same shape. The methods that operate on lsst.afw.image.Exposure allow for the 

151 input images to be misregistered and potentially be different sizes; by default a 

152 lsst.afw.math.LanczosWarpingKernel is used to perform the astrometric alignment. The methods 

153 that "match" images return a Psf-matched image, while the methods that "subtract" images 

154 return a Psf-matched and template subtracted image. 

155 

156 See each method's returned lsst.pipe.base.Struct for more details. 

157 

158 Debug variables 

159 

160 The lsst.pipe.base.cmdLineTask.CmdLineTask command line task interface supports a 

161 flag -d/--debug to import debug.py from your PYTHONPATH. The relevant contents of debug.py 

162 for this Task include: 

163 

164 .. code-block:: py 

165 

166 import sys 

167 import lsstDebug 

168 def DebugInfo(name): 

169 di = lsstDebug.getInfo(name) 

170 if name == "lsst.ip.diffim.psfMatch": 

171 di.display = True # enable debug output 

172 di.maskTransparency = 80 # display mask transparency 

173 di.displayCandidates = True # show all the candidates and residuals 

174 di.displayKernelBasis = False # show kernel basis functions 

175 di.displayKernelMosaic = True # show kernel realized across the image 

176 di.plotKernelSpatialModel = False # show coefficients of spatial model 

177 di.showBadCandidates = True # show the bad candidates (red) along with good (green) 

178 elif name == "lsst.ip.diffim.imagePsfMatch": 

179 di.display = True # enable debug output 

180 di.maskTransparency = 30 # display mask transparency 

181 di.displayTemplate = True # show full (remapped) template 

182 di.displaySciIm = True # show science image to match to 

183 di.displaySpatialCells = True # show spatial cells 

184 di.displayDiffIm = True # show difference image 

185 di.showBadCandidates = True # show the bad candidates (red) along with good (green) 

186 elif name == "lsst.ip.diffim.diaCatalogSourceSelector": 

187 di.display = False # enable debug output 

188 di.maskTransparency = 30 # display mask transparency 

189 di.displayExposure = True # show exposure with candidates indicated 

190 di.pauseAtEnd = False # pause when done 

191 return di 

192 lsstDebug.Info = DebugInfo 

193 lsstDebug.frame = 1 

194 

195 Note that if you want addional logging info, you may add to your scripts: 

196 

197 .. code-block:: py 

198 

199 import lsst.utils.logging as logUtils 

200 logUtils.trace_set_at("lsst.ip.diffim", 4) 

201 

202 Examples 

203 -------- 

204 A complete example of using ImagePsfMatchTask 

205 

206 This code is imagePsfMatchTask.py in the examples directory, and can be run as e.g. 

207 

208 .. code-block:: none 

209 

210 examples/imagePsfMatchTask.py --debug 

211 examples/imagePsfMatchTask.py --debug --mode="matchExposures" 

212 examples/imagePsfMatchTask.py --debug --template /path/to/templateExp.fits 

213 --science /path/to/scienceExp.fits 

214 

215 Create a subclass of ImagePsfMatchTask that allows us to either match exposures, or subtract exposures: 

216 

217 .. code-block:: none 

218 

219 class MyImagePsfMatchTask(ImagePsfMatchTask): 

220 

221 def __init__(self, args, kwargs): 

222 ImagePsfMatchTask.__init__(self, args, kwargs) 

223 

224 def run(self, templateExp, scienceExp, mode): 

225 if mode == "matchExposures": 

226 return self.matchExposures(templateExp, scienceExp) 

227 elif mode == "subtractExposures": 

228 return self.subtractExposures(templateExp, scienceExp) 

229 

230 And allow the user the freedom to either run the script in default mode, 

231 or point to their own images on disk. 

232 Note that these images must be readable as an lsst.afw.image.Exposure. 

233 

234 We have enabled some minor display debugging in this script via the --debug option. However, if you 

235 have an lsstDebug debug.py in your PYTHONPATH you will get additional debugging displays. The following 

236 block checks for this script: 

237 

238 .. code-block:: py 

239 

240 if args.debug: 

241 try: 

242 import debug 

243 # Since I am displaying 2 images here, set the starting frame number for the LSST debug LSST 

244 debug.lsstDebug.frame = 3 

245 except ImportError as e: 

246 print(e, file=sys.stderr) 

247 

248 Finally, we call a run method that we define below. 

249 First set up a Config and modify some of the parameters. 

250 E.g. use an "Alard-Lupton" sum-of-Gaussian basis, 

251 fit for a differential background, and use low order spatial 

252 variation in the kernel and background: 

253 

254 .. code-block:: py 

255 

256 def run(args): 

257 # 

258 # Create the Config and use sum of gaussian basis 

259 # 

260 config = ImagePsfMatchTask.ConfigClass() 

261 config.kernel.name = "AL" 

262 config.kernel.active.fitForBackground = True 

263 config.kernel.active.spatialKernelOrder = 1 

264 config.kernel.active.spatialBgOrder = 0 

265 

266 Make sure the images (if any) that were sent to the script exist on disk and are readable. If no images 

267 are sent, make some fake data up for the sake of this example script (have a look at the code if you want 

268 more details on generateFakeImages): 

269 

270 .. code-block:: py 

271 

272 # Run the requested method of the Task 

273 if args.template is not None and args.science is not None: 

274 if not os.path.isfile(args.template): 

275 raise FileNotFoundError("Template image %s does not exist" % (args.template)) 

276 if not os.path.isfile(args.science): 

277 raise FileNotFoundError("Science image %s does not exist" % (args.science)) 

278 try: 

279 templateExp = afwImage.ExposureF(args.template) 

280 except Exception as e: 

281 raise RuntimeError("Cannot read template image %s" % (args.template)) 

282 try: 

283 scienceExp = afwImage.ExposureF(args.science) 

284 except Exception as e: 

285 raise RuntimeError("Cannot read science image %s" % (args.science)) 

286 else: 

287 templateExp, scienceExp = generateFakeImages() 

288 config.kernel.active.sizeCellX = 128 

289 config.kernel.active.sizeCellY = 128 

290 

291 Create and run the Task: 

292 

293 .. code-block:: py 

294 

295 # Create the Task 

296 psfMatchTask = MyImagePsfMatchTask(config=config) 

297 # Run the Task 

298 result = psfMatchTask.run(templateExp, scienceExp, args.mode) 

299 

300 And finally provide some optional debugging displays: 

301 

302 .. code-block:: py 

303 

304 if args.debug: 

305 # See if the LSST debug has incremented the frame number; if not start with frame 3 

306 try: 

307 frame = debug.lsstDebug.frame + 1 

308 except Exception: 

309 frame = 3 

310 afwDisplay.Display(frame=frame).mtv(result.matchedExposure, 

311 title="Example script: Matched Template Image") 

312 if "subtractedExposure" in result.getDict(): 

313 afwDisplay.Display(frame=frame + 1).mtv(result.subtractedExposure, 

314 title="Example script: Subtracted Image") 

315 """ 

316 

317 ConfigClass = ImagePsfMatchConfig 

318 

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

320 """Create the ImagePsfMatchTask. 

321 """ 

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

323 self.kConfig = self.config.kernel.active 

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

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

326 # so cannot easily be constructed with makeSubtask 

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

328 parentTask=self) 

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

330 self.selectAlgMetadata = dafBase.PropertyList() 

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

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

333 

334 def getFwhmPix(self, psf, position=None): 

335 """Return the FWHM in pixels of a Psf. 

336 """ 

337 if position is None: 

338 position = psf.getAveragePosition() 

339 sigPix = psf.computeShape(position).getDeterminantRadius() 

340 return sigPix*sigma2fwhm 

341 

342 @timeMethod 

343 def matchExposures(self, templateExposure, scienceExposure, 

344 templateFwhmPix=None, scienceFwhmPix=None, 

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

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

347 

348 Do the following, in order: 

349 

350 - Warp templateExposure to match scienceExposure, 

351 if doWarping True and their WCSs do not already match 

352 - Determine a PSF matching kernel and differential background model 

353 that matches templateExposure to scienceExposure 

354 - Convolve templateExposure by PSF matching kernel 

355 

356 Parameters 

357 ---------- 

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

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

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

361 Exposure whose WCS and PSF are to be matched to 

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 not match: 

374 

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

376 - if `False` then raise an Exception 

377 

378 convolveTemplate : `bool` 

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

380 

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

382 ``templateExposure`` is convolved 

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

384 ``scienceExposure`` is convolved 

385 

386 Returns 

387 ------- 

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

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

390 

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

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

393 

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

395 - the same filter as templateExposure 

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

397 

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

399 - ``backgroundModel`` : differential background model 

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

401 

402 Raises 

403 ------ 

404 RuntimeError 

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

406 ``scienceExposure`` WCSs do not match 

407 """ 

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

409 if doWarping: 

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

411 templatePsf = templateExposure.getPsf() 

412 # Warp PSF before overwriting exposure 

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

414 scienceExposure.getWcs()) 

415 psfWarped = WarpedPsf(templatePsf, xyTransform) 

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

417 templateExposure, 

418 destBBox=scienceExposure.getBBox()) 

419 templateExposure.setPsf(psfWarped) 

420 else: 

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

422 raise RuntimeError("Input images not registered") 

423 

424 if templateFwhmPix is None: 

425 if not templateExposure.hasPsf(): 

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

427 else: 

428 templateFwhmPix = self.getFwhmPix(templateExposure.getPsf()) 

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

430 

431 if scienceFwhmPix is None: 

432 if not scienceExposure.hasPsf(): 

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

434 else: 

435 scienceFwhmPix = self.getFwhmPix(scienceExposure.getPsf()) 

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

437 

438 if convolveTemplate: 

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

440 candidateList = self.makeCandidateList( 

441 templateExposure, scienceExposure, kernelSize, candidateList) 

442 results = self.matchMaskedImages( 

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

444 templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix) 

445 else: 

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

447 candidateList = self.makeCandidateList( 

448 templateExposure, scienceExposure, kernelSize, candidateList) 

449 results = self.matchMaskedImages( 

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

451 templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix) 

452 

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

454 psfMatchedExposure.setFilterLabel(templateExposure.getFilterLabel()) 

455 psfMatchedExposure.setPhotoCalib(scienceExposure.getPhotoCalib()) 

456 results.warpedExposure = templateExposure 

457 results.matchedExposure = psfMatchedExposure 

458 return results 

459 

460 @timeMethod 

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

462 templateFwhmPix=None, scienceFwhmPix=None): 

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

464 

465 Do the following, in order: 

466 

467 - Determine a PSF matching kernel and differential background model 

468 that matches templateMaskedImage to scienceMaskedImage 

469 - Convolve templateMaskedImage by the PSF matching kernel 

470 

471 Parameters 

472 ---------- 

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

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

475 must be warped to match the reference masked image 

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

477 maskedImage whose PSF is to be matched to 

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 result : `callable` 

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

492 

493 - psfMatchedMaskedImage: the PSF-matched masked image = 

494 ``templateMaskedImage`` convolved with psfMatchingKernel. 

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

496 - psfMatchingKernel: the PSF matching kernel 

497 - backgroundModel: differential background model 

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

499 

500 Raises 

501 ------ 

502 RuntimeError 

503 Raised if input images have different dimensions 

504 """ 

505 import lsstDebug 

506 display = lsstDebug.Info(__name__).display 

507 displayTemplate = lsstDebug.Info(__name__).displayTemplate 

508 displaySciIm = lsstDebug.Info(__name__).displaySciIm 

509 displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells 

510 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

511 if not maskTransparency: 

512 maskTransparency = 0 

513 if display: 

514 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

515 

516 if not candidateList: 

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

518 

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

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

521 raise RuntimeError("Input images different size") 

522 

523 if display and displayTemplate: 

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

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

526 lsstDebug.frame += 1 

527 

528 if display and displaySciIm: 

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

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

531 lsstDebug.frame += 1 

532 

533 kernelCellSet = self._buildCellSet(templateMaskedImage, 

534 scienceMaskedImage, 

535 candidateList) 

536 

537 if display and displaySpatialCells: 

538 diffimUtils.showKernelSpatialCells(scienceMaskedImage, kernelCellSet, 

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

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

541 title="Image to not convolve") 

542 lsstDebug.frame += 1 

543 

544 if templateFwhmPix and scienceFwhmPix: 

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

546 

547 if self.kConfig.useBicForKernelBasis: 

548 tmpKernelCellSet = self._buildCellSet(templateMaskedImage, 

549 scienceMaskedImage, 

550 candidateList) 

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

552 bicDegrees = nbe(tmpKernelCellSet, self.log) 

553 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

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

555 del tmpKernelCellSet 

556 else: 

557 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

558 metadata=self.metadata) 

559 

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

561 

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

563 convolutionControl = afwMath.ConvolutionControl() 

564 convolutionControl.setDoNormalize(False) 

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

566 return pipeBase.Struct( 

567 matchedImage=psfMatchedMaskedImage, 

568 psfMatchingKernel=psfMatchingKernel, 

569 backgroundModel=backgroundModel, 

570 kernelCellSet=kernelCellSet, 

571 ) 

572 

573 @timeMethod 

574 def subtractExposures(self, templateExposure, scienceExposure, 

575 templateFwhmPix=None, scienceFwhmPix=None, 

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

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

578 

579 Do the following, in order: 

580 

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

582 - Determine a PSF matching kernel and differential background model 

583 that matches templateExposure to scienceExposure 

584 - PSF-match templateExposure to scienceExposure 

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

586 

587 Parameters 

588 ---------- 

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

590 Exposure to PSF-match to scienceExposure 

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

592 Reference Exposure 

593 templateFwhmPix : `float` 

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

595 scienceFwhmPix : `float` 

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

597 candidateList : `list`, optional 

598 A list of footprints/maskedImages for kernel candidates; 

599 if `None` then source detection is run. 

600 

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

602 

603 doWarping : `bool` 

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

605 not match: 

606 

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

608 - if `False` then raise an Exception 

609 

610 convolveTemplate : `bool` 

611 Convolve the template image or the science image 

612 

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

614 ``templateExposure`` is convolved 

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

616 ``scienceExposure is`` convolved 

617 

618 Returns 

619 ------- 

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

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

622 

623 - ``subtractedExposure`` : subtracted Exposure 

624 scienceExposure - (matchedImage + backgroundModel) 

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

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

627 and convolving with psfMatchingKernel 

628 - ``psfMatchingKernel`` : PSF matching kernel 

629 - ``backgroundModel`` : differential background model 

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

631 """ 

632 results = self.matchExposures( 

633 templateExposure=templateExposure, 

634 scienceExposure=scienceExposure, 

635 templateFwhmPix=templateFwhmPix, 

636 scienceFwhmPix=scienceFwhmPix, 

637 candidateList=candidateList, 

638 doWarping=doWarping, 

639 convolveTemplate=convolveTemplate 

640 ) 

641 # Always inherit WCS and photocalib from science exposure 

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

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

644 # from the variance planes of the original exposures. 

645 # That recalculation code must be in accordance with the 

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

647 if convolveTemplate: 

648 subtractedMaskedImage = subtractedExposure.maskedImage 

649 subtractedMaskedImage -= results.matchedExposure.maskedImage 

650 subtractedMaskedImage -= results.backgroundModel 

651 else: 

652 subtractedMaskedImage = subtractedExposure.maskedImage 

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

654 subtractedMaskedImage -= results.matchedExposure.maskedImage 

655 subtractedMaskedImage -= results.backgroundModel 

656 

657 # Preserve polarity of differences 

658 subtractedMaskedImage *= -1 

659 

660 # Place back on native photometric scale 

661 subtractedMaskedImage /= results.psfMatchingKernel.computeImage( 

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

663 # We matched to the warped template 

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

665 

666 import lsstDebug 

667 display = lsstDebug.Info(__name__).display 

668 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm 

669 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

670 if not maskTransparency: 

671 maskTransparency = 0 

672 if display: 

673 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

674 if display and displayDiffIm: 

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

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

677 lsstDebug.frame += 1 

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

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

680 lsstDebug.frame += 1 

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

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

683 lsstDebug.frame += 1 

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

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

686 lsstDebug.frame += 1 

687 

688 results.subtractedExposure = subtractedExposure 

689 return results 

690 

691 @timeMethod 

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

693 templateFwhmPix=None, scienceFwhmPix=None): 

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

695 

696 Do the following, in order: 

697 

698 - PSF-match templateMaskedImage to scienceMaskedImage 

699 - Determine the differential background 

700 - Return the difference: scienceMaskedImage 

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

702 

703 Parameters 

704 ---------- 

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

706 MaskedImage to PSF-match to ``scienceMaskedImage`` 

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

708 Reference MaskedImage 

709 templateFwhmPix : `float` 

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

711 scienceFwhmPix : `float` 

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

713 candidateList : `list`, optional 

714 A list of footprints/maskedImages for kernel candidates; 

715 if `None` then source detection is run. 

716 

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

718 

719 Returns 

720 ------- 

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

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

723 

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

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

726 - `psfMatchingKernel`` : PSF matching kernel 

727 - ``backgroundModel`` : differential background model 

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

729 

730 """ 

731 if not candidateList: 

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

733 

734 results = self.matchMaskedImages( 

735 templateMaskedImage=templateMaskedImage, 

736 scienceMaskedImage=scienceMaskedImage, 

737 candidateList=candidateList, 

738 templateFwhmPix=templateFwhmPix, 

739 scienceFwhmPix=scienceFwhmPix, 

740 ) 

741 

742 subtractedMaskedImage = afwImage.MaskedImageF(scienceMaskedImage, True) 

743 subtractedMaskedImage -= results.matchedImage 

744 subtractedMaskedImage -= results.backgroundModel 

745 results.subtractedMaskedImage = subtractedMaskedImage 

746 

747 import lsstDebug 

748 display = lsstDebug.Info(__name__).display 

749 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm 

750 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

751 if not maskTransparency: 

752 maskTransparency = 0 

753 if display: 

754 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

755 if display and displayDiffIm: 

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

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

758 lsstDebug.frame += 1 

759 

760 return results 

761 

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

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

764 

765 This method runs detection and measurement on an exposure. 

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

767 Psf-matching. 

768 

769 Parameters 

770 ---------- 

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

772 Exposure on which to run detection/measurement 

773 sigma : `float` 

774 Detection threshold 

775 doSmooth : `bool` 

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

777 idFactory : 

778 Factory for the generation of Source ids 

779 

780 Returns 

781 ------- 

782 selectSources : 

783 source catalog containing candidates for the Psf-matching 

784 """ 

785 if idFactory: 

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

787 else: 

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

789 mi = exposure.getMaskedImage() 

790 

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

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

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

794 try: 

795 fitBg = self.background.fitBackground(mi) 

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

797 self.background.config.undersampleStyle) 

798 except Exception: 

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

800 bkgd = np.ma.median(miArr) 

801 

802 # Take off background for detection 

803 mi -= bkgd 

804 try: 

805 table.setMetadata(self.selectAlgMetadata) 

806 detRet = self.selectDetection.run( 

807 table=table, 

808 exposure=exposure, 

809 sigma=sigma, 

810 doSmooth=doSmooth 

811 ) 

812 selectSources = detRet.sources 

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

814 finally: 

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

816 mi += bkgd 

817 del bkgd 

818 return selectSources 

819 

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

821 """Make a list of acceptable KernelCandidates. 

822 

823 Accept or generate a list of candidate sources for 

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

825 images for indications of bad pixels 

826 

827 Parameters 

828 ---------- 

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

830 Exposure that will be convolved 

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

832 Exposure that will be matched-to 

833 kernelSize : `float` 

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

835 candidateList : `list`, optional 

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

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

838 meas.algorithms.PsfCandidateF. 

839 

840 Returns 

841 ------- 

842 candidateList : `list` of `dict` 

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

844 field for the Sources deemed to be appropriate for Psf 

845 matching 

846 """ 

847 if candidateList is None: 

848 candidateList = self.getSelectSources(scienceExposure) 

849 

850 if len(candidateList) < 1: 

851 raise RuntimeError("No candidates in candidateList") 

852 

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

854 if len(listTypes) > 1: 

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

856 

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

858 try: 

859 candidateList[0].getSource() 

860 except Exception as e: 

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

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

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

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

865 

866 candidateList = diffimTools.sourceToFootprintList(candidateList, 

867 templateExposure, scienceExposure, 

868 kernelSize, 

869 self.kConfig.detectionConfig, 

870 self.log) 

871 if len(candidateList) == 0: 

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

873 

874 return candidateList 

875 

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

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

878 """Wrapper to set log messages for 

879 `lsst.ip.diffim.makeKernelBasisList`. 

880 

881 Parameters 

882 ---------- 

883 targetFwhmPix : `float`, optional 

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

885 Not used for delta function basis sets. 

886 referenceFwhmPix : `float`, optional 

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

888 Not used for delta function basis sets. 

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

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

891 Not used for delta function basis sets. 

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

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

894 Not used for delta function basis sets. 

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

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

897 Not used for delta function basis sets. 

898 

899 Returns 

900 ------- 

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

902 List of basis kernels. 

903 """ 

904 basisList = makeKernelBasisList(self.kConfig, 

905 targetFwhmPix=targetFwhmPix, 

906 referenceFwhmPix=referenceFwhmPix, 

907 basisDegGauss=basisDegGauss, 

908 basisSigmaGauss=basisSigmaGauss, 

909 metadata=metadata) 

910 if targetFwhmPix == referenceFwhmPix: 

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

912 elif referenceFwhmPix > targetFwhmPix: 

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

914 else: 

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

916 

917 return basisList 

918 

919 def _adaptCellSize(self, candidateList): 

920 """NOT IMPLEMENTED YET. 

921 """ 

922 return self.kConfig.sizeCellX, self.kConfig.sizeCellY 

923 

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

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

926 

927 Parameters 

928 ---------- 

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

930 MaskedImage to PSF-matched to scienceMaskedImage 

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

932 Reference MaskedImage 

933 candidateList : `list` 

934 A list of footprints/maskedImages for kernel candidates; 

935 

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

937 

938 Returns 

939 ------- 

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

941 a SpatialCellSet for use with self._solve 

942 """ 

943 if not candidateList: 

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

945 

946 sizeCellX, sizeCellY = self._adaptCellSize(candidateList) 

947 

948 # Object to store the KernelCandidates for spatial modeling 

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

950 sizeCellX, sizeCellY) 

951 

952 ps = pexConfig.makePropertySet(self.kConfig) 

953 # Place candidates within the spatial grid 

954 for cand in candidateList: 

955 if isinstance(cand, afwDetect.Footprint): 

956 bbox = cand.getBBox() 

957 else: 

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

959 tmi = afwImage.MaskedImageF(templateMaskedImage, bbox) 

960 smi = afwImage.MaskedImageF(scienceMaskedImage, bbox) 

961 

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

963 if 'source' in cand: 

964 cand = cand['source'] 

965 xPos = cand.getCentroid()[0] 

966 yPos = cand.getCentroid()[1] 

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

968 

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

970 kernelCellSet.insertCandidate(cand) 

971 

972 return kernelCellSet 

973 

974 def _validateSize(self, templateMaskedImage, scienceMaskedImage): 

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

976 """ 

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

978 

979 def _validateWcs(self, templateExposure, scienceExposure): 

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

981 """ 

982 templateWcs = templateExposure.getWcs() 

983 scienceWcs = scienceExposure.getWcs() 

984 templateBBox = templateExposure.getBBox() 

985 scienceBBox = scienceExposure.getBBox() 

986 

987 # LLC 

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

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

990 

991 # URC 

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

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

994 

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

996 templateOrigin[0], templateOrigin[1], 

997 templateLimit[0], templateLimit[1]) 

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

999 scienceOrigin[0], scienceOrigin[1], 

1000 scienceLimit[0], scienceLimit[1]) 

1001 

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

1003 templateLimit.getPosition(geom.degrees)) 

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

1005 scienceLimit.getPosition(geom.degrees)) 

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

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

1008 

1009 if ((templateOrigin != scienceOrigin) 

1010 or (templateLimit != scienceLimit) 

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

1012 return False 

1013 return True 

1014 

1015 

1016subtractAlgorithmRegistry = pexConfig.makeRegistry( 

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

1018) 

1019 

1020subtractAlgorithmRegistry.register('al', ImagePsfMatchTask)