lsst.ip.isr 22.0.1-28-gcb2782f+838c70c03d
crosstalk.py
Go to the documentation of this file.
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
27
28import lsst.afw.math
30import lsst.daf.butler
31from lsst.pex.config import Config, Field, ChoiceField, ListField
32from lsst.pipe.base import Task
33
34from 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 ----------
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 -------
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 -------
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 ----------
399 Image containing the amplifier of interest.
401 Amplifier on image to extract.
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 ----------
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
565class CrosstalkConfig(Config):
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
659class 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 ----------
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")
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
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
std::shared_ptr< ImageT > flipImage(ImageT const &inImage, bool flipLR, bool flipTB)