lsst.ip.isr  13.0-15-g0af5a6c+27
crosstalk.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2017 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 <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 """
23 Apply intra-CCD crosstalk corrections
24 """
25 from __future__ import absolute_import, division, print_function
26 
27 __all__ = ["CrosstalkConfig", "CrosstalkTask", "subtractCrosstalk"]
28 
29 from builtins import zip
30 
31 import numpy as np
32 
33 import lsst.afw.math
34 import lsst.afw.table
35 import lsst.afw.detection
36 
37 from lsst.pex.config import Config, Field, ListField
38 from lsst.pipe.base import Task
39 
40 
41 class CrosstalkConfig(Config):
42  """Configuration for intra-CCD crosstalk removal"""
43  minPixelToMask = Field(dtype=float, default=45000,
44  doc="Set crosstalk mask plane for pixels over this value")
45  crosstalkMaskPlane = Field(dtype=str, default="CROSSTALK", doc="Name for crosstalk mask plane")
46 
47 
48 class CrosstalkTask(Task):
49  """Apply intra-CCD crosstalk correction"""
50  ConfigClass = CrosstalkConfig
51 
52  def run(self, exposure):
53  """Apply intra-CCD crosstalk correction
54 
55  Parameters
56  ----------
57  exposure : `lsst.afw.image.Exposure`
58  Exposure for which to remove crosstalk.
59  """
60  detector = exposure.getDetector()
61  if not detector.hasCrosstalk():
62  self.log.warn("Crosstalk correction skipped: no crosstalk coefficients for detector")
63  return
64  self.log.info("Applying crosstalk correction")
65  numAmps = len(exposure.getDetector())
66  subtractCrosstalk(exposure, minPixelToMask=self.config.minPixelToMask,
67  crosstalkStr=self.config.crosstalkMaskPlane)
68 
69 
70 # Flips required to get the corner to the lower-left
71 # (an arbitrary choice; flips are relative, so the choice of reference here is not important)
72 X_FLIP = {lsst.afw.table.LL: False, lsst.afw.table.LR: True,
73  lsst.afw.table.UL: False, lsst.afw.table.UR: True}
74 Y_FLIP = {lsst.afw.table.LL: False, lsst.afw.table.LR: False,
75  lsst.afw.table.UL: True, lsst.afw.table.UR: True}
76 
77 
78 def extractAmp(image, amp, corner):
79  """Return an image of the amp
80 
81  The returned image will have the amp's readout corner in the
82  nominated `corner`.
83 
84  Parameters
85  ----------
86  image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
87  Image containing the amplifier of interest.
88  amp : `lsst.afw.table.AmpInfoRecord`
89  Amplifier information.
90  corner : `lsst.afw.table.ReadoutCorner` or `None`
91  Corner in which to put the amp's readout corner, or `None` for
92  no flipping.
93 
94  Returns
95  -------
96  output : `lsst.afw.image.Image`
97  Image of the amplifier in the standard configuration.
98  """
99  output = image.Factory(image, amp.getBBox())
100  ampCorner = amp.getReadoutCorner()
101  # Flipping is necessary only if the desired configuration doesn't match what we currently have
102  xFlip = X_FLIP[corner] ^ X_FLIP[ampCorner]
103  yFlip = Y_FLIP[corner] ^ Y_FLIP[ampCorner]
104  return lsst.afw.math.flipImage(output, xFlip, yFlip)
105 
106 
107 def calculateBackground(mi, badPixels=["BAD"]):
108  """Calculate median background in image
109 
110  Getting a great background model isn't important for crosstalk correction,
111  since the crosstalk is at a low level. The median should be sufficient.
112 
113  Parameters
114  ----------
115  mi : `lsst.afw.image.MaskedImage`
116  MaskedImage for which to measure background.
117  badPixels : `list` of `str`
118  Mask planes to ignore.
119 
120  Returns
121  -------
122  bg : `float`
123  Median background level.
124  """
125  mask = mi.getMask()
126  stats = lsst.afw.math.StatisticsControl()
127  stats.setAndMask(mask.getPlaneBitMask(badPixels))
128  return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue()
129 
130 
131 def subtractCrosstalk(exposure, badPixels=["BAD"], minPixelToMask=45000, crosstalkStr="CROSSTALK"):
132  """Subtract the intra-CCD crosstalk from an exposure
133 
134  We set the mask plane indicated by ``crosstalkStr`` in a target amplifier
135  for pixels in a source amplifier that exceed `minPixelToMask`. Note that
136  the correction is applied to all pixels in the amplifier, but only those
137  that have a substantial crosstalk are masked with ``crosstalkStr``.
138 
139  The uncorrected image is used as a template for correction. This is good
140  enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's
141  larger you may want to iterate.
142 
143  Parameters
144  ----------
145  exposure : `lsst.afw.image.Exposure`
146  Exposure for which to subtract crosstalk.
147  badPixels : `list` of `str`
148  Mask planes to ignore.
149  minPixelToMask : `float`
150  Minimum pixel value in source amplifier for which to set
151  ``crosstalkStr`` mask plane in target amplifier.
152  crosstalkStr : `str`
153  Mask plane name for pixels greatly modified by crosstalk.
154  """
155  mi = exposure.getMaskedImage()
156  mask = mi.getMask()
157 
158  ccd = exposure.getDetector()
159  numAmps = len(ccd)
160  coeffs = ccd.getCrosstalk()
161  assert coeffs.shape == (numAmps, numAmps)
162 
163  # Set the crosstalkStr bit for the bright pixels (those which will have significant crosstalk correction)
164  crosstalkPlane = mask.addMaskPlane(crosstalkStr)
165  footprints = lsst.afw.detection.FootprintSet(mi, lsst.afw.detection.Threshold(minPixelToMask))
166  footprints.setMask(mask, crosstalkStr)
167  crosstalk = mask.getPlaneBitMask(crosstalkStr)
168 
169  backgrounds = [calculateBackground(mi.Factory(mi, amp.getBBox()), badPixels) for amp in ccd]
170 
171  subtrahend = mi.Factory(mi.getBBox())
172  subtrahend.set((0, 0, 0))
173  for ii, iAmp in enumerate(ccd):
174  iImage = subtrahend.Factory(subtrahend, iAmp.getBBox())
175  for jj, jAmp in enumerate(ccd):
176  if ii == jj:
177  assert coeffs[ii, jj] == 0.0
178  if coeffs[ii, jj] == 0.0:
179  continue
180 
181  jImage = extractAmp(mi, jAmp, iAmp.getReadoutCorner())
182  jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks
183  jImage -= backgrounds[jj]
184 
185  iImage.scaledPlus(coeffs[ii, jj], jImage)
186 
187  # Set crosstalkStr bit only for those pixels that have been significantly modified (i.e., those
188  # masked as such in 'subtrahend'), not necessarily those that are bright originally.
189  mask.clearMaskPlane(crosstalkPlane)
190  mi -= subtrahend # also sets crosstalkStr bit for bright pixels
def subtractCrosstalk(exposure, badPixels=["BAD"], minPixelToMask=45000, crosstalkStr="CROSSTALK")
Definition: crosstalk.py:131
def extractAmp(image, amp, corner)
Definition: crosstalk.py:78
def calculateBackground(mi, badPixels=["BAD"])
Definition: crosstalk.py:107