Coverage for tests/test_overscanCorrection.py: 6%

395 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-05 03:23 -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 updateOverscanConfigFromKwargs(self, config, **kwargs): 

69 """Common config from keywords. 

70 """ 

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

72 if fitType: 

73 config.fitType = fitType 

74 

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

76 if order: 

77 config.order = order 

78 

79 def makeExposure(self, addRamp=False, isTransposed=False): 

80 # Define the camera geometry we'll use. 

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

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

83 

84 ampBuilder = cameraGeom.Amplifier.Builder() 

85 

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

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

88 

89 if isTransposed is True: 

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

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

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

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

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

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

96 else: 

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

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

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

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

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

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

103 

104 ampBuilder.setRawBBox(fullBBox) 

105 ampBuilder.setRawSerialOverscanBBox(serialOverscanBBox) 

106 ampBuilder.setRawParallelOverscanBBox(parallelOverscanBBox) 

107 ampBuilder.setRawDataBBox(dataBBox) 

108 

109 detectorBuilder.append(ampBuilder) 

110 camera = cameraBuilder.finish() 

111 detector = camera[0] 

112 

113 # Define image data. 

114 maskedImage = afwImage.MaskedImageF(fullBBox) 

115 maskedImage.set(2, 0x0, 1) 

116 

117 dataImage = afwImage.MaskedImageF(maskedImage, dataBBox) 

118 dataImage.set(10, 0x0, 1) 

119 

120 if addRamp: 

121 for column in range(dataBBox.getWidth()): 

122 maskedImage.image.array[:, column] += column 

123 

124 exposure = afwImage.ExposureF(maskedImage, None) 

125 exposure.setDetector(detector) 

126 return exposure 

127 

128 def checkOverscanCorrectionY(self, **kwargs): 

129 # We check serial overscan with the "old" and "new" tasks. 

130 for taskClass in (ipIsr.overscan.OverscanCorrectionTask, ipIsr.overscan.SerialOverscanCorrectionTask): 

131 exposure = self.makeExposure(isTransposed=True) 

132 detector = exposure.getDetector() 

133 

134 # These subimages are needed below. 

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

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

137 

138 config = taskClass.ConfigClass() 

139 self.updateOverscanConfigFromKwargs(config, **kwargs) 

140 

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

142 # Add a bad point to test outlier rejection. 

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

144 

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

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

147 config.numSigmaClip = 2.7 

148 

149 overscanTask = taskClass(config=config) 

150 _ = overscanTask.run(exposure, detector.getAmplifiers()[0], isTransposed=True) 

151 

152 height = maskedImage.getHeight() 

153 width = maskedImage.getWidth() 

154 for j in range(height): 

155 for i in range(width): 

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

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

158 elif j >= 10 and i < 10: 

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

160 elif i < 10: 

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

162 

163 def checkOverscanCorrectionX(self, **kwargs): 

164 # We check serial ovsercan with "old" and "new" tasks. 

165 for taskClass in (ipIsr.overscan.OverscanCorrectionTask, ipIsr.overscan.SerialOverscanCorrectionTask): 

166 exposure = self.makeExposure(isTransposed=False) 

167 detector = exposure.getDetector() 

168 

169 # These subimages are needed below. 

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

171 

172 config = taskClass.ConfigClass() 

173 self.updateOverscanConfigFromKwargs(config, **kwargs) 

174 

175 overscanTask = taskClass(config=config) 

176 _ = overscanTask.run(exposure, detector.getAmplifiers()[0], isTransposed=False) 

177 

178 height = maskedImage.getHeight() 

179 width = maskedImage.getWidth() 

180 for j in range(height): 

181 for i in range(width): 

182 if i >= 10 and j < 10: 

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

184 elif j < 10: 

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

186 

187 def checkOverscanCorrectionSineWave(self, **kwargs): 

188 """vertical sine wave along long direction""" 

189 # Define the camera geometry we'll use. 

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

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

192 

193 ampBuilder = cameraGeom.Amplifier.Builder() 

194 

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

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

197 

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

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

200 

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

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

203 

204 ampBuilder.setRawBBox(fullBBox) 

205 ampBuilder.setRawSerialOverscanBBox(overscanBBox) 

206 ampBuilder.setRawDataBBox(dataBBox) 

207 

208 detectorBuilder.append(ampBuilder) 

209 camera = cameraBuilder.finish() 

210 detector = camera[0] 

211 

212 for taskClass in (ipIsr.overscan.OverscanCorrectionTask, ipIsr.overscan.SerialOverscanCorrectionTask): 

213 # Define image data. 

214 maskedImage = afwImage.MaskedImageF(fullBBox) 

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

216 

217 overscan = afwImage.MaskedImageF(maskedImage, overscanBBox) 

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

219 

220 exposure = afwImage.ExposureF(maskedImage, None) 

221 exposure.setDetector(detector) 

222 

223 # vertical sine wave along long direction 

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

225 a, w = 15, 5*3.14159 

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

227 sineWave = sineWave.astype(int) 

228 

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

230 maskedImage.image.array += fullImage 

231 

232 config = taskClass.ConfigClass() 

233 self.updateOverscanConfigFromKwargs(config, **kwargs) 

234 

235 overscanTask = taskClass(config=config) 

236 _ = overscanTask.run(exposure, detector.getAmplifiers()[0]) 

237 

238 height = maskedImage.getHeight() 

239 width = maskedImage.getWidth() 

240 

241 for j in range(height): 

242 for i in range(width): 

243 if i >= 70: 

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

245 else: 

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

247 

248 def test_MedianPerRowOverscanCorrection(self): 

249 self.checkOverscanCorrectionY(fitType="MEDIAN_PER_ROW") 

250 self.checkOverscanCorrectionX(fitType="MEDIAN_PER_ROW") 

251 self.checkOverscanCorrectionSineWave(fitType="MEDIAN_PER_ROW") 

252 

253 def test_MedianOverscanCorrection(self): 

254 self.checkOverscanCorrectionY(fitType="MEDIAN") 

255 self.checkOverscanCorrectionX(fitType="MEDIAN") 

256 

257 def checkPolyOverscanCorrectionX(self, **kwargs): 

258 for taskClass in (ipIsr.overscan.OverscanCorrectionTask, ipIsr.overscan.SerialOverscanCorrectionTask): 

259 exposure = self.makeExposure(isTransposed=False) 

260 detector = exposure.getDetector() 

261 

262 # Fill the full serial overscan region with a polynomial, 

263 # all the way into the parallel overscan region. 

264 amp = detector.getAmplifiers()[0] 

265 serialOverscanBBox = amp.getRawSerialOverscanBBox() 

266 imageBBox = amp.getRawDataBBox() 

267 parallelOverscanBBox = amp.getRawParallelOverscanBBox() 

268 imageBBox = imageBBox.expandedTo(parallelOverscanBBox) 

269 

270 serialOverscanBBox = lsst.geom.Box2I( 

271 lsst.geom.Point2I(serialOverscanBBox.getMinX(), 

272 imageBBox.getMinY()), 

273 lsst.geom.Extent2I(serialOverscanBBox.getWidth(), 

274 imageBBox.getHeight()), 

275 ) 

276 

277 overscan = exposure[serialOverscanBBox] 

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

279 

280 bbox = serialOverscanBBox 

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

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

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

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

285 

286 config = taskClass.ConfigClass() 

287 self.updateOverscanConfigFromKwargs(config, **kwargs) 

288 

289 overscanTask = taskClass(config=config) 

290 _ = overscanTask.run(exposure, detector.getAmplifiers()[0], isTransposed=False) 

291 

292 height = maskedImage.getHeight() 

293 width = maskedImage.getWidth() 

294 for j in range(height): 

295 for i in range(width): 

296 if j < 10: 

297 if i == 10: 

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

299 elif i == 11: 

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

301 elif i == 12: 

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

303 else: 

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

305 

306 def checkPolyOverscanCorrectionY(self, **kwargs): 

307 for taskClass in (ipIsr.overscan.OverscanCorrectionTask, ipIsr.overscan.SerialOverscanCorrectionTask): 

308 exposure = self.makeExposure(isTransposed=True) 

309 detector = exposure.getDetector() 

310 

311 # Fill the full serial overscan region with a polynomial, 

312 # all the way into the parallel overscan region. 

313 amp = detector.getAmplifiers()[0] 

314 serialOverscanBBox = amp.getRawSerialOverscanBBox() 

315 imageBBox = amp.getRawDataBBox() 

316 parallelOverscanBBox = amp.getRawParallelOverscanBBox() 

317 imageBBox = imageBBox.expandedTo(parallelOverscanBBox) 

318 

319 serialOverscanBBox = lsst.geom.Box2I( 

320 lsst.geom.Point2I(serialOverscanBBox.getMinX(), imageBBox.getEndY()), 

321 lsst.geom.Extent2I(imageBBox.getWidth(), serialOverscanBBox.getHeight()), 

322 ) 

323 

324 overscan = exposure[serialOverscanBBox] 

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

326 

327 bbox = serialOverscanBBox 

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

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

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

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

332 

333 config = taskClass.ConfigClass() 

334 self.updateOverscanConfigFromKwargs(config, **kwargs) 

335 

336 overscanTask = taskClass(config=config) 

337 _ = overscanTask.run(exposure, detector.getAmplifiers()[0], isTransposed=True) 

338 

339 height = maskedImage.getHeight() 

340 width = maskedImage.getWidth() 

341 for j in range(height): 

342 for i in range(width): 

343 if i < 10: 

344 if j == 10: 

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

346 elif j == 11: 

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

348 elif j == 12: 

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

350 else: 

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

352 

353 def test_PolyOverscanCorrection(self): 

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

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

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

357 

358 def test_SplineOverscanCorrection(self): 

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

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

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

362 

363 def test_overscanCorrection(self): 

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

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

366 

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

368 

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

370 """ 

371 for taskClass in (ipIsr.overscan.OverscanCorrectionTask, ipIsr.overscan.SerialOverscanCorrectionTask): 

372 exposure = self.makeExposure(isTransposed=False) 

373 detector = exposure.getDetector() 

374 amp = detector.getAmplifiers()[0] 

375 

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

377 

378 config = taskClass.ConfigClass() 

379 overscanTask = taskClass(config=config) 

380 oscanResults = overscanTask.run(exposure, amp) 

381 

382 self.assertIsInstance(oscanResults, pipeBase.Struct) 

383 self.assertIsInstance(oscanResults.imageFit, float) 

384 self.assertIsInstance(oscanResults.overscanFit, float) 

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

386 

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

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

389 

390 def test_parallelOverscanCorrection(self): 

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

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

393 

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

395 

396 This test checks that the outputs match, and that the serial 

397 overscan is the trivial value (2.0), and that the parallel 

398 overscan is the median of the ramp inserted (4.5) 

399 """ 

400 for taskType in ("combined", "separate"): 

401 exposure = self.makeExposure(addRamp=True, isTransposed=False) 

402 detector = exposure.getDetector() 

403 amp = detector.getAmplifiers()[0] 

404 

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

406 

407 for fitType in ('MEDIAN', 'MEDIAN_PER_ROW'): 

408 # This tests these two types to cover scalar and vector 

409 # calculations. 

410 exposureCopy = exposure.clone() 

411 

412 if taskType == "combined": 

413 config = ipIsr.overscan.OverscanCorrectionTask.ConfigClass() 

414 config.doParallelOverscan = True 

415 config.fitType = fitType 

416 

417 overscanTask = ipIsr.overscan.OverscanCorrectionTask(config=config) 

418 oscanResults = overscanTask.run(exposureCopy, amp) 

419 else: 

420 configSerial = ipIsr.overscan.SerialOverscanCorrectionTask.ConfigClass() 

421 configSerial.fitType = fitType 

422 

423 serialOverscanTask = ipIsr.overscan.SerialOverscanCorrectionTask(config=configSerial) 

424 serialResults = serialOverscanTask.run(exposureCopy, amp) 

425 

426 configParallel = ipIsr.overscan.ParallelOverscanCorrectionTask.ConfigClass() 

427 configParallel.fitType = fitType 

428 

429 parallelOverscanTask = ipIsr.overscan.ParallelOverscanCorrectionTask( 

430 config=configParallel, 

431 ) 

432 oscanResults = parallelOverscanTask.run(exposureCopy, amp) 

433 

434 self.assertIsInstance(oscanResults, pipeBase.Struct) 

435 if fitType == 'MEDIAN': 

436 self.assertIsInstance(oscanResults.imageFit, float) 

437 self.assertIsInstance(oscanResults.overscanFit, float) 

438 else: 

439 self.assertIsInstance(oscanResults.imageFit, np.ndarray) 

440 self.assertIsInstance(oscanResults.overscanFit, np.ndarray) 

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

442 

443 statAfter = computeImageMedianAndStd(exposureCopy.image[amp.getRawDataBBox()]) 

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

445 

446 # Test the output value for the serial and parallel overscans 

447 if taskType == "combined": 

448 self.assertAlmostEqual(oscanResults.overscanMean[0], 2.0, delta=0.001) 

449 self.assertAlmostEqual(oscanResults.overscanMean[1], 4.5, delta=0.001) 

450 else: 

451 self.assertAlmostEqual(serialResults.overscanMean, 2.0, delta=0.001) 

452 self.assertAlmostEqual(oscanResults.overscanMean, 4.5, delta=0.001) 

453 

454 if fitType != 'MEDIAN': 

455 # The ramp that has been inserted should be fully 

456 # removed by the overscan fit, removing all of the 

457 # signal. This isn't true of the constant fit, so do 

458 # not test that here. 

459 self.assertLess(statAfter[1], statBefore[1]) 

460 self.assertAlmostEqual(statAfter[1], 0.0, delta=0.001) 

461 

462 def test_bleedParallelOverscanCorrection(self): 

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

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

465 

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

467 

468 This test adds a large artificial bleed to the overscan region, 

469 which should be masked and patched with the median of the 

470 other pixels. 

471 """ 

472 for taskType in ("combined", "separate"): 

473 exposure = self.makeExposure(addRamp=True, isTransposed=False) 

474 detector = exposure.getDetector() 

475 amp = detector.getAmplifiers()[0] 

476 

477 maskedImage = exposure.getMaskedImage() 

478 overscanBleedBox = lsst.geom.Box2I(lsst.geom.Point2I(4, 10), 

479 lsst.geom.Extent2I(2, 3)) 

480 overscanBleed = afwImage.MaskedImageF(maskedImage, overscanBleedBox) 

481 overscanBleed.set(110000, 0x0, 1) 

482 

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

484 

485 for fitType in ('MEDIAN', 'MEDIAN_PER_ROW', 'POLY'): 

486 # We only test these three types as this should cover the 

487 # scalar calculations, the generic vector calculations, 

488 # and the specific C++ MEDIAN_PER_ROW case. 

489 exposureCopy = exposure.clone() 

490 

491 if taskType == "combined": 

492 config = ipIsr.overscan.OverscanCorrectionTask.ConfigClass() 

493 config.doParallelOverscan = True 

494 config.parallelOverscanMaskGrowSize = 1 

495 config.fitType = fitType 

496 

497 overscanTask = ipIsr.overscan.OverscanCorrectionTask(config=config) 

498 # This next line is usually run as part of IsrTask 

499 overscanTask.maskParallelOverscan(exposureCopy, detector) 

500 oscanResults = overscanTask.run(exposureCopy, amp) 

501 else: 

502 configSerial = ipIsr.overscan.SerialOverscanCorrectionTask.ConfigClass() 

503 configSerial.fitType = fitType 

504 

505 serialOverscanTask = ipIsr.overscan.SerialOverscanCorrectionTask(config=configSerial) 

506 serialResults = serialOverscanTask.run(exposureCopy, amp) 

507 

508 configParallel = ipIsr.overscan.ParallelOverscanCorrectionTask.ConfigClass() 

509 configParallel.parallelOverscanMaskGrowSize = 1 

510 configParallel.fitType = fitType 

511 

512 parallelOverscanTask = ipIsr.overscan.ParallelOverscanCorrectionTask( 

513 config=configParallel, 

514 ) 

515 # This next line is usually run as part of IsrTask 

516 parallelOverscanTask.maskParallelOverscan(exposureCopy, detector, saturationLevel=100000.) 

517 oscanResults = parallelOverscanTask.run(exposureCopy, amp) 

518 

519 self.assertIsInstance(oscanResults, pipeBase.Struct) 

520 if fitType == 'MEDIAN': 

521 self.assertIsInstance(oscanResults.imageFit, float) 

522 self.assertIsInstance(oscanResults.overscanFit, float) 

523 else: 

524 self.assertIsInstance(oscanResults.imageFit, np.ndarray) 

525 self.assertIsInstance(oscanResults.overscanFit, np.ndarray) 

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

527 

528 statAfter = computeImageMedianAndStd(exposureCopy.image[amp.getRawDataBBox()]) 

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

530 

531 # Test the output value for the serial and parallel 

532 # overscans. 

533 if taskType == "combined": 

534 self.assertAlmostEqual(oscanResults.overscanMean[0], 2.0, delta=0.001) 

535 self.assertAlmostEqual(oscanResults.overscanMean[1], 4.5, delta=0.001) 

536 self.assertAlmostEqual(oscanResults.residualMean[1], 0.0, delta=0.001) 

537 else: 

538 self.assertAlmostEqual(serialResults.overscanMean, 2.0, delta=0.001) 

539 self.assertAlmostEqual(oscanResults.overscanMean, 4.5, delta=0.001) 

540 self.assertAlmostEqual(oscanResults.residualMean, 0.0, delta=0.001) 

541 

542 if fitType != 'MEDIAN': 

543 # Check the bleed isn't oversubtracted. This is the 

544 # average of the two mid-bleed pixels as the patching 

545 # uses the median correction value there, and there is 

546 # still a residual ramp in this region. The large 

547 # delta allows the POLY fit to pass, which has sub-ADU 

548 # differences. 

549 self.assertAlmostEqual(exposureCopy.image.array[5][0], 

550 0.5 * (exposureCopy.image.array[5][4] 

551 + exposureCopy.image.array[5][5]), delta=0.3) 

552 # These fits should also reduce the image stdev, as 

553 # they are modeling the ramp. 

554 self.assertLess(statAfter[1], statBefore[1]) 

555 

556 def test_bleedParallelOverscanCorrectionFailure(self): 

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

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

559 

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

561 

562 This adds a large artificial bleed to the overscan region, 

563 which should be masked and patched with the median of the 

564 other pixels. 

565 """ 

566 for taskType in ("combined", "separate"): 

567 exposure = self.makeExposure(addRamp=True, isTransposed=False) 

568 detector = exposure.getDetector() 

569 amp = detector.getAmplifiers()[0] 

570 

571 maskedImage = exposure.getMaskedImage() 

572 overscanBleedBox = lsst.geom.Box2I(lsst.geom.Point2I(4, 10), 

573 lsst.geom.Extent2I(2, 3)) 

574 overscanBleed = afwImage.MaskedImageF(maskedImage, overscanBleedBox) 

575 overscanBleed.set(10000, 0x0, 1) # This level is below the mask threshold. 

576 

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

578 

579 for fitType in ('MEDIAN', 'MEDIAN_PER_ROW'): 

580 # We only test these three types as this should cover the 

581 # scalar calculations, the generic vector calculations, 

582 # and the specific C++ MEDIAN_PER_ROW case. 

583 exposureCopy = exposure.clone() 

584 

585 if taskType == "combined": 

586 config = ipIsr.overscan.OverscanCorrectionTask.ConfigClass() 

587 config.doParallelOverscan = True 

588 config.parallelOverscanMaskGrowSize = 1 

589 # Ensure we don't mask anything 

590 config.maxDeviation = 100000 

591 config.fitType = fitType 

592 

593 overscanTask = ipIsr.overscan.OverscanCorrectionTask(config=config) 

594 oscanResults = overscanTask.run(exposureCopy, amp) 

595 

596 oscanMeanSerial, oscanMeanParallel = oscanResults.overscanMean 

597 oscanMedianParallel = oscanResults.overscanMedian[1] 

598 else: 

599 configSerial = ipIsr.overscan.SerialOverscanCorrectionTask.ConfigClass() 

600 # Ensure we don't mask anything 

601 configSerial.maxDeviation = 100000 

602 configSerial.fitType = fitType 

603 

604 serialOverscanTask = ipIsr.overscan.SerialOverscanCorrectionTask(config=configSerial) 

605 serialResults = serialOverscanTask.run(exposureCopy, amp) 

606 

607 configParallel = ipIsr.overscan.ParallelOverscanCorrectionTask.ConfigClass() 

608 configParallel.maxDeviation = 100000 

609 configParallel.parallelOverscanMaskGrowSize = 1 

610 configParallel.fitType = fitType 

611 

612 parallelOverscanTask = ipIsr.overscan.ParallelOverscanCorrectionTask( 

613 config=configParallel, 

614 ) 

615 oscanResults = parallelOverscanTask.run(exposureCopy, amp) 

616 

617 oscanMeanSerial = serialResults.overscanMean 

618 oscanMeanParallel = oscanResults.overscanMean 

619 oscanMedianParallel = oscanResults.overscanMedian 

620 

621 self.assertIsInstance(oscanResults, pipeBase.Struct) 

622 if fitType == 'MEDIAN': 

623 self.assertIsInstance(oscanResults.imageFit, float) 

624 self.assertIsInstance(oscanResults.overscanFit, float) 

625 else: 

626 self.assertIsInstance(oscanResults.imageFit, np.ndarray) 

627 self.assertIsInstance(oscanResults.overscanFit, np.ndarray) 

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

629 

630 statAfter = computeImageMedianAndStd(exposureCopy.image[amp.getRawDataBBox()]) 

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

632 

633 # Test the output value for the serial and parallel 

634 # overscans. 

635 self.assertAlmostEqual(oscanMeanSerial, 2.0, delta=0.001) 

636 # These are the wrong values: 

637 if fitType == 'MEDIAN': 

638 # Check that the constant case is now biased, at 6.5 

639 # instead of 4.5: 

640 self.assertAlmostEqual(oscanMeanParallel, 6.5, delta=0.001) 

641 else: 

642 # This is not correcting the bleed, so it will be printed 

643 # onto the image, making the stdev after correction worse 

644 # than before. 

645 self.assertGreater(statAfter[1], statBefore[1]) 

646 

647 # Check that the median overscan value matches the 

648 # constant fit: 

649 self.assertAlmostEqual(oscanMedianParallel, 6.5, delta=0.001) 

650 # Check that the mean isn't what we found before, and 

651 # is larger: 

652 self.assertNotEqual(oscanMeanParallel, 4.5) 

653 self.assertGreater(oscanMeanParallel, 4.5) 

654 self.assertGreater(exposureCopy.image.array[5][0], 

655 0.5 * (exposureCopy.image.array[5][4] 

656 + exposureCopy.image.array[5][5])) 

657 

658 def test_overscanCorrection_isNotInt(self): 

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

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

661 """ 

662 for taskClass in (ipIsr.overscan.OverscanCorrectionTask, ipIsr.overscan.SerialOverscanCorrectionTask): 

663 exposure = self.makeExposure(isTransposed=False) 

664 detector = exposure.getDetector() 

665 amp = detector.getAmplifiers()[0] 

666 

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

668 'NATURAL_SPLINE', 'CUBIC_SPLINE'): 

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

670 order = 3 

671 else: 

672 order = 1 

673 

674 config = taskClass.ConfigClass() 

675 config.order = order 

676 config.fitType = fitType 

677 

678 overscanTask = taskClass(config=config) 

679 

680 response = overscanTask.run(exposure, amp) 

681 

682 self.assertIsInstance(response, pipeBase.Struct, 

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

684 self.assertIsNotNone(response.imageFit, 

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

686 self.assertIsNotNone(response.overscanFit, 

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

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

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

690 

691 

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

693 pass 

694 

695 

696def setup_module(module): 

697 lsst.utils.tests.init() 

698 

699 

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

701 lsst.utils.tests.init() 

702 unittest.main()