23 Apply intra-detector crosstalk corrections
30 from lsst.pex.config
import Config, Field, ChoiceField, ListField
33 __all__ = [
"CrosstalkConfig",
"CrosstalkTask",
"subtractCrosstalk",
"writeCrosstalkCoeffs",
38 """Configuration for intra-detector crosstalk removal."""
39 minPixelToMask = Field(
41 doc=
"Set crosstalk mask plane for pixels over this value.",
44 crosstalkMaskPlane = Field(
46 doc=
"Name for crosstalk mask plane.",
49 crosstalkBackgroundMethod = ChoiceField(
51 doc=
"Type of background subtraction to use when applying correction.",
54 "None":
"Do no background subtraction.",
55 "AMP":
"Subtract amplifier-by-amplifier background levels.",
56 "DETECTOR":
"Subtract detector level background."
59 useConfigCoefficients = Field(
61 doc=
"Ignore the detector crosstalk information in favor of CrosstalkConfig values?",
64 crosstalkValues = ListField(
66 doc=(
"Amplifier-indexed crosstalk coefficients to use. This should be arranged as a 1 x nAmp**2 "
67 "list of coefficients, such that when reshaped by crosstalkShape, the result is nAmp x nAmp. "
68 "This matrix should be structured so CT * [amp0 amp1 amp2 ...]^T returns the column "
69 "vector [corr0 corr1 corr2 ...]^T."),
72 crosstalkShape = ListField(
74 doc=
"Shape of the coefficient array. This should be equal to [nAmp, nAmp].",
79 """Return a 2-D numpy array of crosstalk coefficients in the proper shape.
83 detector : `lsst.afw.cameraGeom.detector`
84 Detector that is to be crosstalk corrected.
88 coeffs : `numpy.ndarray`
89 Crosstalk coefficients that can be used to correct the detector.
94 Raised if no coefficients could be generated from this detector/configuration.
98 if detector
is not None:
100 if coeffs.shape != (nAmp, nAmp):
101 raise RuntimeError(
"Constructed crosstalk coeffients do not match detector shape. " +
102 f
"{coeffs.shape} {nAmp}")
104 elif detector
is not None and detector.hasCrosstalk()
is True:
106 return detector.getCrosstalk()
108 raise RuntimeError(
"Attempted to correct crosstalk without crosstalk coefficients")
111 """Return a boolean indicating if crosstalk coefficients exist.
115 detector : `lsst.afw.cameraGeom.detector`
116 Detector that is to be crosstalk corrected.
120 hasCrosstalk : `bool`
121 True if this detector/configuration has crosstalk coefficients defined.
125 elif detector
is not None and detector.hasCrosstalk()
is True:
132 """Apply intra-detector crosstalk correction."""
133 ConfigClass = CrosstalkConfig
134 _DefaultName =
'isrCrosstalk'
137 """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk.
141 dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
142 Butler reference of the detector data to be processed.
146 lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk
150 def run(self, exposure, crosstalkSources=None, isTrimmed=False):
151 """Apply intra-detector crosstalk correction
155 exposure : `lsst.afw.image.Exposure`
156 Exposure for which to remove crosstalk.
157 crosstalkSources : `defaultdict`, optional
158 Image data and crosstalk coefficients from other detectors/amps that are
159 sources of crosstalk in exposure.
160 The default for intra-detector crosstalk here is None.
162 The image is already trimmed.
163 This should no longer be needed once DM-15409 is resolved.
168 Raised if called for a detector that does not have a
169 crosstalk correction.
171 Raised if crosstalkSources is not None
172 and not a numpy array or a dictionary.
174 if crosstalkSources
is not None:
175 if isinstance(crosstalkSources, np.ndarray):
176 coeffs = crosstalkSources
177 elif isinstance(crosstalkSources, dict):
180 for fKey, fValue
in crosstalkSources.items():
181 for sKey, sValue
in fValue.items():
184 tempDict = crosstalkSources[firstKey][secondKey]
186 for thirdKey
in tempDict:
188 for fourthKey
in tempDict[thirdKey]:
189 value = tempDict[thirdKey][fourthKey]
190 tempList.append(value)
191 coeffs.append(tempList)
192 coeffs = np.array(coeffs)
194 raise TypeError(
"crosstalkSources not of the correct type: `np.array` or `dict`")
196 detector = exposure.getDetector()
197 if not self.config.hasCrosstalk(detector=detector):
198 raise RuntimeError(
"Attempted to correct crosstalk without crosstalk coefficients")
199 coeffs = self.config.getCrosstalk(detector=detector)
201 self.log.
info(
"Applying crosstalk correction.")
203 minPixelToMask=self.config.minPixelToMask,
204 crosstalkStr=self.config.crosstalkMaskPlane, isTrimmed=isTrimmed,
205 backgroundMethod=self.config.crosstalkBackgroundMethod)
210 X_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL:
False, lsst.afw.cameraGeom.ReadoutCorner.LR:
True,
211 lsst.afw.cameraGeom.ReadoutCorner.UL:
False, lsst.afw.cameraGeom.ReadoutCorner.UR:
True}
212 Y_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL:
False, lsst.afw.cameraGeom.ReadoutCorner.LR:
False,
213 lsst.afw.cameraGeom.ReadoutCorner.UL:
True, lsst.afw.cameraGeom.ReadoutCorner.UR:
True}
217 def run(self, exposure, crosstalkSources=None):
218 self.
log.
info(
"Not performing any crosstalk correction")
222 """Return an image of the amp
224 The returned image will have the amp's readout corner in the
229 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
230 Image containing the amplifier of interest.
231 amp : `lsst.afw.table.AmpInfoRecord`
232 Amplifier information.
233 corner : `lsst.afw.table.ReadoutCorner` or `None`
234 Corner in which to put the amp's readout corner, or `None` for
237 The image is already trimmed.
238 This should no longer be needed once DM-15409 is resolved.
242 output : `lsst.afw.image.Image`
243 Image of the amplifier in the standard configuration.
245 output = image[amp.getBBox()
if isTrimmed
else amp.getRawDataBBox()]
246 ampCorner = amp.getReadoutCorner()
248 xFlip = X_FLIP[corner] ^ X_FLIP[ampCorner]
249 yFlip = Y_FLIP[corner] ^ Y_FLIP[ampCorner]
254 """Calculate median background in image
256 Getting a great background model isn't important for crosstalk correction,
257 since the crosstalk is at a low level. The median should be sufficient.
261 mi : `lsst.afw.image.MaskedImage`
262 MaskedImage for which to measure background.
263 badPixels : `list` of `str`
264 Mask planes to ignore.
269 Median background level.
273 stats.setAndMask(mask.getPlaneBitMask(badPixels))
278 badPixels=["BAD"], minPixelToMask=45000,
279 crosstalkStr="CROSSTALK", isTrimmed=False,
280 backgroundMethod="None"):
281 """Subtract the intra-detector crosstalk from an exposure
283 We set the mask plane indicated by ``crosstalkStr`` in a target amplifier
284 for pixels in a source amplifier that exceed `minPixelToMask`. Note that
285 the correction is applied to all pixels in the amplifier, but only those
286 that have a substantial crosstalk are masked with ``crosstalkStr``.
288 The uncorrected image is used as a template for correction. This is good
289 enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's
290 larger you may want to iterate.
292 This method needs unittests (DM-18876), but such testing requires
293 DM-18610 to allow the test detector to have the crosstalk
298 exposure : `lsst.afw.image.Exposure`
299 Exposure for which to subtract crosstalk.
300 crosstalkCoeffs : `numpy.ndarray`
301 Coefficients to use to correct crosstalk.
302 badPixels : `list` of `str`
303 Mask planes to ignore.
304 minPixelToMask : `float`
305 Minimum pixel value (relative to the background level) in
306 source amplifier for which to set ``crosstalkStr`` mask plane
309 Mask plane name for pixels greatly modified by crosstalk.
311 The image is already trimmed.
312 This should no longer be needed once DM-15409 is resolved.
313 backgroundMethod : `str`
314 Method used to subtract the background. "AMP" uses
315 amplifier-by-amplifier background levels, "DETECTOR" uses full
316 exposure/maskedImage levels. Any other value results in no
317 background subtraction.
319 mi = exposure.getMaskedImage()
322 ccd = exposure.getDetector()
324 if crosstalkCoeffs
is None:
325 coeffs = ccd.getCrosstalk()
327 coeffs = crosstalkCoeffs
328 assert coeffs.shape == (numAmps, numAmps)
336 backgrounds = [0.0
for amp
in ccd]
337 if backgroundMethod
is None:
339 elif backgroundMethod ==
"AMP":
341 elif backgroundMethod ==
"DETECTOR":
345 crosstalkPlane = mask.addMaskPlane(crosstalkStr)
347 thresholdBackground))
348 footprints.setMask(mask, crosstalkStr)
349 crosstalk = mask.getPlaneBitMask(crosstalkStr)
352 subtrahend = mi.Factory(mi.getBBox())
353 subtrahend.set((0, 0, 0))
354 for ii, iAmp
in enumerate(ccd):
355 iImage = subtrahend[iAmp.getBBox()
if isTrimmed
else iAmp.getRawDataBBox()]
356 for jj, jAmp
in enumerate(ccd):
358 assert coeffs[ii, jj] == 0.0
359 if coeffs[ii, jj] == 0.0:
362 jImage =
extractAmp(mi, jAmp, iAmp.getReadoutCorner(), isTrimmed)
363 jImage.getMask().getArray()[:] &= crosstalk
364 jImage -= backgrounds[jj]
366 iImage.scaledPlus(coeffs[ii, jj], jImage)
370 mask.clearMaskPlane(crosstalkPlane)
375 """Write a yaml file containing the crosstalk coefficients
377 The coeff array is indexed by [i, j] where i and j are amplifiers
378 corresponding to the amplifiers in det
382 outputFileName : `str`
383 Name of output yaml file
384 coeff : `numpy.array(namp, namp)`
385 numpy array of coefficients
386 det : `lsst.afw.cameraGeom.Detector`
387 Used to provide the list of amplifier names;
388 if None use ['0', '1', ...]
390 Name of detector, used to index the yaml file
391 If all detectors are identical could be the type (e.g. ITL)
393 Indent width to use when writing the yaml file
397 ampNames = [str(i)
for i
in range(coeff.shape[0])]
399 ampNames = [a.getName()
for a
in det]
401 assert coeff.shape == (len(ampNames), len(ampNames))
405 with open(outputFileName,
"w")
as fd:
406 print(indent*
" " +
"crosstalk :", file=fd)
408 print(indent*
" " +
"%s :" % crosstalkName, file=fd)
411 for i, ampNameI
in enumerate(ampNames):
412 print(indent*
" " +
"%s : {" % ampNameI, file=fd)
414 print(indent*
" ", file=fd, end=
'')
416 for j, ampNameJ
in enumerate(ampNames):
417 print(
"%s : %11.4e, " % (ampNameJ, coeff[i, j]), file=fd,
418 end=
'\n' + indent*
" " if j%4 == 3
else '')