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.
kernel.active.singleKernelClipping =
False
83 self.
kernel.active.kernelSumClipping =
False
84 self.
kernel.active.spatialKernelClipping =
False
85 self.
kernel.active.checkConditionNumber =
False
88 self.
kernel.active.constantVarianceWeighting =
True
91 self.
kernel.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 ``pipetask`` command line interface supports a
126 flag --debug to
import @b 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 Create a subclass of ModelPsfMatchTask that accepts two exposures.
163 Note that the
"template" exposure contains the Psf that will get matched to,
164 and the
"science" exposure
is the one that will be convolved:
166 .. code-block :: none
169 def __init__(self, *args, **kwargs):
170 ModelPsfMatchTask.__init__(self, *args, **kwargs)
171 def run(self, templateExp, scienceExp):
172 return ModelPsfMatchTask.run(self, scienceExp, templateExp.getPsf())
174 And allow the user the freedom to either run the script
in default mode,
175 or point to their own images on disk. Note that these
178 .. code-block :: none
180 if __name__ ==
"__main__":
182 parser = argparse.ArgumentParser(description=
"Demonstrate the use of ModelPsfMatchTask")
183 parser.add_argument(
"--debug",
"-d", action=
"store_true", help=
"Load debug.py?", default=
False)
184 parser.add_argument(
"--template",
"-t", help=
"Template Exposure to use", default=
None)
185 parser.add_argument(
"--science",
"-s", help=
"Science Exposure to use", default=
None)
186 args = parser.parse_args()
188 We have enabled some minor display debugging
in this script via the –debug option.
189 However,
if you have an lsstDebug debug.py
in your PYTHONPATH you will get additional
190 debugging displays. The following block checks
for this script:
192 .. code-block :: none
198 debug.lsstDebug.frame = 3
199 except ImportError
as e:
200 print(e, file=sys.stderr)
202 Finally, we call a run method that we define below.
203 First set up a Config
and modify some of the parameters.
204 In particular we don
't want to "grow" the sizes of the kernel or KernelCandidates,
205 since we are operating with fixed–size images (i.e. the size of the input Psf models).
207 .. code-block :: none
213 config = ModelPsfMatchTask.ConfigClass()
214 config.kernel.active.scaleByFwhm =
False
216 Make sure the images (
if any) that were sent to the script exist on disk
and are readable.
217 If no images are sent, make some fake data up
for the sake of this example script
218 (have a look at the code
if you want more details on generateFakeData):
220 .. code-block :: none
223 if args.template
is not None and args.science
is not None:
224 if not os.path.isfile(args.template):
225 raise FileNotFoundError(
"Template image %s does not exist" % (args.template))
226 if not os.path.isfile(args.science):
227 raise FileNotFoundError(
"Science image %s does not exist" % (args.science))
229 templateExp = afwImage.ExposureF(args.template)
230 except Exception
as e:
231 raise RuntimeError(
"Cannot read template image %s" % (args.template))
233 scienceExp = afwImage.ExposureF(args.science)
234 except Exception
as e:
235 raise RuntimeError(
"Cannot read science image %s" % (args.science))
237 templateExp, scienceExp = generateFakeData()
238 config.kernel.active.sizeCellX = 128
239 config.kernel.active.sizeCellY = 128
241 .. code-block :: none
244 afwDisplay.Display(frame=1).mtv(templateExp, title=
"Example script: Input Template")
245 afwDisplay.Display(frame=2).mtv(scienceExp, title=
"Example script: Input Science Image")
247 Create
and run the Task:
249 .. code-block :: none
252 psfMatchTask = MyModelPsfMatchTask(config=config)
254 result = psfMatchTask.run(templateExp, scienceExp)
256 And
finally provide optional debugging display of the Psf-matched (via the Psf models) science image:
258 .. code-block :: none
263 frame = debug.lsstDebug.frame + 1
266 afwDisplay.Display(frame=frame).mtv(result.psfMatchedExposure,
267 title=
"Example script: Matched Science Image")
270 ConfigClass = ModelPsfMatchConfig
273 """Create a ModelPsfMatchTask
278 arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
280 keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
284 Upon initialization, the kernel configuration is defined by self.config.kernel.active. This Task
285 does have a
run() method, which
is the default way to call the Task.
287 PsfMatchTask.__init__(self, *args, **kwargs)
291 def run(self, exposure, referencePsfModel, kernelSum=1.0):
292 """Psf-match an exposure to a model Psf
297 Exposure to Psf-match to the reference Psf model;
298 it must return a valid PSF model via exposure.getPsf()
300 The Psf model to match to
301 kernelSum : `float`, optional
302 A multipicative factor to apply to the kernel sum (default=1.0)
307 - ``psfMatchedExposure`` : the Psf-matched Exposure.
308 This has the same parent bbox, Wcs, PhotoCalib
and
309 Filter
as the input Exposure but no Psf.
310 In theory the Psf should equal referencePsfModel but
311 the match
is likely
not exact.
312 - ``psfMatchingKernel`` : the spatially varying Psf-matching kernel
313 - ``kernelCellSet`` : SpatialCellSet used to solve
for the Psf-matching kernel
314 - ``referencePsfModel`` : Validated
and/
or modified reference model used
319 if the Exposure does
not contain a Psf model
321 if not exposure.hasPsf():
322 raise RuntimeError(
"exposure does not contain a Psf model")
324 maskedImage = exposure.getMaskedImage()
326 self.log.info(
"compute Psf-matching kernel")
328 kernelCellSet = result.kernelCellSet
329 referencePsfModel = result.referencePsfModel
332 sciAvgPos = exposure.getPsf().getAveragePosition()
333 modelAvgPos = referencePsfModel.getAveragePosition()
334 fwhmScience = exposure.getPsf().computeShape(sciAvgPos).getDeterminantRadius()*sigma2fwhm
335 fwhmModel = referencePsfModel.computeShape(modelAvgPos).getDeterminantRadius()*sigma2fwhm
337 basisList = makeKernelBasisList(self.
kConfigkConfig, fwhmScience, fwhmModel, metadata=self.metadata)
338 spatialSolution, psfMatchingKernel, backgroundModel = self.
_solve(kernelCellSet, basisList)
340 if psfMatchingKernel.isSpatiallyVarying():
341 sParameters = np.array(psfMatchingKernel.getSpatialParameters())
342 sParameters[0][0] = kernelSum
343 psfMatchingKernel.setSpatialParameters(sParameters)
345 kParameters = np.array(psfMatchingKernel.getKernelParameters())
346 kParameters[0] = kernelSum
347 psfMatchingKernel.setKernelParameters(kParameters)
349 self.log.info(
"Psf-match science exposure to reference")
350 psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs())
351 psfMatchedExposure.info.id = exposure.info.id
352 psfMatchedExposure.setFilter(exposure.getFilter())
353 psfMatchedExposure.setPhotoCalib(exposure.getPhotoCalib())
354 psfMatchedExposure.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo())
355 psfMatchedExposure.setPsf(referencePsfModel)
356 psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
361 convolutionControl.setDoNormalize(
True)
362 afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, convolutionControl)
364 self.log.info(
"done")
365 return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
366 psfMatchingKernel=psfMatchingKernel,
367 kernelCellSet=kernelCellSet,
368 metadata=self.metadata,
371 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
372 """Print diagnostic information on spatial kernel and background fit
374 The debugging diagnostics are not really useful here, since the images we are matching have
375 no variance. Thus override the _diagnostic method to generate no logging information
"""
379 """Build a SpatialCellSet for use with the solve method
384 The science exposure that will be convolved; must contain a Psf
386 Psf model to match to
391 - ``kernelCellSet`` : a SpatialCellSet to be used by self._solve
392 - ``referencePsfModel`` : Validated and/
or modified
393 reference model used to populate the SpatialCellSet
397 If the reference Psf model
and science Psf model have different dimensions,
398 adjust the referencePsfModel (the model to which the exposure PSF will be matched)
399 to match that of the science Psf. If the science Psf dimensions vary across the image,
400 as is common
with a WarpedPsf, either pad
or clip (depending on config.padPsf)
401 the dimensions to be constant.
406 scienceBBox = exposure.getBBox()
410 sciencePsfModel = exposure.getPsf()
412 dimenR = referencePsfModel.getLocalKernel(scienceBBox.getCenter()).getDimensions()
414 regionSizeX, regionSizeY = scienceBBox.getDimensions()
415 scienceX0, scienceY0 = scienceBBox.getMin()
419 nCellX = regionSizeX//sizeCellX
420 nCellY = regionSizeY//sizeCellY
422 if nCellX == 0
or nCellY == 0:
423 raise ValueError(
"Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
424 (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
430 for row
in range(nCellY):
431 posY = sizeCellY*row + sizeCellY//2 + scienceY0
432 for col
in range(nCellX):
433 posX = sizeCellX*col + sizeCellX//2 + scienceX0
434 widthS, heightS = sciencePsfModel.computeBBox(
geom.Point2D(posX, posY)).getDimensions()
435 widthList.append(widthS)
436 heightList.append(heightS)
438 psfSize = max(max(heightList), max(widthList))
440 if self.config.doAutoPadPsf:
442 paddingPix = max(0, minPsfSize - psfSize)
444 if self.config.padPsfBy % 2 != 0:
445 raise ValueError(
"Config padPsfBy (%i pixels) must be even number." %
446 self.config.padPsfBy)
447 paddingPix = self.config.padPsfBy
450 self.log.debug(
"Padding Science PSF from (%d, %d) to (%d, %d) pixels",
451 psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize)
452 psfSize += paddingPix
455 maxKernelSize = psfSize - 1
456 if maxKernelSize % 2 == 0:
460 Kernel size (%d) too big to match Psfs of size %d.
461 Please reconfigure by setting one of the following:
462 1) kernel size to <= %d
465 """ % (self.kConfig.kernelSize, psfSize,
467 raise ValueError(message)
471 if (dimenR != dimenS):
473 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
474 self.log.info(
"Adjusted dimensions of reference PSF model from %s to %s", dimenR, dimenS)
475 except Exception
as e:
476 self.log.warning(
"Zero padding or clipping the reference PSF model of type %s and dimensions"
477 " %s to the science Psf dimensions %s because: %s",
478 referencePsfModel.__class__.__name__, dimenR, dimenS, e)
482 for row
in range(nCellY):
484 posY = sizeCellY*row + sizeCellY//2 + scienceY0
486 for col
in range(nCellX):
488 posX = sizeCellX*col + sizeCellX//2 + scienceX0
490 getTraceLogger(self.log, 4).debug(
"Creating Psf candidate at %.1f %.1f", posX, posY)
499 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, ps)
500 kernelCellSet.insertCandidate(kc)
504 displaySpatialCells =
lsstDebug.Info(__name__).displaySpatialCells
506 if not maskTransparency:
509 afwDisplay.setDefaultMaskTransparency(maskTransparency)
510 if display
and displaySpatialCells:
511 dituils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
512 symb=
"o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
513 ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
514 title=
"Image to be convolved")
516 return pipeBase.Struct(kernelCellSet=kernelCellSet,
517 referencePsfModel=referencePsfModel,
521 """Return a MaskedImage of the a PSF Model of specified dimensions
523 rawKernel = psfModel.computeKernelImage(geom.Point2D(posX, posY)).convertF()
524 if dimensions
is None:
525 dimensions = rawKernel.getDimensions()
526 if rawKernel.getDimensions() == dimensions:
530 kernelIm = afwImage.ImageF(dimensions)
532 (dimensions.getY() - rawKernel.getHeight())//2),
533 rawKernel.getDimensions())
534 kernelIm.assign(rawKernel, bboxToPlace)
537 kernelVar = afwImage.ImageF(dimensions, 1.0)
538 return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)
def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg)
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())