227 """Construct a calibration from a dictionary of properties.
229 Must be implemented by the specific calibration subclasses.
234 Dictionary of properties.
238 calib : `lsst.ip.isr.CalibType`
239 Constructed calibration.
244 Raised if the supplied dictionary is for a different
249 if calib._OBSTYPE != dictionary[
'metadata'][
'OBSTYPE']:
250 raise RuntimeError(f
"Incorrect crosstalk supplied. Expected {calib._OBSTYPE}, "
251 f
"found {dictionary['metadata']['OBSTYPE']}")
253 calib.setMetadata(dictionary[
'metadata'])
255 if 'detectorName' in dictionary:
256 calib._detectorName = dictionary.get(
'detectorName')
257 elif 'DETECTOR_NAME' in dictionary:
258 calib._detectorName = dictionary.get(
'DETECTOR_NAME')
259 elif 'DET_NAME' in dictionary[
'metadata']:
260 calib._detectorName = dictionary[
'metadata'][
'DET_NAME']
262 calib._detectorName =
None
264 if 'detectorSerial' in dictionary:
265 calib._detectorSerial = dictionary.get(
'detectorSerial')
266 elif 'DETECTOR_SERIAL' in dictionary:
267 calib._detectorSerial = dictionary.get(
'DETECTOR_SERIAL')
268 elif 'DET_SER' in dictionary[
'metadata']:
269 calib._detectorSerial = dictionary[
'metadata'][
'DET_SER']
271 calib._detectorSerial =
None
273 if 'detectorId' in dictionary:
274 calib._detectorId = dictionary.get(
'detectorId')
275 elif 'DETECTOR' in dictionary:
276 calib._detectorId = dictionary.get(
'DETECTOR')
277 elif 'DETECTOR' in dictionary[
'metadata']:
278 calib._detectorId = dictionary[
'metadata'][
'DETECTOR']
279 elif calib._detectorSerial:
280 calib._detectorId = calib._detectorSerial
282 calib._detectorId =
None
284 if 'instrument' in dictionary:
285 calib._instrument = dictionary.get(
'instrument')
286 elif 'INSTRUME' in dictionary[
'metadata']:
287 calib._instrument = dictionary[
'metadata'][
'INSTRUME']
289 calib._instrument =
None
291 calib.hasCrosstalk = dictionary.get(
'hasCrosstalk',
292 dictionary[
'metadata'].get(
'HAS_CROSSTALK',
False))
293 if calib.hasCrosstalk:
294 calib.nAmp = dictionary.get(
'nAmp', dictionary[
'metadata'].get(
'NAMP', 0))
295 calib.crosstalkShape = (calib.nAmp, calib.nAmp)
296 calib.coeffs = np.array(dictionary[
'coeffs']).reshape(calib.crosstalkShape)
297 calib.crosstalkRatiosUnits = dictionary.get(
298 'crosstalkRatiosUnits',
299 dictionary[
'metadata'].get(
'CROSSTALK_RATIOS_UNITS',
'adu'))
300 if 'coeffErr' in dictionary:
301 calib.coeffErr = np.array(dictionary[
'coeffErr']).reshape(calib.crosstalkShape)
303 calib.coeffErr = np.zeros_like(calib.coeffs)
304 if 'coeffNum' in dictionary:
305 calib.coeffNum = np.array(dictionary[
'coeffNum']).reshape(calib.crosstalkShape)
307 calib.coeffNum = np.zeros_like(calib.coeffs, dtype=int)
308 if 'coeffValid' in dictionary:
309 calib.coeffValid = np.array(dictionary[
'coeffValid']).reshape(calib.crosstalkShape)
311 calib.coeffValid = np.ones_like(calib.coeffs, dtype=bool)
312 if 'coeffsSqr' in dictionary:
313 calib.coeffsSqr = np.array(dictionary[
'coeffsSqr']).reshape(calib.crosstalkShape)
315 calib.coeffsSqr = np.zeros_like(calib.coeffs)
316 if 'coeffErrSqr' in dictionary:
317 calib.coeffErrSqr = np.array(dictionary[
'coeffErrSqr']).reshape(calib.crosstalkShape)
319 calib.coeffErrSqr = np.zeros_like(calib.coeffs)
320 if 'ampGainRatios' in dictionary:
321 calib.ampGainRatios = np.array(dictionary[
'ampGainRatios']).reshape(calib.crosstalkShape)
323 calib.ampGainRatios = np.zeros_like(calib.coeffs)
324 if 'fitGains' in dictionary:
327 fitGains = np.array(dictionary[
'fitGains'])
328 if len(fitGains) == 1:
330 calib.fitGains = np.zeros(calib.nAmp)
332 calib.fitGains = np.array(dictionary[
'fitGains']).reshape(calib.nAmp)
334 calib.fitGains = np.zeros(calib.nAmp)
336 calib.interChip = dictionary.get(
'interChip',
None)
338 for detector
in calib.interChip:
339 coeffVector = calib.interChip[detector]
340 calib.interChip[detector] = np.array(coeffVector).reshape(calib.crosstalkShape)
342 calib.updateMetadata()
489 def extractAmp(image, ampToFlip, ampTarget, isTrimmed=False, fullAmplifier=False, parallelOverscan=None):
490 """Extract the image data from an amp, flipped to match ampTarget.
494 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
495 Image containing the amplifier of interest.
496 amp : `lsst.afw.cameraGeom.Amplifier`
497 Amplifier on image to extract.
498 ampTarget : `lsst.afw.cameraGeom.Amplifier`
499 Target amplifier that the extracted image will be flipped
501 isTrimmed : `bool`, optional
502 The image is already trimmed.
503 fullAmplifier : `bool`, optional
504 Use full amplifier and not just imaging region.
505 parallelOverscan : `bool`, optional
506 This has been deprecated and is unused, and will be removed
511 output : `lsst.afw.image.Image`
512 Amplifier from image, flipped to desired configuration.
513 This will always return a copy of the original data.
515 if parallelOverscan
is not None:
517 "The parallelOverscan option has been deprecated and will be removed after v29.",
521 X_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL:
False,
522 lsst.afw.cameraGeom.ReadoutCorner.LR:
True,
523 lsst.afw.cameraGeom.ReadoutCorner.UL:
False,
524 lsst.afw.cameraGeom.ReadoutCorner.UR:
True}
525 Y_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL:
False,
526 lsst.afw.cameraGeom.ReadoutCorner.LR:
False,
527 lsst.afw.cameraGeom.ReadoutCorner.UL:
True,
528 lsst.afw.cameraGeom.ReadoutCorner.UR:
True}
530 bbox = CrosstalkCalib._getAppropriateBBox(ampToFlip, isTrimmed, fullAmplifier)
533 sourceAmpCorner = ampToFlip.getReadoutCorner()
534 targetAmpCorner = ampTarget.getReadoutCorner()
538 xFlip = X_FLIP[targetAmpCorner] ^ X_FLIP[sourceAmpCorner]
539 yFlip = Y_FLIP[targetAmpCorner] ^ Y_FLIP[sourceAmpCorner]
588 crosstalkCoeffsSqr=None, crosstalkCoeffsValid=None,
589 badPixels=["BAD"], minPixelToMask=45000, doSubtrahendMasking=False,
590 crosstalkStr="CROSSTALK", isTrimmed=None,
591 backgroundMethod="None", doSqrCrosstalk=False, fullAmplifier=False,
592 parallelOverscan=None, detectorConfig=None, badAmpDict=None,
593 ignoreVariance=False):
594 """Subtract the crosstalk from thisExposure, optionally using a
597 We set the mask plane indicated by ``crosstalkStr`` in a
598 target amplifier for pixels in a source amplifier that exceed
599 ``minPixelToMask``, if ``doSubtrahendMasking`` is False. With
600 that enabled, the mask is only set if the absolute value of
601 the correction applied exceeds ``minPixelToMask``. Note that
602 the correction is applied to all pixels in the amplifier, but
603 only those that have a substantial crosstalk are masked with
606 The uncorrected image is used as a template for correction. This is
607 good enough if the crosstalk is small (e.g., coefficients < ~ 1e-3),
608 but if it's larger you may want to iterate.
612 thisExposure : `lsst.afw.image.Exposure`
613 Exposure for which to subtract crosstalk.
614 sourceExposure : `lsst.afw.image.Exposure`, optional
615 Exposure to use as the source of the crosstalk. If not set,
616 thisExposure is used as the source (intra-detector crosstalk).
617 crosstalkCoeffs : `numpy.ndarray`, optional.
618 Coefficients to use to correct crosstalk.
619 crosstalkCoeffsSqr : `numpy.ndarray`, optional.
620 Quadratic coefficients to use to correct crosstalk.
621 crosstalkCoeffsValid : `numpy.ndarray`, optional
622 Boolean array that is True where coefficients are valid.
623 badPixels : `list` of `str`, optional
624 Mask planes to ignore.
625 minPixelToMask : `float`, optional
626 Minimum pixel value to set the ``crosstalkStr`` mask
627 plane. If doSubtrahendMasking is True, this is calculated
628 from the absolute magnitude of the subtrahend image.
629 Otherwise, this sets the minimum source value to use to
631 doSubtrahendMasking : `bool`, optional
632 If true, the mask is calculated from the properties of the
633 subtrahend image, not from the brightness of the source
635 crosstalkStr : `str`, optional
636 Mask plane name for pixels greatly modified by crosstalk
637 (above minPixelToMask).
638 isTrimmed : `bool`, optional
639 This option has been deprecated and does not do anything.
640 It will be removed after v29.
641 backgroundMethod : `str`, optional
642 Method used to subtract the background. "AMP" uses
643 amplifier-by-amplifier background levels, "DETECTOR" uses full
644 exposure/maskedImage levels. Any other value results in no
645 background subtraction.
646 doSqrCrosstalk: `bool`, optional
647 Should the quadratic crosstalk coefficients be used for the
648 crosstalk correction?
649 fullAmplifier : `bool`, optional
650 Use full amplifier and not just imaging region.
651 parallelOverscan : `bool`, optional
652 This option is deprecated and will be removed after v29.
653 detectorConfig : `lsst.ip.isr.overscanDetectorConfig`, optional
654 Per-amplifier configs to use if parallelOverscan is True.
655 This option is deprecated and will be removed after v29.
656 badAmpDict : `dict` [`str`, `bool`], optional
657 Dictionary to identify bad amplifiers that should not be
658 source or target for crosstalk correction.
659 ignoreVariance : `bool`, optional
660 Ignore the variance plane when doing crosstalk correction?
665 For a given image I, we want to find the crosstalk subtrahend
668 The subtrahend image is the sum of all crosstalk contributions
669 that appear in I, so we can build it up by amplifier. Each
670 amplifier A in image I sees the contributions from all other
671 amplifiers B_v != A. For the current linear model, we set `sImage`
672 equal to the segment of the subtrahend image CT corresponding to
673 amplifier A, and then build it up as:
674 simage_linear = sum_v coeffsA_v * (B_v - bkg_v) where coeffsA_v
675 is the vector of crosstalk coefficients for sources that cause
676 images in amplifier A. The bkg_v term in this equation is
677 identically 0.0 for all cameras except obs_subaru (and is only
678 non-zero there for historical reasons).
679 To include the non-linear term, we can again add to the subtrahend
680 image using the same loop, as:
682 simage_nonlinear = sum_v (coeffsA_v * B_v) + (NLcoeffsA_v * B_v * B_v)
683 = sum_v linear_term_v + nonlinear_term_v
685 where coeffsA_v is the linear term, and NLcoeffsA_v are the quadratic
686 component. For LSSTCam, it has been observed that the linear_term_v >>
689 targetMaskedImage = thisExposure.maskedImage
690 targetDetector = thisExposure.getDetector()
692 self.
fromDetector(targetDetector, coeffVector=crosstalkCoeffs)
695 if isTrimmed
is not None:
697 "The isTrimmed option has been deprecated and will be removed after v29.",
703 if parallelOverscan
is not None:
705 "The parallelOverscan option has been deprecated and will be removed after v29.",
708 if detectorConfig
is not None:
710 "The detectorConfig option has been deprecated and will be removed after v29.",
714 numAmps = len(targetDetector)
715 if numAmps != self.
nAmp:
716 raise RuntimeError(f
"Crosstalk built for {self.nAmp} in {self._detectorName}, received "
717 f
"{numAmps} in {targetDetector.getName()}")
719 if doSqrCrosstalk
and crosstalkCoeffsSqr
is None:
720 raise RuntimeError(
"Attempted to perform NL crosstalk correction without NL "
721 "crosstalk coefficients.")
723 if fullAmplifier
and (backgroundMethod !=
"None"):
724 raise RuntimeError(
"Cannot do full amplifier with background subtraction.")
727 sourceMaskedImage = sourceExposure.maskedImage
728 sourceDetector = sourceExposure.getDetector()
730 sourceMaskedImage = targetMaskedImage
731 sourceDetector = targetDetector
733 if crosstalkCoeffs
is not None:
734 coeffs = np.asarray(crosstalkCoeffs).copy()
736 coeffs = np.asarray(self.
coeffs).copy()
737 self.
log.debug(
"CT COEFF: %s", coeffs)
740 coeffsSqr = np.asarray(crosstalkCoeffsSqr).copy()
741 self.
log.debug(
"CT COEFF SQR: %s", coeffsSqr)
743 coeffsSqr = np.zeros_like(coeffs)
746 badCoeffs = ~np.isfinite(coeffs) | (coeffs == 0.0)
747 coeffs[badCoeffs] = 0.0
748 coeffsSqr[badCoeffs] = 0.0
749 if crosstalkCoeffsValid
is not None:
750 coeffs[~crosstalkCoeffsValid] = 0.0
751 coeffsSqr[~crosstalkCoeffsValid] = 0.0
754 for index, amp
in enumerate(sourceDetector):
755 if badAmpDict[amp.getName()]:
756 coeffs[index, :] = 0.0
757 coeffs[:, index] = 0.0
760 backgrounds = {amp.getName(): 0.0
for amp
in sourceDetector}
761 if backgroundMethod ==
"AMP":
769 for amp
in sourceDetector
771 elif backgroundMethod ==
"DETECTOR":
773 backgrounds = {amp.getName(): background
for amp
in sourceDetector}
776 sourceCrosstalkPlane = sourceMaskedImage.mask.addMaskPlane(crosstalkStr)
777 if sourceExposure
is not None:
778 targetCrosstalkPlane = targetMaskedImage.mask.addMaskPlane(crosstalkStr)
780 targetCrosstalkPlane = sourceCrosstalkPlane
785 if not doSubtrahendMasking:
788 toMask = (sourceMaskedImage.image.array >= (minPixelToMask + thresholdBackground))
789 sourceMaskedImage.mask.array[toMask] |= sourceMaskedImage.mask.getPlaneBitMask(crosstalkStr)
791 crosstalk = sourceMaskedImage.mask.getPlaneBitMask(crosstalkStr)
795 subtrahend = targetMaskedImage.Factory(targetMaskedImage.getBBox())
796 subtrahend.set((0, 0, 0))
800 imageOnly = ignoreVariance
and doSubtrahendMasking
802 for sourceIndex, sourceAmp
in enumerate(sourceDetector):
803 for targetIndex, targetAmp
in enumerate(targetDetector):
804 coeff = coeffs[sourceIndex, targetIndex]
805 coeffSqr = coeffsSqr[sourceIndex, targetIndex]
816 sourceMaskedImage.image,
820 fullAmplifier=fullAmplifier,
822 targetImage = subtrahend[targetBBox].image
829 fullAmplifier=fullAmplifier,
831 targetImage = subtrahend[targetBBox]
834 sourceImage.mask.array[:] &= crosstalk
836 if backgrounds[sourceAmp.getName()] != 0.0:
837 sourceImage -= backgrounds[sourceAmp.getName()]
842 targetImage.scaledPlus(coeff, sourceImage)
844 sourceImage.scaledMultiplies(1.0, sourceImage)
845 targetImage.scaledPlus(coeffSqr, sourceImage)
849 sourceMaskedImage.mask.clearMaskPlane(sourceCrosstalkPlane)
850 if sourceExposure
is not None:
851 targetMaskedImage.mask.clearMaskPlane(targetCrosstalkPlane)
853 if doSubtrahendMasking:
862 subtrahend.mask.clearMaskPlane(targetCrosstalkPlane)
872 subtrahendBackgrounds = {}
873 for amp
in targetDetector:
877 ampData = subtrahend[bbox]
878 background = np.median(ampData.image.array)
879 subtrahendBackgrounds[amp.getName()] = background
880 ampData.image.array[:, :] -= background
881 self.
log.debug(f
"Subtrahend background level: {amp.getName()} {background}")
883 toMask = (subtrahend.image.array >= minPixelToMask) | (subtrahend.image.array <= -minPixelToMask)
884 subtrahend.mask.array[toMask] |= subtrahend.mask.getPlaneBitMask(crosstalkStr)
887 for amp
in targetDetector:
889 ampData = subtrahend[bbox]
890 background = subtrahendBackgrounds[amp.getName()]
891 ampData.image.array[:, :] += background
896 targetMaskedImage -= subtrahend