Coverage for python/lsst/ip/isr/isrStatistics.py: 25%
66 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-20 09:19 +0000
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-20 09:19 +0000
1# This file is part of ip_isr.
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/>.
21import numpy as np
22import lsst.afw.math as afwMath
23import lsst.afw.image as afwImage
24import lsst.pipe.base as pipeBase
25import lsst.pex.config as pexConfig
27from lsst.afw.cameraGeom import ReadoutCorner
30class IsrStatisticsTaskConfig(pexConfig.Config):
31 """Image statistics options.
32 """
33 doCtiStatistics = pexConfig.Field(
34 dtype=bool,
35 doc="Measure CTI statistics from image and overscans?",
36 default=False,
37 )
38 stat = pexConfig.Field(
39 dtype=str,
40 default='MEANCLIP',
41 doc="Statistic name to use to measure regions.",
42 )
43 nSigmaClip = pexConfig.Field(
44 dtype=float,
45 default=3.0,
46 doc="Clipping threshold for background",
47 )
48 nIter = pexConfig.Field(
49 dtype=int,
50 default=3,
51 doc="Clipping iterations for background",
52 )
53 badMask = pexConfig.ListField(
54 dtype=str,
55 default=["BAD", "INTRP", "SAT"],
56 doc="Mask planes to ignore when identifying source pixels."
57 )
60class IsrStatisticsTask(pipeBase.Task):
61 """Task to measure arbitrary statistics on ISR processed exposures.
63 The goal is to wrap a number of optional measurements that are
64 useful for calibration production and detector stability.
65 """
66 ConfigClass = IsrStatisticsTaskConfig
67 _DefaultName = "isrStatistics"
69 def __init__(self, statControl=None, **kwargs):
70 super().__init__(**kwargs)
71 self.statControl = afwMath.StatisticsControl(self.config.nSigmaClip, self.config.nIter,
72 afwImage.Mask.getPlaneBitMask(self.config.badMask))
73 self.statType = afwMath.stringToStatisticsProperty(self.config.stat)
75 def run(self, inputExp, ptc=None, overscanResults=None, **kwargs):
76 """Task to run arbitrary statistics.
78 The statistics should be measured by individual methods, and
79 add to the dictionary in the return struct.
81 Parameters
82 ----------
83 inputExp : `lsst.afw.image.Exposure`
84 The exposure to measure.
85 ptc : `lsst.ip.isr.PtcDataset`, optional
86 A PTC object containing gains to use.
87 overscanResults : `list` [`lsst.pipe.base.Struct`], optional
88 List of overscan results. Expected fields are:
90 ``imageFit``
91 Value or fit subtracted from the amplifier image data
92 (scalar or `lsst.afw.image.Image`).
93 ``overscanFit``
94 Value or fit subtracted from the overscan image data
95 (scalar or `lsst.afw.image.Image`).
96 ``overscanImage``
97 Image of the overscan region with the overscan
98 correction applied (`lsst.afw.image.Image`). This
99 quantity is used to estimate the amplifier read noise
100 empirically.
102 Returns
103 -------
104 resultStruct : `lsst.pipe.base.Struct`
105 Contains the measured statistics as a dict stored in a
106 field named ``results``.
108 Raises
109 ------
110 RuntimeError
111 Raised if the amplifier gains could not be found.
112 """
113 # Find gains.
114 detector = inputExp.getDetector()
115 if ptc is not None:
116 gains = ptc.gain
117 elif detector is not None:
118 gains = {amp.getName(): amp.getGain() for amp in detector.getAmplifiers()}
119 else:
120 raise RuntimeError("No source of gains provided.")
121 if self.config.doCtiStatistics:
122 ctiResults = self.measureCti(inputExp, overscanResults, gains)
124 return pipeBase.Struct(
125 results={'CTI': ctiResults, },
126 )
128 def measureCti(self, inputExp, overscans, gains):
129 """Task to measure CTI statistics.
131 Parameters
132 ----------
133 inputExp : `lsst.afw.image.Exposure`
134 Exposure to measure.
135 overscans : `list` [`lsst.pipe.base.Struct`]
136 List of overscan results. Expected fields are:
138 ``imageFit``
139 Value or fit subtracted from the amplifier image data
140 (scalar or `lsst.afw.image.Image`).
141 ``overscanFit``
142 Value or fit subtracted from the overscan image data
143 (scalar or `lsst.afw.image.Image`).
144 ``overscanImage``
145 Image of the overscan region with the overscan
146 correction applied (`lsst.afw.image.Image`). This
147 quantity is used to estimate the amplifier read noise
148 empirically.
149 gains : `dict` [`str` `float]
150 Dictionary of per-amplifier gains, indexed by amplifier name.
152 Returns
153 -------
154 outputStats : `dict` [`str`, [`dict` [`str`,`float]]
155 Dictionary of measurements, keyed by amplifier name and
156 statistics segment.
157 """
158 outputStats = {}
160 detector = inputExp.getDetector()
161 image = inputExp.image
163 # Ensure we have the same number of overscans as amplifiers.
164 assert len(overscans) == len(detector.getAmplifiers())
166 for ampIter, amp in enumerate(detector.getAmplifiers()):
167 ampStats = {}
168 gain = gains[amp.getName()]
169 readoutCorner = amp.getReadoutCorner()
170 # Full data region.
171 dataRegion = image[amp.getBBox()]
172 ampStats['IMAGE_MEAN'] = afwMath.makeStatistics(dataRegion, self.statType,
173 self.statControl).getValue()
175 # First and last image columns.
176 pixelA = afwMath.makeStatistics(dataRegion.array[:, 0],
177 self.statType,
178 self.statControl).getValue()
179 pixelZ = afwMath.makeStatistics(dataRegion.array[:, -1],
180 self.statType,
181 self.statControl).getValue()
183 # We want these relative to the readout corner. If that's
184 # on the right side, we need to swap them.
185 if readoutCorner in (ReadoutCorner.LR, ReadoutCorner.UR):
186 ampStats['FIRST_MEAN'] = pixelZ
187 ampStats['LAST_MEAN'] = pixelA
188 else:
189 ampStats['FIRST_MEAN'] = pixelA
190 ampStats['LAST_MEAN'] = pixelZ
192 # Measure the columns of the overscan.
193 if overscans[ampIter] is None:
194 # The amplifier is likely entirely bad, and needs to
195 # be skipped.
196 self.log.warn("No overscan information available for ISR statistics for amp %s.",
197 amp.getName())
198 nCols = amp.getSerialOverscanBBox().getWidth()
199 ampStats['OVERSCAN_COLUMNS'] = np.full((nCols, ), np.nan)
200 ampStats['OVERSCAN_VALUES'] = np.full((nCols, ), np.nan)
201 else:
202 overscanImage = overscans[ampIter].overscanImage
203 columns = []
204 values = []
205 for column in range(0, overscanImage.getWidth()):
206 osMean = afwMath.makeStatistics(overscanImage.image.array[:, column],
207 self.statType, self.statControl).getValue()
208 columns.append(column)
209 values.append(gain * osMean)
211 # We want these relative to the readout corner. If that's
212 # on the right side, we need to swap them.
213 if readoutCorner in (ReadoutCorner.LR, ReadoutCorner.UR):
214 ampStats['OVERSCAN_COLUMNS'] = list(reversed(columns))
215 ampStats['OVERSCAN_VALUES'] = list(reversed(values))
216 else:
217 ampStats['OVERSCAN_COLUMNS'] = columns
218 ampStats['OVERSCAN_VALUES'] = values
220 outputStats[amp.getName()] = ampStats
222 return outputStats