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