Coverage for python/lsst/pipe/tasks/scaleZeroPoint.py: 35%
85 statements
« prev ^ index » next coverage.py v6.5.0, created at 2024-02-08 07:10 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2024-02-08 07:10 +0000
1#
2# LSST Data Management System
3# Copyright 2008, 2009, 2010, 2011, 2012 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22import numpy
23import lsst.geom as geom
24import lsst.afw.image as afwImage
25import lsst.pex.config as pexConfig
26import lsst.pipe.base as pipeBase
27from lsst.pipe.tasks.selectImages import BaseSelectImagesTask
29__all__ = ["ImageScaler", "SpatialImageScaler", "ScaleZeroPointTask"]
32class ImageScaler:
33 """A class that scales an image
35 This version uses a single scalar. Fancier versions may use a spatially varying scale.
36 """
38 def __init__(self, scale=1.0):
39 """Construct an ImageScaler
41 @param[in] scale: scale correction to apply (see scaleMaskedImage);
42 """
43 self._scale = scale
45 def scaleMaskedImage(self, maskedImage):
46 """Scale the specified image or masked image in place.
48 @param[in,out] maskedImage: masked image to scale
49 """
50 maskedImage *= self._scale
53class SpatialImageScaler(ImageScaler):
54 """Multiplicative image scaler using interpolation over a grid of points.
56 Contains the x, y positions in tract coordinates and the scale factors.
57 Interpolates only when scaleMaskedImage() or getInterpImage() is called.
59 Currently the only type of 'interpolation' implemented is CONSTANT which calculates the mean.
60 """
62 def __init__(self, interpStyle, xList, yList, scaleList):
63 """Constructor
65 @param[in] interpStyle: interpolation style (CONSTANT is only option)
66 @param[in] xList: list of X pixel positions
67 @param[in] yList: list of Y pixel positions
68 @param[in] scaleList: list of multiplicative scale factors at (x,y)
70 @raise RuntimeError if the lists have different lengths
71 """
72 if len(xList) != len(yList) or len(xList) != len(scaleList):
73 raise RuntimeError(
74 "len(xList)=%s len(yList)=%s, len(scaleList)=%s but all lists must have the same length" %
75 (len(xList), len(yList), len(scaleList)))
77 # Eventually want this do be: self.interpStyle = getattr(afwMath.Interpolate2D, interpStyle)
78 self._xList = xList
79 self._yList = yList
80 self._scaleList = scaleList
82 def scaleMaskedImage(self, maskedImage):
83 """Apply scale correction to the specified masked image
85 @param[in,out] image to scale; scale is applied in place
86 """
87 scale = self.getInterpImage(maskedImage.getBBox())
88 maskedImage *= scale
90 def getInterpImage(self, bbox):
91 """Return an image containing the scale correction with same bounding box as supplied.
93 @param[in] bbox: integer bounding box for image (geom.Box2I)
94 """
95 npoints = len(self._xList)
97 if npoints < 1:
98 raise RuntimeError("Cannot create scaling image. Found no fluxMag0s to interpolate")
100 image = afwImage.ImageF(bbox, numpy.mean(self._scaleList))
102 return image
105class ScaleZeroPointConfig(pexConfig.Config):
106 """Config for ScaleZeroPointTask
107 """
108 zeroPoint = pexConfig.Field(
109 dtype=float,
110 doc="desired photometric zero point",
111 default=27.0,
112 )
115class SpatialScaleZeroPointConfig(ScaleZeroPointConfig):
116 selectFluxMag0 = pexConfig.ConfigurableField(
117 doc="Task to select data to compute spatially varying photometric zeropoint",
118 target=BaseSelectImagesTask,
119 )
121 interpStyle = pexConfig.ChoiceField(
122 dtype=str,
123 doc="Algorithm to interpolate the flux scalings;"
124 "Currently only one choice implemented",
125 default="CONSTANT",
126 allowed={
127 "CONSTANT": "Use a single constant value",
128 }
129 )
132class ScaleZeroPointTask(pipeBase.Task):
133 """Compute scale factor to scale exposures to a desired photometric zero point
135 This simple version assumes that the zero point is spatially invariant.
136 """
137 ConfigClass = ScaleZeroPointConfig
138 _DefaultName = "scaleZeroPoint"
140 def __init__(self, *args, **kwargs):
141 """Construct a ScaleZeroPointTask
142 """
143 pipeBase.Task.__init__(self, *args, **kwargs)
145 # flux at mag=0 is 10^(zeroPoint/2.5) because m = -2.5*log10(F/F0)
146 fluxMag0 = 10**(0.4 * self.config.zeroPoint)
147 self._photoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0.0)
149 def run(self, exposure, dataRef=None):
150 """Scale the specified exposure to the desired photometric zeropoint
152 @param[in,out] exposure: exposure to scale; masked image is scaled in place
153 @param[in] dataRef: dataRef for exposure.
154 Not used, but in API so that users can switch between spatially variant
155 and invariant tasks
156 @return a pipeBase.Struct containing:
157 - imageScaler: the image scaling object used to scale exposure
158 """
159 imageScaler = self.computeImageScaler(exposure=exposure, dataRef=dataRef)
160 mi = exposure.getMaskedImage()
161 imageScaler.scaleMaskedImage(mi)
162 return pipeBase.Struct(
163 imageScaler=imageScaler,
164 )
166 def computeImageScaler(self, exposure, dataRef=None):
167 """Compute image scaling object for a given exposure.
169 @param[in] exposure: exposure for which scaling is desired
170 @param[in] dataRef: dataRef for exposure.
171 Not used, but in API so that users can switch between spatially variant
172 and invariant tasks
173 """
174 scale = self.scaleFromPhotoCalib(exposure.getPhotoCalib()).scale
175 return ImageScaler(scale)
177 def getPhotoCalib(self):
178 """Get desired PhotoCalib
180 @return calibration (lsst.afw.image.PhotoCalib) with fluxMag0 set appropriately for config.zeroPoint
181 """
182 return self._photoCalib
184 def scaleFromPhotoCalib(self, calib):
185 """Compute the scale for the specified PhotoCalib
187 Compute scale, such that if pixelCalib describes the photometric zeropoint of a pixel
188 then the following scales that pixel to the photometric zeropoint specified by config.zeroPoint:
189 scale = computeScale(pixelCalib)
190 pixel *= scale
192 @return a pipeBase.Struct containing:
193 - scale, as described above.
195 @note: returns a struct to leave room for scaleErr in a future implementation.
196 """
197 fluxAtZeroPoint = calib.magnitudeToInstFlux(self.config.zeroPoint)
198 return pipeBase.Struct(
199 scale=1.0 / fluxAtZeroPoint,
200 )
202 def scaleFromFluxMag0(self, fluxMag0):
203 """Compute the scale for the specified fluxMag0
205 This is a wrapper around scaleFromPhotoCalib, which see for more information
207 @param[in] fluxMag0
208 @return a pipeBase.Struct containing:
209 - scale, as described in scaleFromPhotoCalib.
210 """
211 calib = afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0.0)
212 return self.scaleFromPhotoCalib(calib)
215class SpatialScaleZeroPointTask(ScaleZeroPointTask):
216 """Compute spatially varying scale factor to scale exposures to a desired photometric zero point
217 """
218 ConfigClass = SpatialScaleZeroPointConfig
219 _DefaultName = "scaleZeroPoint"
221 def __init__(self, *args, **kwargs):
222 ScaleZeroPointTask.__init__(self, *args, **kwargs)
223 self.makeSubtask("selectFluxMag0")
225 def run(self, exposure, dataRef):
226 """Scale the specified exposure to the desired photometric zeropoint
228 @param[in,out] exposure: exposure to scale; masked image is scaled in place
229 @param[in] dataRef: dataRef for exposure
231 @return a pipeBase.Struct containing:
232 - imageScaler: the image scaling object used to scale exposure
233 """
234 imageScaler = self.computeImageScaler(exposure=exposure, dataRef=dataRef)
235 mi = exposure.getMaskedImage()
236 imageScaler.scaleMaskedImage(mi)
237 return pipeBase.Struct(
238 imageScaler=imageScaler,
239 )
241 def computeImageScaler(self, exposure, dataRef):
242 """Compute image scaling object for a given exposure.
244 @param[in] exposure: exposure for which scaling is desired. Only wcs and bbox are used.
245 @param[in] dataRef: dataRef of exposure
246 dataRef.dataId used to retrieve all applicable fluxMag0's from a database.
247 @return a SpatialImageScaler
248 """
250 wcs = exposure.getWcs()
252 fluxMagInfoList = self.selectFluxMag0.run(dataRef.dataId).fluxMagInfoList
254 xList = []
255 yList = []
256 scaleList = []
258 for fluxMagInfo in fluxMagInfoList:
259 # find center of field in tract coordinates
260 if not fluxMagInfo.coordList:
261 raise RuntimeError("no x,y data for fluxMagInfo")
262 ctr = geom.Extent2D()
263 for coord in fluxMagInfo.coordList:
264 # accumulate x, y
265 ctr += geom.Extent2D(wcs.skyToPixel(coord))
266 # and find average x, y as the center of the chip
267 ctr = geom.Point2D(ctr / len(fluxMagInfo.coordList))
268 xList.append(ctr.getX())
269 yList.append(ctr.getY())
270 scaleList.append(self.scaleFromFluxMag0(fluxMagInfo.fluxMag0).scale)
272 self.log.info("Found %d flux scales for interpolation: %s",
273 len(scaleList), [f"{s:%0.4f}" for s in scaleList])
274 return SpatialImageScaler(
275 interpStyle=self.config.interpStyle,
276 xList=xList,
277 yList=yList,
278 scaleList=scaleList,
279 )