24 from .
import diffimLib
32 from lsst.utils.timer
import timeMethod
33 from .makeKernelBasisList
import makeKernelBasisList
34 from .psfMatch
import PsfMatchTask, PsfMatchConfigAL
35 from .
import utils
as dituils
37 __all__ = (
"ModelPsfMatchTask",
"ModelPsfMatchConfig")
39 sigma2fwhm = 2.*np.sqrt(2.*np.log(2.))
43 nextInt = int(np.ceil(x))
44 return nextInt + 1
if nextInt%2 == 0
else nextInt
48 """Configuration for model-to-model Psf matching"""
50 kernel = pexConfig.ConfigChoiceField(
57 doAutoPadPsf = pexConfig.Field(
59 doc=(
"If too small, automatically pad the science Psf? "
60 "Pad to smallest dimensions appropriate for the matching kernel dimensions, "
61 "as specified by autoPadPsfTo. If false, pad by the padPsfBy config."),
64 autoPadPsfTo = pexConfig.RangeField(
66 doc=(
"Minimum Science Psf dimensions as a fraction of matching kernel dimensions. "
67 "If the dimensions of the Psf to be matched are less than the "
68 "matching kernel dimensions * autoPadPsfTo, pad Science Psf to this size. "
69 "Ignored if doAutoPadPsf=False."),
74 padPsfBy = pexConfig.Field(
76 doc=
"Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True",
82 self.
kernelkernel.active.singleKernelClipping =
False
83 self.
kernelkernel.active.kernelSumClipping =
False
84 self.
kernelkernel.active.spatialKernelClipping =
False
85 self.
kernelkernel.active.checkConditionNumber =
False
88 self.
kernelkernel.active.constantVarianceWeighting =
True
91 self.
kernelkernel.active.scaleByFwhm =
False
95 """Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure
100 This Task differs from ImagePsfMatchTask in that it matches two Psf _models_, by realizing
101 them in an Exposure-sized SpatialCellSet and then inserting each Psf-image pair into KernelCandidates.
102 Because none of the pairs of sources that are to be matched should be invalid, all sigma clipping is
103 turned off in ModelPsfMatchConfig. And because there is no tracked _variance_ in the Psf images, the
104 debugging and logging QA info should be interpreted with caution.
106 One item of note is that the sizes of Psf models are fixed (e.g. its defined as a 21x21 matrix). When the
107 Psf-matching kernel is being solved for, the Psf "image" is convolved with each kernel basis function,
108 leading to a loss of information around the borders.
109 This pixel loss will be problematic for the numerical
110 stability of the kernel solution if the size of the convolution kernel
111 (set by ModelPsfMatchConfig.kernelSize) is much bigger than: psfSize//2.
112 Thus the sizes of Psf-model matching kernels are typically smaller
113 than their image-matching counterparts. If the size of the kernel is too small, the convolved stars will
114 look "boxy"; if the kernel is too large, the kernel solution will be "noisy". This is a trade-off that
115 needs careful attention for a given dataset.
117 The primary use case for this Task is in matching an Exposure to a
118 constant-across-the-sky Psf model for the purposes of image coaddition.
119 It is important to note that in the code, the "template" Psf is the Psf
120 that the science image gets matched to. In this sense the order of template and science image are
121 reversed, compared to ImagePsfMatchTask, which operates on the template image.
125 The `lsst.pipe.base.cmdLineTask.CmdLineTask` command line task interface supports a
126 flag -d/--debug to import debug.py from your PYTHONPATH. The relevant contents of debug.py
127 for this Task include:
134 di = lsstDebug.getInfo(name)
135 if name == "lsst.ip.diffim.psfMatch":
136 di.display = True # global
137 di.maskTransparency = 80 # mask transparency
138 di.displayCandidates = True # show all the candidates and residuals
139 di.displayKernelBasis = False # show kernel basis functions
140 di.displayKernelMosaic = True # show kernel realized across the image
141 di.plotKernelSpatialModel = False # show coefficients of spatial model
142 di.showBadCandidates = True # show the bad candidates (red) along with good (green)
143 elif name == "lsst.ip.diffim.modelPsfMatch":
144 di.display = True # global
145 di.maskTransparency = 30 # mask transparency
146 di.displaySpatialCells = True # show spatial cells before the fit
148 lsstDebug.Info = DebugInfo
151 Note that if you want addional logging info, you may add to your scripts:
155 import lsst.log.utils as logUtils
156 logUtils.traceSetAt("lsst.ip.diffim", 4)
160 A complete example of using ModelPsfMatchTask
162 This code is modelPsfMatchTask.py in the examples directory, and can be run as e.g.
164 .. code-block :: none
166 examples/modelPsfMatchTask.py
167 examples/modelPsfMatchTask.py --debug
168 examples/modelPsfMatchTask.py --debug --template /path/to/templateExp.fits
169 --science /path/to/scienceExp.fits
171 Create a subclass of ModelPsfMatchTask that accepts two exposures.
172 Note that the "template" exposure contains the Psf that will get matched to,
173 and the "science" exposure is the one that will be convolved:
175 .. code-block :: none
177 class MyModelPsfMatchTask(ModelPsfMatchTask):
178 def __init__(self, *args, **kwargs):
179 ModelPsfMatchTask.__init__(self, *args, **kwargs)
180 def run(self, templateExp, scienceExp):
181 return ModelPsfMatchTask.run(self, scienceExp, templateExp.getPsf())
183 And allow the user the freedom to either run the script in default mode,
184 or point to their own images on disk. Note that these
185 images must be readable as an lsst.afw.image.Exposure:
187 .. code-block :: none
189 if __name__ == "__main__":
191 parser = argparse.ArgumentParser(description="Demonstrate the use of ModelPsfMatchTask")
192 parser.add_argument("--debug", "-d", action="store_true", help="Load debug.py?", default=False)
193 parser.add_argument("--template", "-t", help="Template Exposure to use", default=None)
194 parser.add_argument("--science", "-s", help="Science Exposure to use", default=None)
195 args = parser.parse_args()
197 We have enabled some minor display debugging in this script via the –debug option.
198 However, if you have an lsstDebug debug.py in your PYTHONPATH you will get additional
199 debugging displays. The following block checks for this script:
201 .. code-block :: none
206 # Since I am displaying 2 images here, set the starting frame number for the LSST debug LSST
207 debug.lsstDebug.frame = 3
208 except ImportError as e:
209 print(e, file=sys.stderr)
211 Finally, we call a run method that we define below.
212 First set up a Config and modify some of the parameters.
213 In particular we don't want to "grow" the sizes of the kernel or KernelCandidates,
214 since we are operating with fixed–size images (i.e. the size of the input Psf models).
216 .. code-block :: none
220 # Create the Config and use sum of gaussian basis
222 config = ModelPsfMatchTask.ConfigClass()
223 config.kernel.active.scaleByFwhm = False
225 Make sure the images (if any) that were sent to the script exist on disk and are readable.
226 If no images are sent, make some fake data up for the sake of this example script
227 (have a look at the code if you want more details on generateFakeData):
229 .. code-block :: none
231 # Run the requested method of the Task
232 if args.template is not None and args.science is not None:
233 if not os.path.isfile(args.template):
234 raise FileNotFoundError("Template image %s does not exist" % (args.template))
235 if not os.path.isfile(args.science):
236 raise FileNotFoundError("Science image %s does not exist" % (args.science))
238 templateExp = afwImage.ExposureF(args.template)
239 except Exception as e:
240 raise RuntimeError("Cannot read template image %s" % (args.template))
242 scienceExp = afwImage.ExposureF(args.science)
243 except Exception as e:
244 raise RuntimeError("Cannot read science image %s" % (args.science))
246 templateExp, scienceExp = generateFakeData()
247 config.kernel.active.sizeCellX = 128
248 config.kernel.active.sizeCellY = 128
250 .. code-block :: none
253 afwDisplay.Display(frame=1).mtv(templateExp, title="Example script: Input Template")
254 afwDisplay.Display(frame=2).mtv(scienceExp, title="Example script: Input Science Image")
256 Create and run the Task:
258 .. code-block :: none
261 psfMatchTask = MyModelPsfMatchTask(config=config)
263 result = psfMatchTask.run(templateExp, scienceExp)
265 And finally provide optional debugging display of the Psf-matched (via the Psf models) science image:
267 .. code-block :: none
270 # See if the LSST debug has incremented the frame number; if not start with frame 3
272 frame = debug.lsstDebug.frame + 1
275 afwDisplay.Display(frame=frame).mtv(result.psfMatchedExposure,
276 title="Example script: Matched Science Image")
279 ConfigClass = ModelPsfMatchConfig
282 """Create a ModelPsfMatchTask
287 arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
289 keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
293 Upon initialization, the kernel configuration is defined by self.config.kernel.active. This Task
294 does have a run() method, which is the default way to call the Task.
296 PsfMatchTask.__init__(self, *args, **kwargs)
300 def run(self, exposure, referencePsfModel, kernelSum=1.0):
301 """Psf-match an exposure to a model Psf
305 exposure : `lsst.afw.image.Exposure`
306 Exposure to Psf-match to the reference Psf model;
307 it must return a valid PSF model via exposure.getPsf()
308 referencePsfModel : `lsst.afw.detection.Psf`
309 The Psf model to match to
310 kernelSum : `float`, optional
311 A multipicative factor to apply to the kernel sum (default=1.0)
316 - ``psfMatchedExposure`` : the Psf-matched Exposure.
317 This has the same parent bbox, Wcs, PhotoCalib and
318 Filter as the input Exposure but no Psf.
319 In theory the Psf should equal referencePsfModel but
320 the match is likely not exact.
321 - ``psfMatchingKernel`` : the spatially varying Psf-matching kernel
322 - ``kernelCellSet`` : SpatialCellSet used to solve for the Psf-matching kernel
323 - ``referencePsfModel`` : Validated and/or modified reference model used
328 if the Exposure does not contain a Psf model
330 if not exposure.hasPsf():
331 raise RuntimeError(
"exposure does not contain a Psf model")
333 maskedImage = exposure.getMaskedImage()
335 self.log.
info(
"compute Psf-matching kernel")
337 kernelCellSet = result.kernelCellSet
338 referencePsfModel = result.referencePsfModel
339 fwhmScience = exposure.getPsf().computeShape().getDeterminantRadius()*sigma2fwhm
340 fwhmModel = referencePsfModel.computeShape().getDeterminantRadius()*sigma2fwhm
343 spatialSolution, psfMatchingKernel, backgroundModel = self.
_solve_solve(kernelCellSet, basisList)
345 if psfMatchingKernel.isSpatiallyVarying():
346 sParameters = np.array(psfMatchingKernel.getSpatialParameters())
347 sParameters[0][0] = kernelSum
348 psfMatchingKernel.setSpatialParameters(sParameters)
350 kParameters = np.array(psfMatchingKernel.getKernelParameters())
351 kParameters[0] = kernelSum
352 psfMatchingKernel.setKernelParameters(kParameters)
354 self.log.
info(
"Psf-match science exposure to reference")
355 psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
356 psfMatchedExposure.info.id = exposure.info.id
357 psfMatchedExposure.setFilterLabel(exposure.getFilterLabel())
358 psfMatchedExposure.setPhotoCalib(exposure.getPhotoCalib())
359 psfMatchedExposure.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo())
360 psfMatchedExposure.setPsf(referencePsfModel)
361 psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
366 convolutionControl.setDoNormalize(
True)
367 afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, convolutionControl)
369 self.log.
info(
"done")
370 return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
371 psfMatchingKernel=psfMatchingKernel,
372 kernelCellSet=kernelCellSet,
373 metadata=self.metadata,
376 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
377 """Print diagnostic information on spatial kernel and background fit
379 The debugging diagnostics are not really useful here, since the images we are matching have
380 no variance. Thus override the _diagnostic method to generate no logging information"""
383 def _buildCellSet(self, exposure, referencePsfModel):
384 """Build a SpatialCellSet for use with the solve method
388 exposure : `lsst.afw.image.Exposure`
389 The science exposure that will be convolved; must contain a Psf
390 referencePsfModel : `lsst.afw.detection.Psf`
391 Psf model to match to
396 - ``kernelCellSet`` : a SpatialCellSet to be used by self._solve
397 - ``referencePsfModel`` : Validated and/or modified
398 reference model used to populate the SpatialCellSet
402 If the reference Psf model and science Psf model have different dimensions,
403 adjust the referencePsfModel (the model to which the exposure PSF will be matched)
404 to match that of the science Psf. If the science Psf dimensions vary across the image,
405 as is common with a WarpedPsf, either pad or clip (depending on config.padPsf)
406 the dimensions to be constant.
408 sizeCellX = self.kConfig.sizeCellX
409 sizeCellY = self.kConfig.sizeCellY
411 scienceBBox = exposure.getBBox()
415 sciencePsfModel = exposure.getPsf()
417 dimenR = referencePsfModel.getLocalKernel().getDimensions()
418 psfWidth, psfHeight = dimenR
420 regionSizeX, regionSizeY = scienceBBox.getDimensions()
421 scienceX0, scienceY0 = scienceBBox.getMin()
425 nCellX = regionSizeX//sizeCellX
426 nCellY = regionSizeY//sizeCellY
428 if nCellX == 0
or nCellY == 0:
429 raise ValueError(
"Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
430 (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
436 for row
in range(nCellY):
437 posY = sizeCellY*row + sizeCellY//2 + scienceY0
438 for col
in range(nCellX):
439 posX = sizeCellX*col + sizeCellX//2 + scienceX0
440 widthS, heightS = sciencePsfModel.computeBBox(
geom.Point2D(posX, posY)).getDimensions()
441 widthList.append(widthS)
442 heightList.append(heightS)
444 psfSize =
max(
max(heightList),
max(widthList))
446 if self.config.doAutoPadPsf:
447 minPsfSize =
nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo)
448 paddingPix =
max(0, minPsfSize - psfSize)
450 if self.config.padPsfBy % 2 != 0:
451 raise ValueError(
"Config padPsfBy (%i pixels) must be even number." %
452 self.config.padPsfBy)
453 paddingPix = self.config.padPsfBy
456 self.log.
debug(
"Padding Science PSF from (%d, %d) to (%d, %d) pixels",
457 psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize)
458 psfSize += paddingPix
461 maxKernelSize = psfSize - 1
462 if maxKernelSize % 2 == 0:
464 if self.kConfig.kernelSize > maxKernelSize:
466 Kernel size (%d) too big to match Psfs of size %d.
467 Please reconfigure by setting one of the following:
468 1) kernel size to <= %d
471 """ % (self.kConfig.kernelSize, psfSize,
472 maxKernelSize, self.kConfig.kernelSize - maxKernelSize)
473 raise ValueError(message)
477 if (dimenR != dimenS):
479 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
480 self.log.
info(
"Adjusted dimensions of reference PSF model from %s to %s", dimenR, dimenS)
481 except Exception
as e:
482 self.log.
warning(
"Zero padding or clipping the reference PSF model of type %s and dimensions"
483 " %s to the science Psf dimensions %s because: %s",
484 referencePsfModel.__class__.__name__, dimenR, dimenS, e)
487 ps = pexConfig.makePropertySet(self.kConfig)
488 for row
in range(nCellY):
490 posY = sizeCellY*row + sizeCellY//2 + scienceY0
492 for col
in range(nCellX):
494 posX = sizeCellX*col + sizeCellX//2 + scienceX0
496 log.log(
"TRACE4." + self.log.name, log.DEBUG,
497 "Creating Psf candidate at %.1f %.1f", posX, posY)
500 referenceMI = self._makePsfMaskedImage(referencePsfModel, posX, posY, dimensions=dimenR)
503 scienceMI = self._makePsfMaskedImage(sciencePsfModel, posX, posY, dimensions=dimenR)
506 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, ps)
507 kernelCellSet.insertCandidate(kc)
511 displaySpatialCells =
lsstDebug.Info(__name__).displaySpatialCells
513 if not maskTransparency:
516 afwDisplay.setDefaultMaskTransparency(maskTransparency)
517 if display
and displaySpatialCells:
518 dituils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
519 symb=
"o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
520 ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
521 title=
"Image to be convolved")
523 return pipeBase.Struct(kernelCellSet=kernelCellSet,
524 referencePsfModel=referencePsfModel,
527 def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None):
528 """Return a MaskedImage of the a PSF Model of specified dimensions
530 rawKernel = psfModel.computeKernelImage(
geom.Point2D(posX, posY)).convertF()
531 if dimensions
is None:
532 dimensions = rawKernel.getDimensions()
533 if rawKernel.getDimensions() == dimensions:
537 kernelIm = afwImage.ImageF(dimensions)
539 (dimensions.getY() - rawKernel.getHeight())//2),
540 rawKernel.getDimensions())
541 kernelIm.assign(rawKernel, bboxToPlace)
544 kernelVar = afwImage.ImageF(dimensions, 1.0)
545 return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)
Represent a 2-dimensional array of bitmask pixels.
Parameters to control convolution.
A collection of SpatialCells covering an entire image.
An integer coordinate rectangle.
def __init__(self, *args, **kwargs)
def run(self, exposure, referencePsfModel, kernelSum=1.0)
def _buildCellSet(self, exposure, referencePsfModel)
def _buildCellSet(self, *args)
def _solve(self, kernelCellSet, basisList, returnOnExcept=False)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
def makeKernelBasisList(config, targetFwhmPix=None, referenceFwhmPix=None, basisDegGauss=None, basisSigmaGauss=None, metadata=None)