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 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.log.utils as logUtils 

200 logUtils.traceSetAt("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): 

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

336 """ 

337 sigPix = psf.computeShape().getDeterminantRadius() 

338 return sigPix*sigma2fwhm 

339 

340 @timeMethod 

341 def matchExposures(self, templateExposure, scienceExposure, 

342 templateFwhmPix=None, scienceFwhmPix=None, 

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

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

345 

346 Do the following, in order: 

347 

348 - Warp templateExposure to match scienceExposure, 

349 if doWarping True and their WCSs do not already match 

350 - Determine a PSF matching kernel and differential background model 

351 that matches templateExposure to scienceExposure 

352 - Convolve templateExposure by PSF matching kernel 

353 

354 Parameters 

355 ---------- 

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

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

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

359 Exposure whose WCS and PSF are to be matched to 

360 templateFwhmPix :`float` 

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

362 scienceFwhmPix : `float` 

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

364 candidateList : `list`, optional 

365 a list of footprints/maskedImages for kernel candidates; 

366 if `None` then source detection is run. 

367 

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

369 

370 doWarping : `bool` 

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

372 

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

374 - if `False` then raise an Exception 

375 

376 convolveTemplate : `bool` 

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

378 

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

380 ``templateExposure`` is convolved 

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

382 ``scienceExposure`` is convolved 

383 

384 Returns 

385 ------- 

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

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

388 

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

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

391 

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

393 - the same filter as templateExposure 

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

395 

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

397 - ``backgroundModel`` : differential background model 

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

399 

400 Raises 

401 ------ 

402 RuntimeError 

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

404 ``scienceExposure`` WCSs do not match 

405 """ 

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

407 if doWarping: 

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

409 templatePsf = templateExposure.getPsf() 

410 # Warp PSF before overwriting exposure 

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

412 scienceExposure.getWcs()) 

413 psfWarped = WarpedPsf(templatePsf, xyTransform) 

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

415 templateExposure, 

416 destBBox=scienceExposure.getBBox()) 

417 templateExposure.setPsf(psfWarped) 

418 else: 

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

420 raise RuntimeError("Input images not registered") 

421 

422 if templateFwhmPix is None: 

423 if not templateExposure.hasPsf(): 

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

425 else: 

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

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

428 

429 if scienceFwhmPix is None: 

430 if not scienceExposure.hasPsf(): 

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

432 else: 

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

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

435 

436 if convolveTemplate: 

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

438 candidateList = self.makeCandidateList( 

439 templateExposure, scienceExposure, kernelSize, candidateList) 

440 results = self.matchMaskedImages( 

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

442 templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix) 

443 else: 

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

445 candidateList = self.makeCandidateList( 

446 templateExposure, scienceExposure, kernelSize, candidateList) 

447 results = self.matchMaskedImages( 

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

449 templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix) 

450 

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

452 psfMatchedExposure.setFilterLabel(templateExposure.getFilterLabel()) 

453 psfMatchedExposure.setPhotoCalib(scienceExposure.getPhotoCalib()) 

454 results.warpedExposure = templateExposure 

455 results.matchedExposure = psfMatchedExposure 

456 return results 

457 

458 @timeMethod 

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

460 templateFwhmPix=None, scienceFwhmPix=None): 

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

462 

463 Do the following, in order: 

464 

465 - Determine a PSF matching kernel and differential background model 

466 that matches templateMaskedImage to scienceMaskedImage 

467 - Convolve templateMaskedImage by the PSF matching kernel 

468 

469 Parameters 

470 ---------- 

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

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

473 must be warped to match the reference masked image 

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

475 maskedImage whose PSF is to be matched to 

476 templateFwhmPix : `float` 

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

478 scienceFwhmPix : `float` 

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

480 candidateList : `list`, optional 

481 A list of footprints/maskedImages for kernel candidates; 

482 if `None` then source detection is run. 

483 

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

485 

486 Returns 

487 ------- 

488 result : `callable` 

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

490 

491 - psfMatchedMaskedImage: the PSF-matched masked image = 

492 ``templateMaskedImage`` convolved with psfMatchingKernel. 

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

494 - psfMatchingKernel: the PSF matching kernel 

495 - backgroundModel: differential background model 

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

497 

498 Raises 

499 ------ 

500 RuntimeError 

501 Raised if input images have different dimensions 

502 """ 

503 import lsstDebug 

504 display = lsstDebug.Info(__name__).display 

505 displayTemplate = lsstDebug.Info(__name__).displayTemplate 

506 displaySciIm = lsstDebug.Info(__name__).displaySciIm 

507 displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells 

508 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

509 if not maskTransparency: 

510 maskTransparency = 0 

511 if display: 

512 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

513 

514 if not candidateList: 

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

516 

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

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

519 raise RuntimeError("Input images different size") 

520 

521 if display and displayTemplate: 

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

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

524 lsstDebug.frame += 1 

525 

526 if display and displaySciIm: 

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

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

529 lsstDebug.frame += 1 

530 

531 kernelCellSet = self._buildCellSet(templateMaskedImage, 

532 scienceMaskedImage, 

533 candidateList) 

534 

535 if display and displaySpatialCells: 

536 diffimUtils.showKernelSpatialCells(scienceMaskedImage, kernelCellSet, 

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

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

539 title="Image to not convolve") 

540 lsstDebug.frame += 1 

541 

542 if templateFwhmPix and scienceFwhmPix: 

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

544 

545 if self.kConfig.useBicForKernelBasis: 

546 tmpKernelCellSet = self._buildCellSet(templateMaskedImage, 

547 scienceMaskedImage, 

548 candidateList) 

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

550 bicDegrees = nbe(tmpKernelCellSet, self.log) 

551 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

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

553 del tmpKernelCellSet 

554 else: 

555 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, 

556 metadata=self.metadata) 

557 

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

559 

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

561 convolutionControl = afwMath.ConvolutionControl() 

562 convolutionControl.setDoNormalize(False) 

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

564 return pipeBase.Struct( 

565 matchedImage=psfMatchedMaskedImage, 

566 psfMatchingKernel=psfMatchingKernel, 

567 backgroundModel=backgroundModel, 

568 kernelCellSet=kernelCellSet, 

569 ) 

570 

571 @timeMethod 

572 def subtractExposures(self, templateExposure, scienceExposure, 

573 templateFwhmPix=None, scienceFwhmPix=None, 

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

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

576 

577 Do the following, in order: 

578 

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

580 - Determine a PSF matching kernel and differential background model 

581 that matches templateExposure to scienceExposure 

582 - PSF-match templateExposure to scienceExposure 

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

584 

585 Parameters 

586 ---------- 

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

588 Exposure to PSF-match to scienceExposure 

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

590 Reference Exposure 

591 templateFwhmPix : `float` 

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

593 scienceFwhmPix : `float` 

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

595 candidateList : `list`, optional 

596 A list of footprints/maskedImages for kernel candidates; 

597 if `None` then source detection is run. 

598 

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

600 

601 doWarping : `bool` 

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

603 not match: 

604 

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

606 - if `False` then raise an Exception 

607 

608 convolveTemplate : `bool` 

609 Convolve the template image or the science image 

610 

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

612 ``templateExposure`` is convolved 

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

614 ``scienceExposure is`` convolved 

615 

616 Returns 

617 ------- 

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

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

620 

621 - ``subtractedExposure`` : subtracted Exposure 

622 scienceExposure - (matchedImage + backgroundModel) 

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

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

625 and convolving with psfMatchingKernel 

626 - ``psfMatchingKernel`` : PSF matching kernel 

627 - ``backgroundModel`` : differential background model 

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

629 """ 

630 results = self.matchExposures( 

631 templateExposure=templateExposure, 

632 scienceExposure=scienceExposure, 

633 templateFwhmPix=templateFwhmPix, 

634 scienceFwhmPix=scienceFwhmPix, 

635 candidateList=candidateList, 

636 doWarping=doWarping, 

637 convolveTemplate=convolveTemplate 

638 ) 

639 # Always inherit WCS and photocalib from science exposure 

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

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

642 # from the variance planes of the original exposures. 

643 # That recalculation code must be in accordance with the 

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

645 if convolveTemplate: 

646 subtractedMaskedImage = subtractedExposure.maskedImage 

647 subtractedMaskedImage -= results.matchedExposure.maskedImage 

648 subtractedMaskedImage -= results.backgroundModel 

649 else: 

650 subtractedMaskedImage = subtractedExposure.maskedImage 

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

652 subtractedMaskedImage -= results.matchedExposure.maskedImage 

653 subtractedMaskedImage -= results.backgroundModel 

654 

655 # Preserve polarity of differences 

656 subtractedMaskedImage *= -1 

657 

658 # Place back on native photometric scale 

659 subtractedMaskedImage /= results.psfMatchingKernel.computeImage( 

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

661 # We matched to the warped template 

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

663 

664 import lsstDebug 

665 display = lsstDebug.Info(__name__).display 

666 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm 

667 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

668 if not maskTransparency: 

669 maskTransparency = 0 

670 if display: 

671 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

672 if display and displayDiffIm: 

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

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

675 lsstDebug.frame += 1 

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

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

678 lsstDebug.frame += 1 

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

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

681 lsstDebug.frame += 1 

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

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

684 lsstDebug.frame += 1 

685 

686 results.subtractedExposure = subtractedExposure 

687 return results 

688 

689 @timeMethod 

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

691 templateFwhmPix=None, scienceFwhmPix=None): 

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

693 

694 Do the following, in order: 

695 

696 - PSF-match templateMaskedImage to scienceMaskedImage 

697 - Determine the differential background 

698 - Return the difference: scienceMaskedImage 

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

700 

701 Parameters 

702 ---------- 

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

704 MaskedImage to PSF-match to ``scienceMaskedImage`` 

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

706 Reference MaskedImage 

707 templateFwhmPix : `float` 

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

709 scienceFwhmPix : `float` 

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

711 candidateList : `list`, optional 

712 A list of footprints/maskedImages for kernel candidates; 

713 if `None` then source detection is run. 

714 

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

716 

717 Returns 

718 ------- 

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

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

721 

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

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

724 - `psfMatchingKernel`` : PSF matching kernel 

725 - ``backgroundModel`` : differential background model 

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

727 

728 """ 

729 if not candidateList: 

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

731 

732 results = self.matchMaskedImages( 

733 templateMaskedImage=templateMaskedImage, 

734 scienceMaskedImage=scienceMaskedImage, 

735 candidateList=candidateList, 

736 templateFwhmPix=templateFwhmPix, 

737 scienceFwhmPix=scienceFwhmPix, 

738 ) 

739 

740 subtractedMaskedImage = afwImage.MaskedImageF(scienceMaskedImage, True) 

741 subtractedMaskedImage -= results.matchedImage 

742 subtractedMaskedImage -= results.backgroundModel 

743 results.subtractedMaskedImage = subtractedMaskedImage 

744 

745 import lsstDebug 

746 display = lsstDebug.Info(__name__).display 

747 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm 

748 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

749 if not maskTransparency: 

750 maskTransparency = 0 

751 if display: 

752 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

753 if display and displayDiffIm: 

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

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

756 lsstDebug.frame += 1 

757 

758 return results 

759 

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

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

762 

763 This method runs detection and measurement on an exposure. 

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

765 Psf-matching. 

766 

767 Parameters 

768 ---------- 

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

770 Exposure on which to run detection/measurement 

771 sigma : `float` 

772 Detection threshold 

773 doSmooth : `bool` 

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

775 idFactory : 

776 Factory for the generation of Source ids 

777 

778 Returns 

779 ------- 

780 selectSources : 

781 source catalog containing candidates for the Psf-matching 

782 """ 

783 if idFactory: 

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

785 else: 

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

787 mi = exposure.getMaskedImage() 

788 

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

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

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

792 try: 

793 fitBg = self.background.fitBackground(mi) 

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

795 self.background.config.undersampleStyle) 

796 except Exception: 

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

798 bkgd = np.ma.median(miArr) 

799 

800 # Take off background for detection 

801 mi -= bkgd 

802 try: 

803 table.setMetadata(self.selectAlgMetadata) 

804 detRet = self.selectDetection.run( 

805 table=table, 

806 exposure=exposure, 

807 sigma=sigma, 

808 doSmooth=doSmooth 

809 ) 

810 selectSources = detRet.sources 

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

812 finally: 

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

814 mi += bkgd 

815 del bkgd 

816 return selectSources 

817 

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

819 """Make a list of acceptable KernelCandidates. 

820 

821 Accept or generate a list of candidate sources for 

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

823 images for indications of bad pixels 

824 

825 Parameters 

826 ---------- 

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

828 Exposure that will be convolved 

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

830 Exposure that will be matched-to 

831 kernelSize : `float` 

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

833 candidateList : `list`, optional 

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

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

836 meas.algorithms.PsfCandidateF. 

837 

838 Returns 

839 ------- 

840 candidateList : `list` of `dict` 

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

842 field for the Sources deemed to be appropriate for Psf 

843 matching 

844 """ 

845 if candidateList is None: 

846 candidateList = self.getSelectSources(scienceExposure) 

847 

848 if len(candidateList) < 1: 

849 raise RuntimeError("No candidates in candidateList") 

850 

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

852 if len(listTypes) > 1: 

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

854 

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

856 try: 

857 candidateList[0].getSource() 

858 except Exception as e: 

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

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

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

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

863 

864 candidateList = diffimTools.sourceToFootprintList(candidateList, 

865 templateExposure, scienceExposure, 

866 kernelSize, 

867 self.kConfig.detectionConfig, 

868 self.log) 

869 if len(candidateList) == 0: 

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

871 

872 return candidateList 

873 

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

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

876 """Wrapper to set log messages for 

877 `lsst.ip.diffim.makeKernelBasisList`. 

878 

879 Parameters 

880 ---------- 

881 targetFwhmPix : `float`, optional 

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

883 Not used for delta function basis sets. 

884 referenceFwhmPix : `float`, optional 

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

886 Not used for delta function basis sets. 

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

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

889 Not used for delta function basis sets. 

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

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

892 Not used for delta function basis sets. 

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

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

895 Not used for delta function basis sets. 

896 

897 Returns 

898 ------- 

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

900 List of basis kernels. 

901 """ 

902 basisList = makeKernelBasisList(self.kConfig, 

903 targetFwhmPix=targetFwhmPix, 

904 referenceFwhmPix=referenceFwhmPix, 

905 basisDegGauss=basisDegGauss, 

906 basisSigmaGauss=basisSigmaGauss, 

907 metadata=metadata) 

908 if targetFwhmPix == referenceFwhmPix: 

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

910 elif referenceFwhmPix > targetFwhmPix: 

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

912 else: 

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

914 

915 return basisList 

916 

917 def _adaptCellSize(self, candidateList): 

918 """NOT IMPLEMENTED YET. 

919 """ 

920 return self.kConfig.sizeCellX, self.kConfig.sizeCellY 

921 

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

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

924 

925 Parameters 

926 ---------- 

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

928 MaskedImage to PSF-matched to scienceMaskedImage 

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

930 Reference MaskedImage 

931 candidateList : `list` 

932 A list of footprints/maskedImages for kernel candidates; 

933 

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

935 

936 Returns 

937 ------- 

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

939 a SpatialCellSet for use with self._solve 

940 """ 

941 if not candidateList: 

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

943 

944 sizeCellX, sizeCellY = self._adaptCellSize(candidateList) 

945 

946 # Object to store the KernelCandidates for spatial modeling 

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

948 sizeCellX, sizeCellY) 

949 

950 ps = pexConfig.makePropertySet(self.kConfig) 

951 # Place candidates within the spatial grid 

952 for cand in candidateList: 

953 if isinstance(cand, afwDetect.Footprint): 

954 bbox = cand.getBBox() 

955 else: 

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

957 tmi = afwImage.MaskedImageF(templateMaskedImage, bbox) 

958 smi = afwImage.MaskedImageF(scienceMaskedImage, bbox) 

959 

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

961 if 'source' in cand: 

962 cand = cand['source'] 

963 xPos = cand.getCentroid()[0] 

964 yPos = cand.getCentroid()[1] 

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

966 

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

968 kernelCellSet.insertCandidate(cand) 

969 

970 return kernelCellSet 

971 

972 def _validateSize(self, templateMaskedImage, scienceMaskedImage): 

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

974 """ 

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

976 

977 def _validateWcs(self, templateExposure, scienceExposure): 

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

979 """ 

980 templateWcs = templateExposure.getWcs() 

981 scienceWcs = scienceExposure.getWcs() 

982 templateBBox = templateExposure.getBBox() 

983 scienceBBox = scienceExposure.getBBox() 

984 

985 # LLC 

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

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

988 

989 # URC 

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

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

992 

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

994 templateOrigin[0], templateOrigin[1], 

995 templateLimit[0], templateLimit[1]) 

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

997 scienceOrigin[0], scienceOrigin[1], 

998 scienceLimit[0], scienceLimit[1]) 

999 

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

1001 templateLimit.getPosition(geom.degrees)) 

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

1003 scienceLimit.getPosition(geom.degrees)) 

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

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

1006 

1007 if ((templateOrigin != scienceOrigin) 

1008 or (templateLimit != scienceLimit) 

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

1010 return False 

1011 return True 

1012 

1013 

1014subtractAlgorithmRegistry = pexConfig.makeRegistry( 

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

1016) 

1017 

1018subtractAlgorithmRegistry.register('al', ImagePsfMatchTask)