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