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