LSST Applications  21.0.0+04719a4bac,21.0.0-1-ga51b5d4+f5e6047307,21.0.0-11-g2b59f77+a9c1acf22d,21.0.0-11-ga42c5b2+86977b0b17,21.0.0-12-gf4ce030+76814010d2,21.0.0-13-g1721dae+760e7a6536,21.0.0-13-g3a573fe+768d78a30a,21.0.0-15-g5a7caf0+f21cbc5713,21.0.0-16-g0fb55c1+b60e2d390c,21.0.0-19-g4cded4ca+71a93a33c0,21.0.0-2-g103fe59+bb20972958,21.0.0-2-g45278ab+04719a4bac,21.0.0-2-g5242d73+3ad5d60fb1,21.0.0-2-g7f82c8f+8babb168e8,21.0.0-2-g8f08a60+06509c8b61,21.0.0-2-g8faa9b5+616205b9df,21.0.0-2-ga326454+8babb168e8,21.0.0-2-gde069b7+5e4aea9c2f,21.0.0-2-gecfae73+1d3a86e577,21.0.0-2-gfc62afb+3ad5d60fb1,21.0.0-25-g1d57be3cd+e73869a214,21.0.0-3-g357aad2+ed88757d29,21.0.0-3-g4a4ce7f+3ad5d60fb1,21.0.0-3-g4be5c26+3ad5d60fb1,21.0.0-3-g65f322c+e0b24896a3,21.0.0-3-g7d9da8d+616205b9df,21.0.0-3-ge02ed75+a9c1acf22d,21.0.0-4-g591bb35+a9c1acf22d,21.0.0-4-g65b4814+b60e2d390c,21.0.0-4-gccdca77+0de219a2bc,21.0.0-4-ge8a399c+6c55c39e83,21.0.0-5-gd00fb1e+05fce91b99,21.0.0-6-gc675373+3ad5d60fb1,21.0.0-64-g1122c245+4fb2b8f86e,21.0.0-7-g04766d7+cd19d05db2,21.0.0-7-gdf92d54+04719a4bac,21.0.0-8-g5674e7b+d1bd76f71f,master-gac4afde19b+a9c1acf22d,w.2021.13
LSST Data Management Base Package
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.selectDetectionselectDetection.reEstimateBackground = False
70  self.selectDetectionselectDetection.thresholdValue = 10.0
71 
72  # Minimal set of measurments for star selection
73  self.selectMeasurementselectMeasurement.algorithms.names.clear()
74  self.selectMeasurementselectMeasurement.algorithms.names = ('base_SdssCentroid', 'base_PsfFlux', 'base_PixelFlags',
75  'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord')
76  self.selectMeasurementselectMeasurement.slots.modelFlux = None
77  self.selectMeasurementselectMeasurement.slots.apFlux = None
78  self.selectMeasurementselectMeasurement.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 FileNotFoundError("Template image %s does not exist" % (args.template))
275  if not os.path.isfile(args.science):
276  raise FileNotFoundError("Science image %s does not exist" % (args.science))
277  try:
278  templateExp = afwImage.ExposureF(args.template)
279  except Exception as e:
280  raise RuntimeError("Cannot read template image %s" % (args.template))
281  try:
282  scienceExp = afwImage.ExposureF(args.science)
283  except Exception as e:
284  raise RuntimeError("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.kConfigkConfigkConfig = self.config.kernel.active
323  self._warper_warper = afwMath.Warper.fromConfig(self.kConfigkConfigkConfig.warpingConfig)
324  # the background subtraction task uses a config from an unusual location,
325  # so cannot easily be constructed with makeSubtask
326  self.backgroundbackground = SubtractBackgroundTask(config=self.kConfigkConfigkConfig.afwBackgroundConfig, name="background",
327  parentTask=self)
328  self.selectSchemaselectSchema = afwTable.SourceTable.makeMinimalSchema()
329  self.selectAlgMetadataselectAlgMetadata = dafBase.PropertyList()
330  self.makeSubtask("selectDetection", schema=self.selectSchemaselectSchema)
331  self.makeSubtask("selectMeasurement", schema=self.selectSchemaselectSchema, algMetadata=self.selectAlgMetadataselectAlgMetadata)
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_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_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.getFwhmPixgetFwhmPix(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.getFwhmPixgetFwhmPix(scienceExposure.getPsf())
433  self.log.info("scienceFwhmPix: {}".format(scienceFwhmPix))
434 
435  if convolveTemplate:
436  kernelSize = makeKernelBasisList(self.kConfigkConfigkConfig, templateFwhmPix, scienceFwhmPix)[0].getWidth()
437  candidateList = self.makeCandidateListmakeCandidateList(
438  templateExposure, scienceExposure, kernelSize, candidateList)
439  results = self.matchMaskedImagesmatchMaskedImages(
440  templateExposure.getMaskedImage(), scienceExposure.getMaskedImage(), candidateList,
441  templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix)
442  else:
443  kernelSize = makeKernelBasisList(self.kConfigkConfigkConfig, scienceFwhmPix, templateFwhmPix)[0].getWidth()
444  candidateList = self.makeCandidateListmakeCandidateList(
445  templateExposure, scienceExposure, kernelSize, candidateList)
446  results = self.matchMaskedImagesmatchMaskedImages(
447  scienceExposure.getMaskedImage(), templateExposure.getMaskedImage(), candidateList,
448  templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix)
449 
450  psfMatchedExposure = afwImage.makeExposure(results.matchedImage, scienceExposure.getWcs())
451  psfMatchedExposure.setFilterLabel(templateExposure.getFilterLabel())
452  psfMatchedExposure.setPhotoCalib(scienceExposure.getPhotoCalib())
453  results.warpedExposure = templateExposure
454  results.matchedExposure = psfMatchedExposure
455  return results
456 
457  @pipeBase.timeMethod
458  def matchMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList,
459  templateFwhmPix=None, scienceFwhmPix=None):
460  """PSF-match a MaskedImage (templateMaskedImage) to a reference MaskedImage (scienceMaskedImage).
461 
462  Do the following, in order:
463 
464  - Determine a PSF matching kernel and differential background model
465  that matches templateMaskedImage to scienceMaskedImage
466  - Convolve templateMaskedImage by the PSF matching kernel
467 
468  Parameters
469  ----------
470  templateMaskedImage : `lsst.afw.image.MaskedImage`
471  masked image to PSF-match to the reference masked image;
472  must be warped to match the reference masked image
473  scienceMaskedImage : `lsst.afw.image.MaskedImage`
474  maskedImage whose PSF is to be matched to
475  templateFwhmPix : `float`
476  FWHM (in pixels) of the Psf in the template image (image to convolve)
477  scienceFwhmPix : `float`
478  FWHM (in pixels) of the Psf in the science image
479  candidateList : `list`, optional
480  A list of footprints/maskedImages for kernel candidates;
481  if `None` then source detection is run.
482 
483  - Currently supported: list of Footprints or measAlg.PsfCandidateF
484 
485  Returns
486  -------
487  result : `callable`
488  An `lsst.pipe.base.Struct` containing these fields:
489 
490  - psfMatchedMaskedImage: the PSF-matched masked image =
491  ``templateMaskedImage`` convolved with psfMatchingKernel.
492  This has the same xy0, dimensions and wcs as ``scienceMaskedImage``.
493  - psfMatchingKernel: the PSF matching kernel
494  - backgroundModel: differential background model
495  - kernelCellSet: SpatialCellSet used to solve for the PSF matching kernel
496 
497  Raises
498  ------
499  RuntimeError
500  Raised if input images have different dimensions
501  """
502  import lsstDebug
503  display = lsstDebug.Info(__name__).display
504  displayTemplate = lsstDebug.Info(__name__).displayTemplate
505  displaySciIm = lsstDebug.Info(__name__).displaySciIm
506  displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
507  maskTransparency = lsstDebug.Info(__name__).maskTransparency
508  if not maskTransparency:
509  maskTransparency = 0
510  if display:
511  afwDisplay.setDefaultMaskTransparency(maskTransparency)
512 
513  if not candidateList:
514  raise RuntimeError("Candidate list must be populated by makeCandidateList")
515 
516  if not self._validateSize_validateSize(templateMaskedImage, scienceMaskedImage):
517  self.log.error("ERROR: Input images different size")
518  raise RuntimeError("Input images different size")
519 
520  if display and displayTemplate:
521  disp = afwDisplay.Display(frame=lsstDebug.frame)
522  disp.mtv(templateMaskedImage, title="Image to convolve")
523  lsstDebug.frame += 1
524 
525  if display and displaySciIm:
526  disp = afwDisplay.Display(frame=lsstDebug.frame)
527  disp.mtv(scienceMaskedImage, title="Image to not convolve")
528  lsstDebug.frame += 1
529 
530  kernelCellSet = self._buildCellSet_buildCellSet_buildCellSet(templateMaskedImage,
531  scienceMaskedImage,
532  candidateList)
533 
534  if display and displaySpatialCells:
535  diffimUtils.showKernelSpatialCells(scienceMaskedImage, kernelCellSet,
536  symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
537  ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame,
538  title="Image to not convolve")
539  lsstDebug.frame += 1
540 
541  if templateFwhmPix and scienceFwhmPix:
542  self.log.info("Matching Psf FWHM %.2f -> %.2f pix", templateFwhmPix, scienceFwhmPix)
543 
544  if self.kConfigkConfigkConfig.useBicForKernelBasis:
545  tmpKernelCellSet = self._buildCellSet_buildCellSet_buildCellSet(templateMaskedImage,
546  scienceMaskedImage,
547  candidateList)
548  nbe = diffimTools.NbasisEvaluator(self.kConfigkConfigkConfig, templateFwhmPix, scienceFwhmPix)
549  bicDegrees = nbe(tmpKernelCellSet, self.log)
550  basisList = makeKernelBasisList(self.kConfigkConfigkConfig, templateFwhmPix, scienceFwhmPix,
551  alardDegGauss=bicDegrees[0], metadata=self.metadata)
552  del tmpKernelCellSet
553  else:
554  basisList = makeKernelBasisList(self.kConfigkConfigkConfig, templateFwhmPix, scienceFwhmPix,
555  metadata=self.metadata)
556 
557  spatialSolution, psfMatchingKernel, backgroundModel = self._solve_solve(kernelCellSet, basisList)
558 
559  psfMatchedMaskedImage = afwImage.MaskedImageF(templateMaskedImage.getBBox())
560  convolutionControl = afwMath.ConvolutionControl()
561  convolutionControl.setDoNormalize(False)
562  afwMath.convolve(psfMatchedMaskedImage, templateMaskedImage, psfMatchingKernel, convolutionControl)
563  return pipeBase.Struct(
564  matchedImage=psfMatchedMaskedImage,
565  psfMatchingKernel=psfMatchingKernel,
566  backgroundModel=backgroundModel,
567  kernelCellSet=kernelCellSet,
568  )
569 
570  @pipeBase.timeMethod
571  def subtractExposures(self, templateExposure, scienceExposure,
572  templateFwhmPix=None, scienceFwhmPix=None,
573  candidateList=None, doWarping=True, convolveTemplate=True):
574  """Register, Psf-match and subtract two Exposures.
575 
576  Do the following, in order:
577 
578  - Warp templateExposure to match scienceExposure, if their WCSs do not already match
579  - Determine a PSF matching kernel and differential background model
580  that matches templateExposure to scienceExposure
581  - PSF-match templateExposure to scienceExposure
582  - Compute subtracted exposure (see return values for equation).
583 
584  Parameters
585  ----------
586  templateExposure : `lsst.afw.image.Exposure`
587  Exposure to PSF-match to scienceExposure
588  scienceExposure : `lsst.afw.image.Exposure`
589  Reference Exposure
590  templateFwhmPix : `float`
591  FWHM (in pixels) of the Psf in the template image (image to convolve)
592  scienceFwhmPix : `float`
593  FWHM (in pixels) of the Psf in the science image
594  candidateList : `list`, optional
595  A list of footprints/maskedImages for kernel candidates;
596  if `None` then source detection is run.
597 
598  - Currently supported: list of Footprints or measAlg.PsfCandidateF
599 
600  doWarping : `bool`
601  What to do if ``templateExposure``` and ``scienceExposure`` WCSs do
602  not match:
603 
604  - if `True` then warp ``templateExposure`` to match ``scienceExposure``
605  - if `False` then raise an Exception
606 
607  convolveTemplate : `bool`
608  Convolve the template image or the science image
609 
610  - if `True`, ``templateExposure`` is warped if doWarping,
611  ``templateExposure`` is convolved
612  - if `False`, ``templateExposure`` is warped if doWarping,
613  ``scienceExposure is`` convolved
614 
615  Returns
616  -------
617  result : `lsst.pipe.base.Struct`
618  An `lsst.pipe.base.Struct` containing these fields:
619 
620  - ``subtractedExposure`` : subtracted Exposure
621  scienceExposure - (matchedImage + backgroundModel)
622  - ``matchedImage`` : ``templateExposure`` after warping to match
623  ``templateExposure`` (if doWarping true),
624  and convolving with psfMatchingKernel
625  - ``psfMatchingKernel`` : PSF matching kernel
626  - ``backgroundModel`` : differential background model
627  - ``kernelCellSet`` : SpatialCellSet used to determine PSF matching kernel
628  """
629  results = self.matchExposuresmatchExposures(
630  templateExposure=templateExposure,
631  scienceExposure=scienceExposure,
632  templateFwhmPix=templateFwhmPix,
633  scienceFwhmPix=scienceFwhmPix,
634  candidateList=candidateList,
635  doWarping=doWarping,
636  convolveTemplate=convolveTemplate
637  )
638 
639  subtractedExposure = afwImage.ExposureF(scienceExposure, True)
640  # Note, the decorrelation afterburner re-calculates the variance plane
641  # from the variance planes of the original exposures.
642  # That recalculation code must be in accordance with the
643  # photometric level set here in ``subtractedMaskedImage``.
644  if convolveTemplate:
645  subtractedMaskedImage = subtractedExposure.getMaskedImage()
646  subtractedMaskedImage -= results.matchedExposure.getMaskedImage()
647  subtractedMaskedImage -= results.backgroundModel
648  else:
649  subtractedExposure.setMaskedImage(results.warpedExposure.getMaskedImage())
650  subtractedMaskedImage = subtractedExposure.getMaskedImage()
651  subtractedMaskedImage -= results.matchedExposure.getMaskedImage()
652  subtractedMaskedImage -= results.backgroundModel
653 
654  # Preserve polarity of differences
655  subtractedMaskedImage *= -1
656 
657  # Place back on native photometric scale
658  subtractedMaskedImage /= results.psfMatchingKernel.computeImage(
659  afwImage.ImageD(results.psfMatchingKernel.getDimensions()), False)
660 
661  import lsstDebug
662  display = lsstDebug.Info(__name__).display
663  displayDiffIm = lsstDebug.Info(__name__).displayDiffIm
664  maskTransparency = lsstDebug.Info(__name__).maskTransparency
665  if not maskTransparency:
666  maskTransparency = 0
667  if display:
668  afwDisplay.setDefaultMaskTransparency(maskTransparency)
669  if display and displayDiffIm:
670  disp = afwDisplay.Display(frame=lsstDebug.frame)
671  disp.mtv(templateExposure, title="Template")
672  lsstDebug.frame += 1
673  disp = afwDisplay.Display(frame=lsstDebug.frame)
674  disp.mtv(results.matchedExposure, title="Matched template")
675  lsstDebug.frame += 1
676  disp = afwDisplay.Display(frame=lsstDebug.frame)
677  disp.mtv(scienceExposure, title="Science Image")
678  lsstDebug.frame += 1
679  disp = afwDisplay.Display(frame=lsstDebug.frame)
680  disp.mtv(subtractedExposure, title="Difference Image")
681  lsstDebug.frame += 1
682 
683  results.subtractedExposure = subtractedExposure
684  return results
685 
686  @pipeBase.timeMethod
687  def subtractMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList,
688  templateFwhmPix=None, scienceFwhmPix=None):
689  """Psf-match and subtract two MaskedImages.
690 
691  Do the following, in order:
692 
693  - PSF-match templateMaskedImage to scienceMaskedImage
694  - Determine the differential background
695  - Return the difference: scienceMaskedImage
696  ((warped templateMaskedImage convolved with psfMatchingKernel) + backgroundModel)
697 
698  Parameters
699  ----------
700  templateMaskedImage : `lsst.afw.image.MaskedImage`
701  MaskedImage to PSF-match to ``scienceMaskedImage``
702  scienceMaskedImage : `lsst.afw.image.MaskedImage`
703  Reference MaskedImage
704  templateFwhmPix : `float`
705  FWHM (in pixels) of the Psf in the template image (image to convolve)
706  scienceFwhmPix : `float`
707  FWHM (in pixels) of the Psf in the science image
708  candidateList : `list`, optional
709  A list of footprints/maskedImages for kernel candidates;
710  if `None` then source detection is run.
711 
712  - Currently supported: list of Footprints or measAlg.PsfCandidateF
713 
714  Returns
715  -------
716  results : `lsst.pipe.base.Struct`
717  An `lsst.pipe.base.Struct` containing these fields:
718 
719  - ``subtractedMaskedImage`` : ``scienceMaskedImage`` - (matchedImage + backgroundModel)
720  - ``matchedImage`` : templateMaskedImage convolved with psfMatchingKernel
721  - `psfMatchingKernel`` : PSF matching kernel
722  - ``backgroundModel`` : differential background model
723  - ``kernelCellSet`` : SpatialCellSet used to determine PSF matching kernel
724 
725  """
726  if not candidateList:
727  raise RuntimeError("Candidate list must be populated by makeCandidateList")
728 
729  results = self.matchMaskedImagesmatchMaskedImages(
730  templateMaskedImage=templateMaskedImage,
731  scienceMaskedImage=scienceMaskedImage,
732  candidateList=candidateList,
733  templateFwhmPix=templateFwhmPix,
734  scienceFwhmPix=scienceFwhmPix,
735  )
736 
737  subtractedMaskedImage = afwImage.MaskedImageF(scienceMaskedImage, True)
738  subtractedMaskedImage -= results.matchedImage
739  subtractedMaskedImage -= results.backgroundModel
740  results.subtractedMaskedImage = subtractedMaskedImage
741 
742  import lsstDebug
743  display = lsstDebug.Info(__name__).display
744  displayDiffIm = lsstDebug.Info(__name__).displayDiffIm
745  maskTransparency = lsstDebug.Info(__name__).maskTransparency
746  if not maskTransparency:
747  maskTransparency = 0
748  if display:
749  afwDisplay.setDefaultMaskTransparency(maskTransparency)
750  if display and displayDiffIm:
751  disp = afwDisplay.Display(frame=lsstDebug.frame)
752  disp.mtv(subtractedMaskedImage, title="Subtracted masked image")
753  lsstDebug.frame += 1
754 
755  return results
756 
757  def getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None):
758  """Get sources to use for Psf-matching.
759 
760  This method runs detection and measurement on an exposure.
761  The returned set of sources will be used as candidates for
762  Psf-matching.
763 
764  Parameters
765  ----------
766  exposure : `lsst.afw.image.Exposure`
767  Exposure on which to run detection/measurement
768  sigma : `float`
769  Detection threshold
770  doSmooth : `bool`
771  Whether or not to smooth the Exposure with Psf before detection
772  idFactory :
773  Factory for the generation of Source ids
774 
775  Returns
776  -------
777  selectSources :
778  source catalog containing candidates for the Psf-matching
779  """
780  if idFactory:
781  table = afwTable.SourceTable.make(self.selectSchemaselectSchema, idFactory)
782  else:
783  table = afwTable.SourceTable.make(self.selectSchemaselectSchema)
784  mi = exposure.getMaskedImage()
785 
786  imArr = mi.getImage().getArray()
787  maskArr = mi.getMask().getArray()
788  miArr = np.ma.masked_array(imArr, mask=maskArr)
789  try:
790  fitBg = self.backgroundbackground.fitBackground(mi)
791  bkgd = fitBg.getImageF(self.backgroundbackground.config.algorithm,
792  self.backgroundbackground.config.undersampleStyle)
793  except Exception:
794  self.log.warn("Failed to get background model. Falling back to median background estimation")
795  bkgd = np.ma.extras.median(miArr)
796 
797  # Take off background for detection
798  mi -= bkgd
799  try:
800  table.setMetadata(self.selectAlgMetadataselectAlgMetadata)
801  detRet = self.selectDetection.run(
802  table=table,
803  exposure=exposure,
804  sigma=sigma,
805  doSmooth=doSmooth
806  )
807  selectSources = detRet.sources
808  self.selectMeasurement.run(measCat=selectSources, exposure=exposure)
809  finally:
810  # Put back on the background in case it is needed down stream
811  mi += bkgd
812  del bkgd
813  return selectSources
814 
815  def makeCandidateList(self, templateExposure, scienceExposure, kernelSize, candidateList=None):
816  """Make a list of acceptable KernelCandidates.
817 
818  Accept or generate a list of candidate sources for
819  Psf-matching, and examine the Mask planes in both of the
820  images for indications of bad pixels
821 
822  Parameters
823  ----------
824  templateExposure : `lsst.afw.image.Exposure`
825  Exposure that will be convolved
826  scienceExposure : `lsst.afw.image.Exposure`
827  Exposure that will be matched-to
828  kernelSize : `float`
829  Dimensions of the Psf-matching Kernel, used to grow detection footprints
830  candidateList : `list`, optional
831  List of Sources to examine. Elements must be of type afw.table.Source
832  or a type that wraps a Source and has a getSource() method, such as
833  meas.algorithms.PsfCandidateF.
834 
835  Returns
836  -------
837  candidateList : `list` of `dict`
838  A list of dicts having a "source" and "footprint"
839  field for the Sources deemed to be appropriate for Psf
840  matching
841  """
842  if candidateList is None:
843  candidateList = self.getSelectSourcesgetSelectSources(scienceExposure)
844 
845  if len(candidateList) < 1:
846  raise RuntimeError("No candidates in candidateList")
847 
848  listTypes = set(type(x) for x in candidateList)
849  if len(listTypes) > 1:
850  raise RuntimeError("Candidate list contains mixed types: %s" % [t for t in listTypes])
851 
852  if not isinstance(candidateList[0], afwTable.SourceRecord):
853  try:
854  candidateList[0].getSource()
855  except Exception as e:
856  raise RuntimeError(f"Candidate List is of type: {type(candidateList[0])} "
857  "Can only make candidate list from list of afwTable.SourceRecords, "
858  f"measAlg.PsfCandidateF or other type with a getSource() method: {e}")
859  candidateList = [c.getSource() for c in candidateList]
860 
861  candidateList = diffimTools.sourceToFootprintList(candidateList,
862  templateExposure, scienceExposure,
863  kernelSize,
864  self.kConfigkConfigkConfig.detectionConfig,
865  self.log)
866  if len(candidateList) == 0:
867  raise RuntimeError("Cannot find any objects suitable for KernelCandidacy")
868 
869  return candidateList
870 
871  def _adaptCellSize(self, candidateList):
872  """NOT IMPLEMENTED YET.
873  """
874  return self.kConfigkConfigkConfig.sizeCellX, self.kConfigkConfigkConfig.sizeCellY
875 
876  def _buildCellSet(self, templateMaskedImage, scienceMaskedImage, candidateList):
877  """Build a SpatialCellSet for use with the solve method.
878 
879  Parameters
880  ----------
881  templateMaskedImage : `lsst.afw.image.MaskedImage`
882  MaskedImage to PSF-matched to scienceMaskedImage
883  scienceMaskedImage : `lsst.afw.image.MaskedImage`
884  Reference MaskedImage
885  candidateList : `list`
886  A list of footprints/maskedImages for kernel candidates;
887 
888  - Currently supported: list of Footprints or measAlg.PsfCandidateF
889 
890  Returns
891  -------
892  kernelCellSet : `lsst.afw.math.SpatialCellSet`
893  a SpatialCellSet for use with self._solve
894  """
895  if not candidateList:
896  raise RuntimeError("Candidate list must be populated by makeCandidateList")
897 
898  sizeCellX, sizeCellY = self._adaptCellSize_adaptCellSize(candidateList)
899 
900  # Object to store the KernelCandidates for spatial modeling
901  kernelCellSet = afwMath.SpatialCellSet(templateMaskedImage.getBBox(),
902  sizeCellX, sizeCellY)
903 
904  ps = pexConfig.makePropertySet(self.kConfigkConfigkConfig)
905  # Place candidates within the spatial grid
906  for cand in candidateList:
907  if isinstance(cand, afwDetect.Footprint):
908  bbox = cand.getBBox()
909  else:
910  bbox = cand['footprint'].getBBox()
911  tmi = afwImage.MaskedImageF(templateMaskedImage, bbox)
912  smi = afwImage.MaskedImageF(scienceMaskedImage, bbox)
913 
914  if not isinstance(cand, afwDetect.Footprint):
915  if 'source' in cand:
916  cand = cand['source']
917  xPos = cand.getCentroid()[0]
918  yPos = cand.getCentroid()[1]
919  cand = diffimLib.makeKernelCandidate(xPos, yPos, tmi, smi, ps)
920 
921  self.log.debug("Candidate %d at %f, %f", cand.getId(), cand.getXCenter(), cand.getYCenter())
922  kernelCellSet.insertCandidate(cand)
923 
924  return kernelCellSet
925 
926  def _validateSize(self, templateMaskedImage, scienceMaskedImage):
927  """Return True if two image-like objects are the same size.
928  """
929  return templateMaskedImage.getDimensions() == scienceMaskedImage.getDimensions()
930 
931  def _validateWcs(self, templateExposure, scienceExposure):
932  """Return True if the WCS of the two Exposures have the same origin and extent.
933  """
934  templateWcs = templateExposure.getWcs()
935  scienceWcs = scienceExposure.getWcs()
936  templateBBox = templateExposure.getBBox()
937  scienceBBox = scienceExposure.getBBox()
938 
939  # LLC
940  templateOrigin = templateWcs.pixelToSky(geom.Point2D(templateBBox.getBegin()))
941  scienceOrigin = scienceWcs.pixelToSky(geom.Point2D(scienceBBox.getBegin()))
942 
943  # URC
944  templateLimit = templateWcs.pixelToSky(geom.Point2D(templateBBox.getEnd()))
945  scienceLimit = scienceWcs.pixelToSky(geom.Point2D(scienceBBox.getEnd()))
946 
947  self.log.info("Template Wcs : %f,%f -> %f,%f",
948  templateOrigin[0], templateOrigin[1],
949  templateLimit[0], templateLimit[1])
950  self.log.info("Science Wcs : %f,%f -> %f,%f",
951  scienceOrigin[0], scienceOrigin[1],
952  scienceLimit[0], scienceLimit[1])
953 
954  templateBBox = geom.Box2D(templateOrigin.getPosition(geom.degrees),
955  templateLimit.getPosition(geom.degrees))
956  scienceBBox = geom.Box2D(scienceOrigin.getPosition(geom.degrees),
957  scienceLimit.getPosition(geom.degrees))
958  if not (templateBBox.overlaps(scienceBBox)):
959  raise RuntimeError("Input images do not overlap at all")
960 
961  if ((templateOrigin != scienceOrigin)
962  or (templateLimit != scienceLimit)
963  or (templateExposure.getDimensions() != scienceExposure.getDimensions())):
964  return False
965  return True
966 
967 
968 subtractAlgorithmRegistry = pexConfig.makeRegistry(
969  doc="A registry of subtraction algorithms for use as a subtask in imageDifference",
970 )
971 
972 subtractAlgorithmRegistry.register('al', ImagePsfMatchTask)
table::Key< int > type
Definition: Detector.cc:163
Class to describe the properties of a detected object from an image.
Definition: Footprint.h:63
Parameters to control convolution.
Definition: ConvolveImage.h:50
A collection of SpatialCells covering an entire image.
Definition: SpatialCell.h:387
Record class that contains measurements made on a single exposure.
Definition: Source.h:80
Class for storing ordered metadata with comments.
Definition: PropertyList.h:68
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
def _validateWcs(self, templateExposure, scienceExposure)
def _buildCellSet(self, templateMaskedImage, scienceMaskedImage, candidateList)
def _validateSize(self, templateMaskedImage, scienceMaskedImage)
def subtractMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList, templateFwhmPix=None, scienceFwhmPix=None)
def matchMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList, templateFwhmPix=None, scienceFwhmPix=None)
def getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None)
def subtractExposures(self, templateExposure, scienceExposure, templateFwhmPix=None, scienceFwhmPix=None, candidateList=None, doWarping=True, convolveTemplate=True)
def matchExposures(self, templateExposure, scienceExposure, templateFwhmPix=None, scienceFwhmPix=None, candidateList=None, doWarping=True, convolveTemplate=True)
def makeCandidateList(self, templateExposure, scienceExposure, kernelSize, candidateList=None)
def _solve(self, kernelCellSet, basisList, returnOnExcept=False)
Definition: psfMatch.py:880
A Psf class that maps an arbitrary Psf through a coordinate transformation.
Definition: WarpedPsf.h:50
daf::base::PropertySet * set
Definition: fits.cc:912
std::shared_ptr< TransformPoint2ToPoint2 > makeWcsPairTransform(SkyWcs const &src, SkyWcs const &dst)
A Transform obtained by putting two SkyWcs objects "back to back".
Definition: SkyWcs.cc:151
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
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 >())
A function to return an Exposure of the correct type (cf.
Definition: Exposure.h:462
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
int warpExposure(DestExposureT &destExposure, SrcExposureT const &srcExposure, WarpingControl const &control, typename DestExposureT::MaskedImageT::SinglePixel padValue=lsst::afw::math::edgePixel< typename DestExposureT::MaskedImageT >(typename lsst::afw::image::detail::image_traits< typename DestExposureT::MaskedImageT >::image_category()))
Warp (remap) one exposure to another.
def makeKernelBasisList(config, targetFwhmPix=None, referenceFwhmPix=None, basisDegGauss=None, metadata=None)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)