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 

41 

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

43 

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

45 

46 

47class ImagePsfMatchConfig(pexConfig.Config): 

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

49 """ 

50 kernel = pexConfig.ConfigChoiceField( 

51 doc="kernel type", 

52 typemap=dict( 

53 AL=PsfMatchConfigAL, 

54 DF=PsfMatchConfigDF 

55 ), 

56 default="AL", 

57 ) 

58 selectDetection = pexConfig.ConfigurableField( 

59 target=SourceDetectionTask, 

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

61 ) 

62 selectMeasurement = pexConfig.ConfigurableField( 

63 target=SingleFrameMeasurementTask, 

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

65 ) 

66 

67 def setDefaults(self): 

68 # High sigma detections only 

69 self.selectDetection.reEstimateBackground = False 

70 self.selectDetection.thresholdValue = 10.0 

71 

72 # Minimal set of measurments for star selection 

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

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

75 'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord') 

76 self.selectMeasurement.slots.modelFlux = None 

77 self.selectMeasurement.slots.apFlux = None 

78 self.selectMeasurement.slots.calibFlux = None 

79 

80 

81class ImagePsfMatchTask(PsfMatchTask): 

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

83 

84 Parameters 

85 ---------- 

86 args : 

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

88 kwargs : 

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

90 

91 Notes 

92 ----- 

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

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

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

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

97 (for candidate measurement). 

98 

99 Description 

100 

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

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

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

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

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

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

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

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

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

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

111 and AssessSpatialKernelVisitor. 

112 

113 Sigma clipping of KernelCandidates is performed as follows: 

114 

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

116 if PsfMatchConfig.singleKernelClipping is True 

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

118 if PsfMatchConfig.kernelSumClipping is True 

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

120 if PsfMatchConfig.spatialKernelClipping is True 

121 

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

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

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

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

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

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

128 but the rejection thresholds are set 

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

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

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

132 Objects that are significantly above or below the mean, 

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

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

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

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

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

138 

139 Invoking the Task 

140 

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

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

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

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

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

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

147 

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

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

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

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

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

153 return a Psf-matched and template subtracted image. 

154 

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

156 

157 Debug variables 

158 

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

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

161 for this Task include: 

162 

163 .. code-block:: py 

164 

165 import sys 

166 import lsstDebug 

167 def DebugInfo(name): 

168 di = lsstDebug.getInfo(name) 

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

170 di.display = True # enable debug output 

171 di.maskTransparency = 80 # display mask transparency 

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

173 di.displayKernelBasis = False # show kernel basis functions 

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

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

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

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

178 di.display = True # enable debug output 

179 di.maskTransparency = 30 # display mask transparency 

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

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

182 di.displaySpatialCells = True # show spatial cells 

183 di.displayDiffIm = True # show difference image 

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

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

186 di.display = False # enable debug output 

187 di.maskTransparency = 30 # display mask transparency 

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

189 di.pauseAtEnd = False # pause when done 

190 return di 

191 lsstDebug.Info = DebugInfo 

192 lsstDebug.frame = 1 

193 

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

195 

196 .. code-block:: py 

197 

198 import lsst.log.utils as logUtils 

199 logUtils.traceSetAt("ip.diffim", 4) 

200 

201 Examples 

202 -------- 

203 A complete example of using ImagePsfMatchTask 

204 

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

206 

207 .. code-block:: none 

208 

209 examples/imagePsfMatchTask.py --debug 

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

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

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

213 

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

215 

216 .. code-block:: none 

217 

218 class MyImagePsfMatchTask(ImagePsfMatchTask): 

219 

220 def __init__(self, args, kwargs): 

221 ImagePsfMatchTask.__init__(self, args, kwargs) 

222 

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

224 if mode == "matchExposures": 

225 return self.matchExposures(templateExp, scienceExp) 

226 elif mode == "subtractExposures": 

227 return self.subtractExposures(templateExp, scienceExp) 

228 

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

230 or point to their own images on disk. 

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

232 

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

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

235 block checks for this script: 

236 

237 .. code-block:: py 

238 

239 if args.debug: 

240 try: 

241 import debug 

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

243 debug.lsstDebug.frame = 3 

244 except ImportError as e: 

245 print(e, file=sys.stderr) 

246 

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

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

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

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

251 variation in the kernel and background: 

252 

253 .. code-block:: py 

254 

255 def run(args): 

256 # 

257 # Create the Config and use sum of gaussian basis 

258 # 

259 config = ImagePsfMatchTask.ConfigClass() 

260 config.kernel.name = "AL" 

261 config.kernel.active.fitForBackground = True 

262 config.kernel.active.spatialKernelOrder = 1 

263 config.kernel.active.spatialBgOrder = 0 

264 

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

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

267 more details on generateFakeImages): 

268 

269 .. code-block:: py 

270 

271 # Run the requested method of the Task 

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

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

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

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

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

277 try: 

278 templateExp = afwImage.ExposureF(args.template) 

279 except Exception as e: 

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

281 try: 

282 scienceExp = afwImage.ExposureF(args.science) 

283 except Exception as e: 

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

285 else: 

286 templateExp, scienceExp = generateFakeImages() 

287 config.kernel.active.sizeCellX = 128 

288 config.kernel.active.sizeCellY = 128 

289 

290 Create and run the Task: 

291 

292 .. code-block:: py 

293 

294 # Create the Task 

295 psfMatchTask = MyImagePsfMatchTask(config=config) 

296 # Run the Task 

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

298 

299 And finally provide some optional debugging displays: 

300 

301 .. code-block:: py 

302 

303 if args.debug: 

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

305 try: 

306 frame = debug.lsstDebug.frame + 1 

307 except Exception: 

308 frame = 3 

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

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

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

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

313 title="Example script: Subtracted Image") 

314 """ 

315 

316 ConfigClass = ImagePsfMatchConfig 

317 

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

319 """Create the ImagePsfMatchTask. 

320 """ 

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

322 self.kConfig = self.config.kernel.active 

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

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

325 # so cannot easily be constructed with makeSubtask 

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

327 parentTask=self) 

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

329 self.selectAlgMetadata = dafBase.PropertyList() 

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

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

332 

333 def getFwhmPix(self, psf): 

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

335 """ 

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

337 return sigPix*sigma2fwhm 

338 

339 @pipeBase.timeMethod 

340 def matchExposures(self, templateExposure, scienceExposure, 

341 templateFwhmPix=None, scienceFwhmPix=None, 

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

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

344 

345 Do the following, in order: 

346 

347 - Warp templateExposure to match scienceExposure, 

348 if doWarping True and their WCSs do not already match 

349 - Determine a PSF matching kernel and differential background model 

350 that matches templateExposure to scienceExposure 

351 - Convolve templateExposure by PSF matching kernel 

352 

353 Parameters 

354 ---------- 

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

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

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

358 Exposure whose WCS and PSF are to be matched to 

359 templateFwhmPix :`float` 

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

361 scienceFwhmPix : `float` 

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

363 candidateList : `list`, optional 

364 a list of footprints/maskedImages for kernel candidates; 

365 if `None` then source detection is run. 

366 

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

368 

369 doWarping : `bool` 

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

371 

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

373 - if `False` then raise an Exception 

374 

375 convolveTemplate : `bool` 

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

377 

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

379 ``templateExposure`` is convolved 

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

381 ``scienceExposure`` is convolved 

382 

383 Returns 

384 ------- 

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

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

387 

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

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

390 

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

392 - the same filter as templateExposure 

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

394 

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

396 - ``backgroundModel`` : differential background model 

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

398 

399 Raises 

400 ------ 

401 RuntimeError 

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

403 ``scienceExposure`` WCSs do not match 

404 """ 

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

406 if doWarping: 

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

408 templatePsf = templateExposure.getPsf() 

409 # Warp PSF before overwriting exposure 

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

411 scienceExposure.getWcs()) 

412 psfWarped = WarpedPsf(templatePsf, xyTransform) 

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

414 templateExposure, 

415 destBBox=scienceExposure.getBBox()) 

416 templateExposure.setPsf(psfWarped) 

417 else: 

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

419 raise RuntimeError("Input images not registered") 

420 

421 if templateFwhmPix is None: 

422 if not templateExposure.hasPsf(): 

423 self.log.warn("No estimate of Psf FWHM for template image") 

424 else: 

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

426 self.log.info("templateFwhmPix: {}".format(templateFwhmPix)) 

427 

428 if scienceFwhmPix is None: 

429 if not scienceExposure.hasPsf(): 

430 self.log.warn("No estimate of Psf FWHM for science image") 

431 else: 

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

433 self.log.info("scienceFwhmPix: {}".format(scienceFwhmPix)) 

434 

435 if convolveTemplate: 

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

437 candidateList = self.makeCandidateList( 

438 templateExposure, scienceExposure, kernelSize, candidateList) 

439 results = self.matchMaskedImages( 

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

441 templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix) 

442 else: 

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

444 candidateList = self.makeCandidateList( 

445 templateExposure, scienceExposure, kernelSize, candidateList) 

446 results = self.matchMaskedImages( 

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

448 templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix) 

449 

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

451 psfMatchedExposure.setFilterLabel(templateExposure.getFilterLabel()) 

452 psfMatchedExposure.setPhotoCalib(scienceExposure.getPhotoCalib()) 

453 results.warpedExposure = templateExposure 

454 results.matchedExposure = psfMatchedExposure 

455 return results 

456 

457 @pipeBase.timeMethod 

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

459 templateFwhmPix=None, scienceFwhmPix=None): 

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

461 

462 Do the following, in order: 

463 

464 - Determine a PSF matching kernel and differential background model 

465 that matches templateMaskedImage to scienceMaskedImage 

466 - Convolve templateMaskedImage by the PSF matching kernel 

467 

468 Parameters 

469 ---------- 

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

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

472 must be warped to match the reference masked image 

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

474 maskedImage whose PSF is to be matched to 

475 templateFwhmPix : `float` 

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

477 scienceFwhmPix : `float` 

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

479 candidateList : `list`, optional 

480 A list of footprints/maskedImages for kernel candidates; 

481 if `None` then source detection is run. 

482 

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

484 

485 Returns 

486 ------- 

487 result : `callable` 

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

489 

490 - psfMatchedMaskedImage: the PSF-matched masked image = 

491 ``templateMaskedImage`` convolved with psfMatchingKernel. 

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

493 - psfMatchingKernel: the PSF matching kernel 

494 - backgroundModel: differential background model 

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

496 

497 Raises 

498 ------ 

499 RuntimeError 

500 Raised if input images have different dimensions 

501 """ 

502 import lsstDebug 

503 display = lsstDebug.Info(__name__).display 

504 displayTemplate = lsstDebug.Info(__name__).displayTemplate 

505 displaySciIm = lsstDebug.Info(__name__).displaySciIm 

506 displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells 

507 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

508 if not maskTransparency: 

509 maskTransparency = 0 

510 if display: 

511 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

512 

513 if not candidateList: 

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

515 

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

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

518 raise RuntimeError("Input images different size") 

519 

520 if display and displayTemplate: 

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

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

523 lsstDebug.frame += 1 

524 

525 if display and displaySciIm: 

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

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

528 lsstDebug.frame += 1 

529 

530 kernelCellSet = self._buildCellSet(templateMaskedImage, 

531 scienceMaskedImage, 

532 candidateList) 

533 

534 if display and displaySpatialCells: 

535 diffimUtils.showKernelSpatialCells(scienceMaskedImage, kernelCellSet, 

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

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

538 title="Image to not convolve") 

539 lsstDebug.frame += 1 

540 

541 if templateFwhmPix and scienceFwhmPix: 

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

543 

544 if self.kConfig.useBicForKernelBasis: 

545 tmpKernelCellSet = self._buildCellSet(templateMaskedImage, 

546 scienceMaskedImage, 

547 candidateList) 

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

549 bicDegrees = nbe(tmpKernelCellSet, self.log) 

550 basisList = makeKernelBasisList(self.kConfig, templateFwhmPix, scienceFwhmPix, 

551 alardDegGauss=bicDegrees[0], metadata=self.metadata) 

552 del tmpKernelCellSet 

553 else: 

554 basisList = makeKernelBasisList(self.kConfig, templateFwhmPix, scienceFwhmPix, 

555 metadata=self.metadata) 

556 

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

558 

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

560 convolutionControl = afwMath.ConvolutionControl() 

561 convolutionControl.setDoNormalize(False) 

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

563 return pipeBase.Struct( 

564 matchedImage=psfMatchedMaskedImage, 

565 psfMatchingKernel=psfMatchingKernel, 

566 backgroundModel=backgroundModel, 

567 kernelCellSet=kernelCellSet, 

568 ) 

569 

570 @pipeBase.timeMethod 

571 def subtractExposures(self, templateExposure, scienceExposure, 

572 templateFwhmPix=None, scienceFwhmPix=None, 

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

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

575 

576 Do the following, in order: 

577 

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

579 - Determine a PSF matching kernel and differential background model 

580 that matches templateExposure to scienceExposure 

581 - PSF-match templateExposure to scienceExposure 

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

583 

584 Parameters 

585 ---------- 

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

587 Exposure to PSF-match to scienceExposure 

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

589 Reference Exposure 

590 templateFwhmPix : `float` 

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

592 scienceFwhmPix : `float` 

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

594 candidateList : `list`, optional 

595 A list of footprints/maskedImages for kernel candidates; 

596 if `None` then source detection is run. 

597 

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

599 

600 doWarping : `bool` 

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

602 not match: 

603 

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

605 - if `False` then raise an Exception 

606 

607 convolveTemplate : `bool` 

608 Convolve the template image or the science image 

609 

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

611 ``templateExposure`` is convolved 

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

613 ``scienceExposure is`` convolved 

614 

615 Returns 

616 ------- 

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

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

619 

620 - ``subtractedExposure`` : subtracted Exposure 

621 scienceExposure - (matchedImage + backgroundModel) 

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

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

624 and convolving with psfMatchingKernel 

625 - ``psfMatchingKernel`` : PSF matching kernel 

626 - ``backgroundModel`` : differential background model 

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

628 """ 

629 results = self.matchExposures( 

630 templateExposure=templateExposure, 

631 scienceExposure=scienceExposure, 

632 templateFwhmPix=templateFwhmPix, 

633 scienceFwhmPix=scienceFwhmPix, 

634 candidateList=candidateList, 

635 doWarping=doWarping, 

636 convolveTemplate=convolveTemplate 

637 ) 

638 # Always inherit WCS and photocalib from science exposure 

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

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

641 # from the variance planes of the original exposures. 

642 # That recalculation code must be in accordance with the 

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

644 if convolveTemplate: 

645 subtractedMaskedImage = subtractedExposure.maskedImage 

646 subtractedMaskedImage -= results.matchedExposure.maskedImage 

647 subtractedMaskedImage -= results.backgroundModel 

648 else: 

649 subtractedMaskedImage = subtractedExposure.maskedImage 

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

651 subtractedMaskedImage -= results.matchedExposure.maskedImage 

652 subtractedMaskedImage -= results.backgroundModel 

653 

654 # Preserve polarity of differences 

655 subtractedMaskedImage *= -1 

656 

657 # Place back on native photometric scale 

658 subtractedMaskedImage /= results.psfMatchingKernel.computeImage( 

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

660 # We matched to the warped template 

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

662 

663 import lsstDebug 

664 display = lsstDebug.Info(__name__).display 

665 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm 

666 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

667 if not maskTransparency: 

668 maskTransparency = 0 

669 if display: 

670 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

671 if display and displayDiffIm: 

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

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

674 lsstDebug.frame += 1 

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

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

677 lsstDebug.frame += 1 

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

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

680 lsstDebug.frame += 1 

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

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

683 lsstDebug.frame += 1 

684 

685 results.subtractedExposure = subtractedExposure 

686 return results 

687 

688 @pipeBase.timeMethod 

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

690 templateFwhmPix=None, scienceFwhmPix=None): 

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

692 

693 Do the following, in order: 

694 

695 - PSF-match templateMaskedImage to scienceMaskedImage 

696 - Determine the differential background 

697 - Return the difference: scienceMaskedImage 

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

699 

700 Parameters 

701 ---------- 

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

703 MaskedImage to PSF-match to ``scienceMaskedImage`` 

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

705 Reference MaskedImage 

706 templateFwhmPix : `float` 

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

708 scienceFwhmPix : `float` 

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

710 candidateList : `list`, optional 

711 A list of footprints/maskedImages for kernel candidates; 

712 if `None` then source detection is run. 

713 

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

715 

716 Returns 

717 ------- 

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

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

720 

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

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

723 - `psfMatchingKernel`` : PSF matching kernel 

724 - ``backgroundModel`` : differential background model 

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

726 

727 """ 

728 if not candidateList: 

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

730 

731 results = self.matchMaskedImages( 

732 templateMaskedImage=templateMaskedImage, 

733 scienceMaskedImage=scienceMaskedImage, 

734 candidateList=candidateList, 

735 templateFwhmPix=templateFwhmPix, 

736 scienceFwhmPix=scienceFwhmPix, 

737 ) 

738 

739 subtractedMaskedImage = afwImage.MaskedImageF(scienceMaskedImage, True) 

740 subtractedMaskedImage -= results.matchedImage 

741 subtractedMaskedImage -= results.backgroundModel 

742 results.subtractedMaskedImage = subtractedMaskedImage 

743 

744 import lsstDebug 

745 display = lsstDebug.Info(__name__).display 

746 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm 

747 maskTransparency = lsstDebug.Info(__name__).maskTransparency 

748 if not maskTransparency: 

749 maskTransparency = 0 

750 if display: 

751 afwDisplay.setDefaultMaskTransparency(maskTransparency) 

752 if display and displayDiffIm: 

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

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

755 lsstDebug.frame += 1 

756 

757 return results 

758 

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

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

761 

762 This method runs detection and measurement on an exposure. 

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

764 Psf-matching. 

765 

766 Parameters 

767 ---------- 

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

769 Exposure on which to run detection/measurement 

770 sigma : `float` 

771 Detection threshold 

772 doSmooth : `bool` 

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

774 idFactory : 

775 Factory for the generation of Source ids 

776 

777 Returns 

778 ------- 

779 selectSources : 

780 source catalog containing candidates for the Psf-matching 

781 """ 

782 if idFactory: 

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

784 else: 

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

786 mi = exposure.getMaskedImage() 

787 

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

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

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

791 try: 

792 fitBg = self.background.fitBackground(mi) 

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

794 self.background.config.undersampleStyle) 

795 except Exception: 

796 self.log.warn("Failed to get background model. Falling back to median background estimation") 

797 bkgd = np.ma.extras.median(miArr) 

798 

799 # Take off background for detection 

800 mi -= bkgd 

801 try: 

802 table.setMetadata(self.selectAlgMetadata) 

803 detRet = self.selectDetection.run( 

804 table=table, 

805 exposure=exposure, 

806 sigma=sigma, 

807 doSmooth=doSmooth 

808 ) 

809 selectSources = detRet.sources 

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

811 finally: 

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

813 mi += bkgd 

814 del bkgd 

815 return selectSources 

816 

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

818 """Make a list of acceptable KernelCandidates. 

819 

820 Accept or generate a list of candidate sources for 

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

822 images for indications of bad pixels 

823 

824 Parameters 

825 ---------- 

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

827 Exposure that will be convolved 

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

829 Exposure that will be matched-to 

830 kernelSize : `float` 

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

832 candidateList : `list`, optional 

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

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

835 meas.algorithms.PsfCandidateF. 

836 

837 Returns 

838 ------- 

839 candidateList : `list` of `dict` 

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

841 field for the Sources deemed to be appropriate for Psf 

842 matching 

843 """ 

844 if candidateList is None: 

845 candidateList = self.getSelectSources(scienceExposure) 

846 

847 if len(candidateList) < 1: 

848 raise RuntimeError("No candidates in candidateList") 

849 

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

851 if len(listTypes) > 1: 

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

853 

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

855 try: 

856 candidateList[0].getSource() 

857 except Exception as e: 

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

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

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

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

862 

863 candidateList = diffimTools.sourceToFootprintList(candidateList, 

864 templateExposure, scienceExposure, 

865 kernelSize, 

866 self.kConfig.detectionConfig, 

867 self.log) 

868 if len(candidateList) == 0: 

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

870 

871 return candidateList 

872 

873 def _adaptCellSize(self, candidateList): 

874 """NOT IMPLEMENTED YET. 

875 """ 

876 return self.kConfig.sizeCellX, self.kConfig.sizeCellY 

877 

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

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

880 

881 Parameters 

882 ---------- 

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

884 MaskedImage to PSF-matched to scienceMaskedImage 

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

886 Reference MaskedImage 

887 candidateList : `list` 

888 A list of footprints/maskedImages for kernel candidates; 

889 

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

891 

892 Returns 

893 ------- 

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

895 a SpatialCellSet for use with self._solve 

896 """ 

897 if not candidateList: 

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

899 

900 sizeCellX, sizeCellY = self._adaptCellSize(candidateList) 

901 

902 # Object to store the KernelCandidates for spatial modeling 

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

904 sizeCellX, sizeCellY) 

905 

906 ps = pexConfig.makePropertySet(self.kConfig) 

907 # Place candidates within the spatial grid 

908 for cand in candidateList: 

909 if isinstance(cand, afwDetect.Footprint): 

910 bbox = cand.getBBox() 

911 else: 

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

913 tmi = afwImage.MaskedImageF(templateMaskedImage, bbox) 

914 smi = afwImage.MaskedImageF(scienceMaskedImage, bbox) 

915 

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

917 if 'source' in cand: 

918 cand = cand['source'] 

919 xPos = cand.getCentroid()[0] 

920 yPos = cand.getCentroid()[1] 

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

922 

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

924 kernelCellSet.insertCandidate(cand) 

925 

926 return kernelCellSet 

927 

928 def _validateSize(self, templateMaskedImage, scienceMaskedImage): 

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

930 """ 

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

932 

933 def _validateWcs(self, templateExposure, scienceExposure): 

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

935 """ 

936 templateWcs = templateExposure.getWcs() 

937 scienceWcs = scienceExposure.getWcs() 

938 templateBBox = templateExposure.getBBox() 

939 scienceBBox = scienceExposure.getBBox() 

940 

941 # LLC 

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

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

944 

945 # URC 

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

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

948 

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

950 templateOrigin[0], templateOrigin[1], 

951 templateLimit[0], templateLimit[1]) 

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

953 scienceOrigin[0], scienceOrigin[1], 

954 scienceLimit[0], scienceLimit[1]) 

955 

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

957 templateLimit.getPosition(geom.degrees)) 

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

959 scienceLimit.getPosition(geom.degrees)) 

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

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

962 

963 if ((templateOrigin != scienceOrigin) 

964 or (templateLimit != scienceLimit) 

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

966 return False 

967 return True 

968 

969 

970subtractAlgorithmRegistry = pexConfig.makeRegistry( 

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

972) 

973 

974subtractAlgorithmRegistry.register('al', ImagePsfMatchTask)