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