LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
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.config as pexConfig
24 import lsst.afw.image as afwImage
25 import lsst.afw.math as afwMath
26 import lsst.afw.geom as afwGeom
27 import lsst.afw.table as afwTable
28 import lsst.pipe.base as pipeBase
29 from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask
30 from lsst.meas.base import SingleFrameMeasurementTask
31 from .makeKernelBasisList import makeKernelBasisList
32 from .psfMatch import PsfMatchTask, PsfMatchConfigDF, PsfMatchConfigAL
33 from . import utils as diUtils
34 from . import diffimLib
35 from . import diffimTools
36 import lsst.afw.display.ds9 as ds9
37 
38 sigma2fwhm = 2. * np.sqrt(2. * np.log(2.))
39 
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 
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.log.utils as logUtils
214 logUtils.traceSetAt("ip.diffim", 4)
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  # the background subtraction task uses a config from an unusual location,
285  # so cannot easily be constructed with makeSubtask
286  self.background = SubtractBackgroundTask(config=self.kConfig.afwBackgroundConfig, name="background",
287  parentTask=self)
288  self.selectSchema = afwTable.SourceTable.makeMinimalSchema()
290  self.makeSubtask("selectDetection", schema=self.selectSchema)
291  self.makeSubtask("selectMeasurement", schema=self.selectSchema, algMetadata=self.selectAlgMetadata)
292 
293  def getFwhmPix(self, psf):
294  """!Return the FWHM in pixels of a Psf"""
295  sigPix = psf.computeShape().getDeterminantRadius()
296  return sigPix * sigma2fwhm
297 
298  @pipeBase.timeMethod
299  def matchExposures(self, templateExposure, scienceExposure,
300  templateFwhmPix=None, scienceFwhmPix=None,
301  candidateList=None, doWarping=True, convolveTemplate=True):
302  """!Warp and PSF-match an exposure to the reference
303 
304  Do the following, in order:
305  - Warp templateExposure to match scienceExposure,
306  if doWarping True and their WCSs do not already match
307  - Determine a PSF matching kernel and differential background model
308  that matches templateExposure to scienceExposure
309  - Convolve templateExposure by PSF matching kernel
310 
311  @param templateExposure: Exposure to warp and PSF-match to the reference masked image
312  @param scienceExposure: Exposure whose WCS and PSF are to be matched to
313  @param templateFwhmPix: FWHM (in pixels) of the Psf in the template image (image to convolve)
314  @param scienceFwhmPix: FWHM (in pixels) of the Psf in the science image
315  @param candidateList: a list of footprints/maskedImages for kernel candidates;
316  if None then source detection is run.
317  - Currently supported: list of Footprints or measAlg.PsfCandidateF
318  @param doWarping: what to do if templateExposure's and scienceExposure's WCSs do not match:
319  - if True then warp templateExposure to match scienceExposure
320  - if False then raise an Exception
321  @param convolveTemplate: convolve the template image or the science image
322  - if True, templateExposure is warped if doWarping, templateExposure is convolved
323  - if False, templateExposure is warped if doWarping, scienceExposure is convolved
324 
325  @return a pipeBase.Struct containing these fields:
326  - matchedImage: the PSF-matched exposure =
327  warped templateExposure convolved by psfMatchingKernel. This has:
328  - the same parent bbox, Wcs and Calib as scienceExposure
329  - the same filter as templateExposure
330  - no Psf (because the PSF-matching process does not compute one)
331  - psfMatchingKernel: the PSF matching kernel
332  - backgroundModel: differential background model
333  - kernelCellSet: SpatialCellSet used to solve for the PSF matching kernel
334 
335  Raise a RuntimeError if doWarping is False and templateExposure's and scienceExposure's
336  WCSs do not match
337  """
338  if not self._validateWcs(templateExposure, scienceExposure):
339  if doWarping:
340  self.log.info("Astrometrically registering template to science image")
341  templatePsf = templateExposure.getPsf()
342  templateExposure = self._warper.warpExposure(scienceExposure.getWcs(),
343  templateExposure, destBBox=scienceExposure.getBBox())
344  templateExposure.setPsf(templatePsf)
345  else:
346  self.log.error("ERROR: Input images not registered")
347  raise RuntimeError("Input images not registered")
348 
349  if templateFwhmPix is None:
350  if not templateExposure.hasPsf():
351  self.log.warn("No estimate of Psf FWHM for template image")
352  else:
353  templateFwhmPix = self.getFwhmPix(templateExposure.getPsf())
354  self.log.info("templateFwhmPix: {}".format(templateFwhmPix))
355 
356  if scienceFwhmPix is None:
357  if not scienceExposure.hasPsf():
358  self.log.warn("No estimate of Psf FWHM for science image")
359  else:
360  scienceFwhmPix = self.getFwhmPix(scienceExposure.getPsf())
361  self.log.info("scienceFwhmPix: {}".format(scienceFwhmPix))
362 
363  kernelSize = makeKernelBasisList(self.kConfig, templateFwhmPix, scienceFwhmPix)[0].getWidth()
364  candidateList = self.makeCandidateList(templateExposure, scienceExposure, kernelSize, candidateList)
365 
366  if convolveTemplate:
367  results = self.matchMaskedImages(
368  templateExposure.getMaskedImage(), scienceExposure.getMaskedImage(), candidateList,
369  templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix)
370  else:
371  results = self.matchMaskedImages(
372  scienceExposure.getMaskedImage(), templateExposure.getMaskedImage(), candidateList,
373  templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix)
374 
375  psfMatchedExposure = afwImage.makeExposure(results.matchedImage, scienceExposure.getWcs())
376  psfMatchedExposure.setFilter(templateExposure.getFilter())
377  psfMatchedExposure.setCalib(scienceExposure.getCalib())
378  results.warpedExposure = templateExposure
379  results.matchedExposure = psfMatchedExposure
380  return results
381 
382  @pipeBase.timeMethod
383  def matchMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList,
384  templateFwhmPix=None, scienceFwhmPix=None):
385  """!PSF-match a MaskedImage (templateMaskedImage) to a reference MaskedImage (scienceMaskedImage)
386 
387  Do the following, in order:
388  - Determine a PSF matching kernel and differential background model
389  that matches templateMaskedImage to scienceMaskedImage
390  - Convolve templateMaskedImage by the PSF matching kernel
391 
392  @param templateMaskedImage: masked image to PSF-match to the reference masked image;
393  must be warped to match the reference masked image
394  @param scienceMaskedImage: maskedImage whose PSF is to be matched to
395  @param templateFwhmPix: FWHM (in pixels) of the Psf in the template image (image to convolve)
396  @param scienceFwhmPix: FWHM (in pixels) of the Psf in the science image
397  @param candidateList: a list of footprints/maskedImages for kernel candidates;
398  if None then source detection is run.
399  - Currently supported: list of Footprints or measAlg.PsfCandidateF
400 
401  @return a pipeBase.Struct containing these fields:
402  - psfMatchedMaskedImage: the PSF-matched masked image =
403  templateMaskedImage convolved with psfMatchingKernel.
404  This has the same xy0, dimensions and wcs as scienceMaskedImage.
405  - psfMatchingKernel: the PSF matching kernel
406  - backgroundModel: differential background model
407  - kernelCellSet: SpatialCellSet used to solve for the PSF matching kernel
408 
409  Raise a RuntimeError if input images have different dimensions
410  """
411 
412  import lsstDebug
413  display = lsstDebug.Info(__name__).display
414  displayTemplate = lsstDebug.Info(__name__).displayTemplate
415  displaySciIm = lsstDebug.Info(__name__).displaySciIm
416  displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells
417  maskTransparency = lsstDebug.Info(__name__).maskTransparency
418  if not maskTransparency:
419  maskTransparency = 0
420  if display:
421  ds9.setMaskTransparency(maskTransparency)
422 
423  if not candidateList:
424  raise RuntimeError("Candidate list must be populated by makeCandidateList")
425 
426  if not self._validateSize(templateMaskedImage, scienceMaskedImage):
427  self.log.error("ERROR: Input images different size")
428  raise RuntimeError("Input images different size")
429 
430  if display and displayTemplate:
431  ds9.mtv(templateMaskedImage, frame=lsstDebug.frame, title="Image to convolve")
432  lsstDebug.frame += 1
433 
434  if display and displaySciIm:
435  ds9.mtv(scienceMaskedImage, frame=lsstDebug.frame, title="Image to not convolve")
436  lsstDebug.frame += 1
437 
438  kernelCellSet = self._buildCellSet(templateMaskedImage,
439  scienceMaskedImage,
440  candidateList)
441 
442  if display and displaySpatialCells:
443  diUtils.showKernelSpatialCells(scienceMaskedImage, kernelCellSet,
444  symb="o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW, ctypeBad=ds9.RED,
445  size=4, frame=lsstDebug.frame, title="Image to not convolve")
446  lsstDebug.frame += 1
447 
448  if templateFwhmPix and scienceFwhmPix:
449  self.log.info("Matching Psf FWHM %.2f -> %.2f pix", templateFwhmPix, scienceFwhmPix)
450 
451  if self.kConfig.useBicForKernelBasis:
452  tmpKernelCellSet = self._buildCellSet(templateMaskedImage,
453  scienceMaskedImage,
454  candidateList)
455  nbe = diffimTools.NbasisEvaluator(self.kConfig, templateFwhmPix, scienceFwhmPix)
456  bicDegrees = nbe(tmpKernelCellSet, self.log)
457  basisList = makeKernelBasisList(self.kConfig, templateFwhmPix, scienceFwhmPix,
458  alardDegGauss=bicDegrees[0], metadata=self.metadata)
459  del tmpKernelCellSet
460  else:
461  basisList = makeKernelBasisList(self.kConfig, templateFwhmPix, scienceFwhmPix,
462  metadata=self.metadata)
463 
464  spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList)
465 
466  psfMatchedMaskedImage = afwImage.MaskedImageF(templateMaskedImage.getBBox())
467  doNormalize = False
468  afwMath.convolve(psfMatchedMaskedImage, templateMaskedImage, psfMatchingKernel, doNormalize)
469  return pipeBase.Struct(
470  matchedImage=psfMatchedMaskedImage,
471  psfMatchingKernel=psfMatchingKernel,
472  backgroundModel=backgroundModel,
473  kernelCellSet=kernelCellSet,
474  )
475 
476  @pipeBase.timeMethod
477  def subtractExposures(self, templateExposure, scienceExposure,
478  templateFwhmPix=None, scienceFwhmPix=None,
479  candidateList=None, doWarping=True, convolveTemplate=True):
480  """!Register, Psf-match and subtract two Exposures
481 
482  Do the following, in order:
483  - Warp templateExposure to match scienceExposure, if their WCSs do not already match
484  - Determine a PSF matching kernel and differential background model
485  that matches templateExposure to scienceExposure
486  - PSF-match templateExposure to scienceExposure
487  - Compute subtracted exposure (see return values for equation).
488 
489  @param templateExposure: exposure to PSF-match to scienceExposure
490  @param scienceExposure: reference Exposure
491  @param templateFwhmPix: FWHM (in pixels) of the Psf in the template image (image to convolve)
492  @param scienceFwhmPix: FWHM (in pixels) of the Psf in the science image
493  @param candidateList: a list of footprints/maskedImages for kernel candidates;
494  if None then source detection is run.
495  - Currently supported: list of Footprints or measAlg.PsfCandidateF
496  @param doWarping: what to do if templateExposure's and scienceExposure's WCSs do not match:
497  - if True then warp templateExposure to match scienceExposure
498  - if False then raise an Exception
499  @param convolveTemplate: convolve the template image or the science image
500  - if True, templateExposure is warped if doWarping, templateExposure is convolved
501  - if False, templateExposure is warped if doWarping, scienceExposure is convolved
502 
503  @return a pipeBase.Struct containing these fields:
504  - subtractedExposure: subtracted Exposure = scienceExposure - (matchedImage + backgroundModel)
505  - matchedImage: templateExposure after warping to match templateExposure (if doWarping true),
506  and convolving with psfMatchingKernel
507  - psfMatchingKernel: PSF matching kernel
508  - backgroundModel: differential background model
509  - kernelCellSet: SpatialCellSet used to determine PSF matching kernel
510  """
511  results = self.matchExposures(
512  templateExposure=templateExposure,
513  scienceExposure=scienceExposure,
514  templateFwhmPix=templateFwhmPix,
515  scienceFwhmPix=scienceFwhmPix,
516  candidateList=candidateList,
517  doWarping=doWarping,
518  convolveTemplate=convolveTemplate
519  )
520 
521  subtractedExposure = afwImage.ExposureF(scienceExposure, True)
522  if convolveTemplate:
523  subtractedMaskedImage = subtractedExposure.getMaskedImage()
524  subtractedMaskedImage -= results.matchedExposure.getMaskedImage()
525  subtractedMaskedImage -= results.backgroundModel
526  else:
527  subtractedExposure.setMaskedImage(results.warpedExposure.getMaskedImage())
528  subtractedMaskedImage = subtractedExposure.getMaskedImage()
529  subtractedMaskedImage -= results.matchedExposure.getMaskedImage()
530  subtractedMaskedImage -= results.backgroundModel
531 
532  # Preserve polarity of differences
533  subtractedMaskedImage *= -1
534 
535  # Place back on native photometric scale
536  subtractedMaskedImage /= results.psfMatchingKernel.computeImage(
537  afwImage.ImageD(results.psfMatchingKernel.getDimensions()), False)
538 
539  import lsstDebug
540  display = lsstDebug.Info(__name__).display
541  displayDiffIm = lsstDebug.Info(__name__).displayDiffIm
542  maskTransparency = lsstDebug.Info(__name__).maskTransparency
543  if not maskTransparency:
544  maskTransparency = 0
545  if display:
546  ds9.setMaskTransparency(maskTransparency)
547  if display and displayDiffIm:
548  ds9.mtv(templateExposure, frame=lsstDebug.frame, title="Template")
549  lsstDebug.frame += 1
550  ds9.mtv(results.matchedExposure, frame=lsstDebug.frame, title="Matched template")
551  lsstDebug.frame += 1
552  ds9.mtv(scienceExposure, frame=lsstDebug.frame, title="Science Image")
553  lsstDebug.frame += 1
554  ds9.mtv(subtractedExposure, frame=lsstDebug.frame, title="Difference Image")
555  lsstDebug.frame += 1
556 
557  results.subtractedExposure = subtractedExposure
558  return results
559 
560  @pipeBase.timeMethod
561  def subtractMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList,
562  templateFwhmPix=None, scienceFwhmPix=None):
563  """!Psf-match and subtract two MaskedImages
564 
565  Do the following, in order:
566  - PSF-match templateMaskedImage to scienceMaskedImage
567  - Determine the differential background
568  - Return the difference: scienceMaskedImage -
569  ((warped templateMaskedImage convolved with psfMatchingKernel) + backgroundModel)
570 
571  @param templateMaskedImage: MaskedImage to PSF-match to scienceMaskedImage
572  @param scienceMaskedImage: reference MaskedImage
573  @param templateFwhmPix: FWHM (in pixels) of the Psf in the template image (image to convolve)
574  @param scienceFwhmPix: FWHM (in pixels) of the Psf in the science image
575  @param candidateList: a list of footprints/maskedImages for kernel candidates;
576  if None then source detection is run.
577  - Currently supported: list of Footprints or measAlg.PsfCandidateF
578 
579  @return a pipeBase.Struct containing these fields:
580  - subtractedMaskedImage = scienceMaskedImage - (matchedImage + backgroundModel)
581  - matchedImage: templateMaskedImage convolved with psfMatchingKernel
582  - psfMatchingKernel: PSF matching kernel
583  - backgroundModel: differential background model
584  - kernelCellSet: SpatialCellSet used to determine PSF matching kernel
585  """
586  if not candidateList:
587  raise RuntimeError("Candidate list must be populated by makeCandidateList")
588 
589  results = self.matchMaskedImages(
590  templateMaskedImage=templateMaskedImage,
591  scienceMaskedImage=scienceMaskedImage,
592  candidateList=candidateList,
593  templateFwhmPix=templateFwhmPix,
594  scienceFwhmPix=scienceFwhmPix,
595  )
596 
597  subtractedMaskedImage = afwImage.MaskedImageF(scienceMaskedImage, True)
598  subtractedMaskedImage -= results.matchedImage
599  subtractedMaskedImage -= results.backgroundModel
600  results.subtractedMaskedImage = subtractedMaskedImage
601 
602  import lsstDebug
603  display = lsstDebug.Info(__name__).display
604  displayDiffIm = lsstDebug.Info(__name__).displayDiffIm
605  maskTransparency = lsstDebug.Info(__name__).maskTransparency
606  if not maskTransparency:
607  maskTransparency = 0
608  if display:
609  ds9.setMaskTransparency(maskTransparency)
610  if display and displayDiffIm:
611  ds9.mtv(subtractedMaskedImage, frame=lsstDebug.frame)
612  lsstDebug.frame += 1
613 
614  return results
615 
616  def getSelectSources(self, exposure, sigma=None, doSmooth=True, idFactory=None):
617  """!Get sources to use for Psf-matching
618 
619  This method runs detection and measurement on an exposure.
620  The returned set of sources will be used as candidates for
621  Psf-matching.
622 
623  @param exposure: Exposure on which to run detection/measurement
624  @param sigma: Detection threshold
625  @param doSmooth: Whether or not to smooth the Exposure with Psf before detection
626  @param idFactory: Factory for the generation of Source ids
627 
628  @return source catalog containing candidates for the Psf-matching
629  """
630 
631  if idFactory:
632  table = afwTable.SourceTable.make(self.selectSchema, idFactory)
633  else:
634  table = afwTable.SourceTable.make(self.selectSchema)
635  mi = exposure.getMaskedImage()
636 
637  imArr = mi.getImage().getArray()
638  maskArr = mi.getMask().getArray()
639  miArr = np.ma.masked_array(imArr, mask=maskArr)
640  try:
641  bkgd = self.background.fitBackground(mi).getImageF()
642  except Exception:
643  self.log.warn("Failed to get background model. Falling back to median background estimation")
644  bkgd = np.ma.extras.median(miArr)
645 
646  #Take off background for detection
647  mi -= bkgd
648  try:
649  table.setMetadata(self.selectAlgMetadata)
650  detRet = self.selectDetection.makeSourceCatalog(
651  table=table,
652  exposure=exposure,
653  sigma=sigma,
654  doSmooth=doSmooth
655  )
656  selectSources = detRet.sources
657  self.selectMeasurement.run(measCat=selectSources, exposure=exposure)
658  finally:
659  # Put back on the background in case it is needed down stream
660  mi += bkgd
661  del bkgd
662  return selectSources
663 
664  def makeCandidateList(self, templateExposure, scienceExposure, kernelSize, candidateList=None):
665  """!Make a list of acceptable KernelCandidates
666 
667  Accept or generate a list of candidate sources for
668  Psf-matching, and examine the Mask planes in both of the
669  images for indications of bad pixels
670 
671  @param templateExposure: Exposure that will be convolved
672  @param scienceExposure: Exposure that will be matched-to
673  @param kernelSize: Dimensions of the Psf-matching Kernel, used to grow detection footprints
674  @param candidateList: List of Sources to examine. Elements must be of type afw.table.Source
675  or a type that wraps a Source and has a getSource() method, such as
676  meas.algorithms.PsfCandidateF.
677 
678  @return a list of dicts having a "source" and "footprint"
679  field for the Sources deemed to be appropriate for Psf
680  matching
681  """
682  if candidateList is None:
683  candidateList = self.getSelectSources(scienceExposure)
684 
685  if len(candidateList) < 1:
686  raise RuntimeError("No candidates in candidateList")
687 
688  listTypes = set(type(x) for x in candidateList)
689  if len(listTypes) > 1:
690  raise RuntimeError("Candidate list contains mixed types: %s" % [l for l in listTypes])
691 
692  if not isinstance(candidateList[0], afwTable.SourceRecord):
693  try:
694  candidateList[0].getSource()
695  except Exception as e:
696  raise RuntimeError("Candidate List is of type: %s. " % (type(candidateList[0])) +
697  "Can only make candidate list from list of afwTable.SourceRecords, " +
698  "measAlg.PsfCandidateF or other type with a getSource() method: %s" % (e))
699  candidateList = [c.getSource() for c in candidateList]
700 
701  candidateList = diffimTools.sourceToFootprintList(candidateList,
702  templateExposure, scienceExposure,
703  kernelSize,
704  self.kConfig.detectionConfig,
705  self.log)
706  if len(candidateList) == 0:
707  raise RuntimeError("Cannot find any objects suitable for KernelCandidacy")
708 
709  return candidateList
710 
711  def _adaptCellSize(self, candidateList):
712  """! NOT IMPLEMENTED YET"""
713  return self.kConfig.sizeCellX, self.kConfig.sizeCellY
714 
715  def _buildCellSet(self, templateMaskedImage, scienceMaskedImage, candidateList):
716  """!Build a SpatialCellSet for use with the solve method
717 
718  @param templateMaskedImage: MaskedImage to PSF-matched to scienceMaskedImage
719  @param scienceMaskedImage: reference MaskedImage
720  @param candidateList: a list of footprints/maskedImages for kernel candidates;
721  if None then source detection is run.
722  - Currently supported: list of Footprints or measAlg.PsfCandidateF
723 
724  @return kernelCellSet: a SpatialCellSet for use with self._solve
725  """
726  if not candidateList:
727  raise RuntimeError("Candidate list must be populated by makeCandidateList")
728 
729  sizeCellX, sizeCellY = self._adaptCellSize(candidateList)
730 
731  # Object to store the KernelCandidates for spatial modeling
732  kernelCellSet = afwMath.SpatialCellSet(templateMaskedImage.getBBox(),
733  sizeCellX, sizeCellY)
734 
735  policy = pexConfig.makePolicy(self.kConfig)
736  # Place candidates within the spatial grid
737  for cand in candidateList:
738  bbox = cand['footprint'].getBBox()
739 
740  tmi = afwImage.MaskedImageF(templateMaskedImage, bbox)
741  smi = afwImage.MaskedImageF(scienceMaskedImage, bbox)
742  cand = diffimLib.makeKernelCandidate(cand['source'], tmi, smi, policy)
743 
744  self.log.debug("Candidate %d at %f, %f", cand.getId(), cand.getXCenter(), cand.getYCenter())
745  kernelCellSet.insertCandidate(cand)
746 
747  return kernelCellSet
748 
749  def _validateSize(self, templateMaskedImage, scienceMaskedImage):
750  """!Return True if two image-like objects are the same size
751  """
752  return templateMaskedImage.getDimensions() == scienceMaskedImage.getDimensions()
753 
754  def _validateWcs(self, templateExposure, scienceExposure):
755  """!Return True if the WCS of the two Exposures have the same origin and extent
756  """
757  templateWcs = templateExposure.getWcs()
758  scienceWcs = scienceExposure.getWcs()
759  templateBBox = templateExposure.getBBox()
760  scienceBBox = scienceExposure.getBBox()
761 
762  # LLC
763  templateOrigin = templateWcs.pixelToSky(afwGeom.Point2D(templateBBox.getBegin()))
764  scienceOrigin = scienceWcs.pixelToSky(afwGeom.Point2D(scienceBBox.getBegin()))
765 
766  # URC
767  templateLimit = templateWcs.pixelToSky(afwGeom.Point2D(templateBBox.getEnd()))
768  scienceLimit = scienceWcs.pixelToSky(afwGeom.Point2D(scienceBBox.getEnd()))
769 
770  self.log.info("Template Wcs : %f,%f -> %f,%f",
771  templateOrigin[0], templateOrigin[1],
772  templateLimit[0], templateLimit[1])
773  self.log.info("Science Wcs : %f,%f -> %f,%f",
774  scienceOrigin[0], scienceOrigin[1],
775  scienceLimit[0], scienceLimit[1])
776 
777  templateBBox = afwGeom.Box2D(templateOrigin.getPosition(), templateLimit.getPosition())
778  scienceBBox = afwGeom.Box2D(scienceOrigin.getPosition(), scienceLimit.getPosition())
779  if not (templateBBox.overlaps(scienceBBox)):
780  raise RuntimeError("Input images do not overlap at all")
781 
782  if ((templateOrigin.getPosition() != scienceOrigin.getPosition()) or
783  (templateLimit.getPosition() != scienceLimit.getPosition()) or
784  (templateExposure.getDimensions() != scienceExposure.getDimensions())):
785  return False
786  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
def subtractMaskedImages
Psf-match and subtract two MaskedImages.
def makeCandidateList
Make a list of acceptable KernelCandidates.
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
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:304
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 >())
A function to return an Exposure of the correct type (cf.
Definition: Exposure.h:314
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.