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