24from .
import diffimLib
31from lsst.utils.logging
import getTraceLogger
32from lsst.utils.timer
import timeMethod
33from .makeKernelBasisList
import makeKernelBasisList
34from .psfMatch
import PsfMatchTask, PsfMatchConfigAL
35from .
import utils
as dituils
37__all__ = (
"ModelPsfMatchTask",
"ModelPsfMatchConfig")
39sigma2fwhm = 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:
135 if name ==
"lsst.ip.diffim.psfMatch":
137 di.maskTransparency = 80
138 di.displayCandidates =
True
139 di.displayKernelBasis =
False
140 di.displayKernelMosaic =
True
141 di.plotKernelSpatialModel =
False
142 di.showBadCandidates =
True
143 elif name ==
"lsst.ip.diffim.modelPsfMatch":
145 di.maskTransparency = 30
146 di.displaySpatialCells =
True
151 Note that
if you want addional logging info, you may add to your scripts:
155 import lsst.utils.logging
as logUtils
156 logUtils.trace_set_at(
"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
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
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
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
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
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
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
306 Exposure to Psf-match to the reference Psf model;
307 it must return a valid PSF model via exposure.getPsf()
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
341 sciAvgPos = exposure.getPsf().getAveragePosition()
342 modelAvgPos = referencePsfModel.getAveragePosition()
343 fwhmScience = exposure.getPsf().computeShape(sciAvgPos).getDeterminantRadius()*sigma2fwhm
344 fwhmModel = referencePsfModel.computeShape(modelAvgPos).getDeterminantRadius()*sigma2fwhm
347 spatialSolution, psfMatchingKernel, backgroundModel = self.
_solve_solve(kernelCellSet, basisList)
349 if psfMatchingKernel.isSpatiallyVarying():
350 sParameters = np.array(psfMatchingKernel.getSpatialParameters())
351 sParameters[0][0] = kernelSum
352 psfMatchingKernel.setSpatialParameters(sParameters)
354 kParameters = np.array(psfMatchingKernel.getKernelParameters())
355 kParameters[0] = kernelSum
356 psfMatchingKernel.setKernelParameters(kParameters)
358 self.log.
info(
"Psf-match science exposure to reference")
359 psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
360 psfMatchedExposure.info.id = exposure.info.id
361 psfMatchedExposure.setFilter(exposure.getFilter())
362 psfMatchedExposure.setPhotoCalib(exposure.getPhotoCalib())
363 psfMatchedExposure.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo())
364 psfMatchedExposure.setPsf(referencePsfModel)
365 psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
370 convolutionControl.setDoNormalize(
True)
371 afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, convolutionControl)
373 self.log.
info(
"done")
374 return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
375 psfMatchingKernel=psfMatchingKernel,
376 kernelCellSet=kernelCellSet,
377 metadata=self.metadata,
380 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
381 """Print diagnostic information on spatial kernel and background fit
383 The debugging diagnostics are not really useful here, since the images we are matching have
384 no variance. Thus override the _diagnostic method to generate no logging information
"""
387 def _buildCellSet(self, exposure, referencePsfModel):
388 """Build a SpatialCellSet for use with the solve method
393 The science exposure that will be convolved; must contain a Psf
395 Psf model to match to
400 - ``kernelCellSet`` : a SpatialCellSet to be used by self._solve_solve
401 - ``referencePsfModel`` : Validated and/
or modified
402 reference model used to populate the SpatialCellSet
406 If the reference Psf model
and science Psf model have different dimensions,
407 adjust the referencePsfModel (the model to which the exposure PSF will be matched)
408 to match that of the science Psf. If the science Psf dimensions vary across the image,
409 as is common
with a WarpedPsf, either pad
or clip (depending on config.padPsf)
410 the dimensions to be constant.
415 scienceBBox = exposure.getBBox()
419 sciencePsfModel = exposure.getPsf()
421 dimenR = referencePsfModel.getLocalKernel(scienceBBox.getCenter()).getDimensions()
423 regionSizeX, regionSizeY = scienceBBox.getDimensions()
424 scienceX0, scienceY0 = scienceBBox.getMin()
428 nCellX = regionSizeX//sizeCellX
429 nCellY = regionSizeY//sizeCellY
431 if nCellX == 0
or nCellY == 0:
432 raise ValueError(
"Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
433 (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
439 for row
in range(nCellY):
440 posY = sizeCellY*row + sizeCellY//2 + scienceY0
441 for col
in range(nCellX):
442 posX = sizeCellX*col + sizeCellX//2 + scienceX0
443 widthS, heightS = sciencePsfModel.computeBBox(
geom.Point2D(posX, posY)).getDimensions()
444 widthList.append(widthS)
445 heightList.append(heightS)
447 psfSize =
max(
max(heightList),
max(widthList))
449 if self.config.doAutoPadPsf:
451 paddingPix =
max(0, minPsfSize - psfSize)
453 if self.config.padPsfBy % 2 != 0:
454 raise ValueError(
"Config padPsfBy (%i pixels) must be even number." %
455 self.config.padPsfBy)
456 paddingPix = self.config.padPsfBy
459 self.log.
debug(
"Padding Science PSF from (%d, %d) to (%d, %d) pixels",
460 psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize)
461 psfSize += paddingPix
464 maxKernelSize = psfSize - 1
465 if maxKernelSize % 2 == 0:
469 Kernel size (%d) too big to match Psfs of size %d.
470 Please reconfigure by setting one of the following:
471 1) kernel size to <= %d
474 """ % (self.kConfig.kernelSize, psfSize,
475 maxKernelSize, self.kConfigkConfigkConfig.kernelSize - maxKernelSize)
476 raise ValueError(message)
480 if (dimenR != dimenS):
482 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
483 self.log.
info(
"Adjusted dimensions of reference PSF model from %s to %s", dimenR, dimenS)
484 except Exception
as e:
485 self.log.
warning(
"Zero padding or clipping the reference PSF model of type %s and dimensions"
486 " %s to the science Psf dimensions %s because: %s",
487 referencePsfModel.__class__.__name__, dimenR, dimenS, e)
491 for row
in range(nCellY):
493 posY = sizeCellY*row + sizeCellY//2 + scienceY0
495 for col
in range(nCellX):
497 posX = sizeCellX*col + sizeCellX//2 + scienceX0
499 getTraceLogger(self.log, 4).
debug(
"Creating Psf candidate at %.1f %.1f", posX, posY)
502 referenceMI = self.
_makePsfMaskedImage_makePsfMaskedImage(referencePsfModel, posX, posY, dimensions=dimenR)
505 scienceMI = self.
_makePsfMaskedImage_makePsfMaskedImage(sciencePsfModel, posX, posY, dimensions=dimenR)
508 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, ps)
509 kernelCellSet.insertCandidate(kc)
513 displaySpatialCells =
lsstDebug.Info(__name__).displaySpatialCells
515 if not maskTransparency:
518 afwDisplay.setDefaultMaskTransparency(maskTransparency)
519 if display
and displaySpatialCells:
520 dituils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
521 symb=
"o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
522 ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
523 title=
"Image to be convolved")
525 return pipeBase.Struct(kernelCellSet=kernelCellSet,
526 referencePsfModel=referencePsfModel,
529 def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None):
530 """Return a MaskedImage of the a PSF Model of specified dimensions
532 rawKernel = psfModel.computeKernelImage(geom.Point2D(posX, posY)).convertF()
533 if dimensions
is None:
534 dimensions = rawKernel.getDimensions()
535 if rawKernel.getDimensions() == dimensions:
539 kernelIm = afwImage.ImageF(dimensions)
541 (dimensions.getY() - rawKernel.getHeight())//2),
542 rawKernel.getDimensions())
543 kernelIm.assign(rawKernel, bboxToPlace)
546 kernelVar = afwImage.ImageF(dimensions, 1.0)
547 return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)
A polymorphic base class for representing an image's Point Spread Function.
A class to contain the data, WCS, and other information needed to describe an image of the sky.
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 _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)
def mtv(data, frame=None, title="", wcs=None, *args, **kwargs)
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)