LSSTApplications  17.0+10,17.0+51,17.0+88,18.0.0+10,18.0.0+15,18.0.0+34,18.0.0+4,18.0.0+6,18.0.0-2-ge43143a+6,18.1.0-1-g0001055+2,18.1.0-1-g0896a44+10,18.1.0-1-g1349e88+9,18.1.0-1-g2505f39+7,18.1.0-1-g380d4d4+9,18.1.0-1-g5e4b7ea+2,18.1.0-1-g7e8fceb,18.1.0-1-g85f8cd4+7,18.1.0-1-g9a6769a+3,18.1.0-1-ga1a4c1a+6,18.1.0-1-gc037db8+2,18.1.0-1-gd55f500+3,18.1.0-1-ge10677a+7,18.1.0-10-g73b8679e+12,18.1.0-12-gf30922b,18.1.0-13-g451e75588,18.1.0-13-gbfe7f7f,18.1.0-2-g31c43f9+7,18.1.0-2-g9c63283+9,18.1.0-2-gdf0b915+9,18.1.0-2-gf03bb23+2,18.1.0-3-g52aa583+3,18.1.0-3-g8f4a2b1+1,18.1.0-3-g9cb968e+8,18.1.0-4-g7bbbad0,18.1.0-5-g510c42a+8,18.1.0-5-ga46117f,18.1.0-5-gaeab27e+9,18.1.0-6-gdda7f3e+11,18.1.0-8-g4084bf03+1,w.2019.34
LSSTDataManagementBasePackage
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-detector crosstalk corrections
24 """
25 import numpy as np
26 
27 import lsst.afw.math
28 import lsst.afw.table
29 import lsst.afw.detection
30 from lsst.pex.config import Config, Field, ChoiceField, ListField
31 from lsst.pipe.base import Task
32 
33 __all__ = ["CrosstalkConfig", "CrosstalkTask", "subtractCrosstalk", "writeCrosstalkCoeffs",
34  "NullCrosstalkTask"]
35 
36 
38  """Configuration for intra-detector crosstalk removal."""
39  minPixelToMask = Field(
40  dtype=float,
41  doc="Set crosstalk mask plane for pixels over this value.",
42  default=45000
43  )
44  crosstalkMaskPlane = Field(
45  dtype=str,
46  doc="Name for crosstalk mask plane.",
47  default="CROSSTALK"
48  )
49  crosstalkBackgroundMethod = ChoiceField(
50  dtype=str,
51  doc="Type of background subtraction to use when applying correction.",
52  default="None",
53  allowed={
54  "None": "Do no background subtraction.",
55  "AMP": "Subtract amplifier-by-amplifier background levels.",
56  "DETECTOR": "Subtract detector level background."
57  },
58  )
59  useConfigCoefficients = Field(
60  dtype=bool,
61  doc="Ignore the detector crosstalk information in favor of CrosstalkConfig values?",
62  default=False,
63  )
64  crosstalkValues = ListField(
65  dtype=float,
66  doc=("Amplifier-indexed crosstalk coefficients to use. This should be arranged as a 1 x nAmp**2 "
67  "list of coefficients, such that when reshaped by crosstalkShape, the result is nAmp x nAmp. "
68  "This matrix should be structured so CT * [amp0 amp1 amp2 ...]^T returns the column "
69  "vector [corr0 corr1 corr2 ...]^T."),
70  default=[0.0],
71  )
72  crosstalkShape = ListField(
73  dtype=int,
74  doc="Shape of the coefficient array. This should be equal to [nAmp, nAmp].",
75  default=[1],
76  )
77 
78  def getCrosstalk(self, detector=None):
79  """Return a 2-D numpy array of crosstalk coefficients in the proper shape.
80 
81  Parameters
82  ----------
83  detector : `lsst.afw.cameraGeom.detector`
84  Detector that is to be crosstalk corrected.
85 
86  Returns
87  -------
88  coeffs : `numpy.ndarray`
89  Crosstalk coefficients that can be used to correct the detector.
90 
91  Raises
92  ------
93  RuntimeError
94  Raised if no coefficients could be generated from this detector/configuration.
95  """
96  if self.useConfigCoefficients is True:
97  coeffs = np.array(self.crosstalkValues).reshape(self.crosstalkShape)
98  if detector is not None:
99  nAmp = len(detector)
100  if coeffs.shape != (nAmp, nAmp):
101  raise RuntimeError("Constructed crosstalk coeffients do not match detector shape. " +
102  f"{coeffs.shape} {nAmp}")
103  return coeffs
104  elif detector is not None and detector.hasCrosstalk() is True:
105  # Assume the detector defines itself consistently.
106  return detector.getCrosstalk()
107  else:
108  raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients")
109 
110  def hasCrosstalk(self, detector=None):
111  """Return a boolean indicating if crosstalk coefficients exist.
112 
113  Parameters
114  ----------
115  detector : `lsst.afw.cameraGeom.detector`
116  Detector that is to be crosstalk corrected.
117 
118  Returns
119  -------
120  hasCrosstalk : `bool`
121  True if this detector/configuration has crosstalk coefficients defined.
122  """
123  if self.useConfigCoefficients is True and self.crosstalkValues is not None:
124  return True
125  elif detector is not None and detector.hasCrosstalk() is True:
126  return True
127  else:
128  return False
129 
130 
132  """Apply intra-detector crosstalk correction."""
133  ConfigClass = CrosstalkConfig
134  _DefaultName = 'isrCrosstalk'
135 
136  def prepCrosstalk(self, dataRef):
137  """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk.
138 
139  Parameters
140  ----------
141  dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
142  Butler reference of the detector data to be processed.
143 
144  See also
145  --------
146  lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk
147  """
148  return
149 
150  def run(self, exposure, crosstalkSources=None, isTrimmed=False):
151  """Apply intra-detector crosstalk correction
152 
153  Parameters
154  ----------
155  exposure : `lsst.afw.image.Exposure`
156  Exposure for which to remove crosstalk.
157  crosstalkSources : `defaultdict`, optional
158  Image data and crosstalk coefficients from other detectors/amps that are
159  sources of crosstalk in exposure.
160  The default for intra-detector crosstalk here is None.
161  isTrimmed : `bool`
162  The image is already trimmed.
163  This should no longer be needed once DM-15409 is resolved.
164 
165  Raises
166  ------
167  RuntimeError
168  Raised if called for a detector that does not have a
169  crosstalk correction
170  """
171  detector = exposure.getDetector()
172  if not self.config.hasCrosstalk(detector=detector):
173  raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients")
174  coeffs = self.config.getCrosstalk(detector=detector)
175 
176  self.log.info("Applying crosstalk correction.")
177  subtractCrosstalk(exposure, crosstalkCoeffs=coeffs,
178  minPixelToMask=self.config.minPixelToMask,
179  crosstalkStr=self.config.crosstalkMaskPlane, isTrimmed=isTrimmed,
180  backgroundMethod=self.config.crosstalkBackgroundMethod)
181 
182 
183 # Flips required to get the corner to the lower-left
184 # (an arbitrary choice; flips are relative, so the choice of reference here is not important)
185 X_FLIP = {lsst.afw.table.LL: False, lsst.afw.table.LR: True,
186  lsst.afw.table.UL: False, lsst.afw.table.UR: True}
187 Y_FLIP = {lsst.afw.table.LL: False, lsst.afw.table.LR: False,
188  lsst.afw.table.UL: True, lsst.afw.table.UR: True}
189 
190 
192  def run(self, exposure, crosstalkSources=None):
193  self.log.info("Not performing any crosstalk correction")
194 
195 
196 def extractAmp(image, amp, corner, isTrimmed=False):
197  """Return an image of the amp
198 
199  The returned image will have the amp's readout corner in the
200  nominated `corner`.
201 
202  Parameters
203  ----------
204  image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
205  Image containing the amplifier of interest.
206  amp : `lsst.afw.table.AmpInfoRecord`
207  Amplifier information.
208  corner : `lsst.afw.table.ReadoutCorner` or `None`
209  Corner in which to put the amp's readout corner, or `None` for
210  no flipping.
211  isTrimmed : `bool`
212  The image is already trimmed.
213  This should no longer be needed once DM-15409 is resolved.
214 
215  Returns
216  -------
217  output : `lsst.afw.image.Image`
218  Image of the amplifier in the standard configuration.
219  """
220  output = image[amp.getBBox() if isTrimmed else amp.getRawDataBBox()]
221  ampCorner = amp.getReadoutCorner()
222  # Flipping is necessary only if the desired configuration doesn't match what we currently have
223  xFlip = X_FLIP[corner] ^ X_FLIP[ampCorner]
224  yFlip = Y_FLIP[corner] ^ Y_FLIP[ampCorner]
225  return lsst.afw.math.flipImage(output, xFlip, yFlip)
226 
227 
228 def calculateBackground(mi, badPixels=["BAD"]):
229  """Calculate median background in image
230 
231  Getting a great background model isn't important for crosstalk correction,
232  since the crosstalk is at a low level. The median should be sufficient.
233 
234  Parameters
235  ----------
236  mi : `lsst.afw.image.MaskedImage`
237  MaskedImage for which to measure background.
238  badPixels : `list` of `str`
239  Mask planes to ignore.
240 
241  Returns
242  -------
243  bg : `float`
244  Median background level.
245  """
246  mask = mi.getMask()
248  stats.setAndMask(mask.getPlaneBitMask(badPixels))
249  return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue()
250 
251 
252 def subtractCrosstalk(exposure, crosstalkCoeffs=None,
253  badPixels=["BAD"], minPixelToMask=45000,
254  crosstalkStr="CROSSTALK", isTrimmed=False,
255  backgroundMethod="None"):
256  """Subtract the intra-detector crosstalk from an exposure
257 
258  We set the mask plane indicated by ``crosstalkStr`` in a target amplifier
259  for pixels in a source amplifier that exceed `minPixelToMask`. Note that
260  the correction is applied to all pixels in the amplifier, but only those
261  that have a substantial crosstalk are masked with ``crosstalkStr``.
262 
263  The uncorrected image is used as a template for correction. This is good
264  enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's
265  larger you may want to iterate.
266 
267  This method needs unittests (DM-18876), but such testing requires
268  DM-18610 to allow the test detector to have the crosstalk
269  parameters set.
270 
271  Parameters
272  ----------
273  exposure : `lsst.afw.image.Exposure`
274  Exposure for which to subtract crosstalk.
275  crosstalkCoeffs : `numpy.ndarray`
276  Coefficients to use to correct crosstalk.
277  badPixels : `list` of `str`
278  Mask planes to ignore.
279  minPixelToMask : `float`
280  Minimum pixel value (relative to the background level) in
281  source amplifier for which to set ``crosstalkStr`` mask plane
282  in target amplifier.
283  crosstalkStr : `str`
284  Mask plane name for pixels greatly modified by crosstalk.
285  isTrimmed : `bool`
286  The image is already trimmed.
287  This should no longer be needed once DM-15409 is resolved.
288  backgroundMethod : `str`
289  Method used to subtract the background. "AMP" uses
290  amplifier-by-amplifier background levels, "DETECTOR" uses full
291  exposure/maskedImage levels. Any other value results in no
292  background subtraction.
293  """
294  mi = exposure.getMaskedImage()
295  mask = mi.getMask()
296 
297  ccd = exposure.getDetector()
298  numAmps = len(ccd)
299  if crosstalkCoeffs is None:
300  coeffs = ccd.getCrosstalk()
301  else:
302  coeffs = crosstalkCoeffs
303  assert coeffs.shape == (numAmps, numAmps)
304 
305  # Set background level based on the requested method. The
306  # thresholdBackground holds the offset needed so that we only mask
307  # pixels high relative to the background, not in an absolute
308  # sense.
309  thresholdBackground = calculateBackground(mi, badPixels)
310 
311  backgrounds = [0.0 for amp in ccd]
312  if backgroundMethod is None:
313  pass
314  elif backgroundMethod == "AMP":
315  backgrounds = [calculateBackground(mi[amp.getBBox()], badPixels) for amp in ccd]
316  elif backgroundMethod == "DETECTOR":
317  backgrounds = [calculateBackground(mi, badPixels) for amp in ccd]
318 
319  # Set the crosstalkStr bit for the bright pixels (those which will have significant crosstalk correction)
320  crosstalkPlane = mask.addMaskPlane(crosstalkStr)
321  footprints = lsst.afw.detection.FootprintSet(mi, lsst.afw.detection.Threshold(minPixelToMask +
322  thresholdBackground))
323  footprints.setMask(mask, crosstalkStr)
324  crosstalk = mask.getPlaneBitMask(crosstalkStr)
325 
326  # Do pixel level crosstalk correction.
327  subtrahend = mi.Factory(mi.getBBox())
328  subtrahend.set((0, 0, 0))
329  for ii, iAmp in enumerate(ccd):
330  iImage = subtrahend[iAmp.getBBox() if isTrimmed else iAmp.getRawDataBBox()]
331  for jj, jAmp in enumerate(ccd):
332  if ii == jj:
333  assert coeffs[ii, jj] == 0.0
334  if coeffs[ii, jj] == 0.0:
335  continue
336 
337  jImage = extractAmp(mi, jAmp, iAmp.getReadoutCorner(), isTrimmed)
338  jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks
339  jImage -= backgrounds[jj]
340 
341  iImage.scaledPlus(coeffs[ii, jj], jImage)
342 
343  # Set crosstalkStr bit only for those pixels that have been significantly modified (i.e., those
344  # masked as such in 'subtrahend'), not necessarily those that are bright originally.
345  mask.clearMaskPlane(crosstalkPlane)
346  mi -= subtrahend # also sets crosstalkStr bit for bright pixels
347 
348 
349 def writeCrosstalkCoeffs(outputFileName, coeff, det=None, crosstalkName="Unknown", indent=2):
350  """Write a yaml file containing the crosstalk coefficients
351 
352  The coeff array is indexed by [i, j] where i and j are amplifiers
353  corresponding to the amplifiers in det
354 
355  Parameters
356  ----------
357  outputFileName : `str`
358  Name of output yaml file
359  coeff : `numpy.array(namp, namp)`
360  numpy array of coefficients
361  det : `lsst.afw.cameraGeom.Detector`
362  Used to provide the list of amplifier names;
363  if None use ['0', '1', ...]
364  ccdType : `str`
365  Name of detector, used to index the yaml file
366  If all detectors are identical could be the type (e.g. ITL)
367  indent : `int`
368  Indent width to use when writing the yaml file
369  """
370 
371  if det is None:
372  ampNames = [str(i) for i in range(coeff.shape[0])]
373  else:
374  ampNames = [a.getName() for a in det]
375 
376  assert coeff.shape == (len(ampNames), len(ampNames))
377 
378  dIndent = indent
379  indent = 0
380  with open(outputFileName, "w") as fd:
381  print(indent*" " + "crosstalk :", file=fd)
382  indent += dIndent
383  print(indent*" " + "%s :" % crosstalkName, file=fd)
384  indent += dIndent
385 
386  for i, ampNameI in enumerate(ampNames):
387  print(indent*" " + "%s : {" % ampNameI, file=fd)
388  indent += dIndent
389  print(indent*" ", file=fd, end='')
390 
391  for j, ampNameJ in enumerate(ampNames):
392  print("%s : %11.4e, " % (ampNameJ, coeff[i, j]), file=fd,
393  end='\n' + indent*" " if j%4 == 3 else '')
394  print("}", file=fd)
395 
396  indent -= dIndent
def run(self, exposure, crosstalkSources=None, isTrimmed=False)
Definition: crosstalk.py:150
std::shared_ptr< ImageT > flipImage(ImageT const &inImage, bool flipLR, bool flipTB)
Flip an image left–right and/or top–bottom.
Definition: rotateImage.cc:92
A Threshold is used to pass a threshold value to detection algorithms.
Definition: Threshold.h:43
def run(self, exposure, crosstalkSources=None)
Definition: crosstalk.py:192
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
Pass parameters to a Statistics object.
Definition: Statistics.h:93
def calculateBackground(mi, badPixels=["BAD"])
Definition: crosstalk.py:228
def hasCrosstalk(self, detector=None)
Definition: crosstalk.py:110
def extractAmp(image, amp, corner, isTrimmed=False)
Definition: crosstalk.py:196
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
def getCrosstalk(self, detector=None)
Definition: crosstalk.py:78
def subtractCrosstalk(exposure, crosstalkCoeffs=None, badPixels=["BAD"], minPixelToMask=45000, crosstalkStr="CROSSTALK", isTrimmed=False, backgroundMethod="None")
Definition: crosstalk.py:255
def writeCrosstalkCoeffs(outputFileName, coeff, det=None, crosstalkName="Unknown", indent=2)
Definition: crosstalk.py:349