lsst.ip.diffim g88964a4962+ab87fff37c
Loading...
Searching...
No Matches
subtractImages.py
Go to the documentation of this file.
1# This file is part of ip_diffim.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22import warnings
23
24import numpy as np
25
26import lsst.afw.image
27import lsst.afw.math
28import lsst.geom
29from lsst.utils.introspection import find_outside_stacklevel
30from lsst.ip.diffim.utils import evaluateMeanPsfFwhm, getPsfFwhm
31from lsst.meas.algorithms import ScaleVarianceTask
32import lsst.pex.config
33import lsst.pipe.base
35from lsst.pipe.base import connectionTypes
36from . import MakeKernelTask, DecorrelateALKernelTask
37from lsst.utils.timer import timeMethod
38
39__all__ = ["AlardLuptonSubtractConfig", "AlardLuptonSubtractTask",
40 "AlardLuptonPreconvolveSubtractConfig", "AlardLuptonPreconvolveSubtractTask"]
41
42_dimensions = ("instrument", "visit", "detector")
43_defaultTemplates = {"coaddName": "deep", "fakesType": ""}
44
45
46class SubtractInputConnections(lsst.pipe.base.PipelineTaskConnections,
47 dimensions=_dimensions,
48 defaultTemplates=_defaultTemplates):
49 template = connectionTypes.Input(
50 doc="Input warped template to subtract.",
51 dimensions=("instrument", "visit", "detector"),
52 storageClass="ExposureF",
53 name="{fakesType}{coaddName}Diff_templateExp"
54 )
55 science = connectionTypes.Input(
56 doc="Input science exposure to subtract from.",
57 dimensions=("instrument", "visit", "detector"),
58 storageClass="ExposureF",
59 name="{fakesType}calexp"
60 )
61 sources = connectionTypes.Input(
62 doc="Sources measured on the science exposure; "
63 "used to select sources for making the matching kernel.",
64 dimensions=("instrument", "visit", "detector"),
65 storageClass="SourceCatalog",
66 name="{fakesType}src"
67 )
68 finalizedPsfApCorrCatalog = connectionTypes.Input(
69 doc=("Per-visit finalized psf models and aperture correction maps. "
70 "These catalogs use the detector id for the catalog id, "
71 "sorted on id for fast lookup."),
72 dimensions=("instrument", "visit"),
73 storageClass="ExposureCatalog",
74 name="finalVisitSummary",
75 # TODO: remove on DM-39854.
76 deprecated=(
77 "Deprecated in favor of visitSummary. Will be removed after v26."
78 )
79 )
80 visitSummary = connectionTypes.Input(
81 doc=("Per-visit catalog with final calibration objects. "
82 "These catalogs use the detector id for the catalog id, "
83 "sorted on id for fast lookup."),
84 dimensions=("instrument", "visit"),
85 storageClass="ExposureCatalog",
86 name="finalVisitSummary",
87 )
88
89 def __init__(self, *, config=None):
90 super().__init__(config=config)
91 if not config.doApplyFinalizedPsf:
92 self.inputs.remove("finalizedPsfApCorrCatalog")
93 if not config.doApplyExternalCalibrations or config.doApplyFinalizedPsf:
94 del self.visitSummary
95
96
97class SubtractImageOutputConnections(lsst.pipe.base.PipelineTaskConnections,
98 dimensions=_dimensions,
99 defaultTemplates=_defaultTemplates):
100 difference = connectionTypes.Output(
101 doc="Result of subtracting convolved template from science image.",
102 dimensions=("instrument", "visit", "detector"),
103 storageClass="ExposureF",
104 name="{fakesType}{coaddName}Diff_differenceTempExp",
105 )
106 matchedTemplate = connectionTypes.Output(
107 doc="Warped and PSF-matched template used to create `subtractedExposure`.",
108 dimensions=("instrument", "visit", "detector"),
109 storageClass="ExposureF",
110 name="{fakesType}{coaddName}Diff_matchedExp",
111 )
112
113
114class SubtractScoreOutputConnections(lsst.pipe.base.PipelineTaskConnections,
115 dimensions=_dimensions,
116 defaultTemplates=_defaultTemplates):
117 scoreExposure = connectionTypes.Output(
118 doc="The maximum likelihood image, used for the detection of diaSources.",
119 dimensions=("instrument", "visit", "detector"),
120 storageClass="ExposureF",
121 name="{fakesType}{coaddName}Diff_scoreExp",
122 )
123
124
129class AlardLuptonSubtractBaseConfig(lsst.pex.config.Config):
130 makeKernel = lsst.pex.config.ConfigurableField(
131 target=MakeKernelTask,
132 doc="Task to construct a matching kernel for convolution.",
133 )
134 doDecorrelation = lsst.pex.config.Field(
135 dtype=bool,
136 default=True,
137 doc="Perform diffim decorrelation to undo pixel correlation due to A&L "
138 "kernel convolution? If True, also update the diffim PSF."
139 )
140 decorrelate = lsst.pex.config.ConfigurableField(
141 target=DecorrelateALKernelTask,
142 doc="Task to decorrelate the image difference.",
143 )
144 requiredTemplateFraction = lsst.pex.config.Field(
145 dtype=float,
146 default=0.1,
147 doc="Raise NoWorkFound and do not attempt image subtraction if template covers less than this "
148 " fraction of pixels. Setting to 0 will always attempt image subtraction."
149 )
150 minTemplateFractionForExpectedSuccess = lsst.pex.config.Field(
151 dtype=float,
152 default=0.2,
153 doc="Raise NoWorkFound if PSF-matching fails and template covers less than this fraction of pixels."
154 " If the fraction of pixels covered by the template is less than this value (and greater than"
155 " requiredTemplateFraction) this task is attempted but failure is anticipated and tolerated."
156 )
157 doScaleVariance = lsst.pex.config.Field(
158 dtype=bool,
159 default=True,
160 doc="Scale variance of the image difference?"
161 )
162 scaleVariance = lsst.pex.config.ConfigurableField(
163 target=ScaleVarianceTask,
164 doc="Subtask to rescale the variance of the template to the statistically expected level."
165 )
166 doSubtractBackground = lsst.pex.config.Field(
167 doc="Subtract the background fit when solving the kernel?",
168 dtype=bool,
169 default=True,
170 )
171 doApplyFinalizedPsf = lsst.pex.config.Field(
172 doc="Replace science Exposure's psf and aperture correction map"
173 " with those in finalizedPsfApCorrCatalog.",
174 dtype=bool,
175 default=False,
176 # TODO: remove on DM-39854.
177 deprecated=(
178 "Deprecated in favor of doApplyExternalCalibrations. "
179 "Will be removed after v26."
180 )
181 )
182 doApplyExternalCalibrations = lsst.pex.config.Field(
183 doc=(
184 "Replace science Exposure's calibration objects with those"
185 " in visitSummary. Ignored if `doApplyFinalizedPsf is True."
186 ),
187 dtype=bool,
188 default=False,
189 )
190 detectionThreshold = lsst.pex.config.Field(
191 dtype=float,
192 default=10,
193 doc="Minimum signal to noise ratio of detected sources "
194 "to use for calculating the PSF matching kernel."
195 )
196 detectionThresholdMax = lsst.pex.config.Field(
197 dtype=float,
198 default=500,
199 doc="Maximum signal to noise ratio of detected sources "
200 "to use for calculating the PSF matching kernel."
201 )
202 maxKernelSources = lsst.pex.config.Field(
203 dtype=int,
204 default=1000,
205 doc="Maximum number of sources to use for calculating the PSF matching kernel."
206 "Set to -1 to disable."
207 )
208 minKernelSources = lsst.pex.config.Field(
209 dtype=int,
210 default=3,
211 doc="Minimum number of sources needed for calculating the PSF matching kernel."
212 )
213 badSourceFlags = lsst.pex.config.ListField(
214 dtype=str,
215 doc="Flags that, if set, the associated source should not "
216 "be used to determine the PSF matching kernel.",
217 default=("sky_source", "slot_Centroid_flag",
218 "slot_ApFlux_flag", "slot_PsfFlux_flag",
219 "base_PixelFlags_flag_interpolated",
220 "base_PixelFlags_flag_saturated",
221 "base_PixelFlags_flag_bad",
222 ),
223 )
224 excludeMaskPlanes = lsst.pex.config.ListField(
225 dtype=str,
226 default=("NO_DATA", "BAD", "SAT", "EDGE", "FAKE"),
227 doc="Mask planes to exclude when selecting sources for PSF matching."
228 )
229 badMaskPlanes = lsst.pex.config.ListField(
230 dtype=str,
231 default=("NO_DATA", "BAD", "SAT", "EDGE"),
232 doc="Mask planes to interpolate over."
233 )
234 preserveTemplateMask = lsst.pex.config.ListField(
235 dtype=str,
236 default=("NO_DATA", "BAD", "SAT", "FAKE", "INJECTED", "INJECTED_CORE"),
237 doc="Mask planes from the template to propagate to the image difference."
238 )
239 allowKernelSourceDetection = lsst.pex.config.Field(
240 dtype=bool,
241 default=False,
242 doc="Re-run source detection for kernel candidates if an error is"
243 " encountered while calculating the matching kernel."
244 )
245
246 def setDefaults(self):
247 self.makeKernel.kernel.name = "AL"
248 self.makeKernel.kernel.active.fitForBackground = self.doSubtractBackground
249 self.makeKernel.kernel.active.spatialKernelOrder = 1
250 self.makeKernel.kernel.active.spatialBgOrder = 2
251
252
253class AlardLuptonSubtractConfig(AlardLuptonSubtractBaseConfig, lsst.pipe.base.PipelineTaskConfig,
254 pipelineConnections=AlardLuptonSubtractConnections):
255 mode = lsst.pex.config.ChoiceField(
256 dtype=str,
257 default="convolveTemplate",
258 allowed={"auto": "Choose which image to convolve at runtime.",
259 "convolveScience": "Only convolve the science image.",
260 "convolveTemplate": "Only convolve the template image."},
261 doc="Choose which image to convolve at runtime, or require that a specific image is convolved."
262 )
263
264
265class AlardLuptonSubtractTask(lsst.pipe.base.PipelineTask):
266 """Compute the image difference of a science and template image using
267 the Alard & Lupton (1998) algorithm.
268 """
269 ConfigClass = AlardLuptonSubtractConfig
270 _DefaultName = "alardLuptonSubtract"
271
272 def __init__(self, **kwargs):
273 super().__init__(**kwargs)
274 self.makeSubtask("decorrelate")
275 self.makeSubtask("makeKernel")
276 if self.config.doScaleVariance:
277 self.makeSubtask("scaleVariance")
278
280 # Normalization is an extra, unnecessary, calculation and will result
281 # in mis-subtraction of the images if there are calibration errors.
282 self.convolutionControl.setDoNormalize(False)
283 self.convolutionControl.setDoCopyEdge(True)
284
285 def _applyExternalCalibrations(self, exposure, visitSummary):
286 """Replace calibrations (psf, and ApCorrMap) on this exposure with
287 external ones.".
288
289 Parameters
290 ----------
291 exposure : `lsst.afw.image.exposure.Exposure`
292 Input exposure to adjust calibrations.
293 visitSummary : `lsst.afw.table.ExposureCatalog`
294 Exposure catalog with external calibrations to be applied. Catalog
295 uses the detector id for the catalog id, sorted on id for fast
296 lookup.
297
298 Returns
299 -------
300 exposure : `lsst.afw.image.exposure.Exposure`
301 Exposure with adjusted calibrations.
302 """
303 detectorId = exposure.info.getDetector().getId()
304
305 row = visitSummary.find(detectorId)
306 if row is None:
307 self.log.warning("Detector id %s not found in external calibrations catalog; "
308 "Using original calibrations.", detectorId)
309 else:
310 psf = row.getPsf()
311 apCorrMap = row.getApCorrMap()
312 if psf is None:
313 self.log.warning("Detector id %s has None for psf in "
314 "external calibrations catalog; Using original psf and aperture correction.",
315 detectorId)
316 elif apCorrMap is None:
317 self.log.warning("Detector id %s has None for apCorrMap in "
318 "external calibrations catalog; Using original psf and aperture correction.",
319 detectorId)
320 else:
321 exposure.setPsf(psf)
322 exposure.info.setApCorrMap(apCorrMap)
323
324 return exposure
325
326 @timeMethod
327 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None,
328 visitSummary=None):
329 """PSF match, subtract, and decorrelate two images.
330
331 Parameters
332 ----------
333 template : `lsst.afw.image.ExposureF`
334 Template exposure, warped to match the science exposure.
335 science : `lsst.afw.image.ExposureF`
336 Science exposure to subtract from the template.
337 sources : `lsst.afw.table.SourceCatalog`
338 Identified sources on the science exposure. This catalog is used to
339 select sources in order to perform the AL PSF matching on stamp
340 images around them.
341 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
342 Exposure catalog with finalized psf models and aperture correction
343 maps to be applied. Catalog uses the detector id for the catalog
344 id, sorted on id for fast lookup. Deprecated in favor of
345 ``visitSummary``, and will be removed after v26.
346 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
347 Exposure catalog with external calibrations to be applied. Catalog
348 uses the detector id for the catalog id, sorted on id for fast
349 lookup. Ignored (for temporary backwards compatibility) if
350 ``finalizedPsfApCorrCatalog`` is provided.
351
352 Returns
353 -------
354 results : `lsst.pipe.base.Struct`
355 ``difference`` : `lsst.afw.image.ExposureF`
356 Result of subtracting template and science.
357 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
358 Warped and PSF-matched template exposure.
359 ``backgroundModel`` : `lsst.afw.math.Function2D`
360 Background model that was fit while solving for the
361 PSF-matching kernel
362 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
363 Kernel used to PSF-match the convolved image.
364
365 Raises
366 ------
367 RuntimeError
368 If an unsupported convolution mode is supplied.
369 RuntimeError
370 If there are too few sources to calculate the PSF matching kernel.
371 lsst.pipe.base.NoWorkFound
372 Raised if fraction of good pixels, defined as not having NO_DATA
373 set, is less then the configured requiredTemplateFraction
374 """
375
376 if finalizedPsfApCorrCatalog is not None:
377 warnings.warn(
378 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
379 "argument, and will be removed after v26.",
380 FutureWarning,
381 stacklevel=find_outside_stacklevel("lsst.ip.diffim"),
382 )
383 visitSummary = finalizedPsfApCorrCatalog
384
385 self._prepareInputs(template, science, visitSummary=visitSummary)
386
387 # In the event that getPsfFwhm fails, evaluate the PSF on a grid.
388 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
389 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
390
391 # Calling getPsfFwhm on template.psf fails on some rare occasions when
392 # the template has no input exposures at the average position of the
393 # stars. So we try getPsfFwhm first on template, and if that fails we
394 # evaluate the PSF on a grid specified by fwhmExposure* fields.
395 # To keep consistent definitions for PSF size on the template and
396 # science images, we use the same method for both.
397 try:
398 templatePsfSize = getPsfFwhm(template.psf)
399 sciencePsfSize = getPsfFwhm(science.psf)
401 self.log.info("Unable to evaluate PSF at the average position. "
402 "Evaluting PSF on a grid of points."
403 )
404 templatePsfSize = evaluateMeanPsfFwhm(template,
405 fwhmExposureBuffer=fwhmExposureBuffer,
406 fwhmExposureGrid=fwhmExposureGrid
407 )
408 sciencePsfSize = evaluateMeanPsfFwhm(science,
409 fwhmExposureBuffer=fwhmExposureBuffer,
410 fwhmExposureGrid=fwhmExposureGrid
411 )
412 self.log.info("Science PSF FWHM: %f pixels", sciencePsfSize)
413 self.log.info("Template PSF FWHM: %f pixels", templatePsfSize)
414 self.metadata.add("sciencePsfSize", sciencePsfSize)
415 self.metadata.add("templatePsfSize", templatePsfSize)
416
417 if self.config.mode == "auto":
418 convolveTemplate = _shapeTest(template,
419 science,
420 fwhmExposureBuffer=fwhmExposureBuffer,
421 fwhmExposureGrid=fwhmExposureGrid)
422 if convolveTemplate:
423 if sciencePsfSize < templatePsfSize:
424 self.log.info("Average template PSF size is greater, "
425 "but science PSF greater in one dimension: convolving template image.")
426 else:
427 self.log.info("Science PSF size is greater: convolving template image.")
428 else:
429 self.log.info("Template PSF size is greater: convolving science image.")
430 elif self.config.mode == "convolveTemplate":
431 self.log.info("`convolveTemplate` is set: convolving template image.")
432 convolveTemplate = True
433 elif self.config.mode == "convolveScience":
434 self.log.info("`convolveScience` is set: convolving science image.")
435 convolveTemplate = False
436 else:
437 raise RuntimeError("Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
438
439 try:
440 sourceMask = science.mask.clone()
441 sourceMask.array |= template[science.getBBox()].mask.array
442 selectSources = self._sourceSelector(sources, sourceMask)
443 if convolveTemplate:
444 self.metadata.add("convolvedExposure", "Template")
445 subtractResults = self.runConvolveTemplate(template, science, selectSources)
446 else:
447 self.metadata.add("convolvedExposure", "Science")
448 subtractResults = self.runConvolveScience(template, science, selectSources)
449
450 except (RuntimeError, lsst.pex.exceptions.Exception) as e:
451 self.log.warning("Failed to match template. Checking coverage")
452 # Raise NoWorkFound if template fraction is insufficient
453 checkTemplateIsSufficient(template[science.getBBox()], self.log,
454 self.config.minTemplateFractionForExpectedSuccess,
455 exceptionMessage="Template coverage lower than expected to succeed."
456 f" Failure is tolerable: {e}")
457 # checkTemplateIsSufficient did not raise NoWorkFound, so raise original exception
458 raise e
459
460 return subtractResults
461
462 def runConvolveTemplate(self, template, science, selectSources):
463 """Convolve the template image with a PSF-matching kernel and subtract
464 from the science image.
465
466 Parameters
467 ----------
468 template : `lsst.afw.image.ExposureF`
469 Template exposure, warped to match the science exposure.
470 science : `lsst.afw.image.ExposureF`
471 Science exposure to subtract from the template.
472 selectSources : `lsst.afw.table.SourceCatalog`
473 Identified sources on the science exposure. This catalog is used to
474 select sources in order to perform the AL PSF matching on stamp
475 images around them.
476
477 Returns
478 -------
479 results : `lsst.pipe.base.Struct`
480
481 ``difference`` : `lsst.afw.image.ExposureF`
482 Result of subtracting template and science.
483 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
484 Warped and PSF-matched template exposure.
485 ``backgroundModel`` : `lsst.afw.math.Function2D`
486 Background model that was fit while solving for the PSF-matching kernel
487 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
488 Kernel used to PSF-match the template to the science image.
489 """
490 try:
491 kernelSources = self.makeKernel.selectKernelSources(template, science,
492 candidateList=selectSources,
493 preconvolved=False)
494 kernelResult = self.makeKernel.run(template, science, kernelSources,
495 preconvolved=False)
496 except Exception as e:
497 if self.config.allowKernelSourceDetection:
498 self.log.warning("Error encountered trying to construct the matching kernel"
499 f" Running source detection and retrying. {e}")
500 kernelSources = self.makeKernel.selectKernelSources(template, science,
501 candidateList=None,
502 preconvolved=False)
503 kernelResult = self.makeKernel.run(template, science, kernelSources,
504 preconvolved=False)
505 else:
506 raise e
507
508 matchedTemplate = self._convolveExposure(template, kernelResult.psfMatchingKernel,
510 bbox=science.getBBox(),
511 psf=science.psf,
512 photoCalib=science.photoCalib)
513
514 difference = _subtractImages(science, matchedTemplate,
515 backgroundModel=(kernelResult.backgroundModel
516 if self.config.doSubtractBackground else None))
517 correctedExposure = self.finalize(template, science, difference,
518 kernelResult.psfMatchingKernel,
519 templateMatched=True)
520
521 return lsst.pipe.base.Struct(difference=correctedExposure,
522 matchedTemplate=matchedTemplate,
523 matchedScience=science,
524 backgroundModel=kernelResult.backgroundModel,
525 psfMatchingKernel=kernelResult.psfMatchingKernel)
526
527 def runConvolveScience(self, template, science, selectSources):
528 """Convolve the science image with a PSF-matching kernel and subtract
529 the template image.
530
531 Parameters
532 ----------
533 template : `lsst.afw.image.ExposureF`
534 Template exposure, warped to match the science exposure.
535 science : `lsst.afw.image.ExposureF`
536 Science exposure to subtract from the template.
537 selectSources : `lsst.afw.table.SourceCatalog`
538 Identified sources on the science exposure. This catalog is used to
539 select sources in order to perform the AL PSF matching on stamp
540 images around them.
541
542 Returns
543 -------
544 results : `lsst.pipe.base.Struct`
545
546 ``difference`` : `lsst.afw.image.ExposureF`
547 Result of subtracting template and science.
548 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
549 Warped template exposure. Note that in this case, the template
550 is not PSF-matched to the science image.
551 ``backgroundModel`` : `lsst.afw.math.Function2D`
552 Background model that was fit while solving for the PSF-matching kernel
553 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
554 Kernel used to PSF-match the science image to the template.
555 """
556 bbox = science.getBBox()
557 kernelSources = self.makeKernel.selectKernelSources(science, template,
558 candidateList=selectSources,
559 preconvolved=False)
560 kernelResult = self.makeKernel.run(science, template, kernelSources,
561 preconvolved=False)
562 modelParams = kernelResult.backgroundModel.getParameters()
563 # We must invert the background model if the matching kernel is solved for the science image.
564 kernelResult.backgroundModel.setParameters([-p for p in modelParams])
565
566 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
567 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=False)
568
569 matchedScience = self._convolveExposure(science, kernelResult.psfMatchingKernel,
571 psf=template.psf)
572
573 # Place back on native photometric scale
574 matchedScience.maskedImage /= norm
575 matchedTemplate = template.clone()[bbox]
576 matchedTemplate.maskedImage /= norm
577 matchedTemplate.setPhotoCalib(science.photoCalib)
578
579 difference = _subtractImages(matchedScience, matchedTemplate,
580 backgroundModel=(kernelResult.backgroundModel
581 if self.config.doSubtractBackground else None))
582
583 correctedExposure = self.finalize(template, science, difference,
584 kernelResult.psfMatchingKernel,
585 templateMatched=False)
586
587 return lsst.pipe.base.Struct(difference=correctedExposure,
588 matchedTemplate=matchedTemplate,
589 matchedScience=matchedScience,
590 backgroundModel=kernelResult.backgroundModel,
591 psfMatchingKernel=kernelResult.psfMatchingKernel,)
592
593 def finalize(self, template, science, difference, kernel,
594 templateMatched=True,
595 preConvMode=False,
596 preConvKernel=None,
597 spatiallyVarying=False):
598 """Decorrelate the difference image to undo the noise correlations
599 caused by convolution.
600
601 Parameters
602 ----------
603 template : `lsst.afw.image.ExposureF`
604 Template exposure, warped to match the science exposure.
605 science : `lsst.afw.image.ExposureF`
606 Science exposure to subtract from the template.
607 difference : `lsst.afw.image.ExposureF`
608 Result of subtracting template and science.
609 kernel : `lsst.afw.math.Kernel`
610 An (optionally spatially-varying) PSF matching kernel
611 templateMatched : `bool`, optional
612 Was the template PSF-matched to the science image?
613 preConvMode : `bool`, optional
614 Was the science image preconvolved with its own PSF
615 before PSF matching the template?
616 preConvKernel : `lsst.afw.detection.Psf`, optional
617 If not `None`, then the science image was pre-convolved with
618 (the reflection of) this kernel. Must be normalized to sum to 1.
619 spatiallyVarying : `bool`, optional
620 Compute the decorrelation kernel spatially varying across the image?
621
622 Returns
623 -------
624 correctedExposure : `lsst.afw.image.ExposureF`
625 The decorrelated image difference.
626 """
627 # Erase existing detection mask planes.
628 # We don't want the detection mask from the science image
629
630 self.updateMasks(template, science, difference)
631
632 if self.config.doDecorrelation:
633 self.log.info("Decorrelating image difference.")
634 # We have cleared the template mask plane, so copy the mask plane of
635 # the image difference so that we can calculate correct statistics
636 # during decorrelation
637 correctedExposure = self.decorrelate.run(science, template[science.getBBox()], difference, kernel,
638 templateMatched=templateMatched,
639 preConvMode=preConvMode,
640 preConvKernel=preConvKernel,
641 spatiallyVarying=spatiallyVarying).correctedExposure
642 else:
643 self.log.info("NOT decorrelating image difference.")
644 correctedExposure = difference
645 return correctedExposure
646
647 def updateMasks(self, template, science, difference):
648 """Update the mask planes on images for finalizing."""
649
650 bbox = science.getBBox()
651 mask = difference.mask
652 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
653
654 self.log.info("Adding injected mask planes")
655 mask.addMaskPlane("INJECTED")
656 mask.addMaskPlane("INJECTED_TEMPLATE")
657
658 if "FAKE" in science.mask.getMaskPlaneDict().keys():
659 # propagate the mask plane related to Fake source injection
660 # NOTE: the fake source injection sets FAKE plane, but it should be INJECTED
661 # NOTE: This can be removed in DM-40796
662 diffInjectedBitMask = mask.getPlaneBitMask("INJECTED")
663 diffInjTmpltBitMask = mask.getPlaneBitMask("INJECTED_TEMPLATE")
664
665 scienceFakeBitMask = science.mask.getPlaneBitMask('FAKE')
666 tmpltFakeBitMask = template[bbox].mask.getPlaneBitMask('FAKE')
667
668 injScienceMaskArray = ((science.mask.array & scienceFakeBitMask) > 0) * diffInjectedBitMask
669 injTemplateMaskArray = ((template[bbox].mask.array & tmpltFakeBitMask) > 0) * diffInjTmpltBitMask
670
671 mask.array |= injScienceMaskArray
672 mask.array |= injTemplateMaskArray
673
674 template[bbox].mask.array[...] = difference.mask.array[...]
675
676 @staticmethod
677 def _validateExposures(template, science):
678 """Check that the WCS of the two Exposures match, and the template bbox
679 contains the science bbox.
680
681 Parameters
682 ----------
683 template : `lsst.afw.image.ExposureF`
684 Template exposure, warped to match the science exposure.
685 science : `lsst.afw.image.ExposureF`
686 Science exposure to subtract from the template.
687
688 Raises
689 ------
690 AssertionError
691 Raised if the WCS of the template is not equal to the science WCS,
692 or if the science image is not fully contained in the template
693 bounding box.
694 """
695 assert template.wcs == science.wcs,\
696 "Template and science exposure WCS are not identical."
697 templateBBox = template.getBBox()
698 scienceBBox = science.getBBox()
699
700 assert templateBBox.contains(scienceBBox),\
701 "Template bbox does not contain all of the science image."
702
703 def _convolveExposure(self, exposure, kernel, convolutionControl,
704 bbox=None,
705 psf=None,
706 photoCalib=None,
707 interpolateBadMaskPlanes=False,
708 ):
709 """Convolve an exposure with the given kernel.
710
711 Parameters
712 ----------
713 exposure : `lsst.afw.Exposure`
714 exposure to convolve.
715 kernel : `lsst.afw.math.LinearCombinationKernel`
716 PSF matching kernel computed in the ``makeKernel`` subtask.
717 convolutionControl : `lsst.afw.math.ConvolutionControl`
718 Configuration for convolve algorithm.
719 bbox : `lsst.geom.Box2I`, optional
720 Bounding box to trim the convolved exposure to.
721 psf : `lsst.afw.detection.Psf`, optional
722 Point spread function (PSF) to set for the convolved exposure.
723 photoCalib : `lsst.afw.image.PhotoCalib`, optional
724 Photometric calibration of the convolved exposure.
725
726 Returns
727 -------
728 convolvedExp : `lsst.afw.Exposure`
729 The convolved image.
730 """
731 convolvedExposure = exposure.clone()
732 if psf is not None:
733 convolvedExposure.setPsf(psf)
734 if photoCalib is not None:
735 convolvedExposure.setPhotoCalib(photoCalib)
736 if interpolateBadMaskPlanes and self.config.badMaskPlanes is not None:
737 nInterp = _interpolateImage(convolvedExposure.maskedImage,
738 self.config.badMaskPlanes)
739 self.metadata.add("nInterpolated", nInterp)
740 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
741 lsst.afw.math.convolve(convolvedImage, convolvedExposure.maskedImage, kernel, convolutionControl)
742 convolvedExposure.setMaskedImage(convolvedImage)
743 if bbox is None:
744 return convolvedExposure
745 else:
746 return convolvedExposure[bbox]
747
748 def _sourceSelector(self, sources, mask):
749 """Select sources from a catalog that meet the selection criteria.
750
751 Parameters
752 ----------
753 sources : `lsst.afw.table.SourceCatalog`
754 Input source catalog to select sources from.
755 mask : `lsst.afw.image.Mask`
756 The image mask plane to use to reject sources
757 based on their location on the ccd.
758
759 Returns
760 -------
761 selectSources : `lsst.afw.table.SourceCatalog`
762 The input source catalog, with flagged and low signal-to-noise
763 sources removed.
764
765 Raises
766 ------
767 RuntimeError
768 If there are too few sources to compute the PSF matching kernel
769 remaining after source selection.
770 """
771 flags = np.ones(len(sources), dtype=bool)
772 for flag in self.config.badSourceFlags:
773 try:
774 flags *= ~sources[flag]
775 except Exception as e:
776 self.log.warning("Could not apply source flag: %s", e)
777 signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr()
778 sToNFlag = signalToNoise > self.config.detectionThreshold
779 flags *= sToNFlag
780 sToNFlagMax = signalToNoise < self.config.detectionThresholdMax
781 flags *= sToNFlagMax
782 flags *= self._checkMask(mask, sources, self.config.excludeMaskPlanes)
783 selectSources = sources[flags].copy(deep=True)
784 if (len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
785 signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
786 indices = np.argsort(signalToNoise)
787 indices = indices[-self.config.maxKernelSources:]
788 flags = np.zeros(len(selectSources), dtype=bool)
789 flags[indices] = True
790 selectSources = selectSources[flags].copy(deep=True)
791
792 self.log.info("%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
793 len(selectSources), len(sources), 100*len(selectSources)/len(sources))
794 if len(selectSources) < self.config.minKernelSources:
795 self.log.error("Too few sources to calculate the PSF matching kernel: "
796 "%i selected but %i needed for the calculation.",
797 len(selectSources), self.config.minKernelSources)
798 raise RuntimeError("Cannot compute PSF matching kernel: too few sources selected.")
799 self.metadata.add("nPsfSources", len(selectSources))
800
801 return selectSources
802
803 @staticmethod
804 def _checkMask(mask, sources, excludeMaskPlanes):
805 """Exclude sources that are located on masked pixels.
806
807 Parameters
808 ----------
809 mask : `lsst.afw.image.Mask`
810 The image mask plane to use to reject sources
811 based on the location of their centroid on the ccd.
812 sources : `lsst.afw.table.SourceCatalog`
813 The source catalog to evaluate.
814 excludeMaskPlanes : `list` of `str`
815 List of the names of the mask planes to exclude.
816
817 Returns
818 -------
819 flags : `numpy.ndarray` of `bool`
820 Array indicating whether each source in the catalog should be
821 kept (True) or rejected (False) based on the value of the
822 mask plane at its location.
823 """
824 setExcludeMaskPlanes = [
825 maskPlane for maskPlane in excludeMaskPlanes if maskPlane in mask.getMaskPlaneDict()
826 ]
827
828 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
829
830 xv = np.rint(sources.getX() - mask.getX0())
831 yv = np.rint(sources.getY() - mask.getY0())
832
833 mv = mask.array[yv.astype(int), xv.astype(int)]
834 flags = np.bitwise_and(mv, excludePixelMask) == 0
835 return flags
836
837 def _prepareInputs(self, template, science, visitSummary=None):
838 """Perform preparatory calculations common to all Alard&Lupton Tasks.
839
840 Parameters
841 ----------
842 template : `lsst.afw.image.ExposureF`
843 Template exposure, warped to match the science exposure. The
844 variance plane of the template image is modified in place.
845 science : `lsst.afw.image.ExposureF`
846 Science exposure to subtract from the template. The variance plane
847 of the science image is modified in place.
848 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
849 Exposure catalog with external calibrations to be applied. Catalog
850 uses the detector id for the catalog id, sorted on id for fast
851 lookup.
852 """
853 self._validateExposures(template, science)
854 if visitSummary is not None:
855 self._applyExternalCalibrations(science, visitSummary=visitSummary)
856 checkTemplateIsSufficient(template[science.getBBox()], self.log,
857 requiredTemplateFraction=self.config.requiredTemplateFraction,
858 exceptionMessage="Not attempting subtraction. To force subtraction,"
859 " set config requiredTemplateFraction=0")
860
861 if self.config.doScaleVariance:
862 # Scale the variance of the template and science images before
863 # convolution, subtraction, or decorrelation so that they have the
864 # correct ratio.
865 templateVarFactor = self.scaleVariance.run(template.maskedImage)
866 sciVarFactor = self.scaleVariance.run(science.maskedImage)
867 self.log.info("Template variance scaling factor: %.2f", templateVarFactor)
868 self.metadata.add("scaleTemplateVarianceFactor", templateVarFactor)
869 self.log.info("Science variance scaling factor: %.2f", sciVarFactor)
870 self.metadata.add("scaleScienceVarianceFactor", sciVarFactor)
871 self._clearMask(template)
872
873 def _clearMask(self, template):
874 """Clear the mask plane of the template.
875
876 Parameters
877 ----------
878 template : `lsst.afw.image.ExposureF`
879 Template exposure, warped to match the science exposure.
880 The mask plane will be modified in place.
881 """
882 mask = template.mask
883 clearMaskPlanes = [maskplane for maskplane in mask.getMaskPlaneDict().keys()
884 if maskplane not in self.config.preserveTemplateMask]
885
886 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
887 mask &= ~bitMaskToClear
888
889
891 SubtractScoreOutputConnections):
892 pass
893
894
896 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
897 pass
898
899
901 """Subtract a template from a science image, convolving the science image
902 before computing the kernel, and also convolving the template before
903 subtraction.
904 """
905 ConfigClass = AlardLuptonPreconvolveSubtractConfig
906 _DefaultName = "alardLuptonPreconvolveSubtract"
907
908 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None):
909 """Preconvolve the science image with its own PSF,
910 convolve the template image with a PSF-matching kernel and subtract
911 from the preconvolved science image.
912
913 Parameters
914 ----------
915 template : `lsst.afw.image.ExposureF`
916 The template image, which has previously been warped to the science
917 image. The template bbox will be padded by a few pixels compared to
918 the science bbox.
919 science : `lsst.afw.image.ExposureF`
920 The science exposure.
921 sources : `lsst.afw.table.SourceCatalog`
922 Identified sources on the science exposure. This catalog is used to
923 select sources in order to perform the AL PSF matching on stamp
924 images around them.
925 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
926 Exposure catalog with finalized psf models and aperture correction
927 maps to be applied. Catalog uses the detector id for the catalog
928 id, sorted on id for fast lookup. Deprecated in favor of
929 ``visitSummary``, and will be removed after v26.
930 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
931 Exposure catalog with complete external calibrations. Catalog uses
932 the detector id for the catalog id, sorted on id for fast lookup.
933 Ignored (for temporary backwards compatibility) if
934 ``finalizedPsfApCorrCatalog`` is provided.
935
936 Returns
937 -------
938 results : `lsst.pipe.base.Struct`
939 ``scoreExposure`` : `lsst.afw.image.ExposureF`
940 Result of subtracting the convolved template and science
941 images. Attached PSF is that of the original science image.
942 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
943 Warped and PSF-matched template exposure. Attached PSF is that
944 of the original science image.
945 ``matchedScience`` : `lsst.afw.image.ExposureF`
946 The science exposure after convolving with its own PSF.
947 Attached PSF is that of the original science image.
948 ``backgroundModel`` : `lsst.afw.math.Function2D`
949 Background model that was fit while solving for the
950 PSF-matching kernel
951 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
952 Final kernel used to PSF-match the template to the science
953 image.
954 """
955 if finalizedPsfApCorrCatalog is not None:
956 warnings.warn(
957 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
958 "argument, and will be removed after v26.",
959 FutureWarning,
960 stacklevel=find_outside_stacklevel("lsst.ip.diffim"),
961 )
962 visitSummary = finalizedPsfApCorrCatalog
963
964 self._prepareInputs(template, science, visitSummary=visitSummary)
965
966 # TODO: DM-37212 we need to mirror the kernel in order to get correct cross correlation
967 scienceKernel = science.psf.getKernel()
968 matchedScience = self._convolveExposure(science, scienceKernel, self.convolutionControlconvolutionControl,
969 interpolateBadMaskPlanes=True)
970 self.metadata.add("convolvedExposure", "Preconvolution")
971 try:
972 selectSources = self._sourceSelector(sources, matchedScience.mask)
973 subtractResults = self.runPreconvolve(template, science, matchedScience,
974 selectSources, scienceKernel)
975
976 except (RuntimeError, lsst.pex.exceptions.Exception) as e:
977 self.loglog.warning("Failed to match template. Checking coverage")
978 # Raise NoWorkFound if template fraction is insufficient
979 checkTemplateIsSufficient(template[science.getBBox()], self.loglog,
980 self.config.minTemplateFractionForExpectedSuccess,
981 exceptionMessage="Template coverage lower than expected to succeed."
982 f" Failure is tolerable: {e}")
983 # checkTemplateIsSufficient did not raise NoWorkFound, so raise original exception
984 raise e
985
986 return subtractResults
987
988 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
989 """Convolve the science image with its own PSF, then convolve the
990 template with a matching kernel and subtract to form the Score
991 exposure.
992
993 Parameters
994 ----------
995 template : `lsst.afw.image.ExposureF`
996 Template exposure, warped to match the science exposure.
997 science : `lsst.afw.image.ExposureF`
998 Science exposure to subtract from the template.
999 matchedScience : `lsst.afw.image.ExposureF`
1000 The science exposure, convolved with the reflection of its own PSF.
1001 selectSources : `lsst.afw.table.SourceCatalog`
1002 Identified sources on the science exposure. This catalog is used to
1003 select sources in order to perform the AL PSF matching on stamp
1004 images around them.
1005 preConvKernel : `lsst.afw.math.Kernel`
1006 The reflection of the kernel that was used to preconvolve the
1007 `science` exposure. Must be normalized to sum to 1.
1008
1009 Returns
1010 -------
1011 results : `lsst.pipe.base.Struct`
1012
1013 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1014 Result of subtracting the convolved template and science
1015 images. Attached PSF is that of the original science image.
1016 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1017 Warped and PSF-matched template exposure. Attached PSF is that
1018 of the original science image.
1019 ``matchedScience`` : `lsst.afw.image.ExposureF`
1020 The science exposure after convolving with its own PSF.
1021 Attached PSF is that of the original science image.
1022 ``backgroundModel`` : `lsst.afw.math.Function2D`
1023 Background model that was fit while solving for the
1024 PSF-matching kernel
1025 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1026 Final kernel used to PSF-match the template to the science
1027 image.
1028 """
1029 bbox = science.getBBox()
1030 innerBBox = preConvKernel.shrinkBBox(bbox)
1031
1032 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
1033 candidateList=selectSources,
1034 preconvolved=True)
1035 kernelResult = self.makeKernel.run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1036 preconvolved=True)
1037
1038 matchedTemplate = self._convolveExposure(template, kernelResult.psfMatchingKernel,
1040 bbox=bbox,
1041 psf=science.psf,
1042 interpolateBadMaskPlanes=True,
1043 photoCalib=science.photoCalib)
1044 score = _subtractImages(matchedScience, matchedTemplate,
1045 backgroundModel=(kernelResult.backgroundModel
1046 if self.config.doSubtractBackground else None))
1047 correctedScore = self.finalize(template[bbox], science, score,
1048 kernelResult.psfMatchingKernel,
1049 templateMatched=True, preConvMode=True,
1050 preConvKernel=preConvKernel)
1051
1052 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1053 matchedTemplate=matchedTemplate,
1054 matchedScience=matchedScience,
1055 backgroundModel=kernelResult.backgroundModel,
1056 psfMatchingKernel=kernelResult.psfMatchingKernel)
1057
1058
1059def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.,
1060 exceptionMessage=""):
1061 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1062
1063 Parameters
1064 ----------
1065 templateExposure : `lsst.afw.image.ExposureF`
1066 The template exposure to check
1067 logger : `lsst.log.Log`
1068 Logger for printing output.
1069 requiredTemplateFraction : `float`, optional
1070 Fraction of pixels of the science image required to have coverage
1071 in the template.
1072 exceptionMessage : `str`, optional
1073 Message to include in the exception raised if the template coverage
1074 is insufficient.
1075
1076 Raises
1077 ------
1078 lsst.pipe.base.NoWorkFound
1079 Raised if fraction of good pixels, defined as not having NO_DATA
1080 set, is less than the requiredTemplateFraction
1081 """
1082 # Count the number of pixels with the NO_DATA mask bit set
1083 # counting NaN pixels is insufficient because pixels without data are often intepolated over)
1084 pixNoData = np.count_nonzero(templateExposure.mask.array
1085 & templateExposure.mask.getPlaneBitMask('NO_DATA'))
1086 pixGood = templateExposure.getBBox().getArea() - pixNoData
1087 logger.info("template has %d good pixels (%.1f%%)", pixGood,
1088 100*pixGood/templateExposure.getBBox().getArea())
1089
1090 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
1091 message = ("Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1092 100*pixGood/templateExposure.getBBox().getArea(),
1093 100*requiredTemplateFraction))
1094 raise lsst.pipe.base.NoWorkFound(message + " " + exceptionMessage)
1095
1096
1097def _subtractImages(science, template, backgroundModel=None):
1098 """Subtract template from science, propagating relevant metadata.
1099
1100 Parameters
1101 ----------
1102 science : `lsst.afw.Exposure`
1103 The input science image.
1104 template : `lsst.afw.Exposure`
1105 The template to subtract from the science image.
1106 backgroundModel : `lsst.afw.MaskedImage`, optional
1107 Differential background model
1108
1109 Returns
1110 -------
1111 difference : `lsst.afw.Exposure`
1112 The subtracted image.
1113 """
1114 difference = science.clone()
1115 if backgroundModel is not None:
1116 difference.maskedImage -= backgroundModel
1117 difference.maskedImage -= template.maskedImage
1118 return difference
1119
1120
1121def _shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid):
1122 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1123
1124 Parameters
1125 ----------
1126 exp1 : `~lsst.afw.image.Exposure`
1127 Exposure with the reference point spread function (PSF) to evaluate.
1128 exp2 : `~lsst.afw.image.Exposure`
1129 Exposure with a candidate point spread function (PSF) to evaluate.
1130 fwhmExposureBuffer : `float`
1131 Fractional buffer margin to be left out of all sides of the image
1132 during the construction of the grid to compute mean PSF FWHM in an
1133 exposure, if the PSF is not available at its average position.
1134 fwhmExposureGrid : `int`
1135 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1136 available at its average position.
1137 Returns
1138 -------
1139 result : `bool`
1140 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1141 either dimension.
1142 """
1143 try:
1144 shape1 = getPsfFwhm(exp1.psf, average=False)
1145 shape2 = getPsfFwhm(exp2.psf, average=False)
1147 shape1 = evaluateMeanPsfFwhm(exp1,
1148 fwhmExposureBuffer=fwhmExposureBuffer,
1149 fwhmExposureGrid=fwhmExposureGrid
1150 )
1151 shape2 = evaluateMeanPsfFwhm(exp2,
1152 fwhmExposureBuffer=fwhmExposureBuffer,
1153 fwhmExposureGrid=fwhmExposureGrid
1154 )
1155 return shape1 <= shape2
1156
1157 # Results from getPsfFwhm is a tuple of two values, one for each dimension.
1158 xTest = shape1[0] <= shape2[0]
1159 yTest = shape1[1] <= shape2[1]
1160 return xTest | yTest
1161
1162
1163def _interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None):
1164 """Replace masked image pixels with interpolated values.
1165
1166 Parameters
1167 ----------
1168 maskedImage : `lsst.afw.image.MaskedImage`
1169 Image on which to perform interpolation.
1170 badMaskPlanes : `list` of `str`
1171 List of mask planes to interpolate over.
1172 fallbackValue : `float`, optional
1173 Value to set when interpolation fails.
1174
1175 Returns
1176 -------
1177 result: `float`
1178 The number of masked pixels that were replaced.
1179 """
1180 imgBadMaskPlanes = [
1181 maskPlane for maskPlane in badMaskPlanes if maskPlane in maskedImage.mask.getMaskPlaneDict()
1182 ]
1183
1184 image = maskedImage.image.array
1185 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1186 image[badPixels] = np.nan
1187 if fallbackValue is None:
1188 fallbackValue = np.nanmedian(image)
1189 # For this initial implementation, skip the interpolation and just fill with
1190 # the median value.
1191 image[badPixels] = fallbackValue
1192 return np.sum(badPixels)
Asseses the quality of a candidate given a spatial kernel and background model.
runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel)
run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None)
_prepareInputs(self, template, science, visitSummary=None)
runConvolveTemplate(self, template, science, selectSources)
_convolveExposure(self, exposure, kernel, convolutionControl, bbox=None, psf=None, photoCalib=None, interpolateBadMaskPlanes=False)
runConvolveScience(self, template, science, selectSources)
updateMasks(self, template, science, difference)
run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None)
finalize(self, template, science, difference, kernel, templateMatched=True, preConvMode=False, preConvKernel=None, spatiallyVarying=False)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
_subtractImages(science, template, backgroundModel=None)
_interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None)
checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0., exceptionMessage="")
_shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid)