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