LSST Applications  21.0.0+04719a4bac,21.0.0-1-ga51b5d4+f5e6047307,21.0.0-11-g2b59f77+a9c1acf22d,21.0.0-11-ga42c5b2+86977b0b17,21.0.0-12-gf4ce030+76814010d2,21.0.0-13-g1721dae+760e7a6536,21.0.0-13-g3a573fe+768d78a30a,21.0.0-15-g5a7caf0+f21cbc5713,21.0.0-16-g0fb55c1+b60e2d390c,21.0.0-19-g4cded4ca+71a93a33c0,21.0.0-2-g103fe59+bb20972958,21.0.0-2-g45278ab+04719a4bac,21.0.0-2-g5242d73+3ad5d60fb1,21.0.0-2-g7f82c8f+8babb168e8,21.0.0-2-g8f08a60+06509c8b61,21.0.0-2-g8faa9b5+616205b9df,21.0.0-2-ga326454+8babb168e8,21.0.0-2-gde069b7+5e4aea9c2f,21.0.0-2-gecfae73+1d3a86e577,21.0.0-2-gfc62afb+3ad5d60fb1,21.0.0-25-g1d57be3cd+e73869a214,21.0.0-3-g357aad2+ed88757d29,21.0.0-3-g4a4ce7f+3ad5d60fb1,21.0.0-3-g4be5c26+3ad5d60fb1,21.0.0-3-g65f322c+e0b24896a3,21.0.0-3-g7d9da8d+616205b9df,21.0.0-3-ge02ed75+a9c1acf22d,21.0.0-4-g591bb35+a9c1acf22d,21.0.0-4-g65b4814+b60e2d390c,21.0.0-4-gccdca77+0de219a2bc,21.0.0-4-ge8a399c+6c55c39e83,21.0.0-5-gd00fb1e+05fce91b99,21.0.0-6-gc675373+3ad5d60fb1,21.0.0-64-g1122c245+4fb2b8f86e,21.0.0-7-g04766d7+cd19d05db2,21.0.0-7-gdf92d54+04719a4bac,21.0.0-8-g5674e7b+d1bd76f71f,master-gac4afde19b+a9c1acf22d,w.2021.13
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 : `lsst.log.Log`, 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
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._detectorName_detectorName_detectorName = detector.getName()
154  self._detectorSerial_detectorSerial_detectorSerial = detector.getSerial()
155 
156  self.nAmpnAmp = len(detector)
157  self.crosstalkShapecrosstalkShape = (self.nAmpnAmp, self.nAmpnAmp)
158 
159  if coeffVector is not None:
160  crosstalkCoeffs = coeffVector
161  else:
162  crosstalkCoeffs = detector.getCrosstalk()
163  if len(crosstalkCoeffs) == 1 and crosstalkCoeffs[0] == 0.0:
164  return self
165  self.coeffscoeffs = np.array(crosstalkCoeffs).reshape(self.crosstalkShapecrosstalkShape)
166 
167  if self.coeffscoeffs.shape != self.crosstalkShapecrosstalkShape:
168  raise RuntimeError("Crosstalk coefficients do not match detector shape. "
169  f"{self.crosstalkShape} {self.nAmp}")
170 
171  self.interChipinterChip = {}
172  self.hasCrosstalkhasCrosstalk = True
173  self.updateMetadataupdateMetadataupdateMetadata()
174  return self
175 
176  @classmethod
177  def fromDict(cls, dictionary):
178  """Construct a calibration from a dictionary of properties.
179 
180  Must be implemented by the specific calibration subclasses.
181 
182  Parameters
183  ----------
184  dictionary : `dict`
185  Dictionary of properties.
186 
187  Returns
188  -------
189  calib : `lsst.ip.isr.CalibType`
190  Constructed calibration.
191 
192  Raises
193  ------
194  RuntimeError :
195  Raised if the supplied dictionary is for a different
196  calibration.
197  """
198  calib = cls()
199 
200  if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
201  raise RuntimeError(f"Incorrect crosstalk supplied. Expected {calib._OBSTYPE}, "
202  f"found {dictionary['metadata']['OBSTYPE']}")
203 
204  calib.setMetadata(dictionary['metadata'])
205 
206  if 'detectorName' in dictionary:
207  calib._detectorName = dictionary.get('detectorName')
208  elif 'DETECTOR_NAME' in dictionary:
209  calib._detectorName = dictionary.get('DETECTOR_NAME')
210  elif 'DET_NAME' in dictionary['metadata']:
211  calib._detectorName = dictionary['metadata']['DET_NAME']
212  else:
213  calib._detectorName = None
214 
215  if 'detectorSerial' in dictionary:
216  calib._detectorSerial = dictionary.get('detectorSerial')
217  elif 'DETECTOR_SERIAL' in dictionary:
218  calib._detectorSerial = dictionary.get('DETECTOR_SERIAL')
219  elif 'DET_SER' in dictionary['metadata']:
220  calib._detectorSerial = dictionary['metadata']['DET_SER']
221  else:
222  calib._detectorSerial = None
223 
224  if 'detectorId' in dictionary:
225  calib._detectorId = dictionary.get('detectorId')
226  elif 'DETECTOR' in dictionary:
227  calib._detectorId = dictionary.get('DETECTOR')
228  elif 'DETECTOR' in dictionary['metadata']:
229  calib._detectorId = dictionary['metadata']['DETECTOR']
230  elif calib._detectorSerial:
231  calib._detectorId = calib._detectorSerial
232  else:
233  calib._detectorId = None
234 
235  if 'instrument' in dictionary:
236  calib._instrument = dictionary.get('instrument')
237  elif 'INSTRUME' in dictionary['metadata']:
238  calib._instrument = dictionary['metadata']['INSTRUME']
239  else:
240  calib._instrument = None
241 
242  calib.hasCrosstalk = dictionary.get('hasCrosstalk',
243  dictionary['metadata'].get('HAS_CROSSTALK', False))
244  if calib.hasCrosstalk:
245  calib.nAmp = dictionary.get('nAmp', dictionary['metadata'].get('NAMP', 0))
246  calib.crosstalkShape = (calib.nAmp, calib.nAmp)
247  calib.coeffs = np.array(dictionary['coeffs']).reshape(calib.crosstalkShape)
248  if 'coeffErr' in dictionary:
249  calib.coeffErr = np.array(dictionary['coeffErr']).reshape(calib.crosstalkShape)
250  else:
251  calib.coeffErr = np.zeros_like(calib.coeffs)
252  if 'coeffNum' in dictionary:
253  calib.coeffNum = np.array(dictionary['coeffNum']).reshape(calib.crosstalkShape)
254  else:
255  calib.coeffNum = np.zeros_like(calib.coeffs, dtype=int)
256  if 'coeffValid' in dictionary:
257  calib.coeffValid = np.array(dictionary['coeffValid']).reshape(calib.crosstalkShape)
258  else:
259  calib.coeffValid = np.ones_like(calib.coeffs, dtype=bool)
260 
261  calib.interChip = dictionary.get('interChip', None)
262  if calib.interChip:
263  for detector in calib.interChip:
264  coeffVector = calib.interChip[detector]
265  calib.interChip[detector] = np.array(coeffVector).reshape(calib.crosstalkShape)
266 
267  calib.updateMetadata()
268  return calib
269 
270  def toDict(self):
271  """Return a dictionary containing the calibration properties.
272 
273  The dictionary should be able to be round-tripped through
274  `fromDict`.
275 
276  Returns
277  -------
278  dictionary : `dict`
279  Dictionary of properties.
280  """
281  self.updateMetadataupdateMetadataupdateMetadata()
282 
283  outDict = {}
284  metadata = self.getMetadatagetMetadata()
285  outDict['metadata'] = metadata
286 
287  outDict['hasCrosstalk'] = self.hasCrosstalkhasCrosstalk
288  outDict['nAmp'] = self.nAmpnAmp
289  outDict['crosstalkShape'] = self.crosstalkShapecrosstalkShape
290 
291  ctLength = self.nAmpnAmp*self.nAmpnAmp
292  outDict['coeffs'] = self.coeffscoeffs.reshape(ctLength).tolist()
293 
294  if self.coeffErrcoeffErr is not None:
295  outDict['coeffErr'] = self.coeffErrcoeffErr.reshape(ctLength).tolist()
296  if self.coeffNumcoeffNum is not None:
297  outDict['coeffNum'] = self.coeffNumcoeffNum.reshape(ctLength).tolist()
298  if self.coeffValidcoeffValid is not None:
299  outDict['coeffValid'] = self.coeffValidcoeffValid.reshape(ctLength).tolist()
300 
301  if self.interChipinterChip:
302  outDict['interChip'] = dict()
303  for detector in self.interChipinterChip:
304  outDict['interChip'][detector] = self.interChipinterChip[detector].reshape(ctLength).tolist()
305 
306  return outDict
307 
308  @classmethod
309  def fromTable(cls, tableList):
310  """Construct calibration from a list of tables.
311 
312  This method uses the `fromDict` method to create the
313  calibration, after constructing an appropriate dictionary from
314  the input tables.
315 
316  Parameters
317  ----------
318  tableList : `list` [`lsst.afw.table.Table`]
319  List of tables to use to construct the crosstalk
320  calibration.
321 
322  Returns
323  -------
324  calib : `lsst.ip.isr.CrosstalkCalib`
325  The calibration defined in the tables.
326 
327  """
328  coeffTable = tableList[0]
329 
330  metadata = coeffTable.meta
331  inDict = dict()
332  inDict['metadata'] = metadata
333  inDict['hasCrosstalk'] = metadata['HAS_CROSSTALK']
334  inDict['nAmp'] = metadata['NAMP']
335 
336  inDict['coeffs'] = coeffTable['CT_COEFFS']
337  if 'CT_ERRORS' in coeffTable:
338  inDict['coeffErr'] = coeffTable['CT_ERRORS']
339  if 'CT_COUNTS' in coeffTable:
340  inDict['coeffNum'] = coeffTable['CT_COUNTS']
341  if 'CT_VALID' in coeffTable:
342  inDict['coeffValid'] = coeffTable['CT_VALID']
343 
344  if len(tableList) > 1:
345  inDict['interChip'] = dict()
346  interChipTable = tableList[1]
347  for record in interChipTable:
348  inDict['interChip'][record['IC_SOURCE_DET']] = record['IC_COEFFS']
349 
350  return cls().fromDict(inDict)
351 
352  def toTable(self):
353  """Construct a list of tables containing the information in this calibration.
354 
355  The list of tables should create an identical calibration
356  after being passed to this class's fromTable method.
357 
358  Returns
359  -------
360  tableList : `list` [`lsst.afw.table.Table`]
361  List of tables containing the crosstalk calibration
362  information.
363 
364  """
365  tableList = []
366  self.updateMetadataupdateMetadataupdateMetadata()
367  catalog = Table([{'CT_COEFFS': self.coeffscoeffs.reshape(self.nAmpnAmp*self.nAmpnAmp),
368  'CT_ERRORS': self.coeffErrcoeffErr.reshape(self.nAmpnAmp*self.nAmpnAmp),
369  'CT_COUNTS': self.coeffNumcoeffNum.reshape(self.nAmpnAmp*self.nAmpnAmp),
370  'CT_VALID': self.coeffValidcoeffValid.reshape(self.nAmpnAmp*self.nAmpnAmp),
371  }])
372  # filter None, because astropy can't deal.
373  inMeta = self.getMetadatagetMetadata().toDict()
374  outMeta = {k: v for k, v in inMeta.items() if v is not None}
375  outMeta.update({k: "" for k, v in inMeta.items() if v is None})
376  catalog.meta = outMeta
377  tableList.append(catalog)
378 
379  if self.interChipinterChip:
380  interChipTable = Table([{'IC_SOURCE_DET': sourceDet,
381  'IC_COEFFS': self.interChipinterChip[sourceDet].reshape(self.nAmpnAmp*self.nAmpnAmp)}
382  for sourceDet in self.interChipinterChip.keys()])
383  tableList.append(interChipTable)
384  return tableList
385 
386  # Implementation methods.
387  @staticmethod
388  def extractAmp(image, amp, ampTarget, isTrimmed=False):
389  """Extract the image data from an amp, flipped to match ampTarget.
390 
391  Parameters
392  ----------
393  image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
394  Image containing the amplifier of interest.
395  amp : `lsst.afw.cameraGeom.Amplifier`
396  Amplifier on image to extract.
397  ampTarget : `lsst.afw.cameraGeom.Amplifier`
398  Target amplifier that the extracted image will be flipped
399  to match.
400  isTrimmed : `bool`
401  The image is already trimmed.
402  TODO : DM-15409 will resolve this.
403 
404  Returns
405  -------
406  output : `lsst.afw.image.Image`
407  Image of the amplifier in the desired configuration.
408  """
409  X_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False,
410  lsst.afw.cameraGeom.ReadoutCorner.LR: True,
411  lsst.afw.cameraGeom.ReadoutCorner.UL: False,
412  lsst.afw.cameraGeom.ReadoutCorner.UR: True}
413  Y_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False,
414  lsst.afw.cameraGeom.ReadoutCorner.LR: False,
415  lsst.afw.cameraGeom.ReadoutCorner.UL: True,
416  lsst.afw.cameraGeom.ReadoutCorner.UR: True}
417 
418  output = image[amp.getBBox() if isTrimmed else amp.getRawDataBBox()]
419  thisAmpCorner = amp.getReadoutCorner()
420  targetAmpCorner = ampTarget.getReadoutCorner()
421 
422  # Flipping is necessary only if the desired configuration doesn't match what we currently have
423  xFlip = X_FLIP[targetAmpCorner] ^ X_FLIP[thisAmpCorner]
424  yFlip = Y_FLIP[targetAmpCorner] ^ Y_FLIP[thisAmpCorner]
425  return lsst.afw.math.flipImage(output, xFlip, yFlip)
426 
427  @staticmethod
428  def calculateBackground(mi, badPixels=["BAD"]):
429  """Estimate median background in image.
430 
431  Getting a great background model isn't important for crosstalk correction,
432  since the crosstalk is at a low level. The median should be sufficient.
433 
434  Parameters
435  ----------
436  mi : `lsst.afw.image.MaskedImage`
437  MaskedImage for which to measure background.
438  badPixels : `list` of `str`
439  Mask planes to ignore.
440  Returns
441  -------
442  bg : `float`
443  Median background level.
444  """
445  mask = mi.getMask()
447  stats.setAndMask(mask.getPlaneBitMask(badPixels))
448  return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue()
449 
450  def subtractCrosstalk(self, thisExposure, sourceExposure=None, crosstalkCoeffs=None,
451  badPixels=["BAD"], minPixelToMask=45000,
452  crosstalkStr="CROSSTALK", isTrimmed=False,
453  backgroundMethod="None"):
454  """Subtract the crosstalk from thisExposure, optionally using a different source.
455 
456  We set the mask plane indicated by ``crosstalkStr`` in a target amplifier
457  for pixels in a source amplifier that exceed ``minPixelToMask``. Note that
458  the correction is applied to all pixels in the amplifier, but only those
459  that have a substantial crosstalk are masked with ``crosstalkStr``.
460 
461  The uncorrected image is used as a template for correction. This is good
462  enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's
463  larger you may want to iterate.
464 
465  Parameters
466  ----------
467  thisExposure : `lsst.afw.image.Exposure`
468  Exposure for which to subtract crosstalk.
469  sourceExposure : `lsst.afw.image.Exposure`, optional
470  Exposure to use as the source of the crosstalk. If not set,
471  thisExposure is used as the source (intra-detector crosstalk).
472  crosstalkCoeffs : `numpy.ndarray`, optional.
473  Coefficients to use to correct crosstalk.
474  badPixels : `list` of `str`
475  Mask planes to ignore.
476  minPixelToMask : `float`
477  Minimum pixel value (relative to the background level) in
478  source amplifier for which to set ``crosstalkStr`` mask plane
479  in target amplifier.
480  crosstalkStr : `str`
481  Mask plane name for pixels greatly modified by crosstalk
482  (above minPixelToMask).
483  isTrimmed : `bool`
484  The image is already trimmed.
485  This should no longer be needed once DM-15409 is resolved.
486  backgroundMethod : `str`
487  Method used to subtract the background. "AMP" uses
488  amplifier-by-amplifier background levels, "DETECTOR" uses full
489  exposure/maskedImage levels. Any other value results in no
490  background subtraction.
491  """
492  mi = thisExposure.getMaskedImage()
493  mask = mi.getMask()
494  detector = thisExposure.getDetector()
495  if self.hasCrosstalkhasCrosstalk is False:
496  self.fromDetectorfromDetectorfromDetector(detector, coeffVector=crosstalkCoeffs)
497 
498  numAmps = len(detector)
499  if numAmps != self.nAmpnAmp:
500  raise RuntimeError(f"Crosstalk built for {self.nAmp} in {self._detectorName}, received "
501  f"{numAmps} in {detector.getName()}")
502 
503  if sourceExposure:
504  source = sourceExposure.getMaskedImage()
505  sourceDetector = sourceExposure.getDetector()
506  else:
507  source = mi
508  sourceDetector = detector
509 
510  if crosstalkCoeffs is not None:
511  coeffs = crosstalkCoeffs
512  else:
513  coeffs = self.coeffscoeffs
514  self.loglog.debug("CT COEFF: %s", coeffs)
515  # Set background level based on the requested method. The
516  # thresholdBackground holds the offset needed so that we only mask
517  # pixels high relative to the background, not in an absolute
518  # sense.
519  thresholdBackground = self.calculateBackgroundcalculateBackground(source, badPixels)
520 
521  backgrounds = [0.0 for amp in sourceDetector]
522  if backgroundMethod is None:
523  pass
524  elif backgroundMethod == "AMP":
525  backgrounds = [self.calculateBackgroundcalculateBackground(source[amp.getBBox()], badPixels)
526  for amp in sourceDetector]
527  elif backgroundMethod == "DETECTOR":
528  backgrounds = [self.calculateBackgroundcalculateBackground(source, badPixels) for amp in sourceDetector]
529 
530  # Set the crosstalkStr bit for the bright pixels (those which will have
531  # significant crosstalk correction)
532  crosstalkPlane = mask.addMaskPlane(crosstalkStr)
533  footprints = lsst.afw.detection.FootprintSet(source,
534  lsst.afw.detection.Threshold(minPixelToMask
535  + thresholdBackground))
536  footprints.setMask(mask, crosstalkStr)
537  crosstalk = mask.getPlaneBitMask(crosstalkStr)
538 
539  # Define a subtrahend image to contain all the scaled crosstalk signals
540  subtrahend = source.Factory(source.getBBox())
541  subtrahend.set((0, 0, 0))
542 
543  coeffs = coeffs.transpose()
544  for ii, iAmp in enumerate(sourceDetector):
545  iImage = subtrahend[iAmp.getBBox() if isTrimmed else iAmp.getRawDataBBox()]
546  for jj, jAmp in enumerate(detector):
547  if coeffs[ii, jj] == 0.0:
548  continue
549  jImage = self.extractAmpextractAmp(mi, jAmp, iAmp, isTrimmed)
550  jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks
551  jImage -= backgrounds[jj]
552  iImage.scaledPlus(coeffs[ii, jj], jImage)
553 
554  # Set crosstalkStr bit only for those pixels that have been significantly modified (i.e., those
555  # masked as such in 'subtrahend'), not necessarily those that are bright originally.
556  mask.clearMaskPlane(crosstalkPlane)
557  mi -= subtrahend # also sets crosstalkStr bit for bright pixels
558 
559 
561  """Configuration for intra-detector crosstalk removal."""
562  minPixelToMask = Field(
563  dtype=float,
564  doc="Set crosstalk mask plane for pixels over this value.",
565  default=45000
566  )
567  crosstalkMaskPlane = Field(
568  dtype=str,
569  doc="Name for crosstalk mask plane.",
570  default="CROSSTALK"
571  )
572  crosstalkBackgroundMethod = ChoiceField(
573  dtype=str,
574  doc="Type of background subtraction to use when applying correction.",
575  default="None",
576  allowed={
577  "None": "Do no background subtraction.",
578  "AMP": "Subtract amplifier-by-amplifier background levels.",
579  "DETECTOR": "Subtract detector level background."
580  },
581  )
582  useConfigCoefficients = Field(
583  dtype=bool,
584  doc="Ignore the detector crosstalk information in favor of CrosstalkConfig values?",
585  default=False,
586  )
587  crosstalkValues = ListField(
588  dtype=float,
589  doc=("Amplifier-indexed crosstalk coefficients to use. This should be arranged as a 1 x nAmp**2 "
590  "list of coefficients, such that when reshaped by crosstalkShape, the result is nAmp x nAmp. "
591  "This matrix should be structured so CT * [amp0 amp1 amp2 ...]^T returns the column "
592  "vector [corr0 corr1 corr2 ...]^T."),
593  default=[0.0],
594  )
595  crosstalkShape = ListField(
596  dtype=int,
597  doc="Shape of the coefficient array. This should be equal to [nAmp, nAmp].",
598  default=[1],
599  )
600 
601  def getCrosstalk(self, detector=None):
602  """Return a 2-D numpy array of crosstalk coefficients in the proper shape.
603 
604  Parameters
605  ----------
606  detector : `lsst.afw.cameraGeom.detector`
607  Detector that is to be crosstalk corrected.
608 
609  Returns
610  -------
611  coeffs : `numpy.ndarray`
612  Crosstalk coefficients that can be used to correct the detector.
613 
614  Raises
615  ------
616  RuntimeError
617  Raised if no coefficients could be generated from this detector/configuration.
618  """
619  if self.useConfigCoefficientsuseConfigCoefficients is True:
620  coeffs = np.array(self.crosstalkValuescrosstalkValues).reshape(self.crosstalkShapecrosstalkShape)
621  if detector is not None:
622  nAmp = len(detector)
623  if coeffs.shape != (nAmp, nAmp):
624  raise RuntimeError("Constructed crosstalk coeffients do not match detector shape. "
625  f"{coeffs.shape} {nAmp}")
626  return coeffs
627  elif detector is not None and detector.hasCrosstalk() is True:
628  # Assume the detector defines itself consistently.
629  return detector.getCrosstalk()
630  else:
631  raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients")
632 
633  def hasCrosstalk(self, detector=None):
634  """Return a boolean indicating if crosstalk coefficients exist.
635 
636  Parameters
637  ----------
638  detector : `lsst.afw.cameraGeom.detector`
639  Detector that is to be crosstalk corrected.
640 
641  Returns
642  -------
643  hasCrosstalk : `bool`
644  True if this detector/configuration has crosstalk coefficients defined.
645  """
646  if self.useConfigCoefficientsuseConfigCoefficients is True and self.crosstalkValuescrosstalkValues is not None:
647  return True
648  elif detector is not None and detector.hasCrosstalk() is True:
649  return True
650  else:
651  return False
652 
653 
655  """Apply intra-detector crosstalk correction."""
656  ConfigClass = CrosstalkConfig
657  _DefaultName = 'isrCrosstalk'
658 
659  def prepCrosstalk(self, dataRef, crosstalk=None):
660  """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk.
661 
662  Parameters
663  ----------
664  dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
665  Butler reference of the detector data to be processed.
666  crosstalk : `~lsst.ip.isr.CrosstalkConfig`
667  Crosstalk calibration that will be used.
668 
669  See also
670  --------
671  lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk
672  """
673  return
674 
675  def run(self, exposure, crosstalk=None,
676  crosstalkSources=None, isTrimmed=False, camera=None):
677  """Apply intra-detector crosstalk correction
678 
679  Parameters
680  ----------
681  exposure : `lsst.afw.image.Exposure`
682  Exposure for which to remove crosstalk.
683  crosstalkCalib : `lsst.ip.isr.CrosstalkCalib`, optional
684  External crosstalk calibration to apply. Constructed from
685  detector if not found.
686  crosstalkSources : `defaultdict`, optional
687  Image data for other detectors that are sources of
688  crosstalk in exposure. The keys are expected to be names
689  of the other detectors, with the values containing
690  `lsst.afw.image.Exposure` at the same level of processing
691  as ``exposure``.
692  The default for intra-detector crosstalk here is None.
693  isTrimmed : `bool`, optional
694  The image is already trimmed.
695  This should no longer be needed once DM-15409 is resolved.
696  camera : `lsst.afw.cameraGeom.Camera`, optional
697  Camera associated with this exposure. Only used for
698  inter-chip matching.
699 
700  Raises
701  ------
702  RuntimeError
703  Raised if called for a detector that does not have a
704  crosstalk correction. Also raised if the crosstalkSource
705  is not an expected type.
706  """
707  if not crosstalk:
708  crosstalk = CrosstalkCalib(log=self.log)
709  crosstalk = crosstalk.fromDetector(exposure.getDetector(),
710  coeffVector=self.config.crosstalkValues)
711  if not crosstalk.log:
712  crosstalk.log = self.log
713  if not crosstalk.hasCrosstalk:
714  raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients.")
715 
716  else:
717  self.log.info("Applying crosstalk correction.")
718  crosstalk.subtractCrosstalk(exposure, crosstalkCoeffs=crosstalk.coeffs,
719  minPixelToMask=self.config.minPixelToMask,
720  crosstalkStr=self.config.crosstalkMaskPlane, isTrimmed=isTrimmed,
721  backgroundMethod=self.config.crosstalkBackgroundMethod)
722 
723  if crosstalk.interChip:
724  if crosstalkSources:
725  # Parse crosstalkSources: Identify which detectors we have available
726  if isinstance(crosstalkSources[0], lsst.afw.image.Exposure):
727  # Received afwImage.Exposure
728  sourceNames = [exp.getDetector().getName() for exp in crosstalkSources]
729  elif isinstance(crosstalkSources[0], lsst.daf.butler.DeferredDatasetHandle):
730  # Received dafButler.DeferredDatasetHandle
731  detectorList = [source.dataId['detector'] for source in crosstalkSources]
732  sourceNames = [camera[detector].getName() for detector in detectorList]
733  else:
734  raise RuntimeError("Unknown object passed as crosstalk sources.",
735  type(crosstalkSources[0]))
736 
737  for detName in crosstalk.interChip:
738  if detName not in sourceNames:
739  self.log.warn("Crosstalk lists %s, not found in sources: %s",
740  detName, sourceNames)
741  continue
742  # Get the coefficients.
743  interChipCoeffs = crosstalk.interChip[detName]
744 
745  sourceExposure = crosstalkSources[sourceNames.index(detName)]
746  if isinstance(sourceExposure, lsst.daf.butler.DeferredDatasetHandle):
747  # Dereference the dafButler.DeferredDatasetHandle.
748  sourceExposure = sourceExposure.get()
749  if not isinstance(sourceExposure, lsst.afw.image.Exposure):
750  raise RuntimeError("Unknown object passed as crosstalk sources.",
751  type(sourceExposure))
752 
753  self.log.info("Correcting detector %s with ctSource %s",
754  exposure.getDetector().getName(),
755  sourceExposure.getDetector().getName())
756  crosstalk.subtractCrosstalk(exposure, sourceExposure=sourceExposure,
757  crosstalkCoeffs=interChipCoeffs,
758  minPixelToMask=self.config.minPixelToMask,
759  crosstalkStr=self.config.crosstalkMaskPlane,
760  isTrimmed=isTrimmed,
761  backgroundMethod=self.config.crosstalkBackgroundMethod)
762  else:
763  self.log.warn("Crosstalk contains interChip coefficients, but no sources found!")
764 
765 
767  def run(self, exposure, crosstalkSources=None):
768  self.loglog.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:93
def requiredAttributes(self, value)
Definition: calibType.py:111
def updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition: calibType.py:150
def fromDetector(self, detector)
Definition: calibType.py:432
def __init__(self, detector=None, nAmp=0, **kwargs)
Definition: crosstalk.py:90
def calculateBackground(mi, badPixels=["BAD"])
Definition: crosstalk.py:428
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:453
def extractAmp(image, amp, ampTarget, isTrimmed=False)
Definition: crosstalk.py:388
def hasCrosstalk(self, detector=None)
Definition: crosstalk.py:633
def getCrosstalk(self, detector=None)
Definition: crosstalk.py:601
def prepCrosstalk(self, dataRef, crosstalk=None)
Definition: crosstalk.py:659
def run(self, exposure, crosstalk=None, crosstalkSources=None, isTrimmed=False, camera=None)
Definition: crosstalk.py:676
def run(self, exposure, crosstalkSources=None)
Definition: crosstalk.py:767
def getName(self)
Definition: task.py:274
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:354
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