LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
LSST Data Management Base Package
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 numpy as np
23
24import lsst.afw.image
25import lsst.afw.math
26import lsst.geom
27from lsst.ip.diffim.utils import getPsfFwhm
28from lsst.meas.algorithms import ScaleVarianceTask
29import lsst.pex.config
30import lsst.pipe.base
31from lsst.pipe.base import connectionTypes
32from . import MakeKernelTask, DecorrelateALKernelTask
33from lsst.utils.timer import timeMethod
34
35__all__ = ["AlardLuptonSubtractConfig", "AlardLuptonSubtractTask"]
36
37_dimensions = ("instrument", "visit", "detector")
38_defaultTemplates = {"coaddName": "deep", "fakesType": ""}
39
40
41class SubtractInputConnections(lsst.pipe.base.PipelineTaskConnections,
42 dimensions=_dimensions,
43 defaultTemplates=_defaultTemplates):
44 template = connectionTypes.Input(
45 doc="Input warped template to subtract.",
46 dimensions=("instrument", "visit", "detector"),
47 storageClass="ExposureF",
48 name="{fakesType}{coaddName}Diff_templateExp"
49 )
50 science = connectionTypes.Input(
51 doc="Input science exposure to subtract from.",
52 dimensions=("instrument", "visit", "detector"),
53 storageClass="ExposureF",
54 name="{fakesType}calexp"
55 )
56 sources = connectionTypes.Input(
57 doc="Sources measured on the science exposure; "
58 "used to select sources for making the matching kernel.",
59 dimensions=("instrument", "visit", "detector"),
60 storageClass="SourceCatalog",
61 name="{fakesType}src"
62 )
63 finalizedPsfApCorrCatalog = connectionTypes.Input(
64 doc=("Per-visit finalized psf models and aperture correction maps. "
65 "These catalogs use the detector id for the catalog id, "
66 "sorted on id for fast lookup."),
67 dimensions=("instrument", "visit"),
68 storageClass="ExposureCatalog",
69 name="finalized_psf_ap_corr_catalog",
70 )
71
72
73class SubtractImageOutputConnections(lsst.pipe.base.PipelineTaskConnections,
74 dimensions=_dimensions,
75 defaultTemplates=_defaultTemplates):
76 difference = connectionTypes.Output(
77 doc="Result of subtracting convolved template from science image.",
78 dimensions=("instrument", "visit", "detector"),
79 storageClass="ExposureF",
80 name="{fakesType}{coaddName}Diff_differenceTempExp",
81 )
82 matchedTemplate = connectionTypes.Output(
83 doc="Warped and PSF-matched template used to create `subtractedExposure`.",
84 dimensions=("instrument", "visit", "detector"),
85 storageClass="ExposureF",
86 name="{fakesType}{coaddName}Diff_matchedExp",
87 )
88
89
91
92 def __init__(self, *, config=None):
93 super().__init__(config=config)
94 if not config.doApplyFinalizedPsf:
95 self.inputs.remove("finalizedPsfApCorrCatalog")
96
97
98class AlardLuptonSubtractConfig(lsst.pipe.base.PipelineTaskConfig,
99 pipelineConnections=AlardLuptonSubtractConnections):
101 dtype=str,
102 default="convolveTemplate",
103 allowed={"auto": "Choose which image to convolve at runtime.",
104 "convolveScience": "Only convolve the science image.",
105 "convolveTemplate": "Only convolve the template image."},
106 doc="Choose which image to convolve at runtime, or require that a specific image is convolved."
107 )
109 target=MakeKernelTask,
110 doc="Task to construct a matching kernel for convolution.",
111 )
112 doDecorrelation = lsst.pex.config.Field(
113 dtype=bool,
114 default=True,
115 doc="Perform diffim decorrelation to undo pixel correlation due to A&L "
116 "kernel convolution? If True, also update the diffim PSF."
117 )
119 target=DecorrelateALKernelTask,
120 doc="Task to decorrelate the image difference.",
121 )
122 requiredTemplateFraction = lsst.pex.config.Field(
123 dtype=float,
124 default=0.1,
125 doc="Abort task if template covers less than this fraction of pixels."
126 " Setting to 0 will always attempt image subtraction."
127 )
128 doScaleVariance = lsst.pex.config.Field(
129 dtype=bool,
130 default=True,
131 doc="Scale variance of the image difference?"
132 )
134 target=ScaleVarianceTask,
135 doc="Subtask to rescale the variance of the template to the statistically expected level."
136 )
137 doSubtractBackground = lsst.pex.config.Field(
138 doc="Subtract the background fit when solving the kernel?",
139 dtype=bool,
140 default=True,
141 )
142 doApplyFinalizedPsf = lsst.pex.config.Field(
143 doc="Replace science Exposure's psf and aperture correction map"
144 " with those in finalizedPsfApCorrCatalog.",
145 dtype=bool,
146 default=False,
147 )
148 detectionThreshold = lsst.pex.config.Field(
149 dtype=float,
150 default=10,
151 doc="Minimum signal to noise ration of detected sources "
152 "to use for calculating the PSF matching kernel."
153 )
155 dtype=str,
156 doc="Flags that, if set, the associated source should not "
157 "be used to determine the PSF matching kernel.",
158 default=("sky_source", "slot_Centroid_flag",
159 "slot_ApFlux_flag", "slot_PsfFlux_flag", ),
160 )
161
162 forceCompatibility = lsst.pex.config.Field(
163 dtype=bool,
164 default=False,
165 doc="Set up and run diffim using settings that ensure the results"
166 "are compatible with the old version in pipe_tasks.",
167 deprecated="This option is only for backwards compatibility purposes"
168 " and will be removed after v24.",
169 )
170
171 def setDefaults(self):
172 self.makeKernel.kernel.name = "AL"
173 self.makeKernel.kernel.active.fitForBackground = self.doSubtractBackground
174 self.makeKernel.kernel.active.spatialKernelOrder = 1
175 self.makeKernel.kernel.active.spatialBgOrder = 2
176
177 def validate(self):
178 if self.forceCompatibility and not (self.mode == "convolveTemplate"):
179 msg = f"forceCompatibility=True requires mode='convolveTemplate', but mode was '{self.mode}'."
180 raise lsst.pex.config.FieldValidationError(AlardLuptonSubtractConfig.forceCompatibility,
181 self, msg)
182
183
184class AlardLuptonSubtractTask(lsst.pipe.base.PipelineTask):
185 """Compute the image difference of a science and template image using
186 the Alard & Lupton (1998) algorithm.
187 """
188 ConfigClass = AlardLuptonSubtractConfig
189 _DefaultName = "alardLuptonSubtract"
190
191 def __init__(self, **kwargs):
192 super().__init__(**kwargs)
193 self.makeSubtask("decorrelate")
194 self.makeSubtask("makeKernel")
195 if self.config.doScaleVariance:
196 self.makeSubtask("scaleVariance")
197
199 # Normalization is an extra, unnecessary, calculation and will result
200 # in mis-subtraction of the images if there are calibration errors.
201 self.convolutionControl.setDoNormalize(False)
202 self.convolutionControl.setDoCopyEdge(True)
203
204 def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog):
205 """Replace calibrations (psf, and ApCorrMap) on this exposure with external ones.".
206
207 Parameters
208 ----------
210 Input exposure to adjust calibrations.
211 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`
212 Exposure catalog with finalized psf models and aperture correction
213 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses
214 the detector id for the catalog id, sorted on id for fast lookup.
215
216 Returns
217 -------
219 Exposure with adjusted calibrations.
220 """
221 detectorId = exposure.info.getDetector().getId()
222
223 row = finalizedPsfApCorrCatalog.find(detectorId)
224 if row is None:
225 self.log.warning("Detector id %s not found in finalizedPsfApCorrCatalog; "
226 "Using original psf.", detectorId)
227 else:
228 psf = row.getPsf()
229 apCorrMap = row.getApCorrMap()
230 if psf is None:
231 self.log.warning("Detector id %s has None for psf in "
232 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
233 detectorId)
234 elif apCorrMap is None:
235 self.log.warning("Detector id %s has None for apCorrMap in "
236 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
237 detectorId)
238 else:
239 exposure.setPsf(psf)
240 exposure.info.setApCorrMap(apCorrMap)
241
242 return exposure
243
244 @timeMethod
245 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None):
246 """PSF match, subtract, and decorrelate two images.
247
248 Parameters
249 ----------
250 template : `lsst.afw.image.ExposureF`
251 Template exposure, warped to match the science exposure.
252 science : `lsst.afw.image.ExposureF`
253 Science exposure to subtract from the template.
255 Identified sources on the science exposure. This catalog is used to
256 select sources in order to perform the AL PSF matching on stamp
257 images around them.
258 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
259 Exposure catalog with finalized psf models and aperture correction
260 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses
261 the detector id for the catalog id, sorted on id for fast lookup.
262
263 Returns
264 -------
265 results : `lsst.pipe.base.Struct`
266 ``difference`` : `lsst.afw.image.ExposureF`
267 Result of subtracting template and science.
268 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
269 Warped and PSF-matched template exposure.
270 ``backgroundModel`` : `lsst.afw.math.Function2D`
271 Background model that was fit while solving for the PSF-matching kernel
272 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
273 Kernel used to PSF-match the convolved image.
274
275 Raises
276 ------
277 RuntimeError
278 If an unsupported convolution mode is supplied.
279 RuntimeError
280 If there are too few sources to calculate the PSF matching kernel.
281 lsst.pipe.base.NoWorkFound
282 Raised if fraction of good pixels, defined as not having NO_DATA
283 set, is less then the configured requiredTemplateFraction
284 """
285 self._validateExposures(template, science)
286 if self.config.doApplyFinalizedPsf:
287 self._applyExternalCalibrations(science,
288 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog)
289 checkTemplateIsSufficient(template, self.log,
290 requiredTemplateFraction=self.config.requiredTemplateFraction)
291 if self.config.forceCompatibility:
292 # Compatibility option to maintain old functionality
293 # This should be removed in the future!
294 self.log.warning("Running with `config.forceCompatibility=True`")
295 sources = None
296 sciencePsfSize = getPsfFwhm(science.psf)
297 templatePsfSize = getPsfFwhm(template.psf)
298 self.log.info("Science PSF FWHM: %f pixels", sciencePsfSize)
299 self.log.info("Template PSF FWHM: %f pixels", templatePsfSize)
300 if self.config.mode == "auto":
301 convolveTemplate = _shapeTest(template.psf, science.psf)
302 if convolveTemplate:
303 if sciencePsfSize < templatePsfSize:
304 self.log.info("Average template PSF size is greater, "
305 "but science PSF greater in one dimension: convolving template image.")
306 else:
307 self.log.info("Science PSF size is greater: convolving template image.")
308 else:
309 self.log.info("Template PSF size is greater: convolving science image.")
310 elif self.config.mode == "convolveTemplate":
311 self.log.info("`convolveTemplate` is set: convolving template image.")
312 convolveTemplate = True
313 elif self.config.mode == "convolveScience":
314 self.log.info("`convolveScience` is set: convolving science image.")
315 convolveTemplate = False
316 else:
317 raise RuntimeError("Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
318 # put the template on the same photometric scale as the science image
319 photoCalib = template.getPhotoCalib()
320 self.log.info("Applying photometric calibration to template: %f", photoCalib.getCalibrationMean())
321 template.maskedImage = photoCalib.calibrateImage(template.maskedImage)
322
323 if self.config.doScaleVariance and not self.config.forceCompatibility:
324 # Scale the variance of the template and science images before
325 # convolution, subtraction, or decorrelation so that they have the
326 # correct ratio.
327 templateVarFactor = self.scaleVariance.run(template.maskedImage)
328 sciVarFactor = self.scaleVariance.run(science.maskedImage)
329 self.log.info("Template variance scaling factor: %.2f", templateVarFactor)
330 self.metadata.add("scaleTemplateVarianceFactor", templateVarFactor)
331 self.log.info("Science variance scaling factor: %.2f", sciVarFactor)
332 self.metadata.add("scaleScienceVarianceFactor", sciVarFactor)
333
334 selectSources = self._sourceSelector(sources)
335 self.log.info("%i sources used out of %i from the input catalog", len(selectSources), len(sources))
336 if len(selectSources) < self.config.makeKernel.nStarPerCell:
337 self.log.warning("Too few sources to calculate the PSF matching kernel: "
338 "%i selected but %i needed for the calculation.",
339 len(selectSources), self.config.makeKernel.nStarPerCell)
340 raise RuntimeError("Cannot compute PSF matching kernel: too few sources selected.")
341 if convolveTemplate:
342 subtractResults = self.runConvolveTemplate(template, science, selectSources)
343 else:
344 subtractResults = self.runConvolveScience(template, science, selectSources)
345
346 if self.config.doScaleVariance and self.config.forceCompatibility:
347 # The old behavior scaled the variance of the final image difference.
348 diffimVarFactor = self.scaleVariance.run(subtractResults.difference.maskedImage)
349 self.log.info("Diffim variance scaling factor: %.2f", diffimVarFactor)
350 self.metadata.add("scaleDiffimVarianceFactor", diffimVarFactor)
351
352 return subtractResults
353
354 def runConvolveTemplate(self, template, science, selectSources):
355 """Convolve the template image with a PSF-matching kernel and subtract
356 from the science image.
357
358 Parameters
359 ----------
360 template : `lsst.afw.image.ExposureF`
361 Template exposure, warped to match the science exposure.
362 science : `lsst.afw.image.ExposureF`
363 Science exposure to subtract from the template.
364 selectSources : `lsst.afw.table.SourceCatalog`
365 Identified sources on the science exposure. This catalog is used to
366 select sources in order to perform the AL PSF matching on stamp
367 images around them.
368
369 Returns
370 -------
371 results : `lsst.pipe.base.Struct`
372
373 ``difference`` : `lsst.afw.image.ExposureF`
374 Result of subtracting template and science.
375 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
376 Warped and PSF-matched template exposure.
377 ``backgroundModel`` : `lsst.afw.math.Function2D`
378 Background model that was fit while solving for the PSF-matching kernel
379 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
380 Kernel used to PSF-match the template to the science image.
381 """
382 if self.config.forceCompatibility:
383 # Compatibility option to maintain old behavior
384 # This should be removed in the future!
385 template = template[science.getBBox()]
386
387 kernelSources = self.makeKernel.selectKernelSources(template, science,
388 candidateList=selectSources,
389 preconvolved=False)
390 kernelResult = self.makeKernel.run(template, science, kernelSources,
391 preconvolved=False)
392
393 matchedTemplate = self._convolveExposure(template, kernelResult.psfMatchingKernel,
395 bbox=science.getBBox(),
396 psf=science.psf,
397 photoCalib=science.getPhotoCalib())
398 difference = _subtractImages(science, matchedTemplate,
399 backgroundModel=(kernelResult.backgroundModel
400 if self.config.doSubtractBackground else None))
401 correctedExposure = self.finalize(template, science, difference, kernelResult.psfMatchingKernel,
402 templateMatched=True)
403
404 return lsst.pipe.base.Struct(difference=correctedExposure,
405 matchedTemplate=matchedTemplate,
406 matchedScience=science,
407 backgroundModel=kernelResult.backgroundModel,
408 psfMatchingKernel=kernelResult.psfMatchingKernel)
409
410 def runConvolveScience(self, template, science, selectSources):
411 """Convolve the science image with a PSF-matching kernel and subtract the template image.
412
413 Parameters
414 ----------
415 template : `lsst.afw.image.ExposureF`
416 Template exposure, warped to match the science exposure.
417 science : `lsst.afw.image.ExposureF`
418 Science exposure to subtract from the template.
419 selectSources : `lsst.afw.table.SourceCatalog`
420 Identified sources on the science exposure. This catalog is used to
421 select sources in order to perform the AL PSF matching on stamp
422 images around them.
423
424 Returns
425 -------
426 results : `lsst.pipe.base.Struct`
427
428 ``difference`` : `lsst.afw.image.ExposureF`
429 Result of subtracting template and science.
430 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
431 Warped template exposure. Note that in this case, the template
432 is not PSF-matched to the science image.
433 ``backgroundModel`` : `lsst.afw.math.Function2D`
434 Background model that was fit while solving for the PSF-matching kernel
435 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
436 Kernel used to PSF-match the science image to the template.
437 """
438 if self.config.forceCompatibility:
439 # Compatibility option to maintain old behavior
440 # This should be removed in the future!
441 template = template[science.getBBox()]
442 kernelSources = self.makeKernel.selectKernelSources(science, template,
443 candidateList=selectSources,
444 preconvolved=False)
445 kernelResult = self.makeKernel.run(science, template, kernelSources,
446 preconvolved=False)
447 modelParams = kernelResult.backgroundModel.getParameters()
448 # We must invert the background model if the matching kernel is solved for the science image.
449 kernelResult.backgroundModel.setParameters([-p for p in modelParams])
450
451 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
452 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=False)
453
454 matchedScience = self._convolveExposure(science, kernelResult.psfMatchingKernel,
456 psf=template.psf)
457
458 # Place back on native photometric scale
459 matchedScience.maskedImage /= norm
460 matchedTemplate = template.clone()[science.getBBox()]
461 matchedTemplate.maskedImage /= norm
462 matchedTemplate.setPhotoCalib(science.getPhotoCalib())
463
464 difference = _subtractImages(matchedScience, matchedTemplate,
465 backgroundModel=(kernelResult.backgroundModel
466 if self.config.doSubtractBackground else None))
467
468 correctedExposure = self.finalize(template, science, difference, kernelResult.psfMatchingKernel,
469 templateMatched=False)
470
471 return lsst.pipe.base.Struct(difference=correctedExposure,
472 matchedTemplate=matchedTemplate,
473 matchedScience=matchedScience,
474 backgroundModel=kernelResult.backgroundModel,
475 psfMatchingKernel=kernelResult.psfMatchingKernel,)
476
477 def finalize(self, template, science, difference, kernel,
478 templateMatched=True,
479 preConvMode=False,
480 preConvKernel=None,
481 spatiallyVarying=False):
482 """Decorrelate the difference image to undo the noise correlations
483 caused by convolution.
484
485 Parameters
486 ----------
487 template : `lsst.afw.image.ExposureF`
488 Template exposure, warped to match the science exposure.
489 science : `lsst.afw.image.ExposureF`
490 Science exposure to subtract from the template.
491 difference : `lsst.afw.image.ExposureF`
492 Result of subtracting template and science.
493 kernel : `lsst.afw.math.Kernel`
494 An (optionally spatially-varying) PSF matching kernel
495 templateMatched : `bool`, optional
496 Was the template PSF-matched to the science image?
497 preConvMode : `bool`, optional
498 Was the science image preconvolved with its own PSF
499 before PSF matching the template?
500 preConvKernel : `lsst.afw.detection.Psf`, optional
501 If not `None`, then the science image was pre-convolved with
502 (the reflection of) this kernel. Must be normalized to sum to 1.
503 spatiallyVarying : `bool`, optional
504 Compute the decorrelation kernel spatially varying across the image?
505
506 Returns
507 -------
508 correctedExposure : `lsst.afw.image.ExposureF`
509 The decorrelated image difference.
510 """
511 # Erase existing detection mask planes.
512 # We don't want the detection mask from the science image
513 mask = difference.mask
514 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
515
516 if self.config.doDecorrelation:
517 self.log.info("Decorrelating image difference.")
518 correctedExposure = self.decorrelate.run(science, template[science.getBBox()], difference, kernel,
519 templateMatched=templateMatched,
520 preConvMode=preConvMode,
521 preConvKernel=preConvKernel,
522 spatiallyVarying=spatiallyVarying).correctedExposure
523 else:
524 self.log.info("NOT decorrelating image difference.")
525 correctedExposure = difference
526 return correctedExposure
527
528 @staticmethod
529 def _validateExposures(template, science):
530 """Check that the WCS of the two Exposures match, and the template bbox
531 contains the science bbox.
532
533 Parameters
534 ----------
535 template : `lsst.afw.image.ExposureF`
536 Template exposure, warped to match the science exposure.
537 science : `lsst.afw.image.ExposureF`
538 Science exposure to subtract from the template.
539
540 Raises
541 ------
542 AssertionError
543 Raised if the WCS of the template is not equal to the science WCS,
544 or if the science image is not fully contained in the template
545 bounding box.
546 """
547 assert template.wcs == science.wcs,\
548 "Template and science exposure WCS are not identical."
549 templateBBox = template.getBBox()
550 scienceBBox = science.getBBox()
551
552 assert templateBBox.contains(scienceBBox),\
553 "Template bbox does not contain all of the science image."
554
555 @staticmethod
556 def _convolveExposure(exposure, kernel, convolutionControl,
557 bbox=None,
558 psf=None,
559 photoCalib=None):
560 """Convolve an exposure with the given kernel.
561
562 Parameters
563 ----------
564 exposure : `lsst.afw.Exposure`
565 exposure to convolve.
567 PSF matching kernel computed in the ``makeKernel`` subtask.
568 convolutionControl : `lsst.afw.math.ConvolutionControl`
569 Configuration for convolve algorithm.
570 bbox : `lsst.geom.Box2I`, optional
571 Bounding box to trim the convolved exposure to.
572 psf : `lsst.afw.detection.Psf`, optional
573 Point spread function (PSF) to set for the convolved exposure.
574 photoCalib : `lsst.afw.image.PhotoCalib`, optional
575 Photometric calibration of the convolved exposure.
576
577 Returns
578 -------
579 convolvedExp : `lsst.afw.Exposure`
580 The convolved image.
581 """
582 convolvedExposure = exposure.clone()
583 if psf is not None:
584 convolvedExposure.setPsf(psf)
585 if photoCalib is not None:
586 convolvedExposure.setPhotoCalib(photoCalib)
587 convolvedImage = lsst.afw.image.MaskedImageF(exposure.getBBox())
588 lsst.afw.math.convolve(convolvedImage, exposure.maskedImage, kernel, convolutionControl)
589 convolvedExposure.setMaskedImage(convolvedImage)
590 if bbox is None:
591 return convolvedExposure
592 else:
593 return convolvedExposure[bbox]
594
595 def _sourceSelector(self, sources):
596 """Select sources from a catalog that meet the selection criteria.
597
598 Parameters
599 ----------
601 Input source catalog to select sources from.
602
603 Returns
604 -------
606 The source catalog filtered to include only the selected sources.
607 """
608 flags = [True, ]*len(sources)
609 for flag in self.config.badSourceFlags:
610 try:
611 flags *= ~sources[flag]
612 except Exception as e:
613 self.log.warning("Could not apply source flag: %s", e)
614 sToNFlag = (sources.getPsfInstFlux()/sources.getPsfInstFluxErr()) > self.config.detectionThreshold
615 flags *= sToNFlag
616 selectSources = sources[flags]
617
618 return selectSources.copy(deep=True)
619
620
621def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.):
622 """Raise NoWorkFound if template coverage < requiredTemplateFraction
623
624 Parameters
625 ----------
626 templateExposure : `lsst.afw.image.ExposureF`
627 The template exposure to check
628 logger : `lsst.log.Log`
629 Logger for printing output.
630 requiredTemplateFraction : `float`, optional
631 Fraction of pixels of the science image required to have coverage
632 in the template.
633
634 Raises
635 ------
636 lsst.pipe.base.NoWorkFound
637 Raised if fraction of good pixels, defined as not having NO_DATA
638 set, is less then the configured requiredTemplateFraction
639 """
640 # Count the number of pixels with the NO_DATA mask bit set
641 # counting NaN pixels is insufficient because pixels without data are often intepolated over)
642 pixNoData = np.count_nonzero(templateExposure.mask.array
643 & templateExposure.mask.getPlaneBitMask('NO_DATA'))
644 pixGood = templateExposure.getBBox().getArea() - pixNoData
645 logger.info("template has %d good pixels (%.1f%%)", pixGood,
646 100*pixGood/templateExposure.getBBox().getArea())
647
648 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
649 message = ("Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
650 "To force subtraction, set config requiredTemplateFraction=0." % (
651 100*pixGood/templateExposure.getBBox().getArea(),
652 100*requiredTemplateFraction))
653 raise lsst.pipe.base.NoWorkFound(message)
654
655
656def _subtractImages(science, template, backgroundModel=None):
657 """Subtract template from science, propagating relevant metadata.
658
659 Parameters
660 ----------
661 science : `lsst.afw.Exposure`
662 The input science image.
663 template : `lsst.afw.Exposure`
664 The template to subtract from the science image.
665 backgroundModel : `lsst.afw.MaskedImage`, optional
666 Differential background model
667
668 Returns
669 -------
670 difference : `lsst.afw.Exposure`
671 The subtracted image.
672 """
673 difference = science.clone()
674 if backgroundModel is not None:
675 difference.maskedImage -= backgroundModel
676 difference.maskedImage -= template.maskedImage
677 return difference
678
679
680def _shapeTest(psf1, psf2):
681 """Determine whether psf1 is narrower in either dimension than psf2.
682
683 Parameters
684 ----------
686 Reference point spread function (PSF) to evaluate.
688 Candidate point spread function (PSF) to evaluate.
689
690 Returns
691 -------
692 `bool`
693 Returns True if psf1 is narrower than psf2 in either dimension.
694 """
695 shape1 = getPsfFwhm(psf1, average=False)
696 shape2 = getPsfFwhm(psf2, average=False)
697 xTest = shape1[0] < shape2[0]
698 yTest = shape1[1] < shape2[1]
699 return xTest | yTest
A polymorphic base class for representing an image's Point Spread Function.
Definition: Psf.h:76
The photometric calibration of an exposure.
Definition: PhotoCalib.h:114
Parameters to control convolution.
Definition: ConvolveImage.h:50
Kernels are used for convolution with MaskedImages and (eventually) Images.
Definition: Kernel.h:110
A kernel that is a linear combination of fixed basis kernels.
Definition: Kernel.h:704
Custom catalog class for ExposureRecord/Table.
Definition: Exposure.h:311
An integer coordinate rectangle.
Definition: Box.h:55
def finalize(self, template, science, difference, kernel, templateMatched=True, preConvMode=False, preConvKernel=None, spatiallyVarying=False)
def _convolveExposure(exposure, kernel, convolutionControl, bbox=None, psf=None, photoCalib=None)
def runConvolveTemplate(self, template, science, selectSources)
def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog)
def runConvolveScience(self, template, science, selectSources)
This static class includes a variety of methods for interacting with the the logging module.
Definition: Log.h:724
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.)