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