Coverage for python/lsst/ip/isr/linearize.py : 17%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# LSST Data Management System
3# Copyright 2016 AURA/LSST.
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 abc
24import numpy as np
26from lsst.pipe.base import Struct
27from .applyLookupTable import applyLookupTable
29__all__ = ["LinearizeBase", "LinearizeLookupTable", "LinearizeSquared"]
32def getLinearityTypeByName(linearityTypeName):
33 """Determine the linearity class to use if the type is known.
35 Parameters
36 ----------
37 linearityTypeName : str
38 String name of the linearity type that is needed.
40 Returns
41 -------
42 linearityType : `~lsst.ip.isr.linearize.LinearizeSquared`
43 The appropriate linearity class to use. If no matching class
44 is found, `None` is returned.
45 """
46 for t in [LinearizeLookupTable, LinearizeSquared]:
47 if t.LinearityType == linearityTypeName:
48 return t
49 return None
52class LinearizeBase(metaclass=abc.ABCMeta):
53 """Abstract base class functor for correcting non-linearity
55 Subclasses must define __call__ and set class variable LinearityType to a string
56 that will be used for linearity type in AmpInfoCatalog
57 """
58 LinearityType = None # linearity type, a string used for AmpInfoCatalogs
60 @abc.abstractmethod
61 def __call__(self, image, detector, log=None):
62 """Correct non-linearity
64 @param[in] image image to be corrected (an lsst.afw.image.Image)
65 @param[in] detector detector information (an instance of lsst::afw::cameraGeom::Detector)
66 @param[in] log logger (an lsst.log.Log), or None to disable logging;
67 a warning is logged if amplifiers are skipped or other worrisome events occur
69 @return an lsst.pipe.base.Struct containing at least the following fields:
70 - numAmps number of amplifiers found
71 - numLinearized number of amplifiers linearized
73 @throw RuntimeError if the linearity type is wrong
74 @throw a subclass of Exception if linearization fails for any other reason
75 """
76 pass
78 def checkLinearityType(self, detector):
79 """Verify that the linearity type is correct for this detector
81 @warning only checks the first record of the amp info catalog
83 @param[in] detector detector information (an instance of lsst::afw::cameraGeom::Detector)
85 @throw RuntimeError if anything doesn't match
86 """
87 ampInfoType = detector.getAmplifiers()[0].getLinearityType()
88 if self.LinearityType != ampInfoType:
89 raise RuntimeError("Linearity types don't match: %s != %s" % (self.LinearityType, ampInfoType))
92class LinearizeLookupTable(LinearizeBase):
93 """Correct non-linearity with a persisted lookup table
95 for each i,j of image:
96 rowInd = int(c0)
97 colInd = int(c1 + uncorrImage[i,j])
98 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
100 where c0, c1 are collimation coefficients from the AmpInfoTable of the detector:
101 - c0: row index; used to identify which row of the table to use (typically one per amplifier,
102 though one can have multiple amplifiers use the same table)
103 - c1: column index offset; added to the uncorrected image value before truncation;
104 this supports tables that can handle negative image values; also, if the c1 ends with .5
105 then the nearest index is used instead of truncating to the next smaller index
107 In order to keep related data together, the coefficients are persisted along with the table.
108 """
109 LinearityType = "LookupTable"
111 def __init__(self, table, detector):
112 """Construct a LinearizeLookupTable
114 @param[in] table lookup table; a 2-dimensional array of floats:
115 - one row for each row index (value of coef[0] in the amp info catalog)
116 - one column for each image value
117 To avoid copying the table the last index should vary fastest (numpy default "C" order)
118 @param[in] detector detector information (an instance of lsst::afw::cameraGeom::Detector);
119 the name, serial, and amplifier linearization type and coefficients are saved
121 @throw RuntimeError if table is not 2-dimensional,
122 table has fewer columns than rows (indicating that the indices are swapped),
123 or if any row index (linearity coefficient 0) is out of range
124 """
125 LinearizeBase.__init__(self)
127 self._table = np.array(table, order="C")
128 if len(table.shape) != 2:
129 raise RuntimeError("table shape = %s; must have two dimensions" % (table.shape,))
130 if table.shape[1] < table.shape[0]:
131 raise RuntimeError("table shape = %s; indices are switched" % (table.shape,))
133 self._detectorName = detector.getName()
134 self._detectorSerial = detector.getSerial()
135 self.checkLinearityType(detector)
136 ampInfoCat = detector.getAmplifiers()
137 rowIndList = []
138 colIndOffsetList = []
139 numTableRows = table.shape[0]
140 for ampInfo in ampInfoCat:
141 rowInd, colIndOffset = ampInfo.getLinearityCoeffs()[0:2]
142 rowInd = int(rowInd)
143 if rowInd < 0 or rowInd >= numTableRows:
144 raise RuntimeError("Amplifier %s has rowInd=%s not in range[0, %s)" %
145 (ampInfo.getName(), rowInd, numTableRows))
146 rowIndList.append(int(rowInd))
147 colIndOffsetList.append(colIndOffset)
148 self._rowIndArr = np.array(rowIndList, dtype=int)
149 self._colIndOffsetArr = np.array(colIndOffsetList)
151 def __call__(self, image, detector, log=None):
152 """Correct for non-linearity
154 @param[in] image image to be corrected (an lsst.afw.image.Image)
155 @param[in] detector detector info about image (an lsst.afw.cameraGeom.Detector);
156 the name, serial and number of amplifiers must match persisted data;
157 the bbox from each amplifier is read;
158 the linearization coefficients are ignored in favor of the persisted values
159 @param[in] log logger (an lsst.log.Log), or None to disable logging;
160 a warning is logged if any pixels are out of range of their lookup table
162 @return an lsst.pipe.base.Struct containing:
163 - numAmps number of amplifiers found
164 - numLinearized number of amplifiers linearized (always equal to numAmps for this linearizer)
165 - numOutOfRange number of pixels out of range of their lookup table (summed across all amps)
167 @throw RuntimeError if the linearity type is wrong or if the detector name, serial
168 or number of amplifiers does not match the saved data
169 """
170 self.checkDetector(detector)
171 ampInfoCat = detector.getAmplifiers()
172 numOutOfRange = 0
173 for ampInfo, rowInd, colIndOffset in zip(ampInfoCat, self._rowIndArr, self._colIndOffsetArr):
174 bbox = ampInfo.getBBox()
175 ampView = image.Factory(image, bbox)
176 tableRow = self._table[rowInd, :]
177 numOutOfRange += applyLookupTable(ampView, tableRow, colIndOffset)
179 if numOutOfRange > 0 and log is not None:
180 log.warn("%s pixels of detector \"%s\" were out of range of the linearization table",
181 numOutOfRange, detector.getName())
182 numAmps = len(ampInfoCat)
183 return Struct(
184 numAmps=numAmps,
185 numLinearized=numAmps,
186 numOutOfRange=numOutOfRange,
187 )
189 def checkDetector(self, detector):
190 """Check detector name and serial number, ampInfo table length and linearity type
192 @param[in] detector detector info about image (an lsst.afw.cameraGeom.Detector);
194 @throw RuntimeError if anything doesn't match
195 """
196 if self._detectorName != detector.getName():
197 raise RuntimeError("Detector names don't match: %s != %s" %
198 (self._detectorName, detector.getName()))
199 if self._detectorSerial != detector.getSerial():
200 raise RuntimeError("Detector serial numbers don't match: %s != %s" %
201 (self._detectorSerial, detector.getSerial()))
203 numAmps = len(detector.getAmplifiers())
204 if numAmps != len(self._rowIndArr):
205 raise RuntimeError("Detector number of amps = %s does not match saved value %s" %
206 (numAmps, len(self._rowIndArr)))
207 self.checkLinearityType(detector)
210class LinearizeSquared(LinearizeBase):
211 """Correct non-linearity with a squared model
213 corrImage = uncorrImage + c0*uncorrImage^2
215 where c0 is linearity coefficient 0 in the AmpInfoCatalog of the detector
216 """
217 LinearityType = "Squared"
219 def __call__(self, image, detector, log=None):
220 """Correct for non-linearity
222 @param[in] image image to be corrected (an lsst.afw.image.Image)
223 @param[in] detector detector info about image (an lsst.afw.cameraGeom.Detector)
224 @param[in] log logger (an lsst.log.Log), or None to disable logging;
225 a warning is logged if any amplifiers are skipped because the square coefficient is 0
227 @return an lsst.pipe.base.Struct containing at least the following fields:
228 - nAmps number of amplifiers found
229 - nLinearized number of amplifiers linearized
231 @throw RuntimeError if the linearity type is wrong
232 """
233 self.checkLinearityType(detector)
234 ampInfoCat = detector.getAmplifiers()
235 numLinearized = 0
236 for ampInfo in ampInfoCat:
237 sqCoeff = ampInfo.getLinearityCoeffs()[0]
238 if sqCoeff != 0:
239 bbox = ampInfo.getBBox()
240 ampArr = image.Factory(image, bbox).getArray()
241 ampArr *= (1 + sqCoeff*ampArr)
242 numLinearized += 1
244 numAmps = len(ampInfoCat)
245 if numAmps > numLinearized and log is not None:
246 log.warn("%s of %s amps in detector \"%s\" were not linearized (coefficient = 0)",
247 numAmps - numLinearized, numAmps, detector.getName())
248 return Struct(
249 numAmps=numAmps,
250 numLinearized=numLinearized,
251 )