24 from .
import diffimLib
30 import lsst.pex.config
as pexConfig
32 from .makeKernelBasisList
import makeKernelBasisList
33 from .psfMatch
import PsfMatchTask, PsfMatchConfigAL
34 from .
import utils
as dituils
36 __all__ = (
"ModelPsfMatchTask",
"ModelPsfMatchConfig")
38 sigma2fwhm = 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: 133 di = lsstDebug.getInfo(name) 134 if name == "lsst.ip.diffim.psfMatch": 135 di.display = True # global 136 di.maskTransparency = 80 # mask transparency 137 di.displayCandidates = True # show all the candidates and residuals 138 di.displayKernelBasis = False # show kernel basis functions 139 di.displayKernelMosaic = True # show kernel realized across the image 140 di.plotKernelSpatialModel = False # show coefficients of spatial model 141 di.showBadCandidates = True # show the bad candidates (red) along with good (green) 142 elif name == "lsst.ip.diffim.modelPsfMatch": 143 di.display = True # global 144 di.maskTransparency = 30 # mask transparency 145 di.displaySpatialCells = True # show spatial cells before the fit 147 lsstDebug.Info = DebugInfo 150 Note that if you want addional logging info, you may add to your scripts: 154 import lsst.log.utils as logUtils 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 176 class MyModelPsfMatchTask(ModelPsfMatchTask): 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 184 images must be readable as an lsst.afw.image.Exposure: 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 205 # Since I am displaying 2 images here, set the starting frame number for the LSST debug LSST 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 219 # Create the Config and use sum of gaussian basis 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 230 # Run the requested method of the Task 231 if args.template is not None and args.science is not None: 232 if not os.path.isfile(args.template): 233 raise Exception("Template image %s does not exist" % (args.template)) 234 if not os.path.isfile(args.science): 235 raise Exception("Science image %s does not exist" % (args.science)) 237 templateExp = afwImage.ExposureF(args.template) 238 except Exception as e: 239 raise Exception("Cannot read template image %s" % (args.template)) 241 scienceExp = afwImage.ExposureF(args.science) 242 except Exception as e: 243 raise Exception("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 269 # See if the LSST debug has incremented the frame number; if not start with frame 3 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 304 exposure : `lsst.afw.image.Exposure` 305 Exposure to Psf-match to the reference Psf model; 306 it must return a valid PSF model via exposure.getPsf() 307 referencePsfModel : `lsst.afw.detection.Psf` 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
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.setFilter(exposure.getFilter())
356 psfMatchedExposure.setPhotoCalib(exposure.getPhotoCalib())
357 psfMatchedExposure.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo())
358 psfMatchedExposure.setPsf(referencePsfModel)
359 psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage()
364 afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, doNormalize)
366 self.log.
info(
"done")
367 return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure,
368 psfMatchingKernel=psfMatchingKernel,
369 kernelCellSet=kernelCellSet,
370 metadata=self.metadata,
373 def _diagnostic(self, kernelCellSet, spatialSolution, spatialKernel, spatialBg):
374 """Print diagnostic information on spatial kernel and background fit 376 The debugging diagnostics are not really useful here, since the images we are matching have 377 no variance. Thus override the _diagnostic method to generate no logging information""" 380 def _buildCellSet(self, exposure, referencePsfModel):
381 """Build a SpatialCellSet for use with the solve method 385 exposure : `lsst.afw.image.Exposure` 386 The science exposure that will be convolved; must contain a Psf 387 referencePsfModel : `lsst.afw.detection.Psf` 388 Psf model to match to 393 - ``kernelCellSet`` : a SpatialCellSet to be used by self._solve 394 - ``referencePsfModel`` : Validated and/or modified 395 reference model used to populate the SpatialCellSet 399 If the reference Psf model and science Psf model have different dimensions, 400 adjust the referencePsfModel (the model to which the exposure PSF will be matched) 401 to match that of the science Psf. If the science Psf dimensions vary across the image, 402 as is common with a WarpedPsf, either pad or clip (depending on config.padPsf) 403 the dimensions to be constant. 405 sizeCellX = self.kConfig.sizeCellX
406 sizeCellY = self.kConfig.sizeCellY
408 scienceBBox = exposure.getBBox()
412 sciencePsfModel = exposure.getPsf()
414 dimenR = referencePsfModel.getLocalKernel().getDimensions()
415 psfWidth, psfHeight = dimenR
417 regionSizeX, regionSizeY = scienceBBox.getDimensions()
418 scienceX0, scienceY0 = scienceBBox.getMin()
422 nCellX = regionSizeX//sizeCellX
423 nCellY = regionSizeY//sizeCellY
425 if nCellX == 0
or nCellY == 0:
426 raise ValueError(
"Exposure dimensions=%s and sizeCell=(%s, %s). Insufficient area to match" %
427 (scienceBBox.getDimensions(), sizeCellX, sizeCellY))
433 for row
in range(nCellY):
434 posY = sizeCellY*row + sizeCellY//2 + scienceY0
435 for col
in range(nCellX):
436 posX = sizeCellX*col + sizeCellX//2 + scienceX0
437 widthS, heightS = sciencePsfModel.computeBBox(
geom.Point2D(posX, posY)).getDimensions()
438 widthList.append(widthS)
439 heightList.append(heightS)
441 psfSize =
max(
max(heightList),
max(widthList))
443 if self.config.doAutoPadPsf:
444 minPsfSize =
nextOddInteger(self.kConfig.kernelSize*self.config.autoPadPsfTo)
445 paddingPix =
max(0, minPsfSize - psfSize)
447 if self.config.padPsfBy % 2 != 0:
448 raise ValueError(
"Config padPsfBy (%i pixels) must be even number." %
449 self.config.padPsfBy)
450 paddingPix = self.config.padPsfBy
453 self.log.
info(
"Padding Science PSF from (%s, %s) to (%s, %s) pixels" %
454 (psfSize, psfSize, paddingPix + psfSize, paddingPix + psfSize))
455 psfSize += paddingPix
458 maxKernelSize = psfSize - 1
459 if maxKernelSize % 2 == 0:
461 if self.kConfig.kernelSize > maxKernelSize:
463 Kernel size (%d) too big to match Psfs of size %d. 464 Please reconfigure by setting one of the following: 465 1) kernel size to <= %d 468 """ % (self.kConfig.kernelSize, psfSize,
469 maxKernelSize, self.kConfig.kernelSize - maxKernelSize)
470 raise ValueError(message)
474 if (dimenR != dimenS):
476 referencePsfModel = referencePsfModel.resized(psfSize, psfSize)
477 self.log.
info(
"Adjusted dimensions of reference PSF model from %s to %s" % (dimenR, dimenS))
478 except Exception
as e:
479 self.log.
warn(
"Zero padding or clipping the reference PSF model of type %s and dimensions %s" 480 " to the science Psf dimensions %s because: %s",
481 referencePsfModel.__class__.__name__, dimenR, dimenS, e)
484 ps = pexConfig.makePropertySet(self.kConfig)
485 for row
in range(nCellY):
487 posY = sizeCellY*row + sizeCellY//2 + scienceY0
489 for col
in range(nCellX):
491 posX = sizeCellX*col + sizeCellX//2 + scienceX0
493 log.log(
"TRACE4." + self.log.getName(), log.DEBUG,
494 "Creating Psf candidate at %.1f %.1f", posX, posY)
497 referenceMI = self._makePsfMaskedImage(referencePsfModel, posX, posY, dimensions=dimenR)
500 scienceMI = self._makePsfMaskedImage(sciencePsfModel, posX, posY, dimensions=dimenR)
503 kc = diffimLib.makeKernelCandidate(posX, posY, scienceMI, referenceMI, ps)
504 kernelCellSet.insertCandidate(kc)
508 displaySpatialCells =
lsstDebug.Info(__name__).displaySpatialCells
510 if not maskTransparency:
513 afwDisplay.setDefaultMaskTransparency(maskTransparency)
514 if display
and displaySpatialCells:
515 dituils.showKernelSpatialCells(exposure.getMaskedImage(), kernelCellSet,
516 symb=
"o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
517 ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
518 title=
"Image to be convolved")
520 return pipeBase.Struct(kernelCellSet=kernelCellSet,
521 referencePsfModel=referencePsfModel,
524 def _makePsfMaskedImage(self, psfModel, posX, posY, dimensions=None):
525 """Return a MaskedImage of the a PSF Model of specified dimensions 527 rawKernel = psfModel.computeKernelImage(
geom.Point2D(posX, posY)).convertF()
528 if dimensions
is None:
529 dimensions = rawKernel.getDimensions()
530 if rawKernel.getDimensions() == dimensions:
534 kernelIm = afwImage.ImageF(dimensions)
536 (dimensions.getY() - rawKernel.getHeight())//2),
537 rawKernel.getDimensions())
538 kernelIm.assign(rawKernel, bboxToPlace)
541 kernelVar = afwImage.ImageF(dimensions, 1.0)
542 return afwImage.MaskedImageF(kernelIm, kernelMask, kernelVar)
def makeKernelBasisList(config, targetFwhmPix=None, referenceFwhmPix=None, basisDegGauss=None, metadata=None)
def _buildCellSet(self, exposure, referencePsfModel)
A collection of SpatialCells covering an entire image.
Represent a 2-dimensional array of bitmask pixels.
def _solve(self, kernelCellSet, basisList, returnOnExcept=False)
def run(self, exposure, referencePsfModel, kernelSum=1.0)
def __init__(self, args, kwargs)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
An integer coordinate rectangle.