23 Apply intra-detector crosstalk corrections
26 from astropy.table
import Table
30 from lsst.pex.config
import Config, Field, ChoiceField, ListField
36 __all__ = [
"CrosstalkCalib",
"CrosstalkConfig",
"CrosstalkTask",
41 """Calibration of amp-to-amp crosstalk coefficients.
45 detector : `lsst.afw.cameraGeom.Detector`, optional
46 Detector to use to pull coefficients from.
47 log : `lsst.log.Log`, optional
48 Log to write messages to.
50 Parameters to pass to parent constructor.
52 _OBSTYPE =
'CROSSTALK'
53 _SCHEMA =
'Gen3 Crosstalk'
68 'coeffErr',
'coeffNum',
'interChip'])
73 """Update calibration metadata.
75 This calls the base class's method after ensuring the required
76 calibration keywords will be saved.
80 setDate : `bool`, optional
81 Update the CALIBDATE fields in the metadata to the current
82 time. Defaults to False.
84 Other keyword parameters to set in the metadata.
89 kwargs[
'NAMP'] = self.
nAmp
96 """Set calibration parameters from the detector.
100 detector : `lsst.afw.cameraGeom.Detector`
101 Detector to use to set parameters from.
102 coeffVector : `numpy.array`, optional
103 Use the detector geometry (bounding boxes and flip
104 information), but use ``coeffVector`` instead of the
105 output of ``detector.getCrosstalk()``.
109 calib : `lsst.ip.isr.CrosstalkCalib`
110 The calibration constructed from the detector.
113 if detector.hasCrosstalk()
or coeffVector:
117 self.
nAmp = len(detector)
120 if coeffVector
is not None:
121 crosstalkCoeffs = coeffVector
123 crosstalkCoeffs = detector.getCrosstalk()
124 if len(crosstalkCoeffs) == 1
and crosstalkCoeffs[0] == 0.0:
129 raise RuntimeError(
"Crosstalk coefficients do not match detector shape. "
130 f
"{self.crosstalkShape} {self.nAmp}")
139 """Construct a calibration from a dictionary of properties.
141 Must be implemented by the specific calibration subclasses.
146 Dictionary of properties.
150 calib : `lsst.ip.isr.CalibType`
151 Constructed calibration.
156 Raised if the supplied dictionary is for a different
161 if calib._OBSTYPE != dictionary[
'metadata'][
'OBSTYPE']:
162 raise RuntimeError(f
"Incorrect crosstalk supplied. Expected {calib._OBSTYPE}, "
163 f
"found {dictionary['metadata']['OBSTYPE']}")
165 calib.setMetadata(dictionary[
'metadata'])
166 calib._detectorName = dictionary[
'metadata'][
'DETECTOR']
167 calib._detectorSerial = dictionary[
'metadata'][
'DETECTOR_SERIAL']
169 calib.hasCrosstalk = dictionary.get(
'hasCrosstalk',
170 dictionary[
'metadata'].get(
'HAS_CROSSTALK',
False))
171 if calib.hasCrosstalk:
172 calib.nAmp = dictionary.get(
'nAmp', dictionary[
'metadata'].get(
'NAMP', 0))
173 calib.crosstalkShape = (calib.nAmp, calib.nAmp)
174 calib.coeffs = np.array(dictionary[
'coeffs']).reshape(calib.crosstalkShape)
176 calib.interChip = dictionary.get(
'interChip',
None)
178 for detector
in calib.interChip:
179 coeffVector = calib.interChip[detector]
180 calib.interChip[detector] = np.array(coeffVector).reshape(calib.crosstalkShape)
182 calib.updateMetadata()
186 """Return a dictionary containing the calibration properties.
188 The dictionary should be able to be round-tripped through
194 Dictionary of properties.
200 outDict[
'metadata'] = metadata
203 outDict[
'nAmp'] = self.
nAmp
207 outDict[
'coeffs'] = self.
coeffs.reshape(ctLength).tolist()
210 outDict[
'coeffErr'] = self.
coeffErr.reshape(ctLength).tolist()
212 outDict[
'coeffNum'] = self.
coeffNum.reshape(ctLength).tolist()
215 outDict[
'interChip'] = dict()
217 outDict[
'interChip'][detector] = self.
interChip[detector].reshape(ctLength).tolist()
223 """Construct calibration from a list of tables.
225 This method uses the `fromDict` method to create the
226 calibration, after constructing an appropriate dictionary from
231 tableList : `list` [`lsst.afw.table.Table`]
232 List of tables to use to construct the crosstalk
237 calib : `lsst.ip.isr.CrosstalkCalib`
238 The calibration defined in the tables.
241 coeffTable = tableList[0]
243 metadata = coeffTable.meta
245 inDict[
'metadata'] = metadata
246 inDict[
'hasCrosstalk'] = metadata[
'HAS_CROSSTALK']
247 inDict[
'nAmp'] = metadata[
'NAMP']
249 inDict[
'coeffs'] = coeffTable[
'CT_COEFFS']
250 if len(tableList) > 1:
251 inDict[
'interChip'] = dict()
252 interChipTable = tableList[1]
253 for record
in interChipTable:
254 inDict[
'interChip'][record[
'IC_SOURCE_DET']] = record[
'IC_COEFFS']
259 """Construct a list of tables containing the information in this calibration.
261 The list of tables should create an identical calibration
262 after being passed to this class's fromTable method.
266 tableList : `list` [`lsst.afw.table.Table`]
267 List of tables containing the crosstalk calibration
273 catalog = Table([{
'CT_COEFFS': self.
coeffs.reshape(self.
nAmp*self.
nAmp)}])
275 tableList.append(catalog)
278 interChipTable = Table([{
'IC_SOURCE_DET': sourceDet,
281 tableList.append(interChipTable)
285 def extractAmp(self, image, amp, ampTarget, isTrimmed=False):
286 """Extract the image data from an amp, flipped to match ampTarget.
290 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
291 Image containing the amplifier of interest.
292 amp : `lsst.afw.cameraGeom.Amplifier`
293 Amplifier on image to extract.
294 ampTarget : `lsst.afw.cameraGeom.Amplifier`
295 Target amplifier that the extracted image will be flipped
298 The image is already trimmed.
299 TODO : DM-15409 will resolve this.
303 output : `lsst.afw.image.Image`
304 Image of the amplifier in the desired configuration.
306 X_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL:
False,
307 lsst.afw.cameraGeom.ReadoutCorner.LR:
True,
308 lsst.afw.cameraGeom.ReadoutCorner.UL:
False,
309 lsst.afw.cameraGeom.ReadoutCorner.UR:
True}
310 Y_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL:
False,
311 lsst.afw.cameraGeom.ReadoutCorner.LR:
False,
312 lsst.afw.cameraGeom.ReadoutCorner.UL:
True,
313 lsst.afw.cameraGeom.ReadoutCorner.UR:
True}
315 output = image[amp.getBBox()
if isTrimmed
else amp.getRawDataBBox()]
316 thisAmpCorner = amp.getReadoutCorner()
317 targetAmpCorner = ampTarget.getReadoutCorner()
320 xFlip = X_FLIP[targetAmpCorner] ^ X_FLIP[thisAmpCorner]
321 yFlip = Y_FLIP[targetAmpCorner] ^ Y_FLIP[thisAmpCorner]
322 self.
log.debug(
"Extract amp: %s %s %s %s",
323 amp.getName(), ampTarget.getName(), thisAmpCorner, targetAmpCorner)
327 """Estimate median background in image.
329 Getting a great background model isn't important for crosstalk correction,
330 since the crosstalk is at a low level. The median should be sufficient.
334 mi : `lsst.afw.image.MaskedImage`
335 MaskedImage for which to measure background.
336 badPixels : `list` of `str`
337 Mask planes to ignore.
341 Median background level.
345 stats.setAndMask(mask.getPlaneBitMask(badPixels))
349 badPixels=["BAD"], minPixelToMask=45000,
350 crosstalkStr="CROSSTALK", isTrimmed=False,
351 backgroundMethod="None"):
352 """Subtract the crosstalk from thisExposure, optionally using a different source.
354 We set the mask plane indicated by ``crosstalkStr`` in a target amplifier
355 for pixels in a source amplifier that exceed ``minPixelToMask``. Note that
356 the correction is applied to all pixels in the amplifier, but only those
357 that have a substantial crosstalk are masked with ``crosstalkStr``.
359 The uncorrected image is used as a template for correction. This is good
360 enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's
361 larger you may want to iterate.
365 thisExposure : `lsst.afw.image.Exposure`
366 Exposure for which to subtract crosstalk.
367 sourceExposure : `lsst.afw.image.Exposure`, optional
368 Exposure to use as the source of the crosstalk. If not set,
369 thisExposure is used as the source (intra-detector crosstalk).
370 crosstalkCoeffs : `numpy.ndarray`, optional.
371 Coefficients to use to correct crosstalk.
372 badPixels : `list` of `str`
373 Mask planes to ignore.
374 minPixelToMask : `float`
375 Minimum pixel value (relative to the background level) in
376 source amplifier for which to set ``crosstalkStr`` mask plane
379 Mask plane name for pixels greatly modified by crosstalk
380 (above minPixelToMask).
382 The image is already trimmed.
383 This should no longer be needed once DM-15409 is resolved.
384 backgroundMethod : `str`
385 Method used to subtract the background. "AMP" uses
386 amplifier-by-amplifier background levels, "DETECTOR" uses full
387 exposure/maskedImage levels. Any other value results in no
388 background subtraction.
390 mi = thisExposure.getMaskedImage()
392 detector = thisExposure.getDetector()
394 self.
fromDetector(detector, coeffVector=crosstalkCoeffs)
396 numAmps = len(detector)
397 if numAmps != self.
nAmp:
398 raise RuntimeError(f
"Crosstalk built for {self.nAmp} in {self._detectorName}, received "
399 f
"{numAmps} in {detector.getName()}")
402 source = sourceExposure.getMaskedImage()
403 sourceDetector = sourceExposure.getDetector()
406 sourceDetector = detector
408 if crosstalkCoeffs
is not None:
409 coeffs = crosstalkCoeffs
412 self.
log.debug(
"CT COEFF: %s", coeffs)
419 backgrounds = [0.0
for amp
in sourceDetector]
420 if backgroundMethod
is None:
422 elif backgroundMethod ==
"AMP":
424 for amp
in sourceDetector]
425 elif backgroundMethod ==
"DETECTOR":
430 crosstalkPlane = mask.addMaskPlane(crosstalkStr)
433 + thresholdBackground))
434 footprints.setMask(mask, crosstalkStr)
435 crosstalk = mask.getPlaneBitMask(crosstalkStr)
438 subtrahend = source.Factory(source.getBBox())
439 subtrahend.set((0, 0, 0))
441 coeffs = coeffs.transpose()
442 for ii, iAmp
in enumerate(sourceDetector):
443 iImage = subtrahend[iAmp.getBBox()
if isTrimmed
else iAmp.getRawDataBBox()]
444 for jj, jAmp
in enumerate(detector):
445 if coeffs[ii, jj] == 0.0:
447 jImage = self.
extractAmp(mi, jAmp, iAmp, isTrimmed)
448 jImage.getMask().getArray()[:] &= crosstalk
449 jImage -= backgrounds[jj]
450 iImage.scaledPlus(coeffs[ii, jj], jImage)
454 mask.clearMaskPlane(crosstalkPlane)
459 """Configuration for intra-detector crosstalk removal."""
460 minPixelToMask = Field(
462 doc=
"Set crosstalk mask plane for pixels over this value.",
465 crosstalkMaskPlane = Field(
467 doc=
"Name for crosstalk mask plane.",
470 crosstalkBackgroundMethod = ChoiceField(
472 doc=
"Type of background subtraction to use when applying correction.",
475 "None":
"Do no background subtraction.",
476 "AMP":
"Subtract amplifier-by-amplifier background levels.",
477 "DETECTOR":
"Subtract detector level background."
480 useConfigCoefficients = Field(
482 doc=
"Ignore the detector crosstalk information in favor of CrosstalkConfig values?",
485 crosstalkValues = ListField(
487 doc=(
"Amplifier-indexed crosstalk coefficients to use. This should be arranged as a 1 x nAmp**2 "
488 "list of coefficients, such that when reshaped by crosstalkShape, the result is nAmp x nAmp. "
489 "This matrix should be structured so CT * [amp0 amp1 amp2 ...]^T returns the column "
490 "vector [corr0 corr1 corr2 ...]^T."),
493 crosstalkShape = ListField(
495 doc=
"Shape of the coefficient array. This should be equal to [nAmp, nAmp].",
500 """Return a 2-D numpy array of crosstalk coefficients in the proper shape.
504 detector : `lsst.afw.cameraGeom.detector`
505 Detector that is to be crosstalk corrected.
509 coeffs : `numpy.ndarray`
510 Crosstalk coefficients that can be used to correct the detector.
515 Raised if no coefficients could be generated from this detector/configuration.
519 if detector
is not None:
521 if coeffs.shape != (nAmp, nAmp):
522 raise RuntimeError(
"Constructed crosstalk coeffients do not match detector shape. "
523 f
"{coeffs.shape} {nAmp}")
525 elif detector
is not None and detector.hasCrosstalk()
is True:
527 return detector.getCrosstalk()
529 raise RuntimeError(
"Attempted to correct crosstalk without crosstalk coefficients")
532 """Return a boolean indicating if crosstalk coefficients exist.
536 detector : `lsst.afw.cameraGeom.detector`
537 Detector that is to be crosstalk corrected.
541 hasCrosstalk : `bool`
542 True if this detector/configuration has crosstalk coefficients defined.
546 elif detector
is not None and detector.hasCrosstalk()
is True:
553 """Apply intra-detector crosstalk correction."""
554 ConfigClass = CrosstalkConfig
555 _DefaultName =
'isrCrosstalk'
558 """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk.
562 dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
563 Butler reference of the detector data to be processed.
564 crosstalk : `~lsst.ip.isr.CrosstalkConfig`
565 Crosstalk calibration that will be used.
569 lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk
573 def run(self, exposure, crosstalk=None,
574 crosstalkSources=None, isTrimmed=False):
575 """Apply intra-detector crosstalk correction
579 exposure : `lsst.afw.image.Exposure`
580 Exposure for which to remove crosstalk.
581 crosstalkCalib : `lsst.ip.isr.CrosstalkCalib`, optional
582 External crosstalk calibration to apply. Constructed from
583 detector if not found.
584 crosstalkSources : `defaultdict`, optional
585 Image data for other detectors that are sources of
586 crosstalk in exposure. The keys are expected to be names
587 of the other detectors, with the values containing
588 `lsst.afw.image.Exposure` at the same level of processing
590 The default for intra-detector crosstalk here is None.
592 The image is already trimmed.
593 This should no longer be needed once DM-15409 is resolved.
598 Raised if called for a detector that does not have a
599 crosstalk correction.
603 crosstalk = crosstalk.fromDetector(exposure.getDetector(),
604 coeffVector=self.config.crosstalkValues)
605 if not crosstalk.log:
606 crosstalk.log = self.log
607 if not crosstalk.hasCrosstalk:
608 raise RuntimeError(
"Attempted to correct crosstalk without crosstalk coefficients.")
611 self.log.info(
"Applying crosstalk correction.")
612 crosstalk.subtractCrosstalk(exposure, crosstalkCoeffs=crosstalk.coeffs,
613 minPixelToMask=self.config.minPixelToMask,
614 crosstalkStr=self.config.crosstalkMaskPlane, isTrimmed=isTrimmed,
615 backgroundMethod=self.config.crosstalkBackgroundMethod)
617 if crosstalk.interChip:
619 for detName
in crosstalk.interChip:
620 if isinstance(crosstalkSources[0],
'lsst.afw.image.Exposure'):
622 sourceNames = [exp.getDetector().getName()
for exp
in crosstalkSources]
625 sourceNames = [expRef.get(datasetType=
'isrOscanCorr').getDetector().getName()
626 for expRef
in crosstalkSources]
627 if detName
not in sourceNames:
628 self.log.warn(
"Crosstalk lists %s, not found in sources: %s",
629 detName, sourceNames)
631 interChipCoeffs = crosstalk.interChip[detName]
632 sourceExposure = crosstalkSources[sourceNames.index(detName)]
633 crosstalk.subtractCrosstalk(exposure, sourceExposure=sourceExposure,
634 crosstalkCoeffs=interChipCoeffs,
635 minPixelToMask=self.config.minPixelToMask,
636 crosstalkStr=self.config.crosstalkMaskPlane,
638 backgroundMethod=self.config.crosstalkBackgroundMethod)
640 self.log.warn(
"Crosstalk contains interChip coefficients, but no sources found!")
644 def run(self, exposure, crosstalkSources=None):
645 self.log.info(
"Not performing any crosstalk correction")