Coverage for tests/test_overscanCorrection.py: 9%
260 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-17 09:44 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-17 09:44 +0000
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#
23import unittest
24import numpy as np
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
34def computeImageMedianAndStd(image):
35 """Function to calculate median and std of image data.
37 Parameters
38 ----------
39 image : `lsst.afw.image.Image`
40 Image to measure statistics on.
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())
52 return (median, std)
55class IsrTestCases(lsst.utils.tests.TestCase):
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
64 order = kwargs.get('order', None)
65 if order:
66 config.overscan.order = order
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)
73 ampBuilder = cameraGeom.Amplifier.Builder()
75 dataBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
76 lsst.geom.Extent2I(10, 10))
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))
93 ampBuilder.setRawBBox(fullBBox)
94 ampBuilder.setRawSerialOverscanBBox(serialOverscanBBox)
95 ampBuilder.setRawParallelOverscanBBox(parallelOverscanBBox)
96 ampBuilder.setRawDataBBox(dataBBox)
98 detectorBuilder.append(ampBuilder)
99 camera = cameraBuilder.finish()
100 detector = camera[0]
102 # Define image data.
103 maskedImage = afwImage.MaskedImageF(fullBBox)
104 maskedImage.set(10, 0x0, 1)
106 overscan = afwImage.MaskedImageF(maskedImage, serialOverscanBBox)
107 overscan.set(2, 0x0, 1)
108 overscan = afwImage.MaskedImageF(maskedImage, parallelOverscanBBox)
109 overscan.set(2, 0x0, 1)
111 exposure = afwImage.ExposureF(maskedImage, None)
112 exposure.setDetector(detector)
113 return exposure
115 def checkOverscanCorrectionY(self, **kwargs):
116 exposure = self.makeExposure(isTransposed=True)
117 detector = exposure.getDetector()
119 # These subimages are needed below.
120 overscan = exposure[detector.getAmplifiers()[0].getRawSerialOverscanBBox()]
121 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()]
123 config = ipIsr.IsrTask.ConfigClass()
124 self.updateConfigFromKwargs(config, **kwargs)
126 if kwargs['fitType'] == "MEDIAN_PER_ROW":
127 # Add a bad point to test outlier rejection.
128 overscan.getImage().getArray()[0, 0] = 12345
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
134 isrTask = ipIsr.IsrTask(config=config)
135 isrTask.overscan.run(exposure, detector.getAmplifiers()[0], isTransposed=True)
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)
148 def checkOverscanCorrectionX(self, **kwargs):
149 exposure = self.makeExposure(isTransposed=False)
150 detector = exposure.getDetector()
152 # These subimages are needed below.
153 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()]
155 config = ipIsr.IsrTask.ConfigClass()
156 self.updateConfigFromKwargs(config, **kwargs)
158 isrTask = ipIsr.IsrTask(config=config)
159 isrTask.overscan.run(exposure, detector.getAmplifiers()[0], isTransposed=False)
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)
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)
176 ampBuilder = cameraGeom.Amplifier.Builder()
178 dataBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
179 lsst.geom.Extent2I(70, 500))
181 fullBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
182 lsst.geom.Extent2I(100, 500))
184 overscanBBox = lsst.geom.Box2I(lsst.geom.Point2I(70, 0),
185 lsst.geom.Extent2I(30, 500))
187 ampBuilder.setRawBBox(fullBBox)
188 ampBuilder.setRawSerialOverscanBBox(overscanBBox)
189 ampBuilder.setRawDataBBox(dataBBox)
191 detectorBuilder.append(ampBuilder)
192 camera = cameraBuilder.finish()
193 detector = camera[0]
195 # Define image data.
196 maskedImage = afwImage.MaskedImageF(fullBBox)
197 maskedImage.set(50, 0x0, 1)
199 overscan = afwImage.MaskedImageF(maskedImage, overscanBBox)
200 overscan.set(0, 0x0, 1)
202 exposure = afwImage.ExposureF(maskedImage, None)
203 exposure.setDetector(detector)
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)
211 fullImage = np.repeat(sineWave, 100).reshape((500, 100))
212 maskedImage.image.array += fullImage
214 config = ipIsr.IsrTask.ConfigClass()
215 self.updateConfigFromKwargs(config, **kwargs)
217 isrTask = ipIsr.IsrTask(config=config)
218 isrTask.overscan.run(exposure, detector.getAmplifiers()[0])
220 height = maskedImage.getHeight()
221 width = maskedImage.getWidth()
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)
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")
235 def test_MedianOverscanCorrection(self):
236 self.checkOverscanCorrectionY(fitType="MEDIAN")
237 self.checkOverscanCorrectionX(fitType="MEDIAN")
239 def checkPolyOverscanCorrectionX(self, **kwargs):
240 exposure = self.makeExposure(isTransposed=False)
241 detector = exposure.getDetector()
243 # These subimages are needed below.
244 overscan = exposure[detector.getAmplifiers()[0].getRawSerialOverscanBBox()]
245 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()]
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
253 config = ipIsr.IsrTask.ConfigClass()
254 self.updateConfigFromKwargs(config, **kwargs)
256 isrTask = ipIsr.IsrTask(config=config)
257 isrTask.overscan.run(exposure, detector.getAmplifiers()[0], isTransposed=False)
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)
273 def checkPolyOverscanCorrectionY(self, **kwargs):
274 exposure = self.makeExposure(isTransposed=True)
275 detector = exposure.getDetector()
277 # These subimages are needed below.
278 overscan = exposure[detector.getAmplifiers()[0].getRawSerialOverscanBBox()]
279 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()]
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)
288 config = ipIsr.IsrTask.ConfigClass()
289 self.updateConfigFromKwargs(config, **kwargs)
291 isrTask = ipIsr.IsrTask(config=config)
292 isrTask.overscan.run(exposure, detector.getAmplifiers()[0], isTransposed=True)
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)
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)
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)
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.
322 This needs to operate on a RawMock() to have overscan data to use.
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]
330 statBefore = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
332 config = ipIsr.IsrTask.ConfigClass()
333 isrTask = ipIsr.IsrTask(config=config)
334 oscanResults = isrTask.overscan.run(exposure, amp)
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)
341 statAfter = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
342 self.assertLess(statAfter[0], statBefore[0])
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.
348 This needs to operate on a RawMock() to have overscan data to use.
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]
356 statBefore = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
358 config = ipIsr.IsrTask.ConfigClass()
359 config.overscan.doParallelOverscan = True
360 isrTask = ipIsr.IsrTask(config=config)
361 oscanResults = isrTask.overscan.run(exposure, amp)
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)
368 statAfter = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
369 self.assertLess(statAfter[0], statBefore[0])
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.
375 This needs to operate on a RawMock() to have overscan data to use.
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]
383 maskedImage = exposure.getMaskedImage()
384 overscan = afwImage.MaskedImageF(maskedImage, amp.getRawParallelOverscanBBox())
385 overscan.set(400, 0x0, 1)
387 statBefore = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
389 config = ipIsr.IsrTask.ConfigClass()
390 config.overscan.doParallelOverscan = True
391 isrTask = ipIsr.IsrTask(config=config)
392 oscanResults = isrTask.overscan.run(exposure, amp)
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)
399 statAfter = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
400 self.assertLess(statAfter[0], statBefore[0])
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]
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)
421 response = isrTask.overscan.run(exposure, amp)
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}")
433class MemoryTester(lsst.utils.tests.MemoryTestCase):
434 pass
437def setup_module(module):
438 lsst.utils.tests.init()
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()