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