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