Coverage for python / lsst / pipe / tasks / snapCombine.py: 34%
51 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 09:04 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 09:04 +0000
1# This file is part of pipe_tasks.
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/>.
22__all__ = ["SnapCombineConfig", "SnapCombineTask"]
24import collections.abc
26import lsst.afw.image
27import lsst.pex.config as pexConfig
28import lsst.daf.base as dafBase
29import lsst.afw.image as afwImage
30import lsst.pipe.base as pipeBase
31from lsst.coadd.utils import addToCoadd, setCoaddEdgeBits
32from lsst.utils.timer import timeMethod
35class SnapCombineConfig(pexConfig.Config):
36 bad_mask_planes = pexConfig.ListField(
37 dtype=str,
38 doc="Mask planes that, if set, the associated pixels are not included in the combined exposure.",
39 default=(),
40 )
43class SnapCombineTask(pipeBase.Task):
44 """Combine one or two snaps into a single visit image.
45 """
47 ConfigClass = SnapCombineConfig
48 _DefaultName = "snapCombine"
50 def __init__(self, *args, **kwargs):
51 pipeBase.Task.__init__(self, *args, **kwargs)
53 @timeMethod
54 def run(self, exposures):
55 """Combine one or two snaps, returning the combined image.
57 Parameters
58 ----------
59 exposures : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure`]
60 One or two exposures to combine as snaps.
62 Returns
63 -------
64 result : `lsst.pipe.base.Struct`
65 Results as a struct with attributes:
67 ``exposure``
68 Snap-combined exposure.
70 Raises
71 ------
72 RuntimeError
73 Raised if input argument does not contain either 1 or 2 exposures.
74 """
75 if isinstance(exposures, lsst.afw.image.Exposure):
76 return pipeBase.Struct(exposure=exposures)
78 if isinstance(exposures, collections.abc.Sequence) and not isinstance(exposures, str):
79 match len(exposures):
80 case 1:
81 return pipeBase.Struct(exposure=exposures[0])
82 case 2:
83 return self.combine(exposures[0], exposures[1])
84 case n:
85 raise RuntimeError(f"Can only process 1 or 2 snaps, not {n}.")
86 else:
87 raise RuntimeError("`exposures` must be either an afw Exposure (single snap visit), or a "
88 "list/tuple of one or two of them.")
90 def combine(self, snap0, snap1):
91 """Combine two snaps, returning the combined image.
93 Parameters
94 ----------
95 snap0, snap1 : `lsst.afw.image.Exposure`
96 Exposures to combine.
98 Returns
99 -------
100 result : `lsst.pipe.base.Struct`
101 Results as a struct with attributes:
103 ``exposure``
104 Snap-combined exposure.
105 """
106 self.log.info("Merging two snaps with exposure ids: %s, %s", snap0.visitInfo.id, snap1.visitInfo.id)
107 combined = self._add_snaps(snap0, snap1)
109 return pipeBase.Struct(
110 exposure=combined,
111 )
113 def _add_snaps(self, snap0, snap1):
114 """Add two snap exposures together, returning a new exposure.
116 Parameters
117 ----------
118 snap0 : `lsst.afw.image.Exposure`
119 Snap exposure 0.
120 snap1 : `lsst.afw.image.Exposure`
121 Snap exposure 1.
123 Returns
124 -------
125 combined : `lsst.afw.image.Exposure`
126 Combined exposure.
127 """
128 combined = snap0.Factory(snap0, True)
129 combined.maskedImage.set(0)
131 weights = combined.maskedImage.image.Factory(combined.maskedImage.getBBox())
132 weight = 1.0
133 bad_mask = afwImage.Mask.getPlaneBitMask(self.config.bad_mask_planes)
134 addToCoadd(combined.maskedImage, weights, snap0.maskedImage, bad_mask, weight)
135 addToCoadd(combined.maskedImage, weights, snap1.maskedImage, bad_mask, weight)
137 # pre-scaling the weight map instead of post-scaling the combined.maskedImage saves a bit of time
138 # because the weight map is a simple Image instead of a MaskedImage
139 weights *= 0.5 # so result is sum of both images, instead of average
140 combined.maskedImage /= weights
141 setCoaddEdgeBits(combined.maskedImage.getMask(), weights)
143 combined.info.setVisitInfo(self._merge_visit_info(snap0.visitInfo, snap1.visitInfo))
145 return combined
147 def _merge_visit_info(self, info0, info1):
148 """Merge the visitInfo values from the two exposures.
150 In particular:
151 * id will be the id of snap 0.
152 * date will be the average of the dates.
153 * exposure time will be the sum of the times.
155 Parameters
156 ----------
157 info0, info1 : `lsst.afw.image.VisitInfo`
158 Metadata to combine.
160 Returns
161 -------
162 info : `lsst.afw.image.VisitInfo`
163 Combined metadata.
165 """
166 time = info0.exposureTime + info1.exposureTime
167 date = (info0.date.get() + info1.date.get()) / 2.0
168 result = info0.copyWith(exposureTime=time,
169 date=dafBase.DateTime(date)
170 )
171 return result