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