Hide keyboard shortcuts

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 

26 

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 

32 

33__all__ = ["CrosstalkConfig", "CrosstalkTask", "subtractCrosstalk", "writeCrosstalkCoeffs", 

34 "NullCrosstalkTask"] 

35 

36 

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 ) 

77 

78 def getCrosstalk(self, detector=None): 

79 """Return a 2-D numpy array of crosstalk coefficients in the proper shape. 

80 

81 Parameters 

82 ---------- 

83 detector : `lsst.afw.cameraGeom.detector` 

84 Detector that is to be crosstalk corrected. 

85 

86 Returns 

87 ------- 

88 coeffs : `numpy.ndarray` 

89 Crosstalk coefficients that can be used to correct the detector. 

90 

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") 

109 

110 def hasCrosstalk(self, detector=None): 

111 """Return a boolean indicating if crosstalk coefficients exist. 

112 

113 Parameters 

114 ---------- 

115 detector : `lsst.afw.cameraGeom.detector` 

116 Detector that is to be crosstalk corrected. 

117 

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 

129 

130 

131class CrosstalkTask(Task): 

132 """Apply intra-detector crosstalk correction.""" 

133 ConfigClass = CrosstalkConfig 

134 _DefaultName = 'isrCrosstalk' 

135 

136 def prepCrosstalk(self, dataRef): 

137 """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk. 

138 

139 Parameters 

140 ---------- 

141 dataRef : `daf.persistence.butlerSubset.ButlerDataRef` 

142 Butler reference of the detector data to be processed. 

143 

144 See also 

145 -------- 

146 lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk 

147 """ 

148 return 

149 

150 def run(self, exposure, crosstalkSources=None, isTrimmed=False): 

151 """Apply intra-detector crosstalk correction 

152 

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. 

164 

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) 

175 

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) 

181 

182 

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} 

189 

190 

191class NullCrosstalkTask(CrosstalkTask): 

192 def run(self, exposure, crosstalkSources=None): 

193 self.log.info("Not performing any crosstalk correction") 

194 

195 

196def extractAmp(image, amp, corner, isTrimmed=False): 

197 """Return an image of the amp 

198 

199 The returned image will have the amp's readout corner in the 

200 nominated `corner`. 

201 

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. 

214 

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) 

226 

227 

228def calculateBackground(mi, badPixels=["BAD"]): 

229 """Calculate median background in image 

230 

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. 

233 

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. 

240 

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() 

250 

251 

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 

257 

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``. 

262 

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. 

266 

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. 

270 

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() 

296 

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) 

304 

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) 

310 

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] 

318 

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) 

325 

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 

336 

337 jImage = extractAmp(mi, jAmp, iAmp.getReadoutCorner(), isTrimmed) 

338 jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks 

339 jImage -= backgrounds[jj] 

340 

341 iImage.scaledPlus(coeffs[ii, jj], jImage) 

342 

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 

347 

348 

349def writeCrosstalkCoeffs(outputFileName, coeff, det=None, crosstalkName="Unknown", indent=2): 

350 """Write a yaml file containing the crosstalk coefficients 

351 

352 The coeff array is indexed by [i, j] where i and j are amplifiers 

353 corresponding to the amplifiers in det 

354 

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 """ 

370 

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] 

375 

376 assert coeff.shape == (len(ampNames), len(ampNames)) 

377 

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 

385 

386 for i, ampNameI in enumerate(ampNames): 

387 print(indent*" " + "%s : {" % ampNameI, file=fd) 

388 indent += dIndent 

389 print(indent*" ", file=fd, end='') 

390 

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) 

395 

396 indent -= dIndent