lsst.ip.diffim gd0838329fc+f3ac227b3c
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 if "FAKE" in science.mask.getMaskPlaneDict().keys():
655 # propagate the mask plane related to Fake source injection
656 # NOTE: the fake source injection sets FAKE plane, but it should be INJECTED
657 # NOTE: This can be removed in DM-40796
658
659 self.log.info("Adding injected mask planes")
660 mask.addMaskPlane("INJECTED")
661 diffInjectedBitMask = mask.getPlaneBitMask("INJECTED")
662
663 mask.addMaskPlane("INJECTED_TEMPLATE")
664 diffInjTmpltBitMask = mask.getPlaneBitMask("INJECTED_TEMPLATE")
665
666 scienceFakeBitMask = science.mask.getPlaneBitMask('FAKE')
667 tmpltFakeBitMask = template[bbox].mask.getPlaneBitMask('FAKE')
668
669 injScienceMaskArray = ((science.mask.array & scienceFakeBitMask) > 0) * diffInjectedBitMask
670 injTemplateMaskArray = ((template[bbox].mask.array & tmpltFakeBitMask) > 0) * diffInjTmpltBitMask
671
672 mask.array |= injScienceMaskArray
673 mask.array |= injTemplateMaskArray
674
675 template[bbox].mask.array[...] = difference.mask.array[...]
676
677 @staticmethod
678 def _validateExposures(template, science):
679 """Check that the WCS of the two Exposures match, and the template bbox
680 contains the science bbox.
681
682 Parameters
683 ----------
684 template : `lsst.afw.image.ExposureF`
685 Template exposure, warped to match the science exposure.
686 science : `lsst.afw.image.ExposureF`
687 Science exposure to subtract from the template.
688
689 Raises
690 ------
691 AssertionError
692 Raised if the WCS of the template is not equal to the science WCS,
693 or if the science image is not fully contained in the template
694 bounding box.
695 """
696 assert template.wcs == science.wcs,\
697 "Template and science exposure WCS are not identical."
698 templateBBox = template.getBBox()
699 scienceBBox = science.getBBox()
700
701 assert templateBBox.contains(scienceBBox),\
702 "Template bbox does not contain all of the science image."
703
704 def _convolveExposure(self, exposure, kernel, convolutionControl,
705 bbox=None,
706 psf=None,
707 photoCalib=None,
708 interpolateBadMaskPlanes=False,
709 ):
710 """Convolve an exposure with the given kernel.
711
712 Parameters
713 ----------
714 exposure : `lsst.afw.Exposure`
715 exposure to convolve.
716 kernel : `lsst.afw.math.LinearCombinationKernel`
717 PSF matching kernel computed in the ``makeKernel`` subtask.
718 convolutionControl : `lsst.afw.math.ConvolutionControl`
719 Configuration for convolve algorithm.
720 bbox : `lsst.geom.Box2I`, optional
721 Bounding box to trim the convolved exposure to.
722 psf : `lsst.afw.detection.Psf`, optional
723 Point spread function (PSF) to set for the convolved exposure.
724 photoCalib : `lsst.afw.image.PhotoCalib`, optional
725 Photometric calibration of the convolved exposure.
726
727 Returns
728 -------
729 convolvedExp : `lsst.afw.Exposure`
730 The convolved image.
731 """
732 convolvedExposure = exposure.clone()
733 if psf is not None:
734 convolvedExposure.setPsf(psf)
735 if photoCalib is not None:
736 convolvedExposure.setPhotoCalib(photoCalib)
737 if interpolateBadMaskPlanes and self.config.badMaskPlanes is not None:
738 nInterp = _interpolateImage(convolvedExposure.maskedImage,
739 self.config.badMaskPlanes)
740 self.metadata.add("nInterpolated", nInterp)
741 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
742 lsst.afw.math.convolve(convolvedImage, convolvedExposure.maskedImage, kernel, convolutionControl)
743 convolvedExposure.setMaskedImage(convolvedImage)
744 if bbox is None:
745 return convolvedExposure
746 else:
747 return convolvedExposure[bbox]
748
749 def _sourceSelector(self, sources, mask):
750 """Select sources from a catalog that meet the selection criteria.
751
752 Parameters
753 ----------
754 sources : `lsst.afw.table.SourceCatalog`
755 Input source catalog to select sources from.
756 mask : `lsst.afw.image.Mask`
757 The image mask plane to use to reject sources
758 based on their location on the ccd.
759
760 Returns
761 -------
762 selectSources : `lsst.afw.table.SourceCatalog`
763 The input source catalog, with flagged and low signal-to-noise
764 sources removed.
765
766 Raises
767 ------
768 RuntimeError
769 If there are too few sources to compute the PSF matching kernel
770 remaining after source selection.
771 """
772 flags = np.ones(len(sources), dtype=bool)
773 for flag in self.config.badSourceFlags:
774 try:
775 flags *= ~sources[flag]
776 except Exception as e:
777 self.log.warning("Could not apply source flag: %s", e)
778 signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr()
779 sToNFlag = signalToNoise > self.config.detectionThreshold
780 flags *= sToNFlag
781 sToNFlagMax = signalToNoise < self.config.detectionThresholdMax
782 flags *= sToNFlagMax
783 flags *= self._checkMask(mask, sources, self.config.excludeMaskPlanes)
784 selectSources = sources[flags].copy(deep=True)
785 if (len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
786 signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
787 indices = np.argsort(signalToNoise)
788 indices = indices[-self.config.maxKernelSources:]
789 flags = np.zeros(len(selectSources), dtype=bool)
790 flags[indices] = True
791 selectSources = selectSources[flags].copy(deep=True)
792
793 self.log.info("%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
794 len(selectSources), len(sources), 100*len(selectSources)/len(sources))
795 if len(selectSources) < self.config.minKernelSources:
796 self.log.error("Too few sources to calculate the PSF matching kernel: "
797 "%i selected but %i needed for the calculation.",
798 len(selectSources), self.config.minKernelSources)
799 raise RuntimeError("Cannot compute PSF matching kernel: too few sources selected.")
800 self.metadata.add("nPsfSources", len(selectSources))
801
802 return selectSources
803
804 @staticmethod
805 def _checkMask(mask, sources, excludeMaskPlanes):
806 """Exclude sources that are located on masked pixels.
807
808 Parameters
809 ----------
810 mask : `lsst.afw.image.Mask`
811 The image mask plane to use to reject sources
812 based on the location of their centroid on the ccd.
813 sources : `lsst.afw.table.SourceCatalog`
814 The source catalog to evaluate.
815 excludeMaskPlanes : `list` of `str`
816 List of the names of the mask planes to exclude.
817
818 Returns
819 -------
820 flags : `numpy.ndarray` of `bool`
821 Array indicating whether each source in the catalog should be
822 kept (True) or rejected (False) based on the value of the
823 mask plane at its location.
824 """
825 setExcludeMaskPlanes = [
826 maskPlane for maskPlane in excludeMaskPlanes if maskPlane in mask.getMaskPlaneDict()
827 ]
828
829 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
830
831 xv = np.rint(sources.getX() - mask.getX0())
832 yv = np.rint(sources.getY() - mask.getY0())
833
834 mv = mask.array[yv.astype(int), xv.astype(int)]
835 flags = np.bitwise_and(mv, excludePixelMask) == 0
836 return flags
837
838 def _prepareInputs(self, template, science, visitSummary=None):
839 """Perform preparatory calculations common to all Alard&Lupton Tasks.
840
841 Parameters
842 ----------
843 template : `lsst.afw.image.ExposureF`
844 Template exposure, warped to match the science exposure. The
845 variance plane of the template image is modified in place.
846 science : `lsst.afw.image.ExposureF`
847 Science exposure to subtract from the template. The variance plane
848 of the science image is modified in place.
849 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
850 Exposure catalog with external calibrations to be applied. Catalog
851 uses the detector id for the catalog id, sorted on id for fast
852 lookup.
853 """
854 self._validateExposures(template, science)
855 if visitSummary is not None:
856 self._applyExternalCalibrations(science, visitSummary=visitSummary)
857 checkTemplateIsSufficient(template[science.getBBox()], self.log,
858 requiredTemplateFraction=self.config.requiredTemplateFraction,
859 exceptionMessage="Not attempting subtraction. To force subtraction,"
860 " set config requiredTemplateFraction=0")
861
862 if self.config.doScaleVariance:
863 # Scale the variance of the template and science images before
864 # convolution, subtraction, or decorrelation so that they have the
865 # correct ratio.
866 templateVarFactor = self.scaleVariance.run(template.maskedImage)
867 sciVarFactor = self.scaleVariance.run(science.maskedImage)
868 self.log.info("Template variance scaling factor: %.2f", templateVarFactor)
869 self.metadata.add("scaleTemplateVarianceFactor", templateVarFactor)
870 self.log.info("Science variance scaling factor: %.2f", sciVarFactor)
871 self.metadata.add("scaleScienceVarianceFactor", sciVarFactor)
872 self._clearMask(template)
873
874 def _clearMask(self, template):
875 """Clear the mask plane of the template.
876
877 Parameters
878 ----------
879 template : `lsst.afw.image.ExposureF`
880 Template exposure, warped to match the science exposure.
881 The mask plane will be modified in place.
882 """
883 mask = template.mask
884 clearMaskPlanes = [maskplane for maskplane in mask.getMaskPlaneDict().keys()
885 if maskplane not in self.config.preserveTemplateMask]
886
887 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
888 mask &= ~bitMaskToClear
889
890
892 SubtractScoreOutputConnections):
893 pass
894
895
897 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
898 pass
899
900
902 """Subtract a template from a science image, convolving the science image
903 before computing the kernel, and also convolving the template before
904 subtraction.
905 """
906 ConfigClass = AlardLuptonPreconvolveSubtractConfig
907 _DefaultName = "alardLuptonPreconvolveSubtract"
908
909 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None):
910 """Preconvolve the science image with its own PSF,
911 convolve the template image with a PSF-matching kernel and subtract
912 from the preconvolved science image.
913
914 Parameters
915 ----------
916 template : `lsst.afw.image.ExposureF`
917 The template image, which has previously been warped to the science
918 image. The template bbox will be padded by a few pixels compared to
919 the science bbox.
920 science : `lsst.afw.image.ExposureF`
921 The science exposure.
922 sources : `lsst.afw.table.SourceCatalog`
923 Identified sources on the science exposure. This catalog is used to
924 select sources in order to perform the AL PSF matching on stamp
925 images around them.
926 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
927 Exposure catalog with finalized psf models and aperture correction
928 maps to be applied. Catalog uses the detector id for the catalog
929 id, sorted on id for fast lookup. Deprecated in favor of
930 ``visitSummary``, and will be removed after v26.
931 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
932 Exposure catalog with complete external calibrations. Catalog uses
933 the detector id for the catalog id, sorted on id for fast lookup.
934 Ignored (for temporary backwards compatibility) if
935 ``finalizedPsfApCorrCatalog`` is provided.
936
937 Returns
938 -------
939 results : `lsst.pipe.base.Struct`
940 ``scoreExposure`` : `lsst.afw.image.ExposureF`
941 Result of subtracting the convolved template and science
942 images. Attached PSF is that of the original science image.
943 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
944 Warped and PSF-matched template exposure. Attached PSF is that
945 of the original science image.
946 ``matchedScience`` : `lsst.afw.image.ExposureF`
947 The science exposure after convolving with its own PSF.
948 Attached PSF is that of the original science image.
949 ``backgroundModel`` : `lsst.afw.math.Function2D`
950 Background model that was fit while solving for the
951 PSF-matching kernel
952 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
953 Final kernel used to PSF-match the template to the science
954 image.
955 """
956 if finalizedPsfApCorrCatalog is not None:
957 warnings.warn(
958 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
959 "argument, and will be removed after v26.",
960 FutureWarning,
961 stacklevel=find_outside_stacklevel("lsst.ip.diffim"),
962 )
963 visitSummary = finalizedPsfApCorrCatalog
964
965 self._prepareInputs(template, science, visitSummary=visitSummary)
966
967 # TODO: DM-37212 we need to mirror the kernel in order to get correct cross correlation
968 scienceKernel = science.psf.getKernel()
969 matchedScience = self._convolveExposure(science, scienceKernel, self.convolutionControlconvolutionControl,
970 interpolateBadMaskPlanes=True)
971 self.metadata.add("convolvedExposure", "Preconvolution")
972 try:
973 selectSources = self._sourceSelector(sources, matchedScience.mask)
974 subtractResults = self.runPreconvolve(template, science, matchedScience,
975 selectSources, scienceKernel)
976
977 except (RuntimeError, lsst.pex.exceptions.Exception) as e:
978 self.loglog.warning("Failed to match template. Checking coverage")
979 # Raise NoWorkFound if template fraction is insufficient
980 checkTemplateIsSufficient(template[science.getBBox()], self.loglog,
981 self.config.minTemplateFractionForExpectedSuccess,
982 exceptionMessage="Template coverage lower than expected to succeed."
983 f" Failure is tolerable: {e}")
984 # checkTemplateIsSufficient did not raise NoWorkFound, so raise original exception
985 raise e
986
987 return subtractResults
988
989 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
990 """Convolve the science image with its own PSF, then convolve the
991 template with a matching kernel and subtract to form the Score
992 exposure.
993
994 Parameters
995 ----------
996 template : `lsst.afw.image.ExposureF`
997 Template exposure, warped to match the science exposure.
998 science : `lsst.afw.image.ExposureF`
999 Science exposure to subtract from the template.
1000 matchedScience : `lsst.afw.image.ExposureF`
1001 The science exposure, convolved with the reflection of its own PSF.
1002 selectSources : `lsst.afw.table.SourceCatalog`
1003 Identified sources on the science exposure. This catalog is used to
1004 select sources in order to perform the AL PSF matching on stamp
1005 images around them.
1006 preConvKernel : `lsst.afw.math.Kernel`
1007 The reflection of the kernel that was used to preconvolve the
1008 `science` exposure. Must be normalized to sum to 1.
1009
1010 Returns
1011 -------
1012 results : `lsst.pipe.base.Struct`
1013
1014 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1015 Result of subtracting the convolved template and science
1016 images. Attached PSF is that of the original science image.
1017 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1018 Warped and PSF-matched template exposure. Attached PSF is that
1019 of the original science image.
1020 ``matchedScience`` : `lsst.afw.image.ExposureF`
1021 The science exposure after convolving with its own PSF.
1022 Attached PSF is that of the original science image.
1023 ``backgroundModel`` : `lsst.afw.math.Function2D`
1024 Background model that was fit while solving for the
1025 PSF-matching kernel
1026 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1027 Final kernel used to PSF-match the template to the science
1028 image.
1029 """
1030 bbox = science.getBBox()
1031 innerBBox = preConvKernel.shrinkBBox(bbox)
1032
1033 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
1034 candidateList=selectSources,
1035 preconvolved=True)
1036 kernelResult = self.makeKernel.run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1037 preconvolved=True)
1038
1039 matchedTemplate = self._convolveExposure(template, kernelResult.psfMatchingKernel,
1041 bbox=bbox,
1042 psf=science.psf,
1043 interpolateBadMaskPlanes=True,
1044 photoCalib=science.photoCalib)
1045 score = _subtractImages(matchedScience, matchedTemplate,
1046 backgroundModel=(kernelResult.backgroundModel
1047 if self.config.doSubtractBackground else None))
1048 correctedScore = self.finalize(template[bbox], science, score,
1049 kernelResult.psfMatchingKernel,
1050 templateMatched=True, preConvMode=True,
1051 preConvKernel=preConvKernel)
1052
1053 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1054 matchedTemplate=matchedTemplate,
1055 matchedScience=matchedScience,
1056 backgroundModel=kernelResult.backgroundModel,
1057 psfMatchingKernel=kernelResult.psfMatchingKernel)
1058
1059
1060def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.,
1061 exceptionMessage=""):
1062 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1063
1064 Parameters
1065 ----------
1066 templateExposure : `lsst.afw.image.ExposureF`
1067 The template exposure to check
1068 logger : `lsst.log.Log`
1069 Logger for printing output.
1070 requiredTemplateFraction : `float`, optional
1071 Fraction of pixels of the science image required to have coverage
1072 in the template.
1073 exceptionMessage : `str`, optional
1074 Message to include in the exception raised if the template coverage
1075 is insufficient.
1076
1077 Raises
1078 ------
1079 lsst.pipe.base.NoWorkFound
1080 Raised if fraction of good pixels, defined as not having NO_DATA
1081 set, is less than the requiredTemplateFraction
1082 """
1083 # Count the number of pixels with the NO_DATA mask bit set
1084 # counting NaN pixels is insufficient because pixels without data are often intepolated over)
1085 pixNoData = np.count_nonzero(templateExposure.mask.array
1086 & templateExposure.mask.getPlaneBitMask('NO_DATA'))
1087 pixGood = templateExposure.getBBox().getArea() - pixNoData
1088 logger.info("template has %d good pixels (%.1f%%)", pixGood,
1089 100*pixGood/templateExposure.getBBox().getArea())
1090
1091 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
1092 message = ("Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1093 100*pixGood/templateExposure.getBBox().getArea(),
1094 100*requiredTemplateFraction))
1095 raise lsst.pipe.base.NoWorkFound(message + " " + exceptionMessage)
1096
1097
1098def _subtractImages(science, template, backgroundModel=None):
1099 """Subtract template from science, propagating relevant metadata.
1100
1101 Parameters
1102 ----------
1103 science : `lsst.afw.Exposure`
1104 The input science image.
1105 template : `lsst.afw.Exposure`
1106 The template to subtract from the science image.
1107 backgroundModel : `lsst.afw.MaskedImage`, optional
1108 Differential background model
1109
1110 Returns
1111 -------
1112 difference : `lsst.afw.Exposure`
1113 The subtracted image.
1114 """
1115 difference = science.clone()
1116 if backgroundModel is not None:
1117 difference.maskedImage -= backgroundModel
1118 difference.maskedImage -= template.maskedImage
1119 return difference
1120
1121
1122def _shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid):
1123 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1124
1125 Parameters
1126 ----------
1127 exp1 : `~lsst.afw.image.Exposure`
1128 Exposure with the reference point spread function (PSF) to evaluate.
1129 exp2 : `~lsst.afw.image.Exposure`
1130 Exposure with a candidate point spread function (PSF) to evaluate.
1131 fwhmExposureBuffer : `float`
1132 Fractional buffer margin to be left out of all sides of the image
1133 during the construction of the grid to compute mean PSF FWHM in an
1134 exposure, if the PSF is not available at its average position.
1135 fwhmExposureGrid : `int`
1136 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1137 available at its average position.
1138 Returns
1139 -------
1140 result : `bool`
1141 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1142 either dimension.
1143 """
1144 try:
1145 shape1 = getPsfFwhm(exp1.psf, average=False)
1146 shape2 = getPsfFwhm(exp2.psf, average=False)
1148 shape1 = evaluateMeanPsfFwhm(exp1,
1149 fwhmExposureBuffer=fwhmExposureBuffer,
1150 fwhmExposureGrid=fwhmExposureGrid
1151 )
1152 shape2 = evaluateMeanPsfFwhm(exp2,
1153 fwhmExposureBuffer=fwhmExposureBuffer,
1154 fwhmExposureGrid=fwhmExposureGrid
1155 )
1156 return shape1 <= shape2
1157
1158 # Results from getPsfFwhm is a tuple of two values, one for each dimension.
1159 xTest = shape1[0] <= shape2[0]
1160 yTest = shape1[1] <= shape2[1]
1161 return xTest | yTest
1162
1163
1164def _interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None):
1165 """Replace masked image pixels with interpolated values.
1166
1167 Parameters
1168 ----------
1169 maskedImage : `lsst.afw.image.MaskedImage`
1170 Image on which to perform interpolation.
1171 badMaskPlanes : `list` of `str`
1172 List of mask planes to interpolate over.
1173 fallbackValue : `float`, optional
1174 Value to set when interpolation fails.
1175
1176 Returns
1177 -------
1178 result: `float`
1179 The number of masked pixels that were replaced.
1180 """
1181 imgBadMaskPlanes = [
1182 maskPlane for maskPlane in badMaskPlanes if maskPlane in maskedImage.mask.getMaskPlaneDict()
1183 ]
1184
1185 image = maskedImage.image.array
1186 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1187 image[badPixels] = np.nan
1188 if fallbackValue is None:
1189 fallbackValue = np.nanmedian(image)
1190 # For this initial implementation, skip the interpolation and just fill with
1191 # the median value.
1192 image[badPixels] = fallbackValue
1193 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)