lsst.ip.isr  17.0.1-9-g77829d8+3
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 
26 import lsst.afw.math
27 import lsst.afw.table
28 import lsst.afw.detection
29 from lsst.pex.config import Config, Field, ChoiceField
30 from lsst.pipe.base import Task
31 
32 __all__ = ["CrosstalkConfig", "CrosstalkTask", "subtractCrosstalk", "writeCrosstalkCoeffs"]
33 
34 
35 class CrosstalkConfig(Config):
36  """Configuration for intra-CCD crosstalk removal"""
37  minPixelToMask = Field(
38  dtype=float,
39  doc="Set crosstalk mask plane for pixels over this value.",
40  default=45000
41  )
42  crosstalkMaskPlane = Field(
43  dtype=str,
44  doc="Name for crosstalk mask plane.",
45  default="CROSSTALK"
46  )
47  crosstalkBackgroundMethod = ChoiceField(
48  dtype=str,
49  doc="Type of background subtraction to use when applying correction.",
50  default="None",
51  allowed={
52  "None": "Do no background subtraction.",
53  "AMP": "Subtract amplifier-by-amplifier background levels.",
54  "DETECTOR": "Subtract detector level background."
55  },
56  )
57 
58 
59 class CrosstalkTask(Task):
60  """Apply intra-CCD crosstalk correction"""
61  ConfigClass = CrosstalkConfig
62 
63  def prepCrosstalk(self, dataRef):
64  """Placeholder for crosstalk preparation method, e.g., for inter-CCD crosstalk.
65 
66  Parameters
67  ----------
68  dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
69  Butler reference of the detector data to be processed.
70 
71  See also
72  --------
73  lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk
74  """
75  return
76 
77  def run(self, exposure, crosstalkSources=None, isTrimmed=False):
78  """Apply intra-CCD crosstalk correction
79 
80  Parameters
81  ----------
82  exposure : `lsst.afw.image.Exposure`
83  Exposure for which to remove crosstalk.
84  crosstalkSources : `defaultdict`, optional
85  Image data and crosstalk coefficients from other CCDs/amps that are
86  sources of crosstalk in exposure.
87  The default for intra-CCD crosstalk here is None.
88  isTrimmed : `bool`
89  The image is already trimmed.
90  This should no longer be needed once DM-15409 is resolved.
91 
92  Raises
93  ------
94  RuntimeError
95  Raised if called for a detector that does not have a
96  crosstalk correction
97  """
98  detector = exposure.getDetector()
99  if not detector.hasCrosstalk():
100  raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients")
101  self.log.info("Applying crosstalk correction")
102  subtractCrosstalk(exposure, minPixelToMask=self.config.minPixelToMask,
103  crosstalkStr=self.config.crosstalkMaskPlane, isTrimmed=isTrimmed,
104  backgroundMethod=self.config.crosstalkBackgroundMethod)
105 
106 
107 # Flips required to get the corner to the lower-left
108 # (an arbitrary choice; flips are relative, so the choice of reference here is not important)
109 X_FLIP = {lsst.afw.table.LL: False, lsst.afw.table.LR: True,
110  lsst.afw.table.UL: False, lsst.afw.table.UR: True}
111 Y_FLIP = {lsst.afw.table.LL: False, lsst.afw.table.LR: False,
112  lsst.afw.table.UL: True, lsst.afw.table.UR: True}
113 
114 
116  def run(self, exposure, crosstalkSources=None):
117  self.log.info("Not performing any crosstalk correction")
118 
119 
120 def extractAmp(image, amp, corner, isTrimmed=False):
121  """Return an image of the amp
122 
123  The returned image will have the amp's readout corner in the
124  nominated `corner`.
125 
126  Parameters
127  ----------
128  image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
129  Image containing the amplifier of interest.
130  amp : `lsst.afw.table.AmpInfoRecord`
131  Amplifier information.
132  corner : `lsst.afw.table.ReadoutCorner` or `None`
133  Corner in which to put the amp's readout corner, or `None` for
134  no flipping.
135  isTrimmed : `bool`
136  The image is already trimmed.
137  This should no longer be needed once DM-15409 is resolved.
138 
139  Returns
140  -------
141  output : `lsst.afw.image.Image`
142  Image of the amplifier in the standard configuration.
143  """
144  output = image[amp.getBBox() if isTrimmed else amp.getRawDataBBox()]
145  ampCorner = amp.getReadoutCorner()
146  # Flipping is necessary only if the desired configuration doesn't match what we currently have
147  xFlip = X_FLIP[corner] ^ X_FLIP[ampCorner]
148  yFlip = Y_FLIP[corner] ^ Y_FLIP[ampCorner]
149  return lsst.afw.math.flipImage(output, xFlip, yFlip)
150 
151 
152 def calculateBackground(mi, badPixels=["BAD"]):
153  """Calculate median background in image
154 
155  Getting a great background model isn't important for crosstalk correction,
156  since the crosstalk is at a low level. The median should be sufficient.
157 
158  Parameters
159  ----------
160  mi : `lsst.afw.image.MaskedImage`
161  MaskedImage for which to measure background.
162  badPixels : `list` of `str`
163  Mask planes to ignore.
164 
165  Returns
166  -------
167  bg : `float`
168  Median background level.
169  """
170  mask = mi.getMask()
172  stats.setAndMask(mask.getPlaneBitMask(badPixels))
173  return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue()
174 
175 
176 def subtractCrosstalk(exposure, badPixels=["BAD"], minPixelToMask=45000,
177  crosstalkStr="CROSSTALK", isTrimmed=False,
178  backgroundMethod="None"):
179  """Subtract the intra-CCD crosstalk from an exposure
180 
181  We set the mask plane indicated by ``crosstalkStr`` in a target amplifier
182  for pixels in a source amplifier that exceed `minPixelToMask`. Note that
183  the correction is applied to all pixels in the amplifier, but only those
184  that have a substantial crosstalk are masked with ``crosstalkStr``.
185 
186  The uncorrected image is used as a template for correction. This is good
187  enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's
188  larger you may want to iterate.
189 
190  This method needs unittests (DM-18876), but such testing requires
191  DM-18610 to allow the test detector to have the crosstalk
192  parameters set.
193 
194  Parameters
195  ----------
196  exposure : `lsst.afw.image.Exposure`
197  Exposure for which to subtract crosstalk.
198  badPixels : `list` of `str`
199  Mask planes to ignore.
200  minPixelToMask : `float`
201  Minimum pixel value (relative to the background level) in
202  source amplifier for which to set ``crosstalkStr`` mask plane
203  in target amplifier.
204  crosstalkStr : `str`
205  Mask plane name for pixels greatly modified by crosstalk.
206  isTrimmed : `bool`
207  The image is already trimmed.
208  This should no longer be needed once DM-15409 is resolved.
209  backgroundMethod : `str`
210  Method used to subtract the background. "AMP" uses
211  amplifier-by-amplifier background levels, "DETECTOR" uses full
212  exposure/maskedImage levels. Any other value results in no
213  background subtraction.
214  """
215  mi = exposure.getMaskedImage()
216  mask = mi.getMask()
217 
218  ccd = exposure.getDetector()
219  numAmps = len(ccd)
220  coeffs = ccd.getCrosstalk()
221  assert coeffs.shape == (numAmps, numAmps)
222 
223  # Set background level based on the requested method. The
224  # thresholdBackground holds the offset needed so that we only mask
225  # pixels high relative to the background, not in an absolute
226  # sense.
227  thresholdBackground = calculateBackground(mi, badPixels)
228 
229  backgrounds = [0.0 for amp in ccd]
230  if backgroundMethod is None:
231  pass
232  elif backgroundMethod == "AMP":
233  backgrounds = [calculateBackground(mi[amp.getBBox()], badPixels) for amp in ccd]
234  elif backgroundMethod == "DETECTOR":
235  backgrounds = [calculateBackground(mi, badPixels) for amp in ccd]
236 
237  # Set the crosstalkStr bit for the bright pixels (those which will have significant crosstalk correction)
238  crosstalkPlane = mask.addMaskPlane(crosstalkStr)
239  footprints = lsst.afw.detection.FootprintSet(mi, lsst.afw.detection.Threshold(minPixelToMask +
240  thresholdBackground))
241  footprints.setMask(mask, crosstalkStr)
242  crosstalk = mask.getPlaneBitMask(crosstalkStr)
243 
244  # Do pixel level crosstalk correction.
245  subtrahend = mi.Factory(mi.getBBox())
246  subtrahend.set((0, 0, 0))
247  for ii, iAmp in enumerate(ccd):
248  iImage = subtrahend[iAmp.getBBox() if isTrimmed else iAmp.getRawDataBBox()]
249  for jj, jAmp in enumerate(ccd):
250  if ii == jj:
251  assert coeffs[ii, jj] == 0.0
252  if coeffs[ii, jj] == 0.0:
253  continue
254 
255  jImage = extractAmp(mi, jAmp, iAmp.getReadoutCorner(), isTrimmed)
256  jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks
257  jImage -= backgrounds[jj]
258 
259  iImage.scaledPlus(coeffs[ii, jj], jImage)
260 
261  # Set crosstalkStr bit only for those pixels that have been significantly modified (i.e., those
262  # masked as such in 'subtrahend'), not necessarily those that are bright originally.
263  mask.clearMaskPlane(crosstalkPlane)
264  mi -= subtrahend # also sets crosstalkStr bit for bright pixels
265 
266 
267 def writeCrosstalkCoeffs(outputFileName, coeff, det=None, crosstalkName="Unknown", indent=2):
268  """Write a yaml file containing the crosstalk coefficients
269 
270  The coeff array is indexed by [i, j] where i and j are amplifiers
271  corresponding to the amplifiers in det
272 
273  Parameters
274  ----------
275  outputFileName : `str`
276  Name of output yaml file
277  coeff : `numpy.array(namp, namp)`
278  numpy array of coefficients
279  det : `lsst.afw.cameraGeom.Detector`
280  Used to provide the list of amplifier names;
281  if None use ['0', '1', ...]
282  ccdType : `str`
283  Name of CCD, used to index the yaml file
284  If all CCDs are identical could be the type (e.g. ITL)
285  indent : `int`
286  Indent width to use when writing the yaml file
287  """
288 
289  if det is None:
290  ampNames = [str(i) for i in range(coeff.shape[0])]
291  else:
292  ampNames = [a.getName() for a in det]
293 
294  assert coeff.shape == (len(ampNames), len(ampNames))
295 
296  dIndent = indent
297  indent = 0
298  with open(outputFileName, "w") as fd:
299  print(indent*" " + "crosstalk :", file=fd)
300  indent += dIndent
301  print(indent*" " + "%s :" % crosstalkName, file=fd)
302  indent += dIndent
303 
304  for i, ampNameI in enumerate(ampNames):
305  print(indent*" " + "%s : {" % ampNameI, file=fd)
306  indent += dIndent
307  print(indent*" ", file=fd, end='')
308 
309  for j, ampNameJ in enumerate(ampNames):
310  print("%s : %11.4e, " % (ampNameJ, coeff[i, j]), file=fd,
311  end='\n' + indent*" " if j%4 == 3 else '')
312  print("}", file=fd)
313 
314  indent -= dIndent
def run(self, exposure, crosstalkSources=None, isTrimmed=False)
Definition: crosstalk.py:77
std::shared_ptr< ImageT > flipImage(ImageT const &inImage, bool flipLR, bool flipTB)
def subtractCrosstalk(exposure, badPixels=["BAD"], minPixelToMask=45000, crosstalkStr="CROSSTALK", isTrimmed=False, backgroundMethod="None")
Definition: crosstalk.py:178
def run(self, exposure, crosstalkSources=None)
Definition: crosstalk.py:116
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
def calculateBackground(mi, badPixels=["BAD"])
Definition: crosstalk.py:152
def extractAmp(image, amp, corner, isTrimmed=False)
Definition: crosstalk.py:120
def prepCrosstalk(self, dataRef)
Definition: crosstalk.py:63
def writeCrosstalkCoeffs(outputFileName, coeff, det=None, crosstalkName="Unknown", indent=2)
Definition: crosstalk.py:267