LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
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 from astropy.table import Table
27 
28 import lsst.afw.math
29 import lsst.afw.detection
30 import lsst.daf.butler
31 from lsst.pex.config import Config, Field, ChoiceField, ListField
32 from lsst.pipe.base import Task
33 
34 from lsst.ip.isr import IsrCalib
35 
36 
37 __all__ = ["CrosstalkCalib", "CrosstalkConfig", "CrosstalkTask",
38  "NullCrosstalkTask"]
39 
40 
42  """Calibration of amp-to-amp crosstalk coefficients.
43 
44  Parameters
45  ----------
46  detector : `lsst.afw.cameraGeom.Detector`, optional
47  Detector to use to pull coefficients from.
48  nAmp : `int`, optional
49  Number of amplifiers to initialize.
50  log : `logging.Logger`, optional
51  Log to write messages to.
52  **kwargs :
53  Parameters to pass to parent constructor.
54 
55  Notes
56  -----
57  The crosstalk attributes stored are:
58 
59  hasCrosstalk : `bool`
60  Whether there is crosstalk defined for this detector.
61  nAmp : `int`
62  Number of amplifiers in this detector.
63  crosstalkShape : `tuple` [`int`, `int`]
64  A tuple containing the shape of the ``coeffs`` matrix. This
65  should be equivalent to (``nAmp``, ``nAmp``).
66  coeffs : `np.ndarray`
67  A matrix containing the crosstalk coefficients. coeff[i][j]
68  contains the coefficients to calculate the contribution
69  amplifier_j has on amplifier_i (each row[i] contains the
70  corrections for detector_i).
71  coeffErr : `np.ndarray`, optional
72  A matrix (as defined by ``coeffs``) containing the standard
73  distribution of the crosstalk measurements.
74  coeffNum : `np.ndarray`, optional
75  A matrix containing the number of pixel pairs used to measure
76  the ``coeffs`` and ``coeffErr``.
77  coeffValid : `np.ndarray`, optional
78  A matrix of Boolean values indicating if the coefficient is
79  valid, defined as abs(coeff) > coeffErr / sqrt(coeffNum).
80  interChip : `dict` [`np.ndarray`]
81  A dictionary keyed by detectorName containing ``coeffs``
82  matrices used to correct for inter-chip crosstalk with a
83  source on the detector indicated.
84 
85  """
86  _OBSTYPE = 'CROSSTALK'
87  _SCHEMA = 'Gen3 Crosstalk'
88  _VERSION = 1.0
89 
90  def __init__(self, detector=None, nAmp=0, **kwargs):
91  self.hasCrosstalkhasCrosstalk = False
92  self.nAmpnAmp = nAmp if nAmp else 0
93  self.crosstalkShapecrosstalkShape = (self.nAmpnAmp, self.nAmpnAmp)
94 
95  self.coeffscoeffs = np.zeros(self.crosstalkShapecrosstalkShape) if self.nAmpnAmp else None
96  self.coeffErrcoeffErr = np.zeros(self.crosstalkShapecrosstalkShape) if self.nAmpnAmp else None
97  self.coeffNumcoeffNum = np.zeros(self.crosstalkShapecrosstalkShape,
98  dtype=int) if self.nAmpnAmp else None
99  self.coeffValidcoeffValid = np.zeros(self.crosstalkShapecrosstalkShape,
100  dtype=bool) if self.nAmpnAmp else None
101  self.interChipinterChip = {}
102 
103  super().__init__(**kwargs)
104  self.requiredAttributesrequiredAttributesrequiredAttributesrequiredAttributes.update(['hasCrosstalk', 'nAmp', 'coeffs',
105  'coeffErr', 'coeffNum', 'coeffValid',
106  'interChip'])
107  if detector:
108  self.fromDetectorfromDetectorfromDetector(detector)
109 
110  def updateMetadata(self, setDate=False, **kwargs):
111  """Update calibration metadata.
112 
113  This calls the base class's method after ensuring the required
114  calibration keywords will be saved.
115 
116  Parameters
117  ----------
118  setDate : `bool`, optional
119  Update the CALIBDATE fields in the metadata to the current
120  time. Defaults to False.
121  kwargs :
122  Other keyword parameters to set in the metadata.
123  """
124  kwargs['DETECTOR'] = self._detectorId_detectorId_detectorId
125  kwargs['DETECTOR_NAME'] = self._detectorName_detectorName_detectorName
126  kwargs['DETECTOR_SERIAL'] = self._detectorSerial_detectorSerial_detectorSerial
127  kwargs['HAS_CROSSTALK'] = self.hasCrosstalkhasCrosstalk
128  kwargs['NAMP'] = self.nAmpnAmp
129  self.crosstalkShapecrosstalkShape = (self.nAmpnAmp, self.nAmpnAmp)
130  kwargs['CROSSTALK_SHAPE'] = self.crosstalkShapecrosstalkShape
131 
132  super().updateMetadata(setDate=setDate, **kwargs)
133 
134  def fromDetector(self, detector, coeffVector=None):
135  """Set calibration parameters from the detector.
136 
137  Parameters
138  ----------
139  detector : `lsst.afw.cameraGeom.Detector`
140  Detector to use to set parameters from.
141  coeffVector : `numpy.array`, optional
142  Use the detector geometry (bounding boxes and flip
143  information), but use ``coeffVector`` instead of the
144  output of ``detector.getCrosstalk()``.
145 
146  Returns
147  -------
148  calib : `lsst.ip.isr.CrosstalkCalib`
149  The calibration constructed from the detector.
150 
151  """
152  if detector.hasCrosstalk() or coeffVector:
153  self._detectorId_detectorId_detectorId = detector.getId()
154  self._detectorName_detectorName_detectorName = detector.getName()
155  self._detectorSerial_detectorSerial_detectorSerial = detector.getSerial()
156 
157  self.nAmpnAmp = len(detector)
158  self.crosstalkShapecrosstalkShape = (self.nAmpnAmp, self.nAmpnAmp)
159 
160  if coeffVector is not None:
161  crosstalkCoeffs = coeffVector
162  else:
163  crosstalkCoeffs = detector.getCrosstalk()
164  if len(crosstalkCoeffs) == 1 and crosstalkCoeffs[0] == 0.0:
165  return self
166  self.coeffscoeffs = np.array(crosstalkCoeffs).reshape(self.crosstalkShapecrosstalkShape)
167 
168  if self.coeffscoeffs.shape != self.crosstalkShapecrosstalkShape:
169  raise RuntimeError("Crosstalk coefficients do not match detector shape. "
170  f"{self.crosstalkShape} {self.nAmp}")
171 
172  self.coeffErrcoeffErr = np.zeros(self.crosstalkShapecrosstalkShape)
173  self.coeffNumcoeffNum = np.zeros(self.crosstalkShapecrosstalkShape, dtype=int)
174  self.coeffValidcoeffValid = np.ones(self.crosstalkShapecrosstalkShape, dtype=bool)
175  self.interChipinterChip = {}
176 
177  self.hasCrosstalkhasCrosstalk = True
178  self.updateMetadataupdateMetadataupdateMetadata()
179  return self
180 
181  @classmethod
182  def fromDict(cls, dictionary):
183  """Construct a calibration from a dictionary of properties.
184 
185  Must be implemented by the specific calibration subclasses.
186 
187  Parameters
188  ----------
189  dictionary : `dict`
190  Dictionary of properties.
191 
192  Returns
193  -------
194  calib : `lsst.ip.isr.CalibType`
195  Constructed calibration.
196 
197  Raises
198  ------
199  RuntimeError :
200  Raised if the supplied dictionary is for a different
201  calibration.
202  """
203  calib = cls()
204 
205  if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
206  raise RuntimeError(f"Incorrect crosstalk supplied. Expected {calib._OBSTYPE}, "
207  f"found {dictionary['metadata']['OBSTYPE']}")
208 
209  calib.setMetadata(dictionary['metadata'])
210 
211  if 'detectorName' in dictionary:
212  calib._detectorName = dictionary.get('detectorName')
213  elif 'DETECTOR_NAME' in dictionary:
214  calib._detectorName = dictionary.get('DETECTOR_NAME')
215  elif 'DET_NAME' in dictionary['metadata']:
216  calib._detectorName = dictionary['metadata']['DET_NAME']
217  else:
218  calib._detectorName = None
219 
220  if 'detectorSerial' in dictionary:
221  calib._detectorSerial = dictionary.get('detectorSerial')
222  elif 'DETECTOR_SERIAL' in dictionary:
223  calib._detectorSerial = dictionary.get('DETECTOR_SERIAL')
224  elif 'DET_SER' in dictionary['metadata']:
225  calib._detectorSerial = dictionary['metadata']['DET_SER']
226  else:
227  calib._detectorSerial = None
228 
229  if 'detectorId' in dictionary:
230  calib._detectorId = dictionary.get('detectorId')
231  elif 'DETECTOR' in dictionary:
232  calib._detectorId = dictionary.get('DETECTOR')
233  elif 'DETECTOR' in dictionary['metadata']:
234  calib._detectorId = dictionary['metadata']['DETECTOR']
235  elif calib._detectorSerial:
236  calib._detectorId = calib._detectorSerial
237  else:
238  calib._detectorId = None
239 
240  if 'instrument' in dictionary:
241  calib._instrument = dictionary.get('instrument')
242  elif 'INSTRUME' in dictionary['metadata']:
243  calib._instrument = dictionary['metadata']['INSTRUME']
244  else:
245  calib._instrument = None
246 
247  calib.hasCrosstalk = dictionary.get('hasCrosstalk',
248  dictionary['metadata'].get('HAS_CROSSTALK', False))
249  if calib.hasCrosstalk:
250  calib.nAmp = dictionary.get('nAmp', dictionary['metadata'].get('NAMP', 0))
251  calib.crosstalkShape = (calib.nAmp, calib.nAmp)
252  calib.coeffs = np.array(dictionary['coeffs']).reshape(calib.crosstalkShape)
253  if 'coeffErr' in dictionary:
254  calib.coeffErr = np.array(dictionary['coeffErr']).reshape(calib.crosstalkShape)
255  else:
256  calib.coeffErr = np.zeros_like(calib.coeffs)
257  if 'coeffNum' in dictionary:
258  calib.coeffNum = np.array(dictionary['coeffNum']).reshape(calib.crosstalkShape)
259  else:
260  calib.coeffNum = np.zeros_like(calib.coeffs, dtype=int)
261  if 'coeffValid' in dictionary:
262  calib.coeffValid = np.array(dictionary['coeffValid']).reshape(calib.crosstalkShape)
263  else:
264  calib.coeffValid = np.ones_like(calib.coeffs, dtype=bool)
265 
266  calib.interChip = dictionary.get('interChip', None)
267  if calib.interChip:
268  for detector in calib.interChip:
269  coeffVector = calib.interChip[detector]
270  calib.interChip[detector] = np.array(coeffVector).reshape(calib.crosstalkShape)
271 
272  calib.updateMetadata()
273  return calib
274 
275  def toDict(self):
276  """Return a dictionary containing the calibration properties.
277 
278  The dictionary should be able to be round-tripped through
279  `fromDict`.
280 
281  Returns
282  -------
283  dictionary : `dict`
284  Dictionary of properties.
285  """
286  self.updateMetadataupdateMetadataupdateMetadata()
287 
288  outDict = {}
289  metadata = self.getMetadatagetMetadata()
290  outDict['metadata'] = metadata
291 
292  outDict['hasCrosstalk'] = self.hasCrosstalkhasCrosstalk
293  outDict['nAmp'] = self.nAmpnAmp
294  outDict['crosstalkShape'] = self.crosstalkShapecrosstalkShape
295 
296  ctLength = self.nAmpnAmp*self.nAmpnAmp
297  outDict['coeffs'] = self.coeffscoeffs.reshape(ctLength).tolist()
298 
299  if self.coeffErrcoeffErr is not None:
300  outDict['coeffErr'] = self.coeffErrcoeffErr.reshape(ctLength).tolist()
301  if self.coeffNumcoeffNum is not None:
302  outDict['coeffNum'] = self.coeffNumcoeffNum.reshape(ctLength).tolist()
303  if self.coeffValidcoeffValid is not None:
304  outDict['coeffValid'] = self.coeffValidcoeffValid.reshape(ctLength).tolist()
305 
306  if self.interChipinterChip:
307  outDict['interChip'] = dict()
308  for detector in self.interChipinterChip:
309  outDict['interChip'][detector] = self.interChipinterChip[detector].reshape(ctLength).tolist()
310 
311  return outDict
312 
313  @classmethod
314  def fromTable(cls, tableList):
315  """Construct calibration from a list of tables.
316 
317  This method uses the `fromDict` method to create the
318  calibration, after constructing an appropriate dictionary from
319  the input tables.
320 
321  Parameters
322  ----------
323  tableList : `list` [`lsst.afw.table.Table`]
324  List of tables to use to construct the crosstalk
325  calibration.
326 
327  Returns
328  -------
329  calib : `lsst.ip.isr.CrosstalkCalib`
330  The calibration defined in the tables.
331 
332  """
333  coeffTable = tableList[0]
334 
335  metadata = coeffTable.meta
336  inDict = dict()
337  inDict['metadata'] = metadata
338  inDict['hasCrosstalk'] = metadata['HAS_CROSSTALK']
339  inDict['nAmp'] = metadata['NAMP']
340 
341  inDict['coeffs'] = coeffTable['CT_COEFFS']
342  if 'CT_ERRORS' in coeffTable.columns:
343  inDict['coeffErr'] = coeffTable['CT_ERRORS']
344  if 'CT_COUNTS' in coeffTable.columns:
345  inDict['coeffNum'] = coeffTable['CT_COUNTS']
346  if 'CT_VALID' in coeffTable.columns:
347  inDict['coeffValid'] = coeffTable['CT_VALID']
348 
349  if len(tableList) > 1:
350  inDict['interChip'] = dict()
351  interChipTable = tableList[1]
352  for record in interChipTable:
353  inDict['interChip'][record['IC_SOURCE_DET']] = record['IC_COEFFS']
354 
355  return cls().fromDict(inDict)
356 
357  def toTable(self):
358  """Construct a list of tables containing the information in this calibration.
359 
360  The list of tables should create an identical calibration
361  after being passed to this class's fromTable method.
362 
363  Returns
364  -------
365  tableList : `list` [`lsst.afw.table.Table`]
366  List of tables containing the crosstalk calibration
367  information.
368 
369  """
370  tableList = []
371  self.updateMetadataupdateMetadataupdateMetadata()
372  catalog = Table([{'CT_COEFFS': self.coeffscoeffs.reshape(self.nAmpnAmp*self.nAmpnAmp),
373  'CT_ERRORS': self.coeffErrcoeffErr.reshape(self.nAmpnAmp*self.nAmpnAmp),
374  'CT_COUNTS': self.coeffNumcoeffNum.reshape(self.nAmpnAmp*self.nAmpnAmp),
375  'CT_VALID': self.coeffValidcoeffValid.reshape(self.nAmpnAmp*self.nAmpnAmp),
376  }])
377  # filter None, because astropy can't deal.
378  inMeta = self.getMetadatagetMetadata().toDict()
379  outMeta = {k: v for k, v in inMeta.items() if v is not None}
380  outMeta.update({k: "" for k, v in inMeta.items() if v is None})
381  catalog.meta = outMeta
382  tableList.append(catalog)
383 
384  if self.interChipinterChip:
385  interChipTable = Table([{'IC_SOURCE_DET': sourceDet,
386  'IC_COEFFS': self.interChipinterChip[sourceDet].reshape(self.nAmpnAmp*self.nAmpnAmp)}
387  for sourceDet in self.interChipinterChip.keys()])
388  tableList.append(interChipTable)
389  return tableList
390 
391  # Implementation methods.
392  @staticmethod
393  def extractAmp(image, amp, ampTarget, isTrimmed=False):
394  """Extract the image data from an amp, flipped to match ampTarget.
395 
396  Parameters
397  ----------
398  image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
399  Image containing the amplifier of interest.
400  amp : `lsst.afw.cameraGeom.Amplifier`
401  Amplifier on image to extract.
402  ampTarget : `lsst.afw.cameraGeom.Amplifier`
403  Target amplifier that the extracted image will be flipped
404  to match.
405  isTrimmed : `bool`
406  The image is already trimmed.
407  TODO : DM-15409 will resolve this.
408 
409  Returns
410  -------
411  output : `lsst.afw.image.Image`
412  Image of the amplifier in the desired configuration.
413  """
414  X_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False,
415  lsst.afw.cameraGeom.ReadoutCorner.LR: True,
416  lsst.afw.cameraGeom.ReadoutCorner.UL: False,
417  lsst.afw.cameraGeom.ReadoutCorner.UR: True}
418  Y_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False,
419  lsst.afw.cameraGeom.ReadoutCorner.LR: False,
420  lsst.afw.cameraGeom.ReadoutCorner.UL: True,
421  lsst.afw.cameraGeom.ReadoutCorner.UR: True}
422 
423  output = image[amp.getBBox() if isTrimmed else amp.getRawDataBBox()]
424  thisAmpCorner = amp.getReadoutCorner()
425  targetAmpCorner = ampTarget.getReadoutCorner()
426 
427  # Flipping is necessary only if the desired configuration doesn't match what we currently have
428  xFlip = X_FLIP[targetAmpCorner] ^ X_FLIP[thisAmpCorner]
429  yFlip = Y_FLIP[targetAmpCorner] ^ Y_FLIP[thisAmpCorner]
430  return lsst.afw.math.flipImage(output, xFlip, yFlip)
431 
432  @staticmethod
433  def calculateBackground(mi, badPixels=["BAD"]):
434  """Estimate median background in image.
435 
436  Getting a great background model isn't important for crosstalk correction,
437  since the crosstalk is at a low level. The median should be sufficient.
438 
439  Parameters
440  ----------
441  mi : `lsst.afw.image.MaskedImage`
442  MaskedImage for which to measure background.
443  badPixels : `list` of `str`
444  Mask planes to ignore.
445  Returns
446  -------
447  bg : `float`
448  Median background level.
449  """
450  mask = mi.getMask()
452  stats.setAndMask(mask.getPlaneBitMask(badPixels))
453  return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue()
454 
455  def subtractCrosstalk(self, thisExposure, sourceExposure=None, crosstalkCoeffs=None,
456  badPixels=["BAD"], minPixelToMask=45000,
457  crosstalkStr="CROSSTALK", isTrimmed=False,
458  backgroundMethod="None"):
459  """Subtract the crosstalk from thisExposure, optionally using a different source.
460 
461  We set the mask plane indicated by ``crosstalkStr`` in a target amplifier
462  for pixels in a source amplifier that exceed ``minPixelToMask``. Note that
463  the correction is applied to all pixels in the amplifier, but only those
464  that have a substantial crosstalk are masked with ``crosstalkStr``.
465 
466  The uncorrected image is used as a template for correction. This is good
467  enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's
468  larger you may want to iterate.
469 
470  Parameters
471  ----------
472  thisExposure : `lsst.afw.image.Exposure`
473  Exposure for which to subtract crosstalk.
474  sourceExposure : `lsst.afw.image.Exposure`, optional
475  Exposure to use as the source of the crosstalk. If not set,
476  thisExposure is used as the source (intra-detector crosstalk).
477  crosstalkCoeffs : `numpy.ndarray`, optional.
478  Coefficients to use to correct crosstalk.
479  badPixels : `list` of `str`
480  Mask planes to ignore.
481  minPixelToMask : `float`
482  Minimum pixel value (relative to the background level) in
483  source amplifier for which to set ``crosstalkStr`` mask plane
484  in target amplifier.
485  crosstalkStr : `str`
486  Mask plane name for pixels greatly modified by crosstalk
487  (above minPixelToMask).
488  isTrimmed : `bool`
489  The image is already trimmed.
490  This should no longer be needed once DM-15409 is resolved.
491  backgroundMethod : `str`
492  Method used to subtract the background. "AMP" uses
493  amplifier-by-amplifier background levels, "DETECTOR" uses full
494  exposure/maskedImage levels. Any other value results in no
495  background subtraction.
496  """
497  mi = thisExposure.getMaskedImage()
498  mask = mi.getMask()
499  detector = thisExposure.getDetector()
500  if self.hasCrosstalkhasCrosstalk is False:
501  self.fromDetectorfromDetectorfromDetector(detector, coeffVector=crosstalkCoeffs)
502 
503  numAmps = len(detector)
504  if numAmps != self.nAmpnAmp:
505  raise RuntimeError(f"Crosstalk built for {self.nAmp} in {self._detectorName}, received "
506  f"{numAmps} in {detector.getName()}")
507 
508  if sourceExposure:
509  source = sourceExposure.getMaskedImage()
510  sourceDetector = sourceExposure.getDetector()
511  else:
512  source = mi
513  sourceDetector = detector
514 
515  if crosstalkCoeffs is not None:
516  coeffs = crosstalkCoeffs
517  else:
518  coeffs = self.coeffscoeffs
519  self.loglog.debug("CT COEFF: %s", coeffs)
520  # Set background level based on the requested method. The
521  # thresholdBackground holds the offset needed so that we only mask
522  # pixels high relative to the background, not in an absolute
523  # sense.
524  thresholdBackground = self.calculateBackgroundcalculateBackground(source, badPixels)
525 
526  backgrounds = [0.0 for amp in sourceDetector]
527  if backgroundMethod is None:
528  pass
529  elif backgroundMethod == "AMP":
530  backgrounds = [self.calculateBackgroundcalculateBackground(source[amp.getBBox()], badPixels)
531  for amp in sourceDetector]
532  elif backgroundMethod == "DETECTOR":
533  backgrounds = [self.calculateBackgroundcalculateBackground(source, badPixels) for amp in sourceDetector]
534 
535  # Set the crosstalkStr bit for the bright pixels (those which will have
536  # significant crosstalk correction)
537  crosstalkPlane = mask.addMaskPlane(crosstalkStr)
538  footprints = lsst.afw.detection.FootprintSet(source,
539  lsst.afw.detection.Threshold(minPixelToMask
540  + thresholdBackground))
541  footprints.setMask(mask, crosstalkStr)
542  crosstalk = mask.getPlaneBitMask(crosstalkStr)
543 
544  # Define a subtrahend image to contain all the scaled crosstalk signals
545  subtrahend = source.Factory(source.getBBox())
546  subtrahend.set((0, 0, 0))
547 
548  coeffs = coeffs.transpose()
549  for ii, iAmp in enumerate(sourceDetector):
550  iImage = subtrahend[iAmp.getBBox() if isTrimmed else iAmp.getRawDataBBox()]
551  for jj, jAmp in enumerate(detector):
552  if coeffs[ii, jj] == 0.0:
553  continue
554  jImage = self.extractAmpextractAmp(mi, jAmp, iAmp, isTrimmed)
555  jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks
556  jImage -= backgrounds[jj]
557  iImage.scaledPlus(coeffs[ii, jj], jImage)
558 
559  # Set crosstalkStr bit only for those pixels that have been significantly modified (i.e., those
560  # masked as such in 'subtrahend'), not necessarily those that are bright originally.
561  mask.clearMaskPlane(crosstalkPlane)
562  mi -= subtrahend # also sets crosstalkStr bit for bright pixels
563 
564 
566  """Configuration for intra-detector crosstalk removal."""
567  minPixelToMask = Field(
568  dtype=float,
569  doc="Set crosstalk mask plane for pixels over this value.",
570  default=45000
571  )
572  crosstalkMaskPlane = Field(
573  dtype=str,
574  doc="Name for crosstalk mask plane.",
575  default="CROSSTALK"
576  )
577  crosstalkBackgroundMethod = ChoiceField(
578  dtype=str,
579  doc="Type of background subtraction to use when applying correction.",
580  default="None",
581  allowed={
582  "None": "Do no background subtraction.",
583  "AMP": "Subtract amplifier-by-amplifier background levels.",
584  "DETECTOR": "Subtract detector level background."
585  },
586  )
587  useConfigCoefficients = Field(
588  dtype=bool,
589  doc="Ignore the detector crosstalk information in favor of CrosstalkConfig values?",
590  default=False,
591  )
592  crosstalkValues = ListField(
593  dtype=float,
594  doc=("Amplifier-indexed crosstalk coefficients to use. This should be arranged as a 1 x nAmp**2 "
595  "list of coefficients, such that when reshaped by crosstalkShape, the result is nAmp x nAmp. "
596  "This matrix should be structured so CT * [amp0 amp1 amp2 ...]^T returns the column "
597  "vector [corr0 corr1 corr2 ...]^T."),
598  default=[0.0],
599  )
600  crosstalkShape = ListField(
601  dtype=int,
602  doc="Shape of the coefficient array. This should be equal to [nAmp, nAmp].",
603  default=[1],
604  )
605 
606  def getCrosstalk(self, detector=None):
607  """Return a 2-D numpy array of crosstalk coefficients in the proper shape.
608 
609  Parameters
610  ----------
611  detector : `lsst.afw.cameraGeom.detector`
612  Detector that is to be crosstalk corrected.
613 
614  Returns
615  -------
616  coeffs : `numpy.ndarray`
617  Crosstalk coefficients that can be used to correct the detector.
618 
619  Raises
620  ------
621  RuntimeError
622  Raised if no coefficients could be generated from this detector/configuration.
623  """
624  if self.useConfigCoefficientsuseConfigCoefficients is True:
625  coeffs = np.array(self.crosstalkValuescrosstalkValues).reshape(self.crosstalkShapecrosstalkShape)
626  if detector is not None:
627  nAmp = len(detector)
628  if coeffs.shape != (nAmp, nAmp):
629  raise RuntimeError("Constructed crosstalk coeffients do not match detector shape. "
630  f"{coeffs.shape} {nAmp}")
631  return coeffs
632  elif detector is not None and detector.hasCrosstalk() is True:
633  # Assume the detector defines itself consistently.
634  return detector.getCrosstalk()
635  else:
636  raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients")
637 
638  def hasCrosstalk(self, detector=None):
639  """Return a boolean indicating if crosstalk coefficients exist.
640 
641  Parameters
642  ----------
643  detector : `lsst.afw.cameraGeom.detector`
644  Detector that is to be crosstalk corrected.
645 
646  Returns
647  -------
648  hasCrosstalk : `bool`
649  True if this detector/configuration has crosstalk coefficients defined.
650  """
651  if self.useConfigCoefficientsuseConfigCoefficients is True and self.crosstalkValuescrosstalkValues is not None:
652  return True
653  elif detector is not None and detector.hasCrosstalk() is True:
654  return True
655  else:
656  return False
657 
658 
659 class CrosstalkTask(Task):
660  """Apply intra-detector crosstalk correction."""
661  ConfigClass = CrosstalkConfig
662  _DefaultName = 'isrCrosstalk'
663 
664  def prepCrosstalk(self, dataRef, crosstalk=None):
665  """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk.
666 
667  Parameters
668  ----------
669  dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
670  Butler reference of the detector data to be processed.
671  crosstalk : `~lsst.ip.isr.CrosstalkConfig`
672  Crosstalk calibration that will be used.
673 
674  See also
675  --------
676  lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk
677  """
678  return
679 
680  def run(self, exposure, crosstalk=None,
681  crosstalkSources=None, isTrimmed=False, camera=None):
682  """Apply intra-detector crosstalk correction
683 
684  Parameters
685  ----------
686  exposure : `lsst.afw.image.Exposure`
687  Exposure for which to remove crosstalk.
688  crosstalkCalib : `lsst.ip.isr.CrosstalkCalib`, optional
689  External crosstalk calibration to apply. Constructed from
690  detector if not found.
691  crosstalkSources : `defaultdict`, optional
692  Image data for other detectors that are sources of
693  crosstalk in exposure. The keys are expected to be names
694  of the other detectors, with the values containing
695  `lsst.afw.image.Exposure` at the same level of processing
696  as ``exposure``.
697  The default for intra-detector crosstalk here is None.
698  isTrimmed : `bool`, optional
699  The image is already trimmed.
700  This should no longer be needed once DM-15409 is resolved.
701  camera : `lsst.afw.cameraGeom.Camera`, optional
702  Camera associated with this exposure. Only used for
703  inter-chip matching.
704 
705  Raises
706  ------
707  RuntimeError
708  Raised if called for a detector that does not have a
709  crosstalk correction. Also raised if the crosstalkSource
710  is not an expected type.
711  """
712  if not crosstalk:
713  crosstalk = CrosstalkCalib(log=self.log)
714  crosstalk = crosstalk.fromDetector(exposure.getDetector(),
715  coeffVector=self.config.crosstalkValues)
716  if not crosstalk.log:
717  crosstalk.log = self.log
718  if not crosstalk.hasCrosstalk:
719  raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients.")
720 
721  else:
722  self.log.info("Applying crosstalk correction.")
723  crosstalk.subtractCrosstalk(exposure, crosstalkCoeffs=crosstalk.coeffs,
724  minPixelToMask=self.config.minPixelToMask,
725  crosstalkStr=self.config.crosstalkMaskPlane, isTrimmed=isTrimmed,
726  backgroundMethod=self.config.crosstalkBackgroundMethod)
727 
728  if crosstalk.interChip:
729  if crosstalkSources:
730  # Parse crosstalkSources: Identify which detectors we have available
731  if isinstance(crosstalkSources[0], lsst.afw.image.Exposure):
732  # Received afwImage.Exposure
733  sourceNames = [exp.getDetector().getName() for exp in crosstalkSources]
734  elif isinstance(crosstalkSources[0], lsst.daf.butler.DeferredDatasetHandle):
735  # Received dafButler.DeferredDatasetHandle
736  detectorList = [source.dataId['detector'] for source in crosstalkSources]
737  sourceNames = [camera[detector].getName() for detector in detectorList]
738  else:
739  raise RuntimeError("Unknown object passed as crosstalk sources.",
740  type(crosstalkSources[0]))
741 
742  for detName in crosstalk.interChip:
743  if detName not in sourceNames:
744  self.log.warning("Crosstalk lists %s, not found in sources: %s",
745  detName, sourceNames)
746  continue
747  # Get the coefficients.
748  interChipCoeffs = crosstalk.interChip[detName]
749 
750  sourceExposure = crosstalkSources[sourceNames.index(detName)]
751  if isinstance(sourceExposure, lsst.daf.butler.DeferredDatasetHandle):
752  # Dereference the dafButler.DeferredDatasetHandle.
753  sourceExposure = sourceExposure.get()
754  if not isinstance(sourceExposure, lsst.afw.image.Exposure):
755  raise RuntimeError("Unknown object passed as crosstalk sources.",
756  type(sourceExposure))
757 
758  self.log.info("Correcting detector %s with ctSource %s",
759  exposure.getDetector().getName(),
760  sourceExposure.getDetector().getName())
761  crosstalk.subtractCrosstalk(exposure, sourceExposure=sourceExposure,
762  crosstalkCoeffs=interChipCoeffs,
763  minPixelToMask=self.config.minPixelToMask,
764  crosstalkStr=self.config.crosstalkMaskPlane,
765  isTrimmed=isTrimmed,
766  backgroundMethod=self.config.crosstalkBackgroundMethod)
767  else:
768  self.log.warning("Crosstalk contains interChip coefficients, but no sources found!")
769 
770 
772  def run(self, exposure, crosstalkSources=None):
773  self.log.info("Not performing any crosstalk correction")
table::Key< int > type
Definition: Detector.cc:163
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
A Threshold is used to pass a threshold value to detection algorithms.
Definition: Threshold.h:43
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
Pass parameters to a Statistics object.
Definition: Statistics.h:92
def requiredAttributes(self, value)
Definition: calibType.py:142
def updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition: calibType.py:181
def fromDetector(self, detector)
Definition: calibType.py:495
def __init__(self, detector=None, nAmp=0, **kwargs)
Definition: crosstalk.py:90
def calculateBackground(mi, badPixels=["BAD"])
Definition: crosstalk.py:433
def fromDetector(self, detector, coeffVector=None)
Definition: crosstalk.py:134
def updateMetadata(self, setDate=False, **kwargs)
Definition: crosstalk.py:110
def subtractCrosstalk(self, thisExposure, sourceExposure=None, crosstalkCoeffs=None, badPixels=["BAD"], minPixelToMask=45000, crosstalkStr="CROSSTALK", isTrimmed=False, backgroundMethod="None")
Definition: crosstalk.py:458
def extractAmp(image, amp, ampTarget, isTrimmed=False)
Definition: crosstalk.py:393
def hasCrosstalk(self, detector=None)
Definition: crosstalk.py:638
def getCrosstalk(self, detector=None)
Definition: crosstalk.py:606
def prepCrosstalk(self, dataRef, crosstalk=None)
Definition: crosstalk.py:664
def run(self, exposure, crosstalk=None, crosstalkSources=None, isTrimmed=False, camera=None)
Definition: crosstalk.py:681
def run(self, exposure, crosstalkSources=None)
Definition: crosstalk.py:772
std::string const & getName() const noexcept
Return a filter's name.
Definition: Filter.h:78
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition: Statistics.h:359
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