Coverage for python/lsst/ip/isr/crosstalk.py : 10%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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"""
23Apply intra-detector crosstalk corrections
24"""
25import numpy as np
26from astropy.table import Table
28import lsst.afw.math
29import lsst.afw.detection
30import lsst.daf.butler
31from lsst.pex.config import Config, Field, ChoiceField, ListField
32from lsst.pipe.base import Task
34from lsst.ip.isr import IsrCalib
37__all__ = ["CrosstalkCalib", "CrosstalkConfig", "CrosstalkTask",
38 "NullCrosstalkTask"]
41class CrosstalkCalib(IsrCalib):
42 """Calibration of amp-to-amp crosstalk coefficients.
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.
55 Notes
56 -----
57 The crosstalk attributes stored are:
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.
85 """
86 _OBSTYPE = 'CROSSTALK'
87 _SCHEMA = 'Gen3 Crosstalk'
88 _VERSION = 1.0
90 def __init__(self, detector=None, nAmp=0, **kwargs):
91 self.hasCrosstalk = False
92 self.nAmp = nAmp if nAmp else 0
93 self.crosstalkShape = (self.nAmp, self.nAmp)
95 self.coeffs = np.zeros(self.crosstalkShape) if self.nAmp else None
96 self.coeffErr = np.zeros(self.crosstalkShape) if self.nAmp else None
97 self.coeffNum = np.zeros(self.crosstalkShape,
98 dtype=int) if self.nAmp else None
99 self.coeffValid = np.zeros(self.crosstalkShape,
100 dtype=bool) if self.nAmp else None
101 self.interChip = {}
103 super().__init__(**kwargs)
104 self.requiredAttributes.update(['hasCrosstalk', 'nAmp', 'coeffs',
105 'coeffErr', 'coeffNum', 'coeffValid',
106 'interChip'])
107 if detector:
108 self.fromDetector(detector)
110 def updateMetadata(self, setDate=False, **kwargs):
111 """Update calibration metadata.
113 This calls the base class's method after ensuring the required
114 calibration keywords will be saved.
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
125 kwargs['DETECTOR_NAME'] = self._detectorName
126 kwargs['DETECTOR_SERIAL'] = self._detectorSerial
127 kwargs['HAS_CROSSTALK'] = self.hasCrosstalk
128 kwargs['NAMP'] = self.nAmp
129 self.crosstalkShape = (self.nAmp, self.nAmp)
130 kwargs['CROSSTALK_SHAPE'] = self.crosstalkShape
132 super().updateMetadata(setDate=setDate, **kwargs)
134 def fromDetector(self, detector, coeffVector=None):
135 """Set calibration parameters from the detector.
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()``.
146 Returns
147 -------
148 calib : `lsst.ip.isr.CrosstalkCalib`
149 The calibration constructed from the detector.
151 """
152 if detector.hasCrosstalk() or coeffVector:
153 self._detectorName = detector.getName()
154 self._detectorSerial = detector.getSerial()
156 self.nAmp = len(detector)
157 self.crosstalkShape = (self.nAmp, self.nAmp)
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.coeffs = np.array(crosstalkCoeffs).reshape(self.crosstalkShape)
167 if self.coeffs.shape != self.crosstalkShape:
168 raise RuntimeError("Crosstalk coefficients do not match detector shape. "
169 f"{self.crosstalkShape} {self.nAmp}")
171 self.interChip = {}
172 self.hasCrosstalk = True
173 self.updateMetadata()
174 return self
176 @classmethod
177 def fromDict(cls, dictionary):
178 """Construct a calibration from a dictionary of properties.
180 Must be implemented by the specific calibration subclasses.
182 Parameters
183 ----------
184 dictionary : `dict`
185 Dictionary of properties.
187 Returns
188 -------
189 calib : `lsst.ip.isr.CalibType`
190 Constructed calibration.
192 Raises
193 ------
194 RuntimeError :
195 Raised if the supplied dictionary is for a different
196 calibration.
197 """
198 calib = cls()
200 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
201 raise RuntimeError(f"Incorrect crosstalk supplied. Expected {calib._OBSTYPE}, "
202 f"found {dictionary['metadata']['OBSTYPE']}")
204 calib.setMetadata(dictionary['metadata'])
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
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
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
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
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)
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)
267 calib.updateMetadata()
268 return calib
270 def toDict(self):
271 """Return a dictionary containing the calibration properties.
273 The dictionary should be able to be round-tripped through
274 `fromDict`.
276 Returns
277 -------
278 dictionary : `dict`
279 Dictionary of properties.
280 """
281 self.updateMetadata()
283 outDict = {}
284 metadata = self.getMetadata()
285 outDict['metadata'] = metadata
287 outDict['hasCrosstalk'] = self.hasCrosstalk
288 outDict['nAmp'] = self.nAmp
289 outDict['crosstalkShape'] = self.crosstalkShape
291 ctLength = self.nAmp*self.nAmp
292 outDict['coeffs'] = self.coeffs.reshape(ctLength).tolist()
294 if self.coeffErr is not None:
295 outDict['coeffErr'] = self.coeffErr.reshape(ctLength).tolist()
296 if self.coeffNum is not None:
297 outDict['coeffNum'] = self.coeffNum.reshape(ctLength).tolist()
298 if self.coeffValid is not None:
299 outDict['coeffValid'] = self.coeffValid.reshape(ctLength).tolist()
301 if self.interChip:
302 outDict['interChip'] = dict()
303 for detector in self.interChip:
304 outDict['interChip'][detector] = self.interChip[detector].reshape(ctLength).tolist()
306 return outDict
308 @classmethod
309 def fromTable(cls, tableList):
310 """Construct calibration from a list of tables.
312 This method uses the `fromDict` method to create the
313 calibration, after constructing an appropriate dictionary from
314 the input tables.
316 Parameters
317 ----------
318 tableList : `list` [`lsst.afw.table.Table`]
319 List of tables to use to construct the crosstalk
320 calibration.
322 Returns
323 -------
324 calib : `lsst.ip.isr.CrosstalkCalib`
325 The calibration defined in the tables.
327 """
328 coeffTable = tableList[0]
330 metadata = coeffTable.meta
331 inDict = dict()
332 inDict['metadata'] = metadata
333 inDict['hasCrosstalk'] = metadata['HAS_CROSSTALK']
334 inDict['nAmp'] = metadata['NAMP']
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']
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']
350 return cls().fromDict(inDict)
352 def toTable(self):
353 """Construct a list of tables containing the information in this calibration.
355 The list of tables should create an identical calibration
356 after being passed to this class's fromTable method.
358 Returns
359 -------
360 tableList : `list` [`lsst.afw.table.Table`]
361 List of tables containing the crosstalk calibration
362 information.
364 """
365 tableList = []
366 self.updateMetadata()
367 catalog = Table([{'CT_COEFFS': self.coeffs.reshape(self.nAmp*self.nAmp),
368 'CT_ERRORS': self.coeffErr.reshape(self.nAmp*self.nAmp),
369 'CT_COUNTS': self.coeffNum.reshape(self.nAmp*self.nAmp),
370 'CT_VALID': self.coeffValid.reshape(self.nAmp*self.nAmp),
371 }])
372 # filter None, because astropy can't deal.
373 inMeta = self.getMetadata().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)
379 if self.interChip:
380 interChipTable = Table([{'IC_SOURCE_DET': sourceDet,
381 'IC_COEFFS': self.interChip[sourceDet].reshape(self.nAmp*self.nAmp)}
382 for sourceDet in self.interChip.keys()])
383 tableList.append(interChipTable)
384 return tableList
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.
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.
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}
418 output = image[amp.getBBox() if isTrimmed else amp.getRawDataBBox()]
419 thisAmpCorner = amp.getReadoutCorner()
420 targetAmpCorner = ampTarget.getReadoutCorner()
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)
427 @staticmethod
428 def calculateBackground(mi, badPixels=["BAD"]):
429 """Estimate median background in image.
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.
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()
446 stats = lsst.afw.math.StatisticsControl()
447 stats.setAndMask(mask.getPlaneBitMask(badPixels))
448 return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue()
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.
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``.
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.
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.hasCrosstalk is False:
496 self.fromDetector(detector, coeffVector=crosstalkCoeffs)
498 numAmps = len(detector)
499 if numAmps != self.nAmp:
500 raise RuntimeError(f"Crosstalk built for {self.nAmp} in {self._detectorName}, received "
501 f"{numAmps} in {detector.getName()}")
503 if sourceExposure:
504 source = sourceExposure.getMaskedImage()
505 sourceDetector = sourceExposure.getDetector()
506 else:
507 source = mi
508 sourceDetector = detector
510 if crosstalkCoeffs is not None:
511 coeffs = crosstalkCoeffs
512 else:
513 coeffs = self.coeffs
514 self.log.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.calculateBackground(source, badPixels)
521 backgrounds = [0.0 for amp in sourceDetector]
522 if backgroundMethod is None:
523 pass
524 elif backgroundMethod == "AMP":
525 backgrounds = [self.calculateBackground(source[amp.getBBox()], badPixels)
526 for amp in sourceDetector]
527 elif backgroundMethod == "DETECTOR":
528 backgrounds = [self.calculateBackground(source, badPixels) for amp in sourceDetector]
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)
539 # Define a subtrahend image to contain all the scaled crosstalk signals
540 subtrahend = source.Factory(source.getBBox())
541 subtrahend.set((0, 0, 0))
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.extractAmp(mi, jAmp, iAmp, isTrimmed)
550 jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks
551 jImage -= backgrounds[jj]
552 iImage.scaledPlus(coeffs[ii, jj], jImage)
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
560class CrosstalkConfig(Config):
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 )
601 def getCrosstalk(self, detector=None):
602 """Return a 2-D numpy array of crosstalk coefficients in the proper shape.
604 Parameters
605 ----------
606 detector : `lsst.afw.cameraGeom.detector`
607 Detector that is to be crosstalk corrected.
609 Returns
610 -------
611 coeffs : `numpy.ndarray`
612 Crosstalk coefficients that can be used to correct the detector.
614 Raises
615 ------
616 RuntimeError
617 Raised if no coefficients could be generated from this detector/configuration.
618 """
619 if self.useConfigCoefficients is True:
620 coeffs = np.array(self.crosstalkValues).reshape(self.crosstalkShape)
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")
633 def hasCrosstalk(self, detector=None):
634 """Return a boolean indicating if crosstalk coefficients exist.
636 Parameters
637 ----------
638 detector : `lsst.afw.cameraGeom.detector`
639 Detector that is to be crosstalk corrected.
641 Returns
642 -------
643 hasCrosstalk : `bool`
644 True if this detector/configuration has crosstalk coefficients defined.
645 """
646 if self.useConfigCoefficients is True and self.crosstalkValues 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
654class CrosstalkTask(Task):
655 """Apply intra-detector crosstalk correction."""
656 ConfigClass = CrosstalkConfig
657 _DefaultName = 'isrCrosstalk'
659 def prepCrosstalk(self, dataRef, crosstalk=None):
660 """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk.
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.
669 See also
670 --------
671 lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk
672 """
673 return
675 def run(self, exposure, crosstalk=None,
676 crosstalkSources=None, isTrimmed=False, camera=None):
677 """Apply intra-detector crosstalk correction
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.
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.")
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)
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]))
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]
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))
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!")
766class NullCrosstalkTask(CrosstalkTask):
767 def run(self, exposure, crosstalkSources=None):
768 self.log.info("Not performing any crosstalk correction")