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

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
27import lsst.afw.math
28import lsst.afw.table
29import lsst.afw.detection
30from lsst.pex.config import Config, Field, ChoiceField, ListField
31from lsst.pipe.base import Task
33__all__ = ["CrosstalkConfig", "CrosstalkTask", "subtractCrosstalk", "writeCrosstalkCoeffs",
34 "NullCrosstalkTask"]
37class CrosstalkConfig(Config):
38 """Configuration for intra-detector crosstalk removal."""
39 minPixelToMask = Field(
40 dtype=float,
41 doc="Set crosstalk mask plane for pixels over this value.",
42 default=45000
43 )
44 crosstalkMaskPlane = Field(
45 dtype=str,
46 doc="Name for crosstalk mask plane.",
47 default="CROSSTALK"
48 )
49 crosstalkBackgroundMethod = ChoiceField(
50 dtype=str,
51 doc="Type of background subtraction to use when applying correction.",
52 default="None",
53 allowed={
54 "None": "Do no background subtraction.",
55 "AMP": "Subtract amplifier-by-amplifier background levels.",
56 "DETECTOR": "Subtract detector level background."
57 },
58 )
59 useConfigCoefficients = Field(
60 dtype=bool,
61 doc="Ignore the detector crosstalk information in favor of CrosstalkConfig values?",
62 default=False,
63 )
64 crosstalkValues = ListField(
65 dtype=float,
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."),
70 default=[0.0],
71 )
72 crosstalkShape = ListField(
73 dtype=int,
74 doc="Shape of the coefficient array. This should be equal to [nAmp, nAmp].",
75 default=[1],
76 )
78 def getCrosstalk(self, detector=None):
79 """Return a 2-D numpy array of crosstalk coefficients in the proper shape.
81 Parameters
82 ----------
83 detector : `lsst.afw.cameraGeom.detector`
84 Detector that is to be crosstalk corrected.
86 Returns
87 -------
88 coeffs : `numpy.ndarray`
89 Crosstalk coefficients that can be used to correct the detector.
91 Raises
92 ------
93 RuntimeError
94 Raised if no coefficients could be generated from this detector/configuration.
95 """
96 if self.useConfigCoefficients is True:
97 coeffs = np.array(self.crosstalkValues).reshape(self.crosstalkShape)
98 if detector is not None:
99 nAmp = len(detector)
100 if coeffs.shape != (nAmp, nAmp):
101 raise RuntimeError("Constructed crosstalk coeffients do not match detector shape. " +
102 f"{coeffs.shape} {nAmp}")
103 return coeffs
104 elif detector is not None and detector.hasCrosstalk() is True:
105 # Assume the detector defines itself consistently.
106 return detector.getCrosstalk()
107 else:
108 raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients")
110 def hasCrosstalk(self, detector=None):
111 """Return a boolean indicating if crosstalk coefficients exist.
113 Parameters
114 ----------
115 detector : `lsst.afw.cameraGeom.detector`
116 Detector that is to be crosstalk corrected.
118 Returns
119 -------
120 hasCrosstalk : `bool`
121 True if this detector/configuration has crosstalk coefficients defined.
122 """
123 if self.useConfigCoefficients is True and self.crosstalkValues is not None:
124 return True
125 elif detector is not None and detector.hasCrosstalk() is True:
126 return True
127 else:
128 return False
131class CrosstalkTask(Task):
132 """Apply intra-detector crosstalk correction."""
133 ConfigClass = CrosstalkConfig
134 _DefaultName = 'isrCrosstalk'
136 def prepCrosstalk(self, dataRef):
137 """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk.
139 Parameters
140 ----------
141 dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
142 Butler reference of the detector data to be processed.
144 See also
145 --------
146 lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk
147 """
148 return
150 def run(self, exposure, crosstalkSources=None, isTrimmed=False):
151 """Apply intra-detector crosstalk correction
153 Parameters
154 ----------
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.
161 isTrimmed : `bool`
162 The image is already trimmed.
163 This should no longer be needed once DM-15409 is resolved.
165 Raises
166 ------
167 RuntimeError
168 Raised if called for a detector that does not have a
169 crosstalk correction
170 """
171 detector = exposure.getDetector()
172 if not self.config.hasCrosstalk(detector=detector):
173 raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients")
174 coeffs = self.config.getCrosstalk(detector=detector)
176 self.log.info("Applying crosstalk correction.")
177 subtractCrosstalk(exposure, crosstalkCoeffs=coeffs,
178 minPixelToMask=self.config.minPixelToMask,
179 crosstalkStr=self.config.crosstalkMaskPlane, isTrimmed=isTrimmed,
180 backgroundMethod=self.config.crosstalkBackgroundMethod)
183# Flips required to get the corner to the lower-left
184# (an arbitrary choice; flips are relative, so the choice of reference here is not important)
185X_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False, lsst.afw.cameraGeom.ReadoutCorner.LR: True,
186 lsst.afw.cameraGeom.ReadoutCorner.UL: False, lsst.afw.cameraGeom.ReadoutCorner.UR: True}
187Y_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False, lsst.afw.cameraGeom.ReadoutCorner.LR: False,
188 lsst.afw.cameraGeom.ReadoutCorner.UL: True, lsst.afw.cameraGeom.ReadoutCorner.UR: True}
191class NullCrosstalkTask(CrosstalkTask):
192 def run(self, exposure, crosstalkSources=None):
193 self.log.info("Not performing any crosstalk correction")
196def extractAmp(image, amp, corner, isTrimmed=False):
197 """Return an image of the amp
199 The returned image will have the amp's readout corner in the
200 nominated `corner`.
202 Parameters
203 ----------
204 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
205 Image containing the amplifier of interest.
206 amp : `lsst.afw.table.AmpInfoRecord`
207 Amplifier information.
208 corner : `lsst.afw.table.ReadoutCorner` or `None`
209 Corner in which to put the amp's readout corner, or `None` for
210 no flipping.
211 isTrimmed : `bool`
212 The image is already trimmed.
213 This should no longer be needed once DM-15409 is resolved.
215 Returns
216 -------
217 output : `lsst.afw.image.Image`
218 Image of the amplifier in the standard configuration.
219 """
220 output = image[amp.getBBox() if isTrimmed else amp.getRawDataBBox()]
221 ampCorner = amp.getReadoutCorner()
222 # Flipping is necessary only if the desired configuration doesn't match what we currently have
223 xFlip = X_FLIP[corner] ^ X_FLIP[ampCorner]
224 yFlip = Y_FLIP[corner] ^ Y_FLIP[ampCorner]
225 return lsst.afw.math.flipImage(output, xFlip, yFlip)
228def calculateBackground(mi, badPixels=["BAD"]):
229 """Calculate median background in image
231 Getting a great background model isn't important for crosstalk correction,
232 since the crosstalk is at a low level. The median should be sufficient.
234 Parameters
235 ----------
236 mi : `lsst.afw.image.MaskedImage`
237 MaskedImage for which to measure background.
238 badPixels : `list` of `str`
239 Mask planes to ignore.
241 Returns
242 -------
243 bg : `float`
244 Median background level.
245 """
246 mask = mi.getMask()
247 stats = lsst.afw.math.StatisticsControl()
248 stats.setAndMask(mask.getPlaneBitMask(badPixels))
249 return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue()
252def subtractCrosstalk(exposure, crosstalkCoeffs=None,
253 badPixels=["BAD"], minPixelToMask=45000,
254 crosstalkStr="CROSSTALK", isTrimmed=False,
255 backgroundMethod="None"):
256 """Subtract the intra-detector crosstalk from an exposure
258 We set the mask plane indicated by ``crosstalkStr`` in a target amplifier
259 for pixels in a source amplifier that exceed `minPixelToMask`. Note that
260 the correction is applied to all pixels in the amplifier, but only those
261 that have a substantial crosstalk are masked with ``crosstalkStr``.
263 The uncorrected image is used as a template for correction. This is good
264 enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's
265 larger you may want to iterate.
267 This method needs unittests (DM-18876), but such testing requires
268 DM-18610 to allow the test detector to have the crosstalk
269 parameters set.
271 Parameters
272 ----------
273 exposure : `lsst.afw.image.Exposure`
274 Exposure for which to subtract crosstalk.
275 crosstalkCoeffs : `numpy.ndarray`
276 Coefficients to use to correct crosstalk.
277 badPixels : `list` of `str`
278 Mask planes to ignore.
279 minPixelToMask : `float`
280 Minimum pixel value (relative to the background level) in
281 source amplifier for which to set ``crosstalkStr`` mask plane
282 in target amplifier.
283 crosstalkStr : `str`
284 Mask plane name for pixels greatly modified by crosstalk.
285 isTrimmed : `bool`
286 The image is already trimmed.
287 This should no longer be needed once DM-15409 is resolved.
288 backgroundMethod : `str`
289 Method used to subtract the background. "AMP" uses
290 amplifier-by-amplifier background levels, "DETECTOR" uses full
291 exposure/maskedImage levels. Any other value results in no
292 background subtraction.
293 """
294 mi = exposure.getMaskedImage()
295 mask = mi.getMask()
297 ccd = exposure.getDetector()
298 numAmps = len(ccd)
299 if crosstalkCoeffs is None:
300 coeffs = ccd.getCrosstalk()
301 else:
302 coeffs = crosstalkCoeffs
303 assert coeffs.shape == (numAmps, numAmps)
305 # Set background level based on the requested method. The
306 # thresholdBackground holds the offset needed so that we only mask
307 # pixels high relative to the background, not in an absolute
308 # sense.
309 thresholdBackground = calculateBackground(mi, badPixels)
311 backgrounds = [0.0 for amp in ccd]
312 if backgroundMethod is None:
313 pass
314 elif backgroundMethod == "AMP":
315 backgrounds = [calculateBackground(mi[amp.getBBox()], badPixels) for amp in ccd]
316 elif backgroundMethod == "DETECTOR":
317 backgrounds = [calculateBackground(mi, badPixels) for amp in ccd]
319 # Set the crosstalkStr bit for the bright pixels (those which will have significant crosstalk correction)
320 crosstalkPlane = mask.addMaskPlane(crosstalkStr)
321 footprints = lsst.afw.detection.FootprintSet(mi, lsst.afw.detection.Threshold(minPixelToMask +
322 thresholdBackground))
323 footprints.setMask(mask, crosstalkStr)
324 crosstalk = mask.getPlaneBitMask(crosstalkStr)
326 # Do pixel level crosstalk correction.
327 subtrahend = mi.Factory(mi.getBBox())
328 subtrahend.set((0, 0, 0))
329 for ii, iAmp in enumerate(ccd):
330 iImage = subtrahend[iAmp.getBBox() if isTrimmed else iAmp.getRawDataBBox()]
331 for jj, jAmp in enumerate(ccd):
332 if ii == jj:
333 assert coeffs[ii, jj] == 0.0
334 if coeffs[ii, jj] == 0.0:
335 continue
337 jImage = extractAmp(mi, jAmp, iAmp.getReadoutCorner(), isTrimmed)
338 jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks
339 jImage -= backgrounds[jj]
341 iImage.scaledPlus(coeffs[ii, jj], jImage)
343 # Set crosstalkStr bit only for those pixels that have been significantly modified (i.e., those
344 # masked as such in 'subtrahend'), not necessarily those that are bright originally.
345 mask.clearMaskPlane(crosstalkPlane)
346 mi -= subtrahend # also sets crosstalkStr bit for bright pixels
349def writeCrosstalkCoeffs(outputFileName, coeff, det=None, crosstalkName="Unknown", indent=2):
350 """Write a yaml file containing the crosstalk coefficients
352 The coeff array is indexed by [i, j] where i and j are amplifiers
353 corresponding to the amplifiers in det
355 Parameters
356 ----------
357 outputFileName : `str`
358 Name of output yaml file
359 coeff : `numpy.array(namp, namp)`
360 numpy array of coefficients
361 det : `lsst.afw.cameraGeom.Detector`
362 Used to provide the list of amplifier names;
363 if None use ['0', '1', ...]
364 ccdType : `str`
365 Name of detector, used to index the yaml file
366 If all detectors are identical could be the type (e.g. ITL)
367 indent : `int`
368 Indent width to use when writing the yaml file
369 """
371 if det is None:
372 ampNames = [str(i) for i in range(coeff.shape[0])]
373 else:
374 ampNames = [a.getName() for a in det]
376 assert coeff.shape == (len(ampNames), len(ampNames))
378 dIndent = indent
379 indent = 0
380 with open(outputFileName, "w") as fd:
381 print(indent*" " + "crosstalk :", file=fd)
382 indent += dIndent
383 print(indent*" " + "%s :" % crosstalkName, file=fd)
384 indent += dIndent
386 for i, ampNameI in enumerate(ampNames):
387 print(indent*" " + "%s : {" % ampNameI, file=fd)
388 indent += dIndent
389 print(indent*" ", file=fd, end='')
391 for j, ampNameJ in enumerate(ampNames):
392 print("%s : %11.4e, " % (ampNameJ, coeff[i, j]), file=fd,
393 end='\n' + indent*" " if j%4 == 3 else '')
394 print("}", file=fd)
396 indent -= dIndent