Coverage for python / lsst / pipe / tasks / scaleZeroPoint.py: 40%
95 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:38 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:38 +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/>.
23from __future__ import annotations
25__all__ = ["ImageScaler", "SpatialImageScaler", "ScaleZeroPointTask"]
27import numpy
28import lsst.geom as geom
29import lsst.afw.image as afwImage
30import lsst.pex.config as pexConfig
31import lsst.pipe.base as pipeBase
32from lsst.pipe.tasks.selectImages import BaseSelectImagesTask
33from deprecated.sphinx import deprecated
36class ImageScaler:
37 """A class that scales an image.
39 This version uses a single scalar. Fancier versions may use a spatially varying scale.
41 Parameters
42 ----------
43 scale : `float`, optional
44 Scale correction to apply (see ``scaleMaskedImage``).
45 """
47 def __init__(self, scale=1.0):
48 self._scale = scale
50 # TODO: Remove this property in DM-49402.
51 @property
52 @deprecated("This property will be removed after v30.", version="v30", category=FutureWarning)
53 def scale(self) -> float:
54 """Scale that it applies to a specified image."""
55 return self._scale
57 def scaleMaskedImage(self, maskedImage):
58 """Scale the specified image or masked image in place.
60 Parameters
61 ----------
62 maskedImage : `lsst.afw.image.MaskedImage`
63 Masked image to scale.
64 """
65 maskedImage *= self._scale
68class SpatialImageScaler(ImageScaler):
69 """Multiplicative image scaler using interpolation over a grid of points.
71 Contains the x, y positions in tract coordinates and the scale factors.
72 Interpolates only when scaleMaskedImage() or getInterpImage() is called.
74 Currently the only type of 'interpolation' implemented is CONSTANT which calculates the mean.
76 Parameters
77 ----------
78 interpStyle : `Unknown`
79 Interpolation style (`CONSTANT` is only option).
80 xList : `list` of `int`
81 List of X pixel positions.
82 yList : `list` of `int`
83 List of Y pixel positions.
84 scaleList : `Unknown`
85 List of multiplicative scale factors at (x,y).
87 Raises
88 ------
89 RuntimeError
90 Raised if the lists have different lengths.
91 """
93 def __init__(self, interpStyle, xList, yList, scaleList):
94 if len(xList) != len(yList) or len(xList) != len(scaleList):
95 raise RuntimeError(
96 "len(xList)=%s len(yList)=%s, len(scaleList)=%s but all lists must have the same length" %
97 (len(xList), len(yList), len(scaleList)))
99 # Eventually want this do be: self.interpStyle = getattr(afwMath.Interpolate2D, interpStyle)
100 self._xList = xList
101 self._yList = yList
102 self._scaleList = scaleList
104 # TODO: Remove this property in DM-49402.
105 @property
106 @deprecated("This property will be removed after v30.", version="v30", category=FutureWarning)
107 def scale(self) -> float:
108 """Mean scale that it applies to a specified image."""
109 return numpy.mean(self._scaleList)
111 def scaleMaskedImage(self, maskedImage):
112 """Apply scale correction to the specified masked image.
114 Parameters
115 ----------
116 maskedImage : `lsst.afw.image.MaskedImage`
117 Masked image to scale; scale is applied in place.
118 """
119 scale = self.getInterpImage(maskedImage.getBBox())
120 maskedImage *= scale
122 def getInterpImage(self, bbox):
123 """Return an image containing the scale correction with same bounding box as supplied.
125 Parameters
126 ----------
127 bbox : `lsst.geom.Box2I`
128 Integer bounding box for image.
130 Raises
131 ------
132 RuntimeError
133 Raised if there are no fluxMag0s to interpolate.
134 """
135 npoints = len(self._xList)
137 if npoints < 1:
138 raise RuntimeError("Cannot create scaling image. Found no fluxMag0s to interpolate")
140 image = afwImage.ImageF(bbox, numpy.mean(self._scaleList))
142 return image
145class ScaleZeroPointConfig(pexConfig.Config):
146 """Config for ScaleZeroPointTask.
147 """
149 zeroPoint = pexConfig.Field(
150 dtype=float,
151 doc="desired photometric zero point",
152 default=27.0,
153 )
156class SpatialScaleZeroPointConfig(ScaleZeroPointConfig):
157 selectFluxMag0 = pexConfig.ConfigurableField(
158 doc="Task to select data to compute spatially varying photometric zeropoint",
159 target=BaseSelectImagesTask,
160 )
162 interpStyle = pexConfig.ChoiceField(
163 dtype=str,
164 doc="Algorithm to interpolate the flux scalings;"
165 "Currently only one choice implemented",
166 default="CONSTANT",
167 allowed={
168 "CONSTANT": "Use a single constant value",
169 }
170 )
173class ScaleZeroPointTask(pipeBase.Task):
174 """Compute scale factor to scale exposures to a desired photometric zero point.
176 This simple version assumes that the zero point is spatially invariant.
177 """
179 ConfigClass = ScaleZeroPointConfig
180 _DefaultName = "scaleZeroPoint"
182 def __init__(self, *args, **kwargs):
183 pipeBase.Task.__init__(self, *args, **kwargs)
185 # flux at mag=0 is 10^(zeroPoint/2.5) because m = -2.5*log10(F/F0)
186 fluxMag0 = 10**(0.4 * self.config.zeroPoint)
187 self._photoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0.0)
189 def run(self, exposure, dataRef=None):
190 """Scale the specified exposure to the desired photometric zeropoint.
192 Parameters
193 ----------
194 exposure : `lsst.afw.image.Exposure`
195 Exposure to scale; masked image is scaled in place.
196 dataRef : `Unknown`, optional
197 Data reference for exposure.
198 Not used, but in API so that users can switch between spatially variant
199 and invariant tasks.
201 Returns
202 -------
203 result : `~lsst.pipe.base.Struct`
204 Results as a struct with attributes:
206 ``imageScaler``
207 The image scaling object used to scale exposure.
208 """
209 imageScaler = self.computeImageScaler(exposure=exposure, dataRef=dataRef)
210 mi = exposure.getMaskedImage()
211 imageScaler.scaleMaskedImage(mi)
212 return pipeBase.Struct(
213 imageScaler=imageScaler,
214 )
216 def computeImageScaler(self, exposure, dataRef=None):
217 """Compute image scaling object for a given exposure.
219 Parameters
220 ----------
221 exposure : `lsst.afw.image.Exposure`
222 Exposure for which scaling is desired.
223 dataRef : `Unknown`, optional
224 Data reference for exposure.
225 Not used, but in API so that users can switch between spatially variant
226 and invariant tasks.
227 """
228 scale = self.scaleFromPhotoCalib(exposure.getPhotoCalib()).scale
229 return ImageScaler(scale)
231 def getPhotoCalib(self):
232 """Get desired PhotoCalib.
234 Returns
235 -------
236 calibration : `lsst.afw.image.PhotoCalib`
237 Calibration with ``fluxMag0`` set appropriately for config.zeroPoint.
238 """
239 return self._photoCalib
241 def scaleFromPhotoCalib(self, calib):
242 """Compute the scale for the specified PhotoCalib.
244 Parameter
245 ---------
246 calib : `lsst.afw.image.PhotoCalib`
247 PhotoCalib object to compute the scale from.
249 Returns
250 -------
251 result : `lsst.pipe.base.Struct`
252 Results as a struct with attributes:
254 `scale`
256 Scale, such that if pixelCalib describes the photometric
257 zeropoint of a pixel then the following scales that pixel to
258 the photometric zeropoint specified by config.zeroPoint:
259 ``scale = computeScale(pixelCalib) pixel *= scale``
261 Notes
262 -----
263 Returns a struct to leave room for scaleErr in a future implementation.
264 """
265 fluxAtZeroPoint = calib.magnitudeToInstFlux(self.config.zeroPoint)
266 return pipeBase.Struct(
267 scale=1.0 / fluxAtZeroPoint,
268 )
270 def scaleFromFluxMag0(self, fluxMag0):
271 """Compute the scale for the specified fluxMag0.
273 This is a wrapper around scaleFromPhotoCalib, which see for more information.
275 Parameters
276 ----------
277 fluxMag0 : `float`
278 Flux at magnitude zero.
280 Returns
281 -------
282 result : `lsst.pipe.base.Struct`
283 Results as a struct with attributes:
285 `scale`
287 Scale, such that if pixelCalib describes the photometric zeropoint
288 of a pixel then the following scales that pixel to the photometric
289 zeropoint specified by config.zeroPoint:
290 ``scale = computeScale(pixelCalib)``
291 ``pixel *= scale``
292 """
293 calib = afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0.0)
294 return self.scaleFromPhotoCalib(calib)
297class SpatialScaleZeroPointTask(ScaleZeroPointTask):
298 """Compute spatially varying scale factor to scale exposures to a desired photometric zero point.
299 """
301 ConfigClass = SpatialScaleZeroPointConfig
302 _DefaultName = "scaleZeroPoint"
304 def __init__(self, *args, **kwargs):
305 ScaleZeroPointTask.__init__(self, *args, **kwargs)
306 self.makeSubtask("selectFluxMag0")
308 def run(self, exposure, dataRef):
309 """Scale the specified exposure to the desired photometric zeropoint.
311 Parameters
312 ----------
313 exposure : `lsst.afw.image.Exposure`
314 Exposure to scale; masked image is scaled in place.
315 dataRef : `Unknown`
316 Data reference for exposure.
318 Returns
319 -------
320 result : `lsst.pipe.base.Struct`
321 Results as a struct with attributes:
323 ``imageScaler``
324 The image scaling object used to scale exposure.
325 """
326 imageScaler = self.computeImageScaler(exposure=exposure, dataRef=dataRef)
327 mi = exposure.getMaskedImage()
328 imageScaler.scaleMaskedImage(mi)
329 return pipeBase.Struct(
330 imageScaler=imageScaler,
331 )
333 def computeImageScaler(self, exposure, dataRef):
334 """Compute image scaling object for a given exposure.
336 Parameters
337 ----------
338 exposure : `lsst.afw.image.Exposure`
339 Exposure for which scaling is desired. Only wcs and bbox are used.
340 dataRef : `Unknown`
341 Data reference of exposure.
342 dataRef.dataId used to retrieve all applicable fluxMag0's from a database.
344 Returns
345 -------
346 result : `SpatialImageScaler`
347 """
348 wcs = exposure.getWcs()
350 fluxMagInfoList = self.selectFluxMag0.run(dataRef.dataId).fluxMagInfoList
352 xList = []
353 yList = []
354 scaleList = []
356 for fluxMagInfo in fluxMagInfoList:
357 # find center of field in tract coordinates
358 if not fluxMagInfo.coordList:
359 raise RuntimeError("no x,y data for fluxMagInfo")
360 ctr = geom.Extent2D()
361 for coord in fluxMagInfo.coordList:
362 # accumulate x, y
363 ctr += geom.Extent2D(wcs.skyToPixel(coord))
364 # and find average x, y as the center of the chip
365 ctr = geom.Point2D(ctr / len(fluxMagInfo.coordList))
366 xList.append(ctr.getX())
367 yList.append(ctr.getY())
368 scaleList.append(self.scaleFromFluxMag0(fluxMagInfo.fluxMag0).scale)
370 self.log.info("Found %d flux scales for interpolation: %s",
371 len(scaleList), [f"{s:%0.4f}" for s in scaleList])
372 return SpatialImageScaler(
373 interpStyle=self.config.interpStyle,
374 xList=xList,
375 yList=yList,
376 scaleList=scaleList,
377 )