Coverage for tests/test_overscanCorrection.py: 9%

260 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-20 02:41 -0700

1# 

2# LSST Data Management System 

3# Copyright 2008, 2009, 2010 LSST Corporation. 

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 <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23import unittest 

24import numpy as np 

25 

26import lsst.utils.tests 

27import lsst.geom 

28import lsst.afw.image as afwImage 

29import lsst.afw.cameraGeom as cameraGeom 

30import lsst.ip.isr as ipIsr 

31import lsst.pipe.base as pipeBase 

32 

33 

34def computeImageMedianAndStd(image): 

35 """Function to calculate median and std of image data. 

36 

37 Parameters 

38 ---------- 

39 image : `lsst.afw.image.Image` 

40 Image to measure statistics on. 

41 

42 Returns 

43 ------- 

44 median : `float` 

45 Image median. 

46 std : `float` 

47 Image stddev. 

48 """ 

49 median = np.nanmedian(image.getArray()) 

50 std = np.nanstd(image.getArray()) 

51 

52 return (median, std) 

53 

54 

55class IsrTestCases(lsst.utils.tests.TestCase): 

56 

57 def updateConfigFromKwargs(self, config, **kwargs): 

58 """Common config from keywords. 

59 """ 

60 fitType = kwargs.get('fitType', None) 

61 if fitType: 

62 config.overscan.fitType = fitType 

63 

64 order = kwargs.get('order', None) 

65 if order: 

66 config.overscan.order = order 

67 

68 def makeExposure(self, isTransposed=False): 

69 # Define the camera geometry we'll use. 

70 cameraBuilder = cameraGeom.Camera.Builder("Fake Camera") 

71 detectorBuilder = cameraBuilder.add("Fake amp", 0) 

72 

73 ampBuilder = cameraGeom.Amplifier.Builder() 

74 

75 dataBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

76 lsst.geom.Extent2I(10, 10)) 

77 

78 if isTransposed is True: 

79 fullBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

80 lsst.geom.Point2I(12, 12)) 

81 serialOverscanBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 10), 

82 lsst.geom.Point2I(9, 12)) 

83 parallelOverscanBBox = lsst.geom.Box2I(lsst.geom.Point2I(10, 0), 

84 lsst.geom.Point2I(12, 9)) 

85 else: 

86 fullBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

87 lsst.geom.Point2I(12, 12)) 

88 serialOverscanBBox = lsst.geom.Box2I(lsst.geom.Point2I(10, 0), 

89 lsst.geom.Point2I(12, 9)) 

90 parallelOverscanBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 10), 

91 lsst.geom.Point2I(9, 12)) 

92 

93 ampBuilder.setRawBBox(fullBBox) 

94 ampBuilder.setRawSerialOverscanBBox(serialOverscanBBox) 

95 ampBuilder.setRawParallelOverscanBBox(parallelOverscanBBox) 

96 ampBuilder.setRawDataBBox(dataBBox) 

97 

98 detectorBuilder.append(ampBuilder) 

99 camera = cameraBuilder.finish() 

100 detector = camera[0] 

101 

102 # Define image data. 

103 maskedImage = afwImage.MaskedImageF(fullBBox) 

104 maskedImage.set(10, 0x0, 1) 

105 

106 overscan = afwImage.MaskedImageF(maskedImage, serialOverscanBBox) 

107 overscan.set(2, 0x0, 1) 

108 overscan = afwImage.MaskedImageF(maskedImage, parallelOverscanBBox) 

109 overscan.set(2, 0x0, 1) 

110 

111 exposure = afwImage.ExposureF(maskedImage, None) 

112 exposure.setDetector(detector) 

113 return exposure 

114 

115 def checkOverscanCorrectionY(self, **kwargs): 

116 exposure = self.makeExposure(isTransposed=True) 

117 detector = exposure.getDetector() 

118 

119 # These subimages are needed below. 

120 overscan = exposure[detector.getAmplifiers()[0].getRawSerialOverscanBBox()] 

121 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()] 

122 

123 config = ipIsr.IsrTask.ConfigClass() 

124 self.updateConfigFromKwargs(config, **kwargs) 

125 

126 if kwargs['fitType'] == "MEDIAN_PER_ROW": 

127 # Add a bad point to test outlier rejection. 

128 overscan.getImage().getArray()[0, 0] = 12345 

129 

130 # Shrink the sigma clipping limit to handle the fact that the 

131 # bad point is not be rejected at higher thresholds (2/0.74). 

132 config.overscan.numSigmaClip = 2.7 

133 

134 isrTask = ipIsr.IsrTask(config=config) 

135 isrTask.overscan.run(exposure, detector.getAmplifiers()[0], isTransposed=True) 

136 

137 height = maskedImage.getHeight() 

138 width = maskedImage.getWidth() 

139 for j in range(height): 

140 for i in range(width): 

141 if j == 10 and i == 0 and kwargs['fitType'] == "MEDIAN_PER_ROW": 

142 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 12343) 

143 elif j >= 10 and i < 10: 

144 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 0) 

145 elif i < 10: 

146 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 8) 

147 

148 def checkOverscanCorrectionX(self, **kwargs): 

149 exposure = self.makeExposure(isTransposed=False) 

150 detector = exposure.getDetector() 

151 

152 # These subimages are needed below. 

153 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()] 

154 

155 config = ipIsr.IsrTask.ConfigClass() 

156 self.updateConfigFromKwargs(config, **kwargs) 

157 

158 isrTask = ipIsr.IsrTask(config=config) 

159 isrTask.overscan.run(exposure, detector.getAmplifiers()[0], isTransposed=False) 

160 

161 height = maskedImage.getHeight() 

162 width = maskedImage.getWidth() 

163 for j in range(height): 

164 for i in range(width): 

165 if i >= 10 and j < 10: 

166 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 0) 

167 elif j < 10: 

168 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 8) 

169 

170 def checkOverscanCorrectionSineWave(self, **kwargs): 

171 """vertical sine wave along long direction""" 

172 # Define the camera geometry we'll use. 

173 cameraBuilder = cameraGeom.Camera.Builder("Fake Camera") 

174 detectorBuilder = cameraBuilder.add("Fake amp", 0) 

175 

176 ampBuilder = cameraGeom.Amplifier.Builder() 

177 

178 dataBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

179 lsst.geom.Extent2I(70, 500)) 

180 

181 fullBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

182 lsst.geom.Extent2I(100, 500)) 

183 

184 overscanBBox = lsst.geom.Box2I(lsst.geom.Point2I(70, 0), 

185 lsst.geom.Extent2I(30, 500)) 

186 

187 ampBuilder.setRawBBox(fullBBox) 

188 ampBuilder.setRawSerialOverscanBBox(overscanBBox) 

189 ampBuilder.setRawDataBBox(dataBBox) 

190 

191 detectorBuilder.append(ampBuilder) 

192 camera = cameraBuilder.finish() 

193 detector = camera[0] 

194 

195 # Define image data. 

196 maskedImage = afwImage.MaskedImageF(fullBBox) 

197 maskedImage.set(50, 0x0, 1) 

198 

199 overscan = afwImage.MaskedImageF(maskedImage, overscanBBox) 

200 overscan.set(0, 0x0, 1) 

201 

202 exposure = afwImage.ExposureF(maskedImage, None) 

203 exposure.setDetector(detector) 

204 

205 # vertical sine wave along long direction 

206 x = np.linspace(0, 2*3.14159, 500) 

207 a, w = 15, 5*3.14159 

208 sineWave = 20 + a*np.sin(w*x) 

209 sineWave = sineWave.astype(int) 

210 

211 fullImage = np.repeat(sineWave, 100).reshape((500, 100)) 

212 maskedImage.image.array += fullImage 

213 

214 config = ipIsr.IsrTask.ConfigClass() 

215 self.updateConfigFromKwargs(config, **kwargs) 

216 

217 isrTask = ipIsr.IsrTask(config=config) 

218 isrTask.overscan.run(exposure, detector.getAmplifiers()[0]) 

219 

220 height = maskedImage.getHeight() 

221 width = maskedImage.getWidth() 

222 

223 for j in range(height): 

224 for i in range(width): 

225 if i >= 70: 

226 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 0.0) 

227 else: 

228 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 50.0) 

229 

230 def test_MedianPerRowOverscanCorrection(self): 

231 self.checkOverscanCorrectionY(fitType="MEDIAN_PER_ROW") 

232 self.checkOverscanCorrectionX(fitType="MEDIAN_PER_ROW") 

233 self.checkOverscanCorrectionSineWave(fitType="MEDIAN_PER_ROW") 

234 

235 def test_MedianOverscanCorrection(self): 

236 self.checkOverscanCorrectionY(fitType="MEDIAN") 

237 self.checkOverscanCorrectionX(fitType="MEDIAN") 

238 

239 def checkPolyOverscanCorrectionX(self, **kwargs): 

240 exposure = self.makeExposure(isTransposed=False) 

241 detector = exposure.getDetector() 

242 

243 # These subimages are needed below. 

244 overscan = exposure[detector.getAmplifiers()[0].getRawSerialOverscanBBox()] 

245 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()] 

246 

247 bbox = detector.getAmplifiers()[0].getRawSerialOverscanBBox() 

248 overscan.getMaskedImage().set(2, 0x0, 1) 

249 for i in range(bbox.getDimensions()[1]): 

250 for j, off in enumerate([-0.5, 0.0, 0.5]): 

251 overscan.image[j, i, afwImage.LOCAL] = 2+i+off 

252 

253 config = ipIsr.IsrTask.ConfigClass() 

254 self.updateConfigFromKwargs(config, **kwargs) 

255 

256 isrTask = ipIsr.IsrTask(config=config) 

257 isrTask.overscan.run(exposure, detector.getAmplifiers()[0], isTransposed=False) 

258 

259 height = maskedImage.getHeight() 

260 width = maskedImage.getWidth() 

261 for j in range(height): 

262 for i in range(width): 

263 if j < 10: 

264 if i == 10: 

265 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], -0.5) 

266 elif i == 11: 

267 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 0) 

268 elif i == 12: 

269 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 0.5) 

270 else: 

271 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 10 - 2 - j) 

272 

273 def checkPolyOverscanCorrectionY(self, **kwargs): 

274 exposure = self.makeExposure(isTransposed=True) 

275 detector = exposure.getDetector() 

276 

277 # These subimages are needed below. 

278 overscan = exposure[detector.getAmplifiers()[0].getRawSerialOverscanBBox()] 

279 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()] 

280 

281 bbox = detector.getAmplifiers()[0].getRawSerialOverscanBBox() 

282 overscan.getMaskedImage().set(2, 0x0, 1) 

283 for i in range(bbox.getDimensions()[0]): 

284 for j, off in enumerate([-0.5, 0.0, 0.5]): 

285 overscan.image[i, j, afwImage.LOCAL] = 2+i+off 

286 # maskedImage.getMaskedImage().set(10, 0x0, 1) 

287 

288 config = ipIsr.IsrTask.ConfigClass() 

289 self.updateConfigFromKwargs(config, **kwargs) 

290 

291 isrTask = ipIsr.IsrTask(config=config) 

292 isrTask.overscan.run(exposure, detector.getAmplifiers()[0], isTransposed=True) 

293 

294 height = maskedImage.getHeight() 

295 width = maskedImage.getWidth() 

296 for j in range(height): 

297 for i in range(width): 

298 if i < 10: 

299 if j == 10: 

300 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], -0.5) 

301 elif j == 11: 

302 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 0) 

303 elif j == 12: 

304 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 0.5) 

305 else: 

306 self.assertEqual(maskedImage.image[i, j, afwImage.LOCAL], 10 - 2 - i) 

307 

308 def test_PolyOverscanCorrection(self): 

309 for fitType in ("POLY", "CHEB", "LEG"): 

310 self.checkPolyOverscanCorrectionX(fitType=fitType, order=5) 

311 self.checkPolyOverscanCorrectionY(fitType=fitType, order=5) 

312 

313 def test_SplineOverscanCorrection(self): 

314 for fitType in ("NATURAL_SPLINE", "CUBIC_SPLINE", "AKIMA_SPLINE"): 

315 self.checkPolyOverscanCorrectionX(fitType=fitType, order=5) 

316 self.checkPolyOverscanCorrectionY(fitType=fitType, order=5) 

317 

318 def test_overscanCorrection(self): 

319 """Expect that this should reduce the image variance with a full fit. 

320 The default fitType of MEDIAN will reduce the median value. 

321 

322 This needs to operate on a RawMock() to have overscan data to use. 

323 

324 The output types may be different when fitType != MEDIAN. 

325 """ 

326 exposure = self.makeExposure(isTransposed=False) 

327 detector = exposure.getDetector() 

328 amp = detector.getAmplifiers()[0] 

329 

330 statBefore = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()]) 

331 

332 config = ipIsr.IsrTask.ConfigClass() 

333 isrTask = ipIsr.IsrTask(config=config) 

334 oscanResults = isrTask.overscan.run(exposure, amp) 

335 

336 self.assertIsInstance(oscanResults, pipeBase.Struct) 

337 self.assertIsInstance(oscanResults.imageFit, float) 

338 self.assertIsInstance(oscanResults.overscanFit, float) 

339 self.assertIsInstance(oscanResults.overscanImage, afwImage.ExposureF) 

340 

341 statAfter = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()]) 

342 self.assertLess(statAfter[0], statBefore[0]) 

343 

344 def test_parallelOverscanCorrection(self): 

345 """Expect that this should reduce the image variance with a full fit. 

346 The default fitType of MEDIAN will reduce the median value. 

347 

348 This needs to operate on a RawMock() to have overscan data to use. 

349 

350 The output types may be different when fitType != MEDIAN. 

351 """ 

352 exposure = self.makeExposure(isTransposed=False) 

353 detector = exposure.getDetector() 

354 amp = detector.getAmplifiers()[0] 

355 

356 statBefore = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()]) 

357 

358 config = ipIsr.IsrTask.ConfigClass() 

359 config.overscan.doParallelOverscan = True 

360 isrTask = ipIsr.IsrTask(config=config) 

361 oscanResults = isrTask.overscan.run(exposure, amp) 

362 

363 self.assertIsInstance(oscanResults, pipeBase.Struct) 

364 self.assertIsInstance(oscanResults.imageFit, float) 

365 self.assertIsInstance(oscanResults.overscanFit, float) 

366 self.assertIsInstance(oscanResults.overscanImage, afwImage.ExposureF) 

367 

368 statAfter = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()]) 

369 self.assertLess(statAfter[0], statBefore[0]) 

370 

371 def test_badParallelOverscanCorrection(self): 

372 """Expect that this should reduce the image variance with a full fit. 

373 The default fitType of MEDIAN will reduce the median value. 

374 

375 This needs to operate on a RawMock() to have overscan data to use. 

376 

377 The output types may be different when fitType != MEDIAN. 

378 """ 

379 exposure = self.makeExposure(isTransposed=False) 

380 detector = exposure.getDetector() 

381 amp = detector.getAmplifiers()[0] 

382 

383 maskedImage = exposure.getMaskedImage() 

384 overscan = afwImage.MaskedImageF(maskedImage, amp.getRawParallelOverscanBBox()) 

385 overscan.set(400, 0x0, 1) 

386 

387 statBefore = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()]) 

388 

389 config = ipIsr.IsrTask.ConfigClass() 

390 config.overscan.doParallelOverscan = True 

391 isrTask = ipIsr.IsrTask(config=config) 

392 oscanResults = isrTask.overscan.run(exposure, amp) 

393 

394 self.assertIsInstance(oscanResults, pipeBase.Struct) 

395 self.assertIsInstance(oscanResults.imageFit, float) 

396 self.assertIsInstance(oscanResults.overscanFit, float) 

397 self.assertIsInstance(oscanResults.overscanImage, afwImage.ExposureF) 

398 

399 statAfter = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()]) 

400 self.assertLess(statAfter[0], statBefore[0]) 

401 

402 def test_overscanCorrection_isNotInt(self): 

403 """Expect smaller median/smaller std after. 

404 Expect exception if overscan fit type isn't known. 

405 """ 

406 exposure = self.makeExposure(isTransposed=False) 

407 detector = exposure.getDetector() 

408 amp = detector.getAmplifiers()[0] 

409 

410 for fitType in ('MEAN', 'MEDIAN', 'MEDIAN_PER_ROW', 'MEANCLIP', 'POLY', 'CHEB', 

411 'NATURAL_SPLINE', 'CUBIC_SPLINE'): 

412 if fitType in ('NATURAL_SPLINE', 'CUBIC_SPLINE'): 

413 order = 3 

414 else: 

415 order = 1 

416 config = ipIsr.IsrTask.ConfigClass() 

417 config.overscan.order = order 

418 config.overscan.fitType = fitType 

419 isrTask = ipIsr.IsrTask(config=config) 

420 

421 response = isrTask.overscan.run(exposure, amp) 

422 

423 self.assertIsInstance(response, pipeBase.Struct, 

424 msg=f"overscanCorrection overscanIsNotInt Bad response: {fitType}") 

425 self.assertIsNotNone(response.imageFit, 

426 msg=f"overscanCorrection overscanIsNotInt Bad imageFit: {fitType}") 

427 self.assertIsNotNone(response.overscanFit, 

428 msg=f"overscanCorrection overscanIsNotInt Bad overscanFit: {fitType}") 

429 self.assertIsInstance(response.overscanImage, afwImage.ExposureF, 

430 msg=f"overscanCorrection overscanIsNotInt Bad overscanImage: {fitType}") 

431 

432 

433class MemoryTester(lsst.utils.tests.MemoryTestCase): 

434 pass 

435 

436 

437def setup_module(module): 

438 lsst.utils.tests.init() 

439 

440 

441if __name__ == "__main__": 441 ↛ 442line 441 didn't jump to line 442, because the condition on line 441 was never true

442 lsst.utils.tests.init() 

443 unittest.main()