Coverage for tests/test_overscanCorrection.py: 6%
395 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-28 03:29 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-28 03:29 -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#
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 updateOverscanConfigFromKwargs(self, config, **kwargs):
69 """Common config from keywords.
70 """
71 fitType = kwargs.get('fitType', None)
72 if fitType:
73 config.fitType = fitType
75 order = kwargs.get('order', None)
76 if order:
77 config.order = order
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)
84 ampBuilder = cameraGeom.Amplifier.Builder()
86 dataBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
87 lsst.geom.Extent2I(10, 10))
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))
104 ampBuilder.setRawBBox(fullBBox)
105 ampBuilder.setRawSerialOverscanBBox(serialOverscanBBox)
106 ampBuilder.setRawParallelOverscanBBox(parallelOverscanBBox)
107 ampBuilder.setRawDataBBox(dataBBox)
109 detectorBuilder.append(ampBuilder)
110 camera = cameraBuilder.finish()
111 detector = camera[0]
113 # Define image data.
114 maskedImage = afwImage.MaskedImageF(fullBBox)
115 maskedImage.set(2, 0x0, 1)
117 dataImage = afwImage.MaskedImageF(maskedImage, dataBBox)
118 dataImage.set(10, 0x0, 1)
120 if addRamp:
121 for column in range(dataBBox.getWidth()):
122 maskedImage.image.array[:, column] += column
124 exposure = afwImage.ExposureF(maskedImage, None)
125 exposure.setDetector(detector)
126 return exposure
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()
134 # These subimages are needed below.
135 overscan = exposure[detector.getAmplifiers()[0].getRawSerialOverscanBBox()]
136 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()]
138 config = taskClass.ConfigClass()
139 self.updateOverscanConfigFromKwargs(config, **kwargs)
141 if kwargs['fitType'] == "MEDIAN_PER_ROW":
142 # Add a bad point to test outlier rejection.
143 overscan.getImage().getArray()[0, 0] = 12345
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
149 overscanTask = taskClass(config=config)
150 _ = overscanTask.run(exposure, detector.getAmplifiers()[0], isTransposed=True)
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)
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()
169 # These subimages are needed below.
170 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()]
172 config = taskClass.ConfigClass()
173 self.updateOverscanConfigFromKwargs(config, **kwargs)
175 overscanTask = taskClass(config=config)
176 _ = overscanTask.run(exposure, detector.getAmplifiers()[0], isTransposed=False)
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)
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)
193 ampBuilder = cameraGeom.Amplifier.Builder()
195 dataBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
196 lsst.geom.Extent2I(70, 500))
198 fullBBox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
199 lsst.geom.Extent2I(100, 500))
201 overscanBBox = lsst.geom.Box2I(lsst.geom.Point2I(70, 0),
202 lsst.geom.Extent2I(30, 500))
204 ampBuilder.setRawBBox(fullBBox)
205 ampBuilder.setRawSerialOverscanBBox(overscanBBox)
206 ampBuilder.setRawDataBBox(dataBBox)
208 detectorBuilder.append(ampBuilder)
209 camera = cameraBuilder.finish()
210 detector = camera[0]
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)
217 overscan = afwImage.MaskedImageF(maskedImage, overscanBBox)
218 overscan.set(0, 0x0, 1)
220 exposure = afwImage.ExposureF(maskedImage, None)
221 exposure.setDetector(detector)
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)
229 fullImage = np.repeat(sineWave, 100).reshape((500, 100))
230 maskedImage.image.array += fullImage
232 config = taskClass.ConfigClass()
233 self.updateOverscanConfigFromKwargs(config, **kwargs)
235 overscanTask = taskClass(config=config)
236 _ = overscanTask.run(exposure, detector.getAmplifiers()[0])
238 height = maskedImage.getHeight()
239 width = maskedImage.getWidth()
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)
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")
253 def test_MedianOverscanCorrection(self):
254 self.checkOverscanCorrectionY(fitType="MEDIAN")
255 self.checkOverscanCorrectionX(fitType="MEDIAN")
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()
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)
270 serialOverscanBBox = lsst.geom.Box2I(
271 lsst.geom.Point2I(serialOverscanBBox.getMinX(),
272 imageBBox.getMinY()),
273 lsst.geom.Extent2I(serialOverscanBBox.getWidth(),
274 imageBBox.getHeight()),
275 )
277 overscan = exposure[serialOverscanBBox]
278 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()]
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
286 config = taskClass.ConfigClass()
287 self.updateOverscanConfigFromKwargs(config, **kwargs)
289 overscanTask = taskClass(config=config)
290 _ = overscanTask.run(exposure, detector.getAmplifiers()[0], isTransposed=False)
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)
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()
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)
319 serialOverscanBBox = lsst.geom.Box2I(
320 lsst.geom.Point2I(serialOverscanBBox.getMinX(), imageBBox.getEndY()),
321 lsst.geom.Extent2I(imageBBox.getWidth(), serialOverscanBBox.getHeight()),
322 )
324 overscan = exposure[serialOverscanBBox]
325 maskedImage = exposure[detector.getAmplifiers()[0].getRawBBox()]
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
333 config = taskClass.ConfigClass()
334 self.updateOverscanConfigFromKwargs(config, **kwargs)
336 overscanTask = taskClass(config=config)
337 _ = overscanTask.run(exposure, detector.getAmplifiers()[0], isTransposed=True)
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)
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)
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)
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.
367 This needs to operate on a RawMock() to have overscan data to use.
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]
376 statBefore = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
378 config = taskClass.ConfigClass()
379 overscanTask = taskClass(config=config)
380 oscanResults = overscanTask.run(exposure, amp)
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)
387 statAfter = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
388 self.assertLess(statAfter[0], statBefore[0])
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.
394 This needs to operate on a RawMock() to have overscan data to use.
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]
405 statBefore = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
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()
412 if taskType == "combined":
413 config = ipIsr.overscan.OverscanCorrectionTask.ConfigClass()
414 config.doParallelOverscan = True
415 config.fitType = fitType
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
423 serialOverscanTask = ipIsr.overscan.SerialOverscanCorrectionTask(config=configSerial)
424 serialResults = serialOverscanTask.run(exposureCopy, amp)
426 configParallel = ipIsr.overscan.ParallelOverscanCorrectionTask.ConfigClass()
427 configParallel.fitType = fitType
429 parallelOverscanTask = ipIsr.overscan.ParallelOverscanCorrectionTask(
430 config=configParallel,
431 )
432 oscanResults = parallelOverscanTask.run(exposureCopy, amp)
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)
443 statAfter = computeImageMedianAndStd(exposureCopy.image[amp.getRawDataBBox()])
444 self.assertLess(statAfter[0], statBefore[0])
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)
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)
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.
466 This needs to operate on a RawMock() to have overscan data to use.
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]
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)
483 statBefore = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
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()
491 if taskType == "combined":
492 config = ipIsr.overscan.OverscanCorrectionTask.ConfigClass()
493 config.doParallelOverscan = True
494 config.parallelOverscanMaskGrowSize = 1
495 config.fitType = fitType
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
505 serialOverscanTask = ipIsr.overscan.SerialOverscanCorrectionTask(config=configSerial)
506 serialResults = serialOverscanTask.run(exposureCopy, amp)
508 configParallel = ipIsr.overscan.ParallelOverscanCorrectionTask.ConfigClass()
509 configParallel.parallelOverscanMaskGrowSize = 1
510 configParallel.fitType = fitType
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)
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)
528 statAfter = computeImageMedianAndStd(exposureCopy.image[amp.getRawDataBBox()])
529 self.assertLess(statAfter[0], statBefore[0])
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)
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])
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.
560 This needs to operate on a RawMock() to have overscan data to use.
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]
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.
577 statBefore = computeImageMedianAndStd(exposure.image[amp.getRawDataBBox()])
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()
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
593 overscanTask = ipIsr.overscan.OverscanCorrectionTask(config=config)
594 oscanResults = overscanTask.run(exposureCopy, amp)
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
604 serialOverscanTask = ipIsr.overscan.SerialOverscanCorrectionTask(config=configSerial)
605 serialResults = serialOverscanTask.run(exposureCopy, amp)
607 configParallel = ipIsr.overscan.ParallelOverscanCorrectionTask.ConfigClass()
608 configParallel.maxDeviation = 100000
609 configParallel.parallelOverscanMaskGrowSize = 1
610 configParallel.fitType = fitType
612 parallelOverscanTask = ipIsr.overscan.ParallelOverscanCorrectionTask(
613 config=configParallel,
614 )
615 oscanResults = parallelOverscanTask.run(exposureCopy, amp)
617 oscanMeanSerial = serialResults.overscanMean
618 oscanMeanParallel = oscanResults.overscanMean
619 oscanMedianParallel = oscanResults.overscanMedian
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)
630 statAfter = computeImageMedianAndStd(exposureCopy.image[amp.getRawDataBBox()])
631 self.assertLess(statAfter[0], statBefore[0])
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])
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]))
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]
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
674 config = taskClass.ConfigClass()
675 config.order = order
676 config.fitType = fitType
678 overscanTask = taskClass(config=config)
680 response = overscanTask.run(exposure, amp)
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}")
692class MemoryTester(lsst.utils.tests.MemoryTestCase):
693 pass
696def setup_module(module):
697 lsst.utils.tests.init()
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()