Coverage for python/lsst/ip/diffim/imagePsfMatch.py: 13%
277 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-08 01:36 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-08 01:36 -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/>.
22import numpy as np
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
43__all__ = ["ImagePsfMatchConfig", "ImagePsfMatchTask", "subtractAlgorithmRegistry"]
45sigma2fwhm = 2.*np.sqrt(2.*np.log(2.))
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 )
68 def setDefaults(self):
69 # High sigma detections only
70 self.selectDetection.reEstimateBackground = False
71 self.selectDetection.thresholdValue = 10.0
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
82class ImagePsfMatchTask(PsfMatchTask):
83 """Psf-match two MaskedImages or Exposures using the sources in the images.
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__
92 """
94 ConfigClass = ImagePsfMatchConfig
96 def __init__(self, *args, **kwargs):
97 """Create the ImagePsfMatchTask.
98 """
99 PsfMatchTask.__init__(self, *args, **kwargs)
100 self.kConfig = self.config.kernel.active
101 self._warper = afwMath.Warper.fromConfig(self.kConfig.warpingConfig)
102 # the background subtraction task uses a config from an unusual location,
103 # so cannot easily be constructed with makeSubtask
104 self.background = SubtractBackgroundTask(config=self.kConfig.afwBackgroundConfig, name="background",
105 parentTask=self)
106 self.selectSchema = afwTable.SourceTable.makeMinimalSchema()
107 self.selectAlgMetadata = dafBase.PropertyList()
108 self.makeSubtask("selectDetection", schema=self.selectSchema)
109 self.makeSubtask("selectMeasurement", schema=self.selectSchema, algMetadata=self.selectAlgMetadata)
111 @timeMethod
112 def matchExposures(self, templateExposure, scienceExposure,
113 templateFwhmPix=None, scienceFwhmPix=None,
114 candidateList=None, doWarping=True, convolveTemplate=True):
115 """Warp and PSF-match an exposure to the reference.
117 Do the following, in order:
119 - Warp templateExposure to match scienceExposure,
120 if doWarping True and their WCSs do not already match
121 - Determine a PSF matching kernel and differential background model
122 that matches templateExposure to scienceExposure
123 - Convolve templateExposure by PSF matching kernel
125 Parameters
126 ----------
127 templateExposure : `lsst.afw.image.Exposure`
128 Exposure to warp and PSF-match to the reference masked image
129 scienceExposure : `lsst.afw.image.Exposure`
130 Exposure whose WCS and PSF are to be matched to
131 templateFwhmPix :`float`
132 FWHM (in pixels) of the Psf in the template image (image to convolve)
133 scienceFwhmPix : `float`
134 FWHM (in pixels) of the Psf in the science image
135 candidateList : `list`, optional
136 a list of footprints/maskedImages for kernel candidates;
137 if `None` then source detection is run.
139 - Currently supported: list of Footprints or measAlg.PsfCandidateF
141 doWarping : `bool`
142 what to do if ``templateExposure`` and ``scienceExposure`` WCSs do not match:
144 - if `True` then warp ``templateExposure`` to match ``scienceExposure``
145 - if `False` then raise an Exception
147 convolveTemplate : `bool`
148 Whether to convolve the template image or the science image:
150 - if `True`, ``templateExposure`` is warped if doWarping,
151 ``templateExposure`` is convolved
152 - if `False`, ``templateExposure`` is warped if doWarping,
153 ``scienceExposure`` is convolved
155 Returns
156 -------
157 results : `lsst.pipe.base.Struct`
158 An `lsst.pipe.base.Struct` containing these fields:
160 - ``matchedImage`` : the PSF-matched exposure =
161 Warped ``templateExposure`` convolved by psfMatchingKernel. This has:
163 - the same parent bbox, Wcs and PhotoCalib as scienceExposure
164 - the same filter as templateExposure
165 - no Psf (because the PSF-matching process does not compute one)
167 - ``psfMatchingKernel`` : the PSF matching kernel
168 - ``backgroundModel`` : differential background model
169 - ``kernelCellSet`` : SpatialCellSet used to solve for the PSF matching kernel
171 Raises
172 ------
173 RuntimeError
174 Raised if doWarping is False and ``templateExposure`` and
175 ``scienceExposure`` WCSs do not match
176 """
177 if not self._validateWcs(templateExposure, scienceExposure):
178 if doWarping:
179 self.log.info("Astrometrically registering template to science image")
180 templatePsf = templateExposure.getPsf()
181 # Warp PSF before overwriting exposure
182 xyTransform = afwGeom.makeWcsPairTransform(templateExposure.getWcs(),
183 scienceExposure.getWcs())
184 psfWarped = WarpedPsf(templatePsf, xyTransform)
185 templateExposure = self._warper.warpExposure(scienceExposure.getWcs(),
186 templateExposure,
187 destBBox=scienceExposure.getBBox())
188 templateExposure.setPsf(psfWarped)
189 else:
190 self.log.error("ERROR: Input images not registered")
191 raise RuntimeError("Input images not registered")
193 if templateFwhmPix is None:
194 if not templateExposure.hasPsf():
195 self.log.warning("No estimate of Psf FWHM for template image")
196 else:
197 templateFwhmPix = diffimUtils.getPsfFwhm(templateExposure.psf)
198 self.log.info("templateFwhmPix: %s", templateFwhmPix)
200 if scienceFwhmPix is None:
201 if not scienceExposure.hasPsf():
202 self.log.warning("No estimate of Psf FWHM for science image")
203 else:
204 scienceFwhmPix = diffimUtils.getPsfFwhm(scienceExposure.psf)
205 self.log.info("scienceFwhmPix: %s", scienceFwhmPix)
207 if convolveTemplate:
208 kernelSize = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix)[0].getWidth()
209 candidateList = self.makeCandidateList(
210 templateExposure, scienceExposure, kernelSize, candidateList)
211 results = self.matchMaskedImages(
212 templateExposure.getMaskedImage(), scienceExposure.getMaskedImage(), candidateList,
213 templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix)
214 else:
215 kernelSize = self.makeKernelBasisList(scienceFwhmPix, templateFwhmPix)[0].getWidth()
216 candidateList = self.makeCandidateList(
217 templateExposure, scienceExposure, kernelSize, candidateList)
218 results = self.matchMaskedImages(
219 scienceExposure.getMaskedImage(), templateExposure.getMaskedImage(), candidateList,
220 templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix)
222 psfMatchedExposure = afwImage.makeExposure(results.matchedImage, scienceExposure.getWcs())
223 psfMatchedExposure.setFilter(templateExposure.getFilter())
224 psfMatchedExposure.setPhotoCalib(scienceExposure.getPhotoCalib())
225 results.warpedExposure = templateExposure
226 results.matchedExposure = psfMatchedExposure
227 return results
229 @timeMethod
230 def matchMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList,
231 templateFwhmPix=None, scienceFwhmPix=None):
232 """PSF-match a MaskedImage (templateMaskedImage) to a reference MaskedImage (scienceMaskedImage).
234 Do the following, in order:
236 - Determine a PSF matching kernel and differential background model
237 that matches templateMaskedImage to scienceMaskedImage
238 - Convolve templateMaskedImage by the PSF matching kernel
240 Parameters
241 ----------
242 templateMaskedImage : `lsst.afw.image.MaskedImage`
243 masked image to PSF-match to the reference masked image;
244 must be warped to match the reference masked image
245 scienceMaskedImage : `lsst.afw.image.MaskedImage`
246 maskedImage whose PSF is to be matched to
247 templateFwhmPix : `float`
248 FWHM (in pixels) of the Psf in the template image (image to convolve)
249 scienceFwhmPix : `float`
250 FWHM (in pixels) of the Psf in the science image
251 candidateList : `list`, optional
252 A list of footprints/maskedImages for kernel candidates;
253 if `None` then source detection is run.
255 - Currently supported: list of Footprints or measAlg.PsfCandidateF
257 Returns
258 -------
259 result : `callable`
260 An `lsst.pipe.base.Struct` containing these fields:
262 - psfMatchedMaskedImage: the PSF-matched masked image =
263 ``templateMaskedImage`` convolved with psfMatchingKernel.
264 This has the same xy0, dimensions and wcs as ``scienceMaskedImage``.
265 - psfMatchingKernel: the PSF matching kernel
266 - backgroundModel: differential background model
267 - kernelCellSet: SpatialCellSet used to solve for the PSF matching kernel
269 Raises
270 ------
271 RuntimeError
272 Raised if input images have different dimensions
273 """
274 import lsstDebug
275 display = lsstDebug.Info(__name__).display
276 displayTemplate = lsstDebug.Info(__name__).displayTemplate
277 displaySciIm = lsstDebug.Info(__name__).displaySciIm
278 displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
279 maskTransparency = lsstDebug.Info(__name__).maskTransparency
280 if not maskTransparency:
281 maskTransparency = 0
282 if display:
283 afwDisplay.setDefaultMaskTransparency(maskTransparency)
285 if not candidateList:
286 raise RuntimeError("Candidate list must be populated by makeCandidateList")
288 if not self._validateSize(templateMaskedImage, scienceMaskedImage):
289 self.log.error("ERROR: Input images different size")
290 raise RuntimeError("Input images different size")
292 if display and displayTemplate:
293 disp = afwDisplay.Display(frame=lsstDebug.frame)
294 disp.mtv(templateMaskedImage, title="Image to convolve")
295 lsstDebug.frame += 1
297 if display and displaySciIm:
298 disp = afwDisplay.Display(frame=lsstDebug.frame)
299 disp.mtv(scienceMaskedImage, title="Image to not convolve")
300 lsstDebug.frame += 1
302 kernelCellSet = self._buildCellSet(templateMaskedImage,
303 scienceMaskedImage,
304 candidateList)
306 if display and displaySpatialCells:
307 diffimUtils.showKernelSpatialCells(scienceMaskedImage, kernelCellSet,
308 symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
309 ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
310 title="Image to not convolve")
311 lsstDebug.frame += 1
313 if templateFwhmPix and scienceFwhmPix:
314 self.log.info("Matching Psf FWHM %.2f -> %.2f pix", templateFwhmPix, scienceFwhmPix)
316 if self.kConfig.useBicForKernelBasis:
317 tmpKernelCellSet = self._buildCellSet(templateMaskedImage,
318 scienceMaskedImage,
319 candidateList)
320 nbe = diffimTools.NbasisEvaluator(self.kConfig, templateFwhmPix, scienceFwhmPix)
321 bicDegrees = nbe(tmpKernelCellSet, self.log)
322 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix,
323 basisDegGauss=bicDegrees[0], metadata=self.metadata)
324 del tmpKernelCellSet
325 else:
326 basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix,
327 metadata=self.metadata)
329 spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
331 psfMatchedMaskedImage = afwImage.MaskedImageF(templateMaskedImage.getBBox())
332 convolutionControl = afwMath.ConvolutionControl()
333 convolutionControl.setDoNormalize(False)
334 afwMath.convolve(psfMatchedMaskedImage, templateMaskedImage, psfMatchingKernel, convolutionControl)
335 return pipeBase.Struct(
336 matchedImage=psfMatchedMaskedImage,
337 psfMatchingKernel=psfMatchingKernel,
338 backgroundModel=backgroundModel,
339 kernelCellSet=kernelCellSet,
340 )
342 @timeMethod
343 def subtractExposures(self, templateExposure, scienceExposure,
344 templateFwhmPix=None, scienceFwhmPix=None,
345 candidateList=None, doWarping=True, convolveTemplate=True):
346 """Register, Psf-match and subtract two Exposures.
348 Do the following, in order:
350 - Warp templateExposure to match scienceExposure, if their WCSs do not already match
351 - Determine a PSF matching kernel and differential background model
352 that matches templateExposure to scienceExposure
353 - PSF-match templateExposure to scienceExposure
354 - Compute subtracted exposure (see return values for equation).
356 Parameters
357 ----------
358 templateExposure : `lsst.afw.image.ExposureF`
359 Exposure to PSF-match to scienceExposure
360 scienceExposure : `lsst.afw.image.ExposureF`
361 Reference Exposure
362 templateFwhmPix : `float`
363 FWHM (in pixels) of the Psf in the template image (image to convolve)
364 scienceFwhmPix : `float`
365 FWHM (in pixels) of the Psf in the science image
366 candidateList : `list`, optional
367 A list of footprints/maskedImages for kernel candidates;
368 if `None` then source detection is run.
370 - Currently supported: list of Footprints or measAlg.PsfCandidateF
372 doWarping : `bool`
373 What to do if ``templateExposure``` and ``scienceExposure`` WCSs do
374 not match:
376 - if `True` then warp ``templateExposure`` to match ``scienceExposure``
377 - if `False` then raise an Exception
379 convolveTemplate : `bool`
380 Convolve the template image or the science image
382 - if `True`, ``templateExposure`` is warped if doWarping,
383 ``templateExposure`` is convolved
384 - if `False`, ``templateExposure`` is warped if doWarping,
385 ``scienceExposure is`` convolved
387 Returns
388 -------
389 result : `lsst.pipe.base.Struct`
390 An `lsst.pipe.base.Struct` containing these fields:
392 - ``subtractedExposure`` : subtracted Exposure
393 scienceExposure - (matchedImage + backgroundModel)
394 - ``matchedImage`` : ``templateExposure`` after warping to match
395 ``templateExposure`` (if doWarping true),
396 and convolving with psfMatchingKernel
397 - ``psfMatchingKernel`` : PSF matching kernel
398 - ``backgroundModel`` : differential background model
399 - ``kernelCellSet`` : SpatialCellSet used to determine PSF matching kernel
400 """
401 results = self.matchExposures(
402 templateExposure=templateExposure,
403 scienceExposure=scienceExposure,
404 templateFwhmPix=templateFwhmPix,
405 scienceFwhmPix=scienceFwhmPix,
406 candidateList=candidateList,
407 doWarping=doWarping,
408 convolveTemplate=convolveTemplate
409 )
410 # Always inherit WCS and photocalib from science exposure
411 subtractedExposure = afwImage.ExposureF(scienceExposure, deep=True)
412 # Note, the decorrelation afterburner re-calculates the variance plane
413 # from the variance planes of the original exposures.
414 # That recalculation code must be in accordance with the
415 # photometric level set here in ``subtractedMaskedImage``.
416 if convolveTemplate:
417 subtractedMaskedImage = subtractedExposure.maskedImage
418 subtractedMaskedImage -= results.matchedExposure.maskedImage
419 subtractedMaskedImage -= results.backgroundModel
420 else:
421 subtractedMaskedImage = subtractedExposure.maskedImage
422 subtractedMaskedImage[:, :] = results.warpedExposure.maskedImage
423 subtractedMaskedImage -= results.matchedExposure.maskedImage
424 subtractedMaskedImage -= results.backgroundModel
426 # Preserve polarity of differences
427 subtractedMaskedImage *= -1
429 # Place back on native photometric scale
430 subtractedMaskedImage /= results.psfMatchingKernel.computeImage(
431 afwImage.ImageD(results.psfMatchingKernel.getDimensions()), False)
432 # We matched to the warped template
433 subtractedExposure.setPsf(results.warpedExposure.getPsf())
435 import lsstDebug
436 display = lsstDebug.Info(__name__).display
437 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm
438 maskTransparency = lsstDebug.Info(__name__).maskTransparency
439 if not maskTransparency:
440 maskTransparency = 0
441 if display:
442 afwDisplay.setDefaultMaskTransparency(maskTransparency)
443 if display and displayDiffIm:
444 disp = afwDisplay.Display(frame=lsstDebug.frame)
445 disp.mtv(templateExposure, title="Template")
446 lsstDebug.frame += 1
447 disp = afwDisplay.Display(frame=lsstDebug.frame)
448 disp.mtv(results.matchedExposure, title="Matched template")
449 lsstDebug.frame += 1
450 disp = afwDisplay.Display(frame=lsstDebug.frame)
451 disp.mtv(scienceExposure, title="Science Image")
452 lsstDebug.frame += 1
453 disp = afwDisplay.Display(frame=lsstDebug.frame)
454 disp.mtv(subtractedExposure, title="Difference Image")
455 lsstDebug.frame += 1
457 results.subtractedExposure = subtractedExposure
458 return results
460 @timeMethod
461 def subtractMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList,
462 templateFwhmPix=None, scienceFwhmPix=None):
463 """Psf-match and subtract two MaskedImages.
465 Do the following, in order:
467 - PSF-match templateMaskedImage to scienceMaskedImage
468 - Determine the differential background
469 - Return the difference: scienceMaskedImage
470 ((warped templateMaskedImage convolved with psfMatchingKernel) + backgroundModel)
472 Parameters
473 ----------
474 templateMaskedImage : `lsst.afw.image.MaskedImage`
475 MaskedImage to PSF-match to ``scienceMaskedImage``
476 scienceMaskedImage : `lsst.afw.image.MaskedImage`
477 Reference MaskedImage
478 templateFwhmPix : `float`
479 FWHM (in pixels) of the Psf in the template image (image to convolve)
480 scienceFwhmPix : `float`
481 FWHM (in pixels) of the Psf in the science image
482 candidateList : `list`, optional
483 A list of footprints/maskedImages for kernel candidates;
484 if `None` then source detection is run.
486 - Currently supported: list of Footprints or measAlg.PsfCandidateF
488 Returns
489 -------
490 results : `lsst.pipe.base.Struct`
491 An `lsst.pipe.base.Struct` containing these fields:
493 - ``subtractedMaskedImage`` : ``scienceMaskedImage`` - (matchedImage + backgroundModel)
494 - ``matchedImage`` : templateMaskedImage convolved with psfMatchingKernel
495 - `psfMatchingKernel`` : PSF matching kernel
496 - ``backgroundModel`` : differential background model
497 - ``kernelCellSet`` : SpatialCellSet used to determine PSF matching kernel
499 """
500 if not candidateList:
501 raise RuntimeError("Candidate list must be populated by makeCandidateList")
503 results = self.matchMaskedImages(
504 templateMaskedImage=templateMaskedImage,
505 scienceMaskedImage=scienceMaskedImage,
506 candidateList=candidateList,
507 templateFwhmPix=templateFwhmPix,
508 scienceFwhmPix=scienceFwhmPix,
509 )
511 subtractedMaskedImage = afwImage.MaskedImageF(scienceMaskedImage, True)
512 subtractedMaskedImage -= results.matchedImage
513 subtractedMaskedImage -= results.backgroundModel
514 results.subtractedMaskedImage = subtractedMaskedImage
516 import lsstDebug
517 display = lsstDebug.Info(__name__).display
518 displayDiffIm = lsstDebug.Info(__name__).displayDiffIm
519 maskTransparency = lsstDebug.Info(__name__).maskTransparency
520 if not maskTransparency:
521 maskTransparency = 0
522 if display:
523 afwDisplay.setDefaultMaskTransparency(maskTransparency)
524 if display and displayDiffIm:
525 disp = afwDisplay.Display(frame=lsstDebug.frame)
526 disp.mtv(subtractedMaskedImage, title="Subtracted masked image")
527 lsstDebug.frame += 1
529 return results
531 def getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None):
532 """Get sources to use for Psf-matching.
534 This method runs detection and measurement on an exposure.
535 The returned set of sources will be used as candidates for
536 Psf-matching.
538 Parameters
539 ----------
540 exposure : `lsst.afw.image.Exposure`
541 Exposure on which to run detection/measurement
542 sigma : `float`
543 Detection threshold
544 doSmooth : `bool`
545 Whether or not to smooth the Exposure with Psf before detection
546 idFactory :
547 Factory for the generation of Source ids
549 Returns
550 -------
551 selectSources :
552 source catalog containing candidates for the Psf-matching
553 """
554 if idFactory:
555 table = afwTable.SourceTable.make(self.selectSchema, idFactory)
556 else:
557 table = afwTable.SourceTable.make(self.selectSchema)
558 mi = exposure.getMaskedImage()
560 imArr = mi.getImage().getArray()
561 maskArr = mi.getMask().getArray()
562 miArr = np.ma.masked_array(imArr, mask=maskArr)
563 try:
564 fitBg = self.background.fitBackground(mi)
565 bkgd = fitBg.getImageF(self.background.config.algorithm,
566 self.background.config.undersampleStyle)
567 except Exception:
568 self.log.warning("Failed to get background model. Falling back to median background estimation")
569 bkgd = np.ma.median(miArr)
571 # Take off background for detection
572 mi -= bkgd
573 try:
574 table.setMetadata(self.selectAlgMetadata)
575 detRet = self.selectDetection.run(
576 table=table,
577 exposure=exposure,
578 sigma=sigma,
579 doSmooth=doSmooth
580 )
581 selectSources = detRet.sources
582 self.selectMeasurement.run(measCat=selectSources, exposure=exposure)
583 finally:
584 # Put back on the background in case it is needed down stream
585 mi += bkgd
586 del bkgd
587 return selectSources
589 def makeCandidateList(self, templateExposure, scienceExposure, kernelSize, candidateList=None):
590 """Make a list of acceptable KernelCandidates.
592 Accept or generate a list of candidate sources for
593 Psf-matching, and examine the Mask planes in both of the
594 images for indications of bad pixels
596 Parameters
597 ----------
598 templateExposure : `lsst.afw.image.Exposure`
599 Exposure that will be convolved
600 scienceExposure : `lsst.afw.image.Exposure`
601 Exposure that will be matched-to
602 kernelSize : `float`
603 Dimensions of the Psf-matching Kernel, used to grow detection footprints
604 candidateList : `list`, optional
605 List of Sources to examine. Elements must be of type afw.table.Source
606 or a type that wraps a Source and has a getSource() method, such as
607 meas.algorithms.PsfCandidateF.
609 Returns
610 -------
611 candidateList : `list` of `dict`
612 A list of dicts having a "source" and "footprint"
613 field for the Sources deemed to be appropriate for Psf
614 matching
615 """
616 if candidateList is None:
617 candidateList = self.getSelectSources(scienceExposure)
619 if len(candidateList) < 1:
620 raise RuntimeError("No candidates in candidateList")
622 listTypes = set(type(x) for x in candidateList)
623 if len(listTypes) > 1:
624 raise RuntimeError("Candidate list contains mixed types: %s" % [t for t in listTypes])
626 if not isinstance(candidateList[0], afwTable.SourceRecord):
627 try:
628 candidateList[0].getSource()
629 except Exception as e:
630 raise RuntimeError(f"Candidate List is of type: {type(candidateList[0])} "
631 "Can only make candidate list from list of afwTable.SourceRecords, "
632 f"measAlg.PsfCandidateF or other type with a getSource() method: {e}")
633 candidateList = [c.getSource() for c in candidateList]
635 candidateList = diffimTools.sourceToFootprintList(candidateList,
636 templateExposure, scienceExposure,
637 kernelSize,
638 self.kConfig.detectionConfig,
639 self.log)
640 if len(candidateList) == 0:
641 raise RuntimeError("Cannot find any objects suitable for KernelCandidacy")
643 return candidateList
645 def makeKernelBasisList(self, targetFwhmPix=None, referenceFwhmPix=None,
646 basisDegGauss=None, basisSigmaGauss=None, metadata=None):
647 """Wrapper to set log messages for
648 `lsst.ip.diffim.makeKernelBasisList`.
650 Parameters
651 ----------
652 targetFwhmPix : `float`, optional
653 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
654 Not used for delta function basis sets.
655 referenceFwhmPix : `float`, optional
656 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
657 Not used for delta function basis sets.
658 basisDegGauss : `list` of `int`, optional
659 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
660 Not used for delta function basis sets.
661 basisSigmaGauss : `list` of `int`, optional
662 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
663 Not used for delta function basis sets.
664 metadata : `lsst.daf.base.PropertySet`, optional
665 Passed on to `lsst.ip.diffim.generateAlardLuptonBasisList`.
666 Not used for delta function basis sets.
668 Returns
669 -------
670 basisList: `list` of `lsst.afw.math.kernel.FixedKernel`
671 List of basis kernels.
672 """
673 basisList = makeKernelBasisList(self.kConfig,
674 targetFwhmPix=targetFwhmPix,
675 referenceFwhmPix=referenceFwhmPix,
676 basisDegGauss=basisDegGauss,
677 basisSigmaGauss=basisSigmaGauss,
678 metadata=metadata)
679 if targetFwhmPix == referenceFwhmPix:
680 self.log.info("Target and reference psf fwhms are equal, falling back to config values")
681 elif referenceFwhmPix > targetFwhmPix:
682 self.log.info("Reference psf fwhm is the greater, normal convolution mode")
683 else:
684 self.log.info("Target psf fwhm is the greater, deconvolution mode")
686 return basisList
688 def _adaptCellSize(self, candidateList):
689 """NOT IMPLEMENTED YET.
690 """
691 return self.kConfig.sizeCellX, self.kConfig.sizeCellY
693 def _buildCellSet(self, templateMaskedImage, scienceMaskedImage, candidateList):
694 """Build a SpatialCellSet for use with the solve method.
696 Parameters
697 ----------
698 templateMaskedImage : `lsst.afw.image.MaskedImage`
699 MaskedImage to PSF-matched to scienceMaskedImage
700 scienceMaskedImage : `lsst.afw.image.MaskedImage`
701 Reference MaskedImage
702 candidateList : `list`
703 A list of footprints/maskedImages for kernel candidates;
705 - Currently supported: list of Footprints or measAlg.PsfCandidateF
707 Returns
708 -------
709 kernelCellSet : `lsst.afw.math.SpatialCellSet`
710 a SpatialCellSet for use with self._solve
711 """
712 if not candidateList:
713 raise RuntimeError("Candidate list must be populated by makeCandidateList")
715 sizeCellX, sizeCellY = self._adaptCellSize(candidateList)
717 # Object to store the KernelCandidates for spatial modeling
718 kernelCellSet = afwMath.SpatialCellSet(templateMaskedImage.getBBox(),
719 sizeCellX, sizeCellY)
721 ps = pexConfig.makePropertySet(self.kConfig)
722 # Place candidates within the spatial grid
723 for cand in candidateList:
724 if isinstance(cand, afwDetect.Footprint):
725 bbox = cand.getBBox()
726 else:
727 bbox = cand['footprint'].getBBox()
728 tmi = afwImage.MaskedImageF(templateMaskedImage, bbox)
729 smi = afwImage.MaskedImageF(scienceMaskedImage, bbox)
731 if not isinstance(cand, afwDetect.Footprint):
732 if 'source' in cand:
733 cand = cand['source']
734 xPos = cand.getCentroid()[0]
735 yPos = cand.getCentroid()[1]
736 cand = diffimLib.makeKernelCandidate(xPos, yPos, tmi, smi, ps)
738 self.log.debug("Candidate %d at %f, %f", cand.getId(), cand.getXCenter(), cand.getYCenter())
739 kernelCellSet.insertCandidate(cand)
741 return kernelCellSet
743 def _validateSize(self, templateMaskedImage, scienceMaskedImage):
744 """Return True if two image-like objects are the same size.
745 """
746 return templateMaskedImage.getDimensions() == scienceMaskedImage.getDimensions()
748 def _validateWcs(self, templateExposure, scienceExposure):
749 """Return True if the WCS of the two Exposures have the same origin and extent.
750 """
751 templateWcs = templateExposure.getWcs()
752 scienceWcs = scienceExposure.getWcs()
753 templateBBox = templateExposure.getBBox()
754 scienceBBox = scienceExposure.getBBox()
756 # LLC
757 templateOrigin = templateWcs.pixelToSky(geom.Point2D(templateBBox.getBegin()))
758 scienceOrigin = scienceWcs.pixelToSky(geom.Point2D(scienceBBox.getBegin()))
760 # URC
761 templateLimit = templateWcs.pixelToSky(geom.Point2D(templateBBox.getEnd()))
762 scienceLimit = scienceWcs.pixelToSky(geom.Point2D(scienceBBox.getEnd()))
764 self.log.info("Template Wcs : %f,%f -> %f,%f",
765 templateOrigin[0], templateOrigin[1],
766 templateLimit[0], templateLimit[1])
767 self.log.info("Science Wcs : %f,%f -> %f,%f",
768 scienceOrigin[0], scienceOrigin[1],
769 scienceLimit[0], scienceLimit[1])
771 templateBBox = geom.Box2D(templateOrigin.getPosition(geom.degrees),
772 templateLimit.getPosition(geom.degrees))
773 scienceBBox = geom.Box2D(scienceOrigin.getPosition(geom.degrees),
774 scienceLimit.getPosition(geom.degrees))
775 if not (templateBBox.overlaps(scienceBBox)):
776 raise RuntimeError("Input images do not overlap at all")
778 if ((templateOrigin != scienceOrigin)
779 or (templateLimit != scienceLimit)
780 or (templateExposure.getDimensions() != scienceExposure.getDimensions())):
781 return False
782 return True
785subtractAlgorithmRegistry = pexConfig.makeRegistry(
786 doc="A registry of subtraction algorithms for use as a subtask in imageDifference",
787)
789subtractAlgorithmRegistry.register('al', ImagePsfMatchTask)