lsst.ip.diffim  17.0.1-7-gfe850ae+3
imagePsfMatch.py
Go to the documentation of this file.
1 # This file is part of ip_diffim.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 
22 __all__ = ["ImagePsfMatchConfig", "ImagePsfMatchTask", "subtractAlgorithmRegistry"]
23 
24 import numpy as np
25 
26 import lsst.daf.base as dafBase
27 import lsst.pex.config as pexConfig
28 import lsst.afw.detection as afwDetect
29 import lsst.afw.image as afwImage
30 import lsst.afw.math as afwMath
31 import lsst.afw.geom as afwGeom
32 import lsst.afw.table as afwTable
33 import lsst.pipe.base as pipeBase
34 from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask
35 from lsst.meas.base import SingleFrameMeasurementTask
36 from .makeKernelBasisList import makeKernelBasisList
37 from .psfMatch import PsfMatchTask, PsfMatchConfigDF, PsfMatchConfigAL
38 from . import utils as diffimUtils
39 from . import diffimLib
40 from . import diffimTools
41 import lsst.afw.display as afwDisplay
42 
43 sigma2fwhm = 2.*np.sqrt(2.*np.log(2.))
44 
45 
46 class ImagePsfMatchConfig(pexConfig.Config):
47  """Configuration for image-to-image Psf matching.
48  """
49  kernel = pexConfig.ConfigChoiceField(
50  doc="kernel type",
51  typemap=dict(
52  AL=PsfMatchConfigAL,
53  DF=PsfMatchConfigDF
54  ),
55  default="AL",
56  )
57  selectDetection = pexConfig.ConfigurableField(
58  target=SourceDetectionTask,
59  doc="Initial detections used to feed stars to kernel fitting",
60  )
61  selectMeasurement = pexConfig.ConfigurableField(
62  target=SingleFrameMeasurementTask,
63  doc="Initial measurements used to feed stars to kernel fitting",
64  )
65 
66  def setDefaults(self):
67  # High sigma detections only
68  self.selectDetection.reEstimateBackground = False
69  self.selectDetection.thresholdValue = 10.0
70 
71  # Minimal set of measurments for star selection
72  self.selectMeasurement.algorithms.names.clear()
73  self.selectMeasurement.algorithms.names = ('base_SdssCentroid', 'base_PsfFlux', 'base_PixelFlags',
74  'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord')
75  self.selectMeasurement.slots.modelFlux = None
76  self.selectMeasurement.slots.apFlux = None
77  self.selectMeasurement.slots.calibFlux = None
78 
79 
81  """Psf-match two MaskedImages or Exposures using the sources in the images.
82 
83  Parameters
84  ----------
85  args :
86  Arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
87  kwargs :
88  Keyword arguments to be passed to lsst.ip.diffim.PsfMatchTask.__init__
89 
90  Notes
91  -----
92  Upon initialization, the kernel configuration is defined by self.config.kernel.active.
93  The task creates an lsst.afw.math.Warper from the subConfig self.config.kernel.active.warpingConfig.
94  A schema for the selection and measurement of candidate lsst.ip.diffim.KernelCandidates is
95  defined, and used to initize subTasks selectDetection (for candidate detection) and selectMeasurement
96  (for candidate measurement).
97 
98  Description
99 
100  Build a Psf-matching kernel using two input images, either as MaskedImages (in which case they need
101  to be astrometrically aligned) or Exposures (in which case astrometric alignment will happen by
102  default but may be turned off). This requires a list of input Sources which may be provided
103  by the calling Task; if not, the Task will perform a coarse source detection
104  and selection for this purpose. Sources are vetted for signal-to-noise and masked pixels
105  (in both the template and science image), and substamps around each acceptable
106  source are extracted and used to create an instance of KernelCandidate.
107  Each KernelCandidate is then placed within a lsst.afw.math.SpatialCellSet, which is used by an ensemble of
108  lsst.afw.math.CandidateVisitor instances to build the Psf-matching kernel. These visitors include, in
109  the order that they are called: BuildSingleKernelVisitor, KernelSumVisitor, BuildSpatialKernelVisitor,
110  and AssessSpatialKernelVisitor.
111 
112  Sigma clipping of KernelCandidates is performed as follows:
113 
114  - BuildSingleKernelVisitor, using the substamp diffim residuals from the per-source kernel fit,
115  if PsfMatchConfig.singleKernelClipping is True
116  - KernelSumVisitor, using the mean and standard deviation of the kernel sum from all candidates,
117  if PsfMatchConfig.kernelSumClipping is True
118  - AssessSpatialKernelVisitor, using the substamp diffim ressiduals from the spatial kernel fit,
119  if PsfMatchConfig.spatialKernelClipping is True
120 
121  The actual solving for the kernel (and differential background model) happens in
122  lsst.ip.diffim.PsfMatchTask._solve. This involves a loop over the SpatialCellSet that first builds the
123  per-candidate matching kernel for the requested number of KernelCandidates per cell
124  (PsfMatchConfig.nStarPerCell). The quality of this initial per-candidate difference image is examined,
125  using moments of the pixel residuals in the difference image normalized by the square root of the variance
126  (i.e. sigma); ideally this should follow a normal (0, 1) distribution,
127  but the rejection thresholds are set
128  by the config (PsfMatchConfig.candidateResidualMeanMax and PsfMatchConfig.candidateResidualStdMax).
129  All candidates that pass this initial build are then examined en masse to find the
130  mean/stdev of the kernel sums across all candidates.
131  Objects that are significantly above or below the mean,
132  typically due to variability or sources that are saturated in one image but not the other,
133  are also rejected.This threshold is defined by PsfMatchConfig.maxKsumSigma.
134  Finally, a spatial model is built using all currently-acceptable candidates,
135  and the spatial model used to derive a second set of (spatial) residuals
136  which are again used to reject bad candidates, using the same thresholds as above.
137 
138  Invoking the Task
139 
140  There is no run() method for this Task. Instead there are 4 methods that
141  may be used to invoke the Psf-matching. These are
142  `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.matchMaskedImages`,
143  `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.subtractMaskedImages`,
144  `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.matchExposures`, and
145  `~lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask.subtractExposures`.
146 
147  The methods that operate on lsst.afw.image.MaskedImage require that the images already be astrometrically
148  aligned, and are the same shape. The methods that operate on lsst.afw.image.Exposure allow for the
149  input images to be misregistered and potentially be different sizes; by default a
150  lsst.afw.math.LanczosWarpingKernel is used to perform the astrometric alignment. The methods
151  that "match" images return a Psf-matched image, while the methods that "subtract" images
152  return a Psf-matched and template subtracted image.
153 
154  See each method's returned lsst.pipe.base.Struct for more details.
155 
156  Debug variables
157 
158  The lsst.pipe.base.cmdLineTask.CmdLineTask command line task interface supports a
159  flag -d/--debug to import debug.py from your PYTHONPATH. The relevant contents of debug.py
160  for this Task include:
161 
162  .. code-block:: py
163 
164  import sys
165  import lsstDebug
166  def DebugInfo(name):
167  di = lsstDebug.getInfo(name)
168  if name == "lsst.ip.diffim.psfMatch":
169  di.display = True # enable debug output
170  di.maskTransparency = 80 # display mask transparency
171  di.displayCandidates = True # show all the candidates and residuals
172  di.displayKernelBasis = False # show kernel basis functions
173  di.displayKernelMosaic = True # show kernel realized across the image
174  di.plotKernelSpatialModel = False # show coefficients of spatial model
175  di.showBadCandidates = True # show the bad candidates (red) along with good (green)
176  elif name == "lsst.ip.diffim.imagePsfMatch":
177  di.display = True # enable debug output
178  di.maskTransparency = 30 # display mask transparency
179  di.displayTemplate = True # show full (remapped) template
180  di.displaySciIm = True # show science image to match to
181  di.displaySpatialCells = True # show spatial cells
182  di.displayDiffIm = True # show difference image
183  di.showBadCandidates = True # show the bad candidates (red) along with good (green)
184  elif name == "lsst.ip.diffim.diaCatalogSourceSelector":
185  di.display = False # enable debug output
186  di.maskTransparency = 30 # display mask transparency
187  di.displayExposure = True # show exposure with candidates indicated
188  di.pauseAtEnd = False # pause when done
189  return di
190  lsstDebug.Info = DebugInfo
191  lsstDebug.frame = 1
192 
193  Note that if you want addional logging info, you may add to your scripts:
194 
195  .. code-block:: py
196 
197  import lsst.log.utils as logUtils
198  logUtils.traceSetAt("ip.diffim", 4)
199 
200  Examples
201  --------
202  A complete example of using ImagePsfMatchTask
203 
204  This code is imagePsfMatchTask.py in the examples directory, and can be run as e.g.
205 
206  .. code-block:: none
207 
208  examples/imagePsfMatchTask.py --debug
209  examples/imagePsfMatchTask.py --debug --mode="matchExposures"
210  examples/imagePsfMatchTask.py --debug --template /path/to/templateExp.fits
211  --science /path/to/scienceExp.fits
212 
213  Create a subclass of ImagePsfMatchTask that allows us to either match exposures, or subtract exposures:
214 
215  .. code-block:: none
216 
217  class MyImagePsfMatchTask(ImagePsfMatchTask):
218 
219  def __init__(self, args, kwargs):
220  ImagePsfMatchTask.__init__(self, args, kwargs)
221 
222  def run(self, templateExp, scienceExp, mode):
223  if mode == "matchExposures":
224  return self.matchExposures(templateExp, scienceExp)
225  elif mode == "subtractExposures":
226  return self.subtractExposures(templateExp, scienceExp)
227 
228  And allow the user the freedom to either run the script in default mode,
229  or point to their own images on disk.
230  Note that these images must be readable as an lsst.afw.image.Exposure.
231 
232  We have enabled some minor display debugging in this script via the --debug option. However, if you
233  have an lsstDebug debug.py in your PYTHONPATH you will get additional debugging displays. The following
234  block checks for this script:
235 
236  .. code-block:: py
237 
238  if args.debug:
239  try:
240  import debug
241  # Since I am displaying 2 images here, set the starting frame number for the LSST debug LSST
242  debug.lsstDebug.frame = 3
243  except ImportError as e:
244  print(e, file=sys.stderr)
245 
246  Finally, we call a run method that we define below.
247  First set up a Config and modify some of the parameters.
248  E.g. use an "Alard-Lupton" sum-of-Gaussian basis,
249  fit for a differential background, and use low order spatial
250  variation in the kernel and background:
251 
252  .. code-block:: py
253 
254  def run(args):
255  #
256  # Create the Config and use sum of gaussian basis
257  #
258  config = ImagePsfMatchTask.ConfigClass()
259  config.kernel.name = "AL"
260  config.kernel.active.fitForBackground = True
261  config.kernel.active.spatialKernelOrder = 1
262  config.kernel.active.spatialBgOrder = 0
263 
264  Make sure the images (if any) that were sent to the script exist on disk and are readable. If no images
265  are sent, make some fake data up for the sake of this example script (have a look at the code if you want
266  more details on generateFakeImages):
267 
268  .. code-block:: py
269 
270  # Run the requested method of the Task
271  if args.template is not None and args.science is not None:
272  if not os.path.isfile(args.template):
273  raise Exception("Template image %s does not exist" % (args.template))
274  if not os.path.isfile(args.science):
275  raise Exception("Science image %s does not exist" % (args.science))
276  try:
277  templateExp = afwImage.ExposureF(args.template)
278  except Exception as e:
279  raise Exception("Cannot read template image %s" % (args.template))
280  try:
281  scienceExp = afwImage.ExposureF(args.science)
282  except Exception as e:
283  raise Exception("Cannot read science image %s" % (args.science))
284  else:
285  templateExp, scienceExp = generateFakeImages()
286  config.kernel.active.sizeCellX = 128
287  config.kernel.active.sizeCellY = 128
288 
289  Create and run the Task:
290 
291  .. code-block:: py
292 
293  # Create the Task
294  psfMatchTask = MyImagePsfMatchTask(config=config)
295  # Run the Task
296  result = psfMatchTask.run(templateExp, scienceExp, args.mode)
297 
298  And finally provide some optional debugging displays:
299 
300  .. code-block:: py
301 
302  if args.debug:
303  # See if the LSST debug has incremented the frame number; if not start with frame 3
304  try:
305  frame = debug.lsstDebug.frame + 1
306  except Exception:
307  frame = 3
308  afwDisplay.Display(frame=frame).mtv(result.matchedExposure,
309  title="Example script: Matched Template Image")
310  if "subtractedExposure" in result.getDict():
311  afwDisplay.Display(frame=frame + 1).mtv(result.subtractedExposure,
312  title="Example script: Subtracted Image")
313  """
314 
315  ConfigClass = ImagePsfMatchConfig
316 
317  def __init__(self, *args, **kwargs):
318  """Create the ImagePsfMatchTask.
319  """
320  PsfMatchTask.__init__(self, *args, **kwargs)
321  self.kConfig = self.config.kernel.active
322  self._warper = afwMath.Warper.fromConfig(self.kConfig.warpingConfig)
323  # the background subtraction task uses a config from an unusual location,
324  # so cannot easily be constructed with makeSubtask
325  self.background = SubtractBackgroundTask(config=self.kConfig.afwBackgroundConfig, name="background",
326  parentTask=self)
327  self.selectSchema = afwTable.SourceTable.makeMinimalSchema()
328  self.selectAlgMetadata = dafBase.PropertyList()
329  self.makeSubtask("selectDetection", schema=self.selectSchema)
330  self.makeSubtask("selectMeasurement", schema=self.selectSchema, algMetadata=self.selectAlgMetadata)
331 
332  def getFwhmPix(self, psf):
333  """Return the FWHM in pixels of a Psf.
334  """
335  sigPix = psf.computeShape().getDeterminantRadius()
336  return sigPix*sigma2fwhm
337 
338  @pipeBase.timeMethod
339  def matchExposures(self, templateExposure, scienceExposure,
340  templateFwhmPix=None, scienceFwhmPix=None,
341  candidateList=None, doWarping=True, convolveTemplate=True):
342  """Warp and PSF-match an exposure to the reference.
343 
344  Do the following, in order:
345 
346  - Warp templateExposure to match scienceExposure,
347  if doWarping True and their WCSs do not already match
348  - Determine a PSF matching kernel and differential background model
349  that matches templateExposure to scienceExposure
350  - Convolve templateExposure by PSF matching kernel
351 
352  Parameters
353  ----------
354  templateExposure : `lsst.afw.image.Exposure`
355  Exposure to warp and PSF-match to the reference masked image
356  scienceExposure : `lsst.afw.image.Exposure`
357  Exposure whose WCS and PSF are to be matched to
358  templateFwhmPix :`float`
359  FWHM (in pixels) of the Psf in the template image (image to convolve)
360  scienceFwhmPix : `float`
361  FWHM (in pixels) of the Psf in the science image
362  candidateList : `list`, optional
363  a list of footprints/maskedImages for kernel candidates;
364  if `None` then source detection is run.
365 
366  - Currently supported: list of Footprints or measAlg.PsfCandidateF
367 
368  doWarping : `bool`
369  what to do if ``templateExposure`` and ``scienceExposure`` WCSs do not match:
370 
371  - if `True` then warp ``templateExposure`` to match ``scienceExposure``
372  - if `False` then raise an Exception
373 
374  convolveTemplate : `bool`
375  Whether to convolve the template image or the science image:
376 
377  - if `True`, ``templateExposure`` is warped if doWarping,
378  ``templateExposure`` is convolved
379  - if `False`, ``templateExposure`` is warped if doWarping,
380  ``scienceExposure`` is convolved
381 
382  Returns
383  -------
384  results : `lsst.pipe.base.Struct`
385  An `lsst.pipe.base.Struct` containing these fields:
386 
387  - ``matchedImage`` : the PSF-matched exposure =
388  Warped ``templateExposure`` convolved by psfMatchingKernel. This has:
389 
390  - the same parent bbox, Wcs and PhotoCalib as scienceExposure
391  - the same filter as templateExposure
392  - no Psf (because the PSF-matching process does not compute one)
393 
394  - ``psfMatchingKernel`` : the PSF matching kernel
395  - ``backgroundModel`` : differential background model
396  - ``kernelCellSet`` : SpatialCellSet used to solve for the PSF matching kernel
397 
398  Raises
399  ------
400  RuntimeError
401  Raised if doWarping is False and ``templateExposure`` and
402  ``scienceExposure`` WCSs do not match
403  """
404  if not self._validateWcs(templateExposure, scienceExposure):
405  if doWarping:
406  self.log.info("Astrometrically registering template to science image")
407  templatePsf = templateExposure.getPsf()
408  templateExposure = self._warper.warpExposure(scienceExposure.getWcs(),
409  templateExposure,
410  destBBox=scienceExposure.getBBox())
411  templateExposure.setPsf(templatePsf)
412  else:
413  self.log.error("ERROR: Input images not registered")
414  raise RuntimeError("Input images not registered")
415 
416  if templateFwhmPix is None:
417  if not templateExposure.hasPsf():
418  self.log.warn("No estimate of Psf FWHM for template image")
419  else:
420  templateFwhmPix = self.getFwhmPix(templateExposure.getPsf())
421  self.log.info("templateFwhmPix: {}".format(templateFwhmPix))
422 
423  if scienceFwhmPix is None:
424  if not scienceExposure.hasPsf():
425  self.log.warn("No estimate of Psf FWHM for science image")
426  else:
427  scienceFwhmPix = self.getFwhmPix(scienceExposure.getPsf())
428  self.log.info("scienceFwhmPix: {}".format(scienceFwhmPix))
429 
430  kernelSize = makeKernelBasisList(self.kConfig, templateFwhmPix, scienceFwhmPix)[0].getWidth()
431  candidateList = self.makeCandidateList(templateExposure, scienceExposure, kernelSize, candidateList)
432 
433  if convolveTemplate:
434  results = self.matchMaskedImages(
435  templateExposure.getMaskedImage(), scienceExposure.getMaskedImage(), candidateList,
436  templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix)
437  else:
438  results = self.matchMaskedImages(
439  scienceExposure.getMaskedImage(), templateExposure.getMaskedImage(), candidateList,
440  templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix)
441 
442  psfMatchedExposure = afwImage.makeExposure(results.matchedImage, scienceExposure.getWcs())
443  psfMatchedExposure.setFilter(templateExposure.getFilter())
444  psfMatchedExposure.setPhotoCalib(scienceExposure.getPhotoCalib())
445  results.warpedExposure = templateExposure
446  results.matchedExposure = psfMatchedExposure
447  return results
448 
449  @pipeBase.timeMethod
450  def matchMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList,
451  templateFwhmPix=None, scienceFwhmPix=None):
452  """PSF-match a MaskedImage (templateMaskedImage) to a reference MaskedImage (scienceMaskedImage).
453 
454  Do the following, in order:
455 
456  - Determine a PSF matching kernel and differential background model
457  that matches templateMaskedImage to scienceMaskedImage
458  - Convolve templateMaskedImage by the PSF matching kernel
459 
460  Parameters
461  ----------
462  templateMaskedImage : `lsst.afw.image.MaskedImage`
463  masked image to PSF-match to the reference masked image;
464  must be warped to match the reference masked image
465  scienceMaskedImage : `lsst.afw.image.MaskedImage`
466  maskedImage whose PSF is to be matched to
467  templateFwhmPix : `float`
468  FWHM (in pixels) of the Psf in the template image (image to convolve)
469  scienceFwhmPix : `float`
470  FWHM (in pixels) of the Psf in the science image
471  candidateList : `list`, optional
472  A list of footprints/maskedImages for kernel candidates;
473  if `None` then source detection is run.
474 
475  - Currently supported: list of Footprints or measAlg.PsfCandidateF
476 
477  Returns
478  -------
479  result : `callable`
480  An `lsst.pipe.base.Struct` containing these fields:
481 
482  - psfMatchedMaskedImage: the PSF-matched masked image =
483  ``templateMaskedImage`` convolved with psfMatchingKernel.
484  This has the same xy0, dimensions and wcs as ``scienceMaskedImage``.
485  - psfMatchingKernel: the PSF matching kernel
486  - backgroundModel: differential background model
487  - kernelCellSet: SpatialCellSet used to solve for the PSF matching kernel
488 
489  Raises
490  ------
491  RuntimeError
492  Raised if input images have different dimensions
493  """
494  import lsstDebug
495  display = lsstDebug.Info(__name__).display
496  displayTemplate = lsstDebug.Info(__name__).displayTemplate
497  displaySciIm = lsstDebug.Info(__name__).displaySciIm
498  displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
499  maskTransparency = lsstDebug.Info(__name__).maskTransparency
500  if not maskTransparency:
501  maskTransparency = 0
502  if display:
503  afwDisplay.setDefaultMaskTransparency(maskTransparency)
504 
505  if not candidateList:
506  raise RuntimeError("Candidate list must be populated by makeCandidateList")
507 
508  if not self._validateSize(templateMaskedImage, scienceMaskedImage):
509  self.log.error("ERROR: Input images different size")
510  raise RuntimeError("Input images different size")
511 
512  if display and displayTemplate:
513  disp = afwDisplay.Display(frame=lsstDebug.frame)
514  disp.mtv(templateMaskedImage, title="Image to convolve")
515  lsstDebug.frame += 1
516 
517  if display and displaySciIm:
518  disp = afwDisplay.Display(frame=lsstDebug.frame)
519  disp.mtv(scienceMaskedImage, title="Image to not convolve")
520  lsstDebug.frame += 1
521 
522  kernelCellSet = self._buildCellSet(templateMaskedImage,
523  scienceMaskedImage,
524  candidateList)
525 
526  if display and displaySpatialCells:
527  diffimUtils.showKernelSpatialCells(scienceMaskedImage, kernelCellSet,
528  symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
529  ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
530  title="Image to not convolve")
531  lsstDebug.frame += 1
532 
533  if templateFwhmPix and scienceFwhmPix:
534  self.log.info("Matching Psf FWHM %.2f -> %.2f pix", templateFwhmPix, scienceFwhmPix)
535 
536  if self.kConfig.useBicForKernelBasis:
537  tmpKernelCellSet = self._buildCellSet(templateMaskedImage,
538  scienceMaskedImage,
539  candidateList)
540  nbe = diffimTools.NbasisEvaluator(self.kConfig, templateFwhmPix, scienceFwhmPix)
541  bicDegrees = nbe(tmpKernelCellSet, self.log)
542  basisList = makeKernelBasisList(self.kConfig, templateFwhmPix, scienceFwhmPix,
543  alardDegGauss=bicDegrees[0], metadata=self.metadata)
544  del tmpKernelCellSet
545  else:
546  basisList = makeKernelBasisList(self.kConfig, templateFwhmPix, scienceFwhmPix,
547  metadata=self.metadata)
548 
549  spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
550 
551  psfMatchedMaskedImage = afwImage.MaskedImageF(templateMaskedImage.getBBox())
552  doNormalize = False
553  afwMath.convolve(psfMatchedMaskedImage, templateMaskedImage, psfMatchingKernel, doNormalize)
554  return pipeBase.Struct(
555  matchedImage=psfMatchedMaskedImage,
556  psfMatchingKernel=psfMatchingKernel,
557  backgroundModel=backgroundModel,
558  kernelCellSet=kernelCellSet,
559  )
560 
561  @pipeBase.timeMethod
562  def subtractExposures(self, templateExposure, scienceExposure,
563  templateFwhmPix=None, scienceFwhmPix=None,
564  candidateList=None, doWarping=True, convolveTemplate=True):
565  """Register, Psf-match and subtract two Exposures.
566 
567  Do the following, in order:
568 
569  - Warp templateExposure to match scienceExposure, if their WCSs do not already match
570  - Determine a PSF matching kernel and differential background model
571  that matches templateExposure to scienceExposure
572  - PSF-match templateExposure to scienceExposure
573  - Compute subtracted exposure (see return values for equation).
574 
575  Parameters
576  ----------
577  templateExposure : `lsst.afw.image.Exposure`
578  Exposure to PSF-match to scienceExposure
579  scienceExposure : `lsst.afw.image.Exposure`
580  Reference Exposure
581  templateFwhmPix : `float`
582  FWHM (in pixels) of the Psf in the template image (image to convolve)
583  scienceFwhmPix : `float`
584  FWHM (in pixels) of the Psf in the science image
585  candidateList : `list`, optional
586  A list of footprints/maskedImages for kernel candidates;
587  if `None` then source detection is run.
588 
589  - Currently supported: list of Footprints or measAlg.PsfCandidateF
590 
591  doWarping : `bool`
592  What to do if ``templateExposure``` and ``scienceExposure`` WCSs do
593  not match:
594 
595  - if `True` then warp ``templateExposure`` to match ``scienceExposure``
596  - if `False` then raise an Exception
597 
598  convolveTemplate : `bool`
599  Convolve the template image or the science image
600 
601  - if `True`, ``templateExposure`` is warped if doWarping,
602  ``templateExposure`` is convolved
603  - if `False`, ``templateExposure`` is warped if doWarping,
604  ``scienceExposure is`` convolved
605 
606  Returns
607  -------
608  result : `lsst.pipe.base.Struct`
609  An `lsst.pipe.base.Struct` containing these fields:
610 
611  - ``subtractedExposure`` : subtracted Exposure
612  scienceExposure - (matchedImage + backgroundModel)
613  - ``matchedImage`` : ``templateExposure`` after warping to match
614  ``templateExposure`` (if doWarping true),
615  and convolving with psfMatchingKernel
616  - ``psfMatchingKernel`` : PSF matching kernel
617  - ``backgroundModel`` : differential background model
618  - ``kernelCellSet`` : SpatialCellSet used to determine PSF matching kernel
619  """
620  results = self.matchExposures(
621  templateExposure=templateExposure,
622  scienceExposure=scienceExposure,
623  templateFwhmPix=templateFwhmPix,
624  scienceFwhmPix=scienceFwhmPix,
625  candidateList=candidateList,
626  doWarping=doWarping,
627  convolveTemplate=convolveTemplate
628  )
629 
630  subtractedExposure = afwImage.ExposureF(scienceExposure, True)
631  if convolveTemplate:
632  subtractedMaskedImage = subtractedExposure.getMaskedImage()
633  subtractedMaskedImage -= results.matchedExposure.getMaskedImage()
634  subtractedMaskedImage -= results.backgroundModel
635  else:
636  subtractedExposure.setMaskedImage(results.warpedExposure.getMaskedImage())
637  subtractedMaskedImage = subtractedExposure.getMaskedImage()
638  subtractedMaskedImage -= results.matchedExposure.getMaskedImage()
639  subtractedMaskedImage -= results.backgroundModel
640 
641  # Preserve polarity of differences
642  subtractedMaskedImage *= -1
643 
644  # Place back on native photometric scale
645  subtractedMaskedImage /= results.psfMatchingKernel.computeImage(
646  afwImage.ImageD(results.psfMatchingKernel.getDimensions()), False)
647 
648  import lsstDebug
649  display = lsstDebug.Info(__name__).display
650  displayDiffIm = lsstDebug.Info(__name__).displayDiffIm
651  maskTransparency = lsstDebug.Info(__name__).maskTransparency
652  if not maskTransparency:
653  maskTransparency = 0
654  if display:
655  afwDisplay.setDefaultMaskTransparency(maskTransparency)
656  if display and displayDiffIm:
657  disp = afwDisplay.Display(frame=lsstDebug.frame)
658  disp.mtv(templateExposure, title="Template")
659  lsstDebug.frame += 1
660  disp = afwDisplay.Display(frame=lsstDebug.frame)
661  disp.mtv(results.matchedExposure, title="Matched template")
662  lsstDebug.frame += 1
663  disp = afwDisplay.Display(frame=lsstDebug.frame)
664  disp.mtv(scienceExposure, title="Science Image")
665  lsstDebug.frame += 1
666  disp = afwDisplay.Display(frame=lsstDebug.frame)
667  disp.mtv(subtractedExposure, title="Difference Image")
668  lsstDebug.frame += 1
669 
670  results.subtractedExposure = subtractedExposure
671  return results
672 
673  @pipeBase.timeMethod
674  def subtractMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList,
675  templateFwhmPix=None, scienceFwhmPix=None):
676  """Psf-match and subtract two MaskedImages.
677 
678  Do the following, in order:
679 
680  - PSF-match templateMaskedImage to scienceMaskedImage
681  - Determine the differential background
682  - Return the difference: scienceMaskedImage
683  ((warped templateMaskedImage convolved with psfMatchingKernel) + backgroundModel)
684 
685  Parameters
686  ----------
687  templateMaskedImage : `lsst.afw.image.MaskedImage`
688  MaskedImage to PSF-match to ``scienceMaskedImage``
689  scienceMaskedImage : `lsst.afw.image.MaskedImage`
690  Reference MaskedImage
691  templateFwhmPix : `float`
692  FWHM (in pixels) of the Psf in the template image (image to convolve)
693  scienceFwhmPix : `float`
694  FWHM (in pixels) of the Psf in the science image
695  candidateList : `list`, optional
696  A list of footprints/maskedImages for kernel candidates;
697  if `None` then source detection is run.
698 
699  - Currently supported: list of Footprints or measAlg.PsfCandidateF
700 
701  Returns
702  -------
703  results : `lsst.pipe.base.Struct`
704  An `lsst.pipe.base.Struct` containing these fields:
705 
706  - ``subtractedMaskedImage`` : ``scienceMaskedImage`` - (matchedImage + backgroundModel)
707  - ``matchedImage`` : templateMaskedImage convolved with psfMatchingKernel
708  - `psfMatchingKernel`` : PSF matching kernel
709  - ``backgroundModel`` : differential background model
710  - ``kernelCellSet`` : SpatialCellSet used to determine PSF matching kernel
711 
712  """
713  if not candidateList:
714  raise RuntimeError("Candidate list must be populated by makeCandidateList")
715 
716  results = self.matchMaskedImages(
717  templateMaskedImage=templateMaskedImage,
718  scienceMaskedImage=scienceMaskedImage,
719  candidateList=candidateList,
720  templateFwhmPix=templateFwhmPix,
721  scienceFwhmPix=scienceFwhmPix,
722  )
723 
724  subtractedMaskedImage = afwImage.MaskedImageF(scienceMaskedImage, True)
725  subtractedMaskedImage -= results.matchedImage
726  subtractedMaskedImage -= results.backgroundModel
727  results.subtractedMaskedImage = subtractedMaskedImage
728 
729  import lsstDebug
730  display = lsstDebug.Info(__name__).display
731  displayDiffIm = lsstDebug.Info(__name__).displayDiffIm
732  maskTransparency = lsstDebug.Info(__name__).maskTransparency
733  if not maskTransparency:
734  maskTransparency = 0
735  if display:
736  afwDisplay.setDefaultMaskTransparency(maskTransparency)
737  if display and displayDiffIm:
738  disp = afwDisplay.Display(frame=lsstDebug.frame)
739  disp.mtv(subtractedMaskedImage, title="Subtracted masked image")
740  lsstDebug.frame += 1
741 
742  return results
743 
744  def getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None):
745  """Get sources to use for Psf-matching.
746 
747  This method runs detection and measurement on an exposure.
748  The returned set of sources will be used as candidates for
749  Psf-matching.
750 
751  Parameters
752  ----------
753  exposure : `lsst.afw.image.Exposure`
754  Exposure on which to run detection/measurement
755  sigma : `float`
756  Detection threshold
757  doSmooth : `bool`
758  Whether or not to smooth the Exposure with Psf before detection
759  idFactory :
760  Factory for the generation of Source ids
761 
762  Returns
763  -------
764  selectSources :
765  source catalog containing candidates for the Psf-matching
766  """
767  if idFactory:
768  table = afwTable.SourceTable.make(self.selectSchema, idFactory)
769  else:
770  table = afwTable.SourceTable.make(self.selectSchema)
771  mi = exposure.getMaskedImage()
772 
773  imArr = mi.getImage().getArray()
774  maskArr = mi.getMask().getArray()
775  miArr = np.ma.masked_array(imArr, mask=maskArr)
776  try:
777  bkgd = self.background.fitBackground(mi).getImageF()
778  except Exception:
779  self.log.warn("Failed to get background model. Falling back to median background estimation")
780  bkgd = np.ma.extras.median(miArr)
781 
782  # Take off background for detection
783  mi -= bkgd
784  try:
785  table.setMetadata(self.selectAlgMetadata)
786  detRet = self.selectDetection.makeSourceCatalog(
787  table=table,
788  exposure=exposure,
789  sigma=sigma,
790  doSmooth=doSmooth
791  )
792  selectSources = detRet.sources
793  self.selectMeasurement.run(measCat=selectSources, exposure=exposure)
794  finally:
795  # Put back on the background in case it is needed down stream
796  mi += bkgd
797  del bkgd
798  return selectSources
799 
800  def makeCandidateList(self, templateExposure, scienceExposure, kernelSize, candidateList=None):
801  """Make a list of acceptable KernelCandidates.
802 
803  Accept or generate a list of candidate sources for
804  Psf-matching, and examine the Mask planes in both of the
805  images for indications of bad pixels
806 
807  Parameters
808  ----------
809  templateExposure : `lsst.afw.image.Exposure`
810  Exposure that will be convolved
811  scienceExposure : `lsst.afw.image.Exposure`
812  Exposure that will be matched-to
813  kernelSize : `float`
814  Dimensions of the Psf-matching Kernel, used to grow detection footprints
815  candidateList : `list`, optional
816  List of Sources to examine. Elements must be of type afw.table.Source
817  or a type that wraps a Source and has a getSource() method, such as
818  meas.algorithms.PsfCandidateF.
819 
820  Returns
821  -------
822  candidateList : `list` of `dict`
823  A list of dicts having a "source" and "footprint"
824  field for the Sources deemed to be appropriate for Psf
825  matching
826  """
827  if candidateList is None:
828  candidateList = self.getSelectSources(scienceExposure)
829 
830  if len(candidateList) < 1:
831  raise RuntimeError("No candidates in candidateList")
832 
833  listTypes = set(type(x) for x in candidateList)
834  if len(listTypes) > 1:
835  raise RuntimeError("Candidate list contains mixed types: %s" % [l for l in listTypes])
836 
837  if not isinstance(candidateList[0], afwTable.SourceRecord):
838  try:
839  candidateList[0].getSource()
840  except Exception as e:
841  raise RuntimeError("Candidate List is of type: %s. " % (type(candidateList[0])) +
842  "Can only make candidate list from list of afwTable.SourceRecords, " +
843  "measAlg.PsfCandidateF or other type with a getSource() method: %s" % (e))
844  candidateList = [c.getSource() for c in candidateList]
845 
846  candidateList = diffimTools.sourceToFootprintList(candidateList,
847  templateExposure, scienceExposure,
848  kernelSize,
849  self.kConfig.detectionConfig,
850  self.log)
851  if len(candidateList) == 0:
852  raise RuntimeError("Cannot find any objects suitable for KernelCandidacy")
853 
854  return candidateList
855 
856  def _adaptCellSize(self, candidateList):
857  """NOT IMPLEMENTED YET.
858  """
859  return self.kConfig.sizeCellX, self.kConfig.sizeCellY
860 
861  def _buildCellSet(self, templateMaskedImage, scienceMaskedImage, candidateList):
862  """Build a SpatialCellSet for use with the solve method.
863 
864  Parameters
865  ----------
866  templateMaskedImage : `lsst.afw.image.MaskedImage`
867  MaskedImage to PSF-matched to scienceMaskedImage
868  scienceMaskedImage : `lsst.afw.image.MaskedImage`
869  Reference MaskedImage
870  candidateList : `list`
871  A list of footprints/maskedImages for kernel candidates;
872 
873  - Currently supported: list of Footprints or measAlg.PsfCandidateF
874 
875  Returns
876  -------
877  kernelCellSet :
878  a SpatialCellSet for use with self._solve
879  """
880  if not candidateList:
881  raise RuntimeError("Candidate list must be populated by makeCandidateList")
882 
883  sizeCellX, sizeCellY = self._adaptCellSize(candidateList)
884 
885  # Object to store the KernelCandidates for spatial modeling
886  kernelCellSet = afwMath.SpatialCellSet(templateMaskedImage.getBBox(),
887  sizeCellX, sizeCellY)
888 
889  policy = pexConfig.makePolicy(self.kConfig)
890  # Place candidates within the spatial grid
891  for cand in candidateList:
892  if isinstance(cand, afwDetect.Footprint):
893  bbox = cand.getBBox()
894  else:
895  bbox = cand['footprint'].getBBox()
896  tmi = afwImage.MaskedImageF(templateMaskedImage, bbox)
897  smi = afwImage.MaskedImageF(scienceMaskedImage, bbox)
898 
899  if not isinstance(cand, afwDetect.Footprint):
900  if 'source' in cand:
901  cand = cand['source']
902  xPos = cand.getCentroid()[0]
903  yPos = cand.getCentroid()[1]
904  cand = diffimLib.makeKernelCandidate(xPos, yPos, tmi, smi, policy)
905 
906  self.log.debug("Candidate %d at %f, %f", cand.getId(), cand.getXCenter(), cand.getYCenter())
907  kernelCellSet.insertCandidate(cand)
908 
909  return kernelCellSet
910 
911  def _validateSize(self, templateMaskedImage, scienceMaskedImage):
912  """Return True if two image-like objects are the same size.
913  """
914  return templateMaskedImage.getDimensions() == scienceMaskedImage.getDimensions()
915 
916  def _validateWcs(self, templateExposure, scienceExposure):
917  """Return True if the WCS of the two Exposures have the same origin and extent.
918  """
919  templateWcs = templateExposure.getWcs()
920  scienceWcs = scienceExposure.getWcs()
921  templateBBox = templateExposure.getBBox()
922  scienceBBox = scienceExposure.getBBox()
923 
924  # LLC
925  templateOrigin = templateWcs.pixelToSky(afwGeom.Point2D(templateBBox.getBegin()))
926  scienceOrigin = scienceWcs.pixelToSky(afwGeom.Point2D(scienceBBox.getBegin()))
927 
928  # URC
929  templateLimit = templateWcs.pixelToSky(afwGeom.Point2D(templateBBox.getEnd()))
930  scienceLimit = scienceWcs.pixelToSky(afwGeom.Point2D(scienceBBox.getEnd()))
931 
932  self.log.info("Template Wcs : %f,%f -> %f,%f",
933  templateOrigin[0], templateOrigin[1],
934  templateLimit[0], templateLimit[1])
935  self.log.info("Science Wcs : %f,%f -> %f,%f",
936  scienceOrigin[0], scienceOrigin[1],
937  scienceLimit[0], scienceLimit[1])
938 
939  templateBBox = afwGeom.Box2D(templateOrigin.getPosition(afwGeom.degrees),
940  templateLimit.getPosition(afwGeom.degrees))
941  scienceBBox = afwGeom.Box2D(scienceOrigin.getPosition(afwGeom.degrees),
942  scienceLimit.getPosition(afwGeom.degrees))
943  if not (templateBBox.overlaps(scienceBBox)):
944  raise RuntimeError("Input images do not overlap at all")
945 
946  if ((templateOrigin != scienceOrigin) or
947  (templateLimit != scienceLimit) or
948  (templateExposure.getDimensions() != scienceExposure.getDimensions())):
949  return False
950  return True
951 
952 
953 subtractAlgorithmRegistry = pexConfig.makeRegistry(
954  doc="A registry of subtraction algorithms for use as a subtask in imageDifference",
955 )
956 
957 subtractAlgorithmRegistry.register('al', ImagePsfMatchTask)
def makeKernelBasisList(config, targetFwhmPix=None, referenceFwhmPix=None, basisDegGauss=None, metadata=None)
def getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None)
def _validateSize(self, templateMaskedImage, scienceMaskedImage)
def _buildCellSet(self, templateMaskedImage, scienceMaskedImage, candidateList)
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
def matchMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList, templateFwhmPix=None, scienceFwhmPix=None)
def _solve(self, kernelCellSet, basisList, returnOnExcept=False)
Definition: psfMatch.py:868
def matchExposures(self, templateExposure, scienceExposure, templateFwhmPix=None, scienceFwhmPix=None, candidateList=None, doWarping=True, convolveTemplate=True)
def makeCandidateList(self, templateExposure, scienceExposure, kernelSize, candidateList=None)
def subtractMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList, templateFwhmPix=None, scienceFwhmPix=None)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
def subtractExposures(self, templateExposure, scienceExposure, templateFwhmPix=None, scienceFwhmPix=None, candidateList=None, doWarping=True, convolveTemplate=True)
def _validateWcs(self, templateExposure, scienceExposure)