lsst.ip.isr  19.0.0-14-g5673ca6
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__ = ["Linearizer",
30  "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared",
31  "LinearizeProportional", "LinearizePolynomial", "LinearizeNone"]
32 
33 
34 class Linearizer(abc.ABC):
35  """Parameter set for linearization.
36 
37  These parameters are included in cameraGeom.Amplifier, but
38  should be accessible externally to allow for testing.
39 
40  Parameters
41  ----------
42  table : `numpy.array`, optional
43  Lookup table; a 2-dimensional array of floats:
44  - one row for each row index (value of coef[0] in the amplifier)
45  - one column for each image value
46  To avoid copying the table the last index should vary fastest
47  (numpy default "C" order)
48  override : `bool`, optional
49  Override the parameters defined in the detector/amplifier.
50  log : `lsst.log.Log`, optional
51  Logger to handle messages.
52 
53  Raises
54  ------
55  RuntimeError :
56  Raised if the supplied table is not 2D, or if the table has fewer
57  columns than rows (indicating that the indices are swapped).
58  """
59  def __init__(self, table=None, detector=None, override=False, log=None):
60  self._detectorName = None
61  self._detectorSerial = None
62 
63  self.linearityCoeffs = dict()
64  self.linearityType = dict()
65  self.linearityThreshold = dict()
66  self.linearityMaximum = dict()
67  self.linearityUnits = dict()
68  self.linearityBBox = dict()
69 
70  self.override = override
71  self.populated = False
72  self.log = log
73 
74  self.tableData = None
75  if table is not None:
76  if len(table.shape) != 2:
77  raise RuntimeError("table shape = %s; must have two dimensions" % (table.shape,))
78  if table.shape[1] < table.shape[0]:
79  raise RuntimeError("table shape = %s; indices are switched" % (table.shape,))
80  self.tableData = np.array(table, order="C")
81 
82  if detector:
83  self.fromDetector(detector)
84 
85  def __call__(self, exposure):
86  """Apply linearity, setting parameters if necessary.
87 
88  Parameters
89  ----------
90  exposure : `lsst.afw.image.Exposure`
91  Exposure to correct.
92 
93  Returns
94  -------
95  output : `lsst.pipe.base.Struct`
96  Linearization results:
97  ``"numAmps"``
98  Number of amplifiers considered.
99  ``"numLinearized"``
100  Number of amplifiers linearized.
101  """
102 
103  def fromDetector(self, detector):
104  """Read linearity parameters from a detector.
105 
106  Parameters
107  ----------
108  detector : `lsst.afw.cameraGeom.detector`
109  Input detector with parameters to use.
110  """
111  self._detectorName = detector.getName()
112  self._detectorSerial = detector.getSerial()
113  self.populated = True
114 
115  # Do not translate Threshold, Maximum, Units.
116  for amp in detector.getAmplifiers():
117  ampName = amp.getName()
118  self.linearityCoeffs[ampName] = amp.getLinearityCoeffs()
119  self.linearityType[ampName] = amp.getLinearityType()
120  self.linearityBBox[ampName] = amp.getBBox()
121 
122  def fromYaml(self, yamlObject):
123  """Read linearity parameters from a dict.
124 
125  Parameters
126  ----------
127  yamlObject : `dict`
128  Dictionary containing detector and amplifier information.
129  """
130  self._detectorName = yamlObject['detectorName']
131  self._detectorSerial = yamlObject['detectorSerial']
132  self.populated = True
133  self.override = True
134 
135  for amp in yamlObject['amplifiers']:
136  ampName = amp['name']
137  self.linearityCoeffs[ampName] = amp.get('linearityCoeffs', None)
138  self.linearityType[ampName] = amp.get('linearityType', 'None')
139  self.linearityBBox[ampName] = amp.get('linearityBBox', None)
140 
141  def toDict(self):
142  """Return linearity parameters as a dict.
143 
144  Returns
145  -------
146  outDict : `dict`:
147  """
148  outDict = {'detectorName': self._detectorName,
149  'detectorSerial': self._detectorSerial,
150  'hasTable': self._table is not None,
151  'amplifiers': dict()}
152  for ampName in self.linearityType:
153  outDict['amplifiers'][ampName] = {'linearityType': self.linearityType[ampName],
154  'linearityCoeffs': self.linearityCoeffs[ampName],
155  'linearityBBox': self.linearityBBox[ampName]}
156 
157  def getLinearityTypeByName(self, linearityTypeName):
158  """Determine the linearity class to use from the type name.
159 
160  Parameters
161  ----------
162  linearityTypeName : str
163  String name of the linearity type that is needed.
164 
165  Returns
166  -------
167  linearityType : `~lsst.ip.isr.linearize.LinearizeBase`
168  The appropriate linearity class to use. If no matching class
169  is found, `None` is returned.
170  """
171  for t in [LinearizeLookupTable,
172  LinearizeSquared,
173  LinearizePolynomial,
174  LinearizeProportional,
175  LinearizeNone]:
176  if t.LinearityType == linearityTypeName:
177  return t
178  return None
179 
180  def validate(self, detector=None, amplifier=None):
181  """Validate linearity for a detector/amplifier.
182 
183  Parameters
184  ----------
185  detector : `lsst.afw.cameraGeom.Detector`, optional
186  Detector to validate, along with its amplifiers.
187  amplifier : `lsst.afw.cameraGeom.Amplifier`, optional
188  Single amplifier to validate.
189 
190  Raises
191  ------
192  RuntimeError :
193  Raised if there is a mismatch in linearity parameters, and
194  the cameraGeom parameters are not being overridden.
195  """
196  amplifiersToCheck = []
197  if detector:
198  if self._detectorName != detector.getName():
199  raise RuntimeError("Detector names don't match: %s != %s" %
200  (self._detectorName, detector.getName()))
201  if self._detectorSerial != detector.getSerial():
202  raise RuntimeError("Detector serial numbers don't match: %s != %s" %
203  (self._detectorSerial, detector.getSerial()))
204  if len(detector.getAmplifiers()) != len(self.linearityCoeffs.keys()):
205  raise RuntimeError("Detector number of amps = %s does not match saved value %s" %
206  (len(detector.getAmplifiers()),
207  len(self.linearityCoeffs.keys())))
208  amplifiersToCheck.extend(detector.getAmplifiers())
209 
210  if amplifier:
211  amplifiersToCheck.extend(amplifier)
212 
213  for amp in amplifiersToCheck:
214  ampName = amp.getName()
215  if ampName not in self.linearityCoeffs.keys():
216  raise RuntimeError("Amplifier %s is not in linearity data" %
217  (ampName, ))
218  if amp.getLinearityType() != self.linearityType[ampName]:
219  if self.override:
220  self.log.warn("Overriding amplifier defined linearityType (%s) for %s",
221  self.linearityType[ampName], ampName)
222  else:
223  raise RuntimeError("Amplifier %s type %s does not match saved value %s" %
224  (ampName, amp.getLinearityType(), self.linearityType[ampName]))
225  if not np.allclose(amp.getLinearityCoeffs(), self.linearityCoeffs[ampName], equal_nan=True):
226  if self.override:
227  self.log.warn("Overriding amplifier defined linearityCoeffs (%s) for %s",
228  self.linearityCoeffs[ampName], ampName)
229  else:
230  raise RuntimeError("Amplifier %s coeffs %s does not match saved value %s" %
231  (ampName, amp.getLinearityCoeffs(), self.linearityCoeffs[ampName]))
232 
233  def applyLinearity(self, image, detector=None, log=None):
234  """Apply the linearity to an image.
235 
236  If the linearity parameters are populated, use those,
237  otherwise use the values from the detector.
238 
239  Parameters
240  ----------
241  image : `~lsst.afw.image.image`
242  Image to correct.
243  detector : `~lsst.afw.cameraGeom.detector`
244  Detector to use for linearity parameters if not already
245  populated.
246  log : `~lsst.log.Log`, optional
247  Log object to use for logging.
248  """
249  if log is None:
250  log = self.log
251 
252  if detector and not self.populated:
253  self.fromDetector(detector)
254 
255  self.validate(detector)
256 
257  numAmps = 0
258  numLinearized = 0
259  numOutOfRange = 0
260  for ampName in self.linearityType.keys():
261  linearizer = self.getLinearityTypeByName(self.linearityType[ampName])
262  numAmps += 1
263  if linearizer is not None:
264  ampView = image.Factory(image, self.linearityBBox[ampName])
265  success, outOfRange = linearizer()(ampView, **{'coeffs': self.linearityCoeffs[ampName],
266  'table': self.tableData,
267  'log': self.log})
268  numOutOfRange += outOfRange
269  if success:
270  numLinearized += 1
271  elif log is not None:
272  log.warn("Amplifier %s did not linearize.",
273  ampName)
274  return Struct(
275  numAmps=numAmps,
276  numLinearized=numLinearized,
277  numOutOfRange=numOutOfRange
278  )
279 
280 
281 class LinearizeBase(metaclass=abc.ABCMeta):
282  """Abstract base class functor for correcting non-linearity.
283 
284  Subclasses must define __call__ and set class variable
285  LinearityType to a string that will be used for linearity type in
286  the cameraGeom.Amplifier.linearityType field.
287 
288  All linearity corrections should be defined in terms of an
289  additive correction, such that:
290 
291  corrected_value = uncorrected_value + f(uncorrected_value)
292  """
293  LinearityType = None # linearity type, a string used for AmpInfoCatalogs
294 
295  @abc.abstractmethod
296  def __call__(self, image, **kwargs):
297  """Correct non-linearity.
298 
299  Parameters
300  ----------
301  image : `lsst.afw.image.Image`
302  Image to be corrected
303  kwargs : `dict`
304  Dictionary of parameter keywords:
305  ``"coeffs"``
306  Coefficient vector (`list` or `numpy.array`).
307  ``"table"``
308  Lookup table data (`numpy.array`).
309  ``"log"``
310  Logger to handle messages (`lsst.log.Log`).
311 
312  Returns
313  -------
314  output : `bool`
315  If true, a correction was applied successfully.
316 
317  Raises
318  ------
319  RuntimeError:
320  Raised if the linearity type listed in the
321  detector does not match the class type.
322  """
323  pass
324 
325 
326 class LinearizeLookupTable(LinearizeBase):
327  """Correct non-linearity with a persisted lookup table.
328 
329  The lookup table consists of entries such that given
330  "coefficients" c0, c1:
331 
332  for each i,j of image:
333  rowInd = int(c0)
334  colInd = int(c1 + uncorrImage[i,j])
335  corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd]
336 
337  - c0: row index; used to identify which row of the table to use
338  (typically one per amplifier, though one can have multiple
339  amplifiers use the same table)
340  - c1: column index offset; added to the uncorrected image value
341  before truncation; this supports tables that can handle
342  negative image values; also, if the c1 ends with .5 then
343  the nearest index is used instead of truncating to the
344  next smaller index
345  """
346  LinearityType = "LookupTable"
347 
348  def __call__(self, image, **kwargs):
349  """Correct for non-linearity.
350 
351  Parameters
352  ----------
353  image : `lsst.afw.image.Image`
354  Image to be corrected
355  kwargs : `dict`
356  Dictionary of parameter keywords:
357  ``"coeffs"``
358  Columnation vector (`list` or `numpy.array`).
359  ``"table"``
360  Lookup table data (`numpy.array`).
361  ``"log"``
362  Logger to handle messages (`lsst.log.Log`).
363 
364  Returns
365  -------
366  output : `bool`
367  If true, a correction was applied successfully.
368 
369  Raises
370  ------
371  RuntimeError:
372  Raised if the requested row index is out of the table
373  bounds.
374  """
375  numOutOfRange = 0
376 
377  rowInd, colIndOffset = kwargs['coeffs'][0:2]
378  table = kwargs['table']
379  log = kwargs['log']
380 
381  numTableRows = table.shape[0]
382  rowInd = int(rowInd)
383  if rowInd < 0 or rowInd > numTableRows:
384  raise RuntimeError("LinearizeLookupTable rowInd=%s not in range[0, %s)" %
385  (rowInd, numTableRows))
386  tableRow = table[rowInd, :]
387  numOutOfRange += applyLookupTable(image, tableRow, colIndOffset)
388 
389  if numOutOfRange > 0 and log is not None:
390  log.warn("%s pixels were out of range of the linearization table",
391  numOutOfRange)
392  if numOutOfRange < image.getArray().size:
393  return True, numOutOfRange
394  else:
395  return False, numOutOfRange
396 
397 
399  """Correct non-linearity with a polynomial mode.
400 
401  corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
402 
403  where c_i are the linearity coefficients for each amplifier.
404  Lower order coefficients are not included as they duplicate other
405  calibration parameters:
406  ``"k0"``
407  A coefficient multiplied by uncorrImage**0 is equivalent to
408  bias level. Irrelevant for correcting non-linearity.
409  ``"k1"``
410  A coefficient multiplied by uncorrImage**1 is proportional
411  to the gain. Not necessary for correcting non-linearity.
412  """
413  LinearityType = "Polynomial"
414 
415  def __call__(self, image, **kwargs):
416  """Correct non-linearity.
417 
418  Parameters
419  ----------
420  image : `lsst.afw.image.Image`
421  Image to be corrected
422  kwargs : `dict`
423  Dictionary of parameter keywords:
424  ``"coeffs"``
425  Coefficient vector (`list` or `numpy.array`).
426  ``"log"``
427  Logger to handle messages (`lsst.log.Log`).
428 
429  Returns
430  -------
431  output : `bool`
432  If true, a correction was applied successfully.
433  """
434  if np.any(np.isfinite(kwargs['coeffs'])):
435  return False, 0
436  if not np.any(kwargs['coeffs']):
437  return False, 0
438 
439  ampArray = image.getArray()
440  correction = np.zeroes_like(ampArray)
441  for coeff, order in enumerate(kwargs['coeffs'], start=2):
442  correction += coeff * np.power(ampArray, order)
443  ampArray += correction
444 
445  return True, 0
446 
447 
449  """Correct non-linearity with a squared model.
450 
451  corrImage = uncorrImage + c0*uncorrImage^2
452 
453  where c0 is linearity coefficient 0 for each amplifier.
454  """
455  LinearityType = "Squared"
456 
457  def __call__(self, image, **kwargs):
458  """Correct for non-linearity.
459 
460  Parameters
461  ----------
462  image : `lsst.afw.image.Image`
463  Image to be corrected
464  kwargs : `dict`
465  Dictionary of parameter keywords:
466  ``"coeffs"``
467  Coefficient vector (`list` or `numpy.array`).
468  ``"log"``
469  Logger to handle messages (`lsst.log.Log`).
470 
471  Returns
472  -------
473  output : `bool`
474  If true, a correction was applied successfully.
475  """
476 
477  sqCoeff = kwargs['coeffs'][0]
478  if sqCoeff != 0:
479  ampArr = image.getArray()
480  ampArr *= (1 + sqCoeff*ampArr)
481  return True, 0
482  else:
483  return False, 0
484 
485 
487  """Do not correct non-linearity.
488  """
489  LinearityType = "Proportional"
490 
491  def __call__(self, image, **kwargs):
492  """Do not correct for non-linearity.
493 
494  Parameters
495  ----------
496  image : `lsst.afw.image.Image`
497  Image to be corrected
498  kwargs : `dict`
499  Dictionary of parameter keywords:
500  ``"coeffs"``
501  Coefficient vector (`list` or `numpy.array`).
502  ``"log"``
503  Logger to handle messages (`lsst.log.Log`).
504 
505  Returns
506  -------
507  output : `bool`
508  If true, a correction was applied successfully.
509  """
510  return True, 0
511 
512 
514  """Do not correct non-linearity.
515  """
516  LinearityType = "None"
517 
518  def __call__(self, image, **kwargs):
519  """Do not correct for non-linearity.
520 
521  Parameters
522  ----------
523  image : `lsst.afw.image.Image`
524  Image to be corrected
525  kwargs : `dict`
526  Dictionary of parameter keywords:
527  ``"coeffs"``
528  Coefficient vector (`list` or `numpy.array`).
529  ``"log"``
530  Logger to handle messages (`lsst.log.Log`).
531 
532  Returns
533  -------
534  output : `bool`
535  If true, a correction was applied successfully.
536  """
537  return True, 0
def fromYaml(self, yamlObject)
Definition: linearize.py:122
def __call__(self, exposure)
Definition: linearize.py:85
def __call__(self, image, kwargs)
Definition: linearize.py:457
def getLinearityTypeByName(self, linearityTypeName)
Definition: linearize.py:157
def fromDetector(self, detector)
Definition: linearize.py:103
def __call__(self, image, kwargs)
Definition: linearize.py:296
def __call__(self, image, kwargs)
Definition: linearize.py:518
def applyLinearity(self, image, detector=None, log=None)
Definition: linearize.py:233
def validate(self, detector=None, amplifier=None)
Definition: linearize.py:180
def __init__(self, table=None, detector=None, override=False, log=None)
Definition: linearize.py:59