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