24from .
import diffimLib
31import lsst.pipe.base
as pipeBase
32from .makeKernelBasisList
import makeKernelBasisList
33from .psfMatch
import PsfMatchTask, PsfMatchConfigAL
34from .
import utils
as dituils
36__all__ = (
"ModelPsfMatchTask",
"ModelPsfMatchConfig")
38sigma2fwhm = 2.*np.sqrt(2.*np.log(2.))
42 nextInt = int(np.ceil(x))
43 return nextInt + 1
if nextInt%2 == 0
else nextInt
47 """Configuration for model-to-model Psf matching"""
49 kernel = pexConfig.ConfigChoiceField(
56 doAutoPadPsf = pexConfig.Field(
58 doc=(
"If too small, automatically pad the science Psf? "
59 "Pad to smallest dimensions appropriate for the matching kernel dimensions, "
60 "as specified by autoPadPsfTo. If false, pad by the padPsfBy config."),
63 autoPadPsfTo = pexConfig.RangeField(
65 doc=(
"Minimum Science Psf dimensions as a fraction of matching kernel dimensions. "
66 "If the dimensions of the Psf to be matched are less than the "
67 "matching kernel dimensions * autoPadPsfTo, pad Science Psf to this size. "
68 "Ignored if doAutoPadPsf=False."),
73 padPsfBy = pexConfig.Field(
75 doc=
"Pixels (even) to pad Science Psf by before matching. Ignored if doAutoPadPsf=True",
81 self.
kernel.active.singleKernelClipping =
False
82 self.
kernel.active.kernelSumClipping =
False
83 self.
kernel.active.spatialKernelClipping =
False
84 self.
kernel.active.checkConditionNumber =
False
87 self.
kernel.active.constantVarianceWeighting =
True
90 self.
kernel.active.scaleByFwhm =
False
94 """Matching of two model Psfs, and application of the Psf-matching kernel to an input Exposure
99 This Task differs from ImagePsfMatchTask
in that it matches two Psf _models_, by realizing
100 them
in an Exposure-sized SpatialCellSet
and then inserting each Psf-image pair into KernelCandidates.
101 Because none of the pairs of sources that are to be matched should be invalid, all sigma clipping
is
102 turned off
in ModelPsfMatchConfig. And because there
is no tracked _variance_
in the Psf images, the
103 debugging
and logging QA info should be interpreted
with caution.
105 One item of note
is that the sizes of Psf models are fixed (e.g. its defined
as a 21x21 matrix). When the
106 Psf-matching kernel
is being solved
for, the Psf
"image" is convolved
with each kernel basis function,
107 leading to a loss of information around the borders.
108 This pixel loss will be problematic
for the numerical
109 stability of the kernel solution
if the size of the convolution kernel
110 (set by ModelPsfMatchConfig.kernelSize)
is much bigger than: psfSize//2.
111 Thus the sizes of Psf-model matching kernels are typically smaller
112 than their image-matching counterparts. If the size of the kernel
is too small, the convolved stars will
113 look
"boxy";
if the kernel
is too large, the kernel solution will be
"noisy". This
is a trade-off that
114 needs careful attention
for a given dataset.
116 The primary use case
for this Task
is in matching an Exposure to a
117 constant-across-the-sky Psf model
for the purposes of image coaddition.
118 It
is important to note that
in the code, the
"template" Psf
is the Psf
119 that the science image gets matched to. In this sense the order of template
and science image are
120 reversed, compared to ImagePsfMatchTask, which operates on the template image.
124 The `lsst.pipe.base.cmdLineTask.CmdLineTask` command line task interface supports a
125 flag -d/--debug to
import debug.py
from your PYTHONPATH. The relevant contents of debug.py
126 for this Task include:
134 if name ==
"lsst.ip.diffim.psfMatch":
136 di.maskTransparency = 80
137 di.displayCandidates =
True
138 di.displayKernelBasis =
False
139 di.displayKernelMosaic =
True
140 di.plotKernelSpatialModel =
False
141 di.showBadCandidates =
True
142 elif name ==
"lsst.ip.diffim.modelPsfMatch":
144 di.maskTransparency = 30
145 di.displaySpatialCells =
True
150 Note that
if you want addional logging info, you may add to your scripts:
155 logUtils.traceSetAt(
"ip.diffim", 4)
159 A complete example of using ModelPsfMatchTask
161 This code
is modelPsfMatchTask.py
in the examples directory,
and can be run
as e.g.
163 .. code-block :: none
165 examples/modelPsfMatchTask.py
166 examples/modelPsfMatchTask.py --debug
167 examples/modelPsfMatchTask.py --debug --template /path/to/templateExp.fits
168 --science /path/to/scienceExp.fits
170 Create a subclass of ModelPsfMatchTask that accepts two exposures.
171 Note that the
"template" exposure contains the Psf that will get matched to,
172 and the
"science" exposure
is the one that will be convolved:
174 .. code-block :: none
177 def __init__(self, *args, **kwargs):
178 ModelPsfMatchTask.__init__(self, *args, **kwargs)
179 def run(self, templateExp, scienceExp):
180 return ModelPsfMatchTask.run(self, scienceExp, templateExp.getPsf())
182 And allow the user the freedom to either run the script
in default mode,
183 or point to their own images on disk. Note that these
186 .. code-block :: none
188 if __name__ ==
"__main__":
190 parser = argparse.ArgumentParser(description=
"Demonstrate the use of ModelPsfMatchTask")
191 parser.add_argument(
"--debug",
"-d", action=
"store_true", help=
"Load debug.py?", default=
False)
192 parser.add_argument(
"--template",
"-t", help=
"Template Exposure to use", default=
None)
193 parser.add_argument(
"--science",
"-s", help=
"Science Exposure to use", default=
None)
194 args = parser.parse_args()
196 We have enabled some minor display debugging
in this script via the –debug option.
197 However,
if you have an lsstDebug debug.py
in your PYTHONPATH you will get additional
198 debugging displays. The following block checks
for this script:
200 .. code-block :: none
206 debug.lsstDebug.frame = 3
207 except ImportError
as e:
208 print(e, file=sys.stderr)
210 Finally, we call a run method that we define below.
211 First set up a Config
and modify some of the parameters.
212 In particular we don
't want to "grow" the sizes of the kernel or KernelCandidates,
213 since we are operating with fixed–size images (i.e. the size of the input Psf models).
215 .. code-block :: none
221 config = ModelPsfMatchTask.ConfigClass()
222 config.kernel.active.scaleByFwhm =
False
224 Make sure the images (
if any) that were sent to the script exist on disk
and are readable.
225 If no images are sent, make some fake data up
for the sake of this example script
226 (have a look at the code
if you want more details on generateFakeData):
228 .. code-block :: none
231 if args.template
is not None and args.science
is not None:
232 if not os.path.isfile(args.template):
233 raise FileNotFoundError(
"Template image %s does not exist" % (args.template))
234 if not os.path.isfile(args.science):
235 raise FileNotFoundError(
"Science image %s does not exist" % (args.science))
237 templateExp = afwImage.ExposureF(args.template)
238 except Exception
as e:
239 raise RuntimeError(
"Cannot read template image %s" % (args.template))
241 scienceExp = afwImage.ExposureF(args.science)
242 except Exception
as e:
243 raise RuntimeError(
"Cannot read science image %s" % (args.science))
245 templateExp, scienceExp = generateFakeData()
246 config.kernel.active.sizeCellX = 128
247 config.kernel.active.sizeCellY = 128
249 .. code-block :: none
252 afwDisplay.Display(frame=1).mtv(templateExp, title=
"Example script: Input Template")
253 afwDisplay.Display(frame=2).mtv(scienceExp, title=
"Example script: Input Science Image")
255 Create
and run the Task:
257 .. code-block :: none
260 psfMatchTask = MyModelPsfMatchTask(config=config)
262 result = psfMatchTask.run(templateExp, scienceExp)
264 And
finally provide optional debugging display of the Psf-matched (via the Psf models) science image:
266 .. code-block :: none
271 frame = debug.lsstDebug.frame + 1
274 afwDisplay.Display(frame=frame).mtv(result.psfMatchedExposure,
275 title=
"Example script: Matched Science Image")
278 ConfigClass = ModelPsfMatchConfig
281 """Create a ModelPsfMatchTask
286 arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
288 keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
292 Upon initialization, the kernel configuration is defined by self.config.kernel.active. This Task
293 does have a
run() method, which
is the default way to call the Task.
295 PsfMatchTask.__init__(self, *args, **kwargs)
299 def run(self, exposure, referencePsfModel, kernelSum=1.0):
300 """Psf-match an exposure to a model Psf
305 Exposure to Psf-match to the reference Psf model;
306 it must return a valid PSF model via exposure.getPsf()
308 The Psf model to match to
309 kernelSum : `float`, optional
310 A multipicative factor to apply to the kernel sum (default=1.0)
315 - ``psfMatchedExposure`` : the Psf-matched Exposure.
316 This has the same parent bbox, Wcs, PhotoCalib
and
317 Filter
as the input Exposure but no Psf.
318 In theory the Psf should equal referencePsfModel but
319 the match
is likely
not exact.
320 - ``psfMatchingKernel`` : the spatially varying Psf-matching kernel
321 - ``kernelCellSet`` : SpatialCellSet used to solve
for the Psf-matching kernel
322 - ``referencePsfModel`` : Validated
and/
or modified reference model used
327 if the Exposure does
not contain a Psf model
329 if not exposure.hasPsf():
330 raise RuntimeError(
"exposure does not contain a Psf model")
332 maskedImage = exposure.getMaskedImage()
334 self.log.info(
"compute Psf-matching kernel")
336 kernelCellSet = result.kernelCellSet
337 referencePsfModel = result.referencePsfModel
338 fwhmScience = exposure.getPsf().computeShape().getDeterminantRadius()*sigma2fwhm
339 fwhmModel = referencePsfModel.computeShape().getDeterminantRadius()*sigma2fwhm
341 basisList = makeKernelBasisList(self.
kConfigkConfig, fwhmScience, fwhmModel, metadata=self.metadata)
342 spatialSolution, psfMatchingKernel, backgroundModel = self.
_solve(kernelCellSet, basisList)
344 if psfMatchingKernel.isSpatiallyVarying():
345 sParameters = np.array(psfMatchingKernel.getSpatialParameters())
346 sParameters[0][0] = kernelSum
347 psfMatchingKernel.setSpatialParameters(sParameters)
349 kParameters = np.array(psfMatchingKernel.getKernelParameters())
350 kParameters[0] = kernelSum
351 psfMatchingKernel.setKernelParameters(kParameters)
353 self.log.info(
"Psf-match science exposure to reference")
354 psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
355 psfMatchedExposure.setFilterLabel(exposure.getFilterLabel())
356 psfMatchedExposure.setPhotoCalib(exposure.getPhotoCalib())
357 psfMatchedExposure.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo())
358 psfMatchedExposure.setPsf(referencePsfModel)
359 psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
364 convolutionControl.setDoNormalize(
True)
365 afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, convolutionControl)
367 self.log.info(
"done")
368 return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
369 psfMatchingKernel=psfMatchingKernel,
370 kernelCellSet=kernelCellSet,
371 metadata=self.metadata,
374 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
375 """Print diagnostic information on spatial kernel and background fit
377 The debugging diagnostics are not really useful here, since the images we are matching have
378 no variance. Thus override the _diagnostic method to generate no logging information
"""
381 def _buildCellSet(self, exposure, referencePsfModel):
382 """Build a SpatialCellSet for use with the solve method
387 The science exposure that will be convolved; must contain a Psf
389 Psf model to match to
394 - ``kernelCellSet`` : a SpatialCellSet to be used by self._solve
395 - ``referencePsfModel`` : Validated and/
or modified
396 reference model used to populate the SpatialCellSet
400 If the reference Psf model
and science Psf model have different dimensions,
401 adjust the referencePsfModel (the model to which the exposure PSF will be matched)
402 to match that of the science Psf. If the science Psf dimensions vary across the image,
403 as is common
with a WarpedPsf, either pad
or clip (depending on config.padPsf)
404 the dimensions to be constant.
409 scienceBBox = exposure.getBBox()
413 sciencePsfModel = exposure.getPsf()
415 dimenR = referencePsfModel.getLocalKernel().getDimensions()
416 psfWidth, psfHeight = dimenR
418 regionSizeX, regionSizeY = scienceBBox.getDimensions()
419 scienceX0, scienceY0 = scienceBBox.getMin()
423 nCellX = regionSizeX//sizeCellX
424 nCellY = regionSizeY//sizeCellY
426 if nCellX == 0
or nCellY == 0:
427 raise ValueError(
"Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
428 (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
434 for row
in range(nCellY):
435 posY = sizeCellY*row + sizeCellY//2 + scienceY0
436 for col
in range(nCellX):
437 posX = sizeCellX*col + sizeCellX//2 + scienceX0
438 widthS, heightS = sciencePsfModel.computeBBox(
geom.Point2D(posX, posY)).getDimensions()
439 widthList.append(widthS)
440 heightList.append(heightS)
442 psfSize = max(max(heightList), max(widthList))
444 if self.config.doAutoPadPsf:
446 paddingPix = max(0, minPsfSize - psfSize)
448 if self.config.padPsfBy % 2 != 0:
449 raise ValueError(
"Config padPsfBy (%i pixels) must be even number." %
450 self.config.padPsfBy)
451 paddingPix = self.config.padPsfBy
454 self.log.debug(
"Padding Science PSF from (%d, %d) to (%d, %d) pixels",
455 psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize)
456 psfSize += paddingPix
459 maxKernelSize = psfSize - 1
460 if maxKernelSize % 2 == 0:
464 Kernel size (%d) too big to match Psfs of size %d.
465 Please reconfigure by setting one of the following:
466 1) kernel size to <= %d
469 """ % (self.kConfig.kernelSize, psfSize,
471 raise ValueError(message)
475 if (dimenR != dimenS):
477 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
478 self.log.info(
"Adjusted dimensions of reference PSF model from %s to %s", dimenR, dimenS)
479 except Exception
as e:
480 self.log.warning(
"Zero padding or clipping the reference PSF model of type %s and dimensions"
481 " %s to the science Psf dimensions %s because: %s",
482 referencePsfModel.__class__.__name__, dimenR, dimenS, e)
486 for row
in range(nCellY):
488 posY = sizeCellY*row + sizeCellY//2 + scienceY0
490 for col
in range(nCellX):
492 posX = sizeCellX*col + sizeCellX//2 + scienceX0
494 log.log(
"TRACE4." + self.log.name, log.DEBUG,
495 "Creating Psf candidate at %.1f %.1f", posX, posY)
504 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, ps)
505 kernelCellSet.insertCandidate(kc)
509 displaySpatialCells =
lsstDebug.Info(__name__).displaySpatialCells
511 if not maskTransparency:
514 afwDisplay.setDefaultMaskTransparency(maskTransparency)
515 if display
and displaySpatialCells:
516 dituils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
517 symb=
"o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
518 ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
519 title=
"Image to be convolved")
521 return pipeBase.Struct(kernelCellSet=kernelCellSet,
522 referencePsfModel=referencePsfModel,
525 def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None):
526 """Return a MaskedImage of the a PSF Model of specified dimensions
528 rawKernel = psfModel.computeKernelImage(geom.Point2D(posX, posY)).convertF()
529 if dimensions
is None:
530 dimensions = rawKernel.getDimensions()
531 if rawKernel.getDimensions() == dimensions:
535 kernelIm = afwImage.ImageF(dimensions)
537 (dimensions.getY() - rawKernel.getHeight())//2),
538 rawKernel.getDimensions())
539 kernelIm.assign(rawKernel, bboxToPlace)
542 kernelVar = afwImage.ImageF(dimensions, 1.0)
543 return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)
def __init__(self, *args, **kwargs)
def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None)
def run(self, exposure, referencePsfModel, kernelSum=1.0)
def _buildCellSet(self, exposure, referencePsfModel)
def _buildCellSet(self, *args)
def _solve(self, kernelCellSet, basisList, returnOnExcept=False)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())