Coverage for tests/test_imageIo2.py: 26%
77 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:34 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:34 -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
25import numpy as np
26import astropy.io.fits
27import lsst.utils.tests
28from lsst.geom import Box2I, Point2I
29from lsst.afw.fits import ImageCompressionOptions, ImageScalingOptions, ImageWriteOptions
30import lsst.afw.image as afwImage
33class ImageIoTestCase(lsst.utils.tests.TestCase):
34 """A test case for Image FITS I/O"""
36 def assertImagesEqual(self, image, original):
37 self.assertEqual(image.getBBox(), original.getBBox())
38 super().assertImagesEqual(image, original)
40 def setUp(self):
41 # ImageL (uint64) is not supported by FITS standard, needs special tests
42 self.IntegerImages = (afwImage.ImageU, afwImage.ImageI)
43 self.FloatImages = (afwImage.ImageF, afwImage.ImageD)
44 self.bbox = Box2I(minimum=Point2I(3, 4), maximum=Point2I(9, 7))
46 def doRoundTrip(self, image, compression=None, scaling=None):
47 if compression is None:
48 compression = dict(algorithm=ImageCompressionOptions.NONE)
49 if scaling is None:
50 scaling = dict(algorithm=ImageScalingOptions.NONE, bitpix=0)
51 options = ImageWriteOptions(compression=ImageCompressionOptions(**compression),
52 scaling=ImageScalingOptions(**scaling))
53 isCompressed = (compression.get("algorithm", ImageCompressionOptions.NONE)
54 != ImageCompressionOptions.NONE)
55 with lsst.utils.tests.getTempFilePath(f"_{type(image).__name__}.fits") as filename:
56 image.writeFits(filename, options=options)
57 readImage = type(image)(filename)
58 with astropy.io.fits.open(filename) as hduList:
59 hdu = hduList[1 if isCompressed else 0]
60 if hdu.data.dtype.byteorder != '=':
61 # numpy 2 suggests a view and two stage swap.
62 tmp_data = hdu.data.byteswap()
63 hdu.data = tmp_data.view(tmp_data.dtype.newbyteorder("S"))
64 return readImage, hdu
66 def runRoundTripTest(self, cls, compression=None, scaling=None, addNaN=False, checkAstropy=True, rtol=0):
67 original = cls(self.bbox)
68 original.array[:, :] = np.random.randint(size=original.array.shape, low=1, high=255, dtype=np.uint8)
69 if addNaN:
70 original[5, 6] = np.nan
72 readImage, hdu = self.doRoundTrip(original, compression=compression, scaling=scaling)
73 self.assertImagesAlmostEqual(original, readImage, rtol=rtol, atol=0)
75 # Integer LSST images never have missing pixels; FITS floating-point images always use NaN
76 self.assertNotIn("BLANK", hdu.header.keys())
78 if checkAstropy:
79 # Compare to what astropy reads, to more-or-less check that we're not abusing FITS
80 hduImage = cls(hdu.data, deep=False, xy0=self.bbox.getMin())
81 self.assertImagesAlmostEqual(original, hduImage, rtol=rtol, atol=0)
83 def testIntegerUncompression(self):
84 """Test round-tripping integer images with no compression or scaling.
85 """
86 for cls in self.IntegerImages:
87 with self.subTest(cls=cls.__name__):
88 self.runRoundTripTest(cls)
90 def testIntegerCompression(self):
91 """Test round-tripping integer images with compression (and no scaling).
92 """
93 for cls in self.IntegerImages:
94 with self.subTest(cls=cls.__name__):
95 self.runRoundTripTest(cls, compression=dict(algorithm=ImageCompressionOptions.RICE))
97 def testUInt64(self):
98 """Test round-tripping uint64 images (ImageL).
100 uint64 is supported by a CFITSIO convention, not the FITS standard,
101 so we can't read them correctly with astropy or compression.
102 """
103 self.runRoundTripTest(afwImage.ImageL, checkAstropy=False)
105 def testFloatUncompressed(self):
106 """Test round-tripping floating-point images with no compression."""
107 for cls in self.FloatImages:
108 with self.subTest(cls=cls.__name__):
109 self.runRoundTripTest(cls, addNaN=True)
111 def testFloatCompressedLossless(self):
112 """Test round-tripping floating-point images with lossless compression."""
113 for cls in self.FloatImages:
114 with self.subTest(cls=cls.__name__):
115 self.runRoundTripTest(
116 cls,
117 compression=dict(algorithm=ImageCompressionOptions.GZIP, quantizeLevel=0),
118 addNaN=True
119 )
121 @unittest.skip("Fix deferred to DM-15644")
122 def testFloatCompressedRange(self):
123 """Test round-tripping floating-point images with lossy compression
124 and RANGE scaling."""
125 for cls in self.FloatImages:
126 with self.subTest(cls=cls.__name__):
127 self.runRoundTripTest(
128 cls,
129 compression=dict(algorithm=ImageCompressionOptions.GZIP, quantizeLevel=1),
130 scaling=dict(algorithm=ImageScalingOptions.RANGE, bitpix=32, fuzz=False),
131 addNaN=True,
132 checkAstropy=True
133 )
135 @unittest.skip("Fix deferred to DM-15644")
136 def testFloatCompressedManual(self):
137 """Test round-tripping floating-point images with lossy compression
138 and MANUAL scaling."""
139 for cls in self.FloatImages:
140 with self.subTest(cls=cls.__name__):
141 self.runRoundTripTest(
142 cls,
143 compression=dict(algorithm=ImageCompressionOptions.GZIP, quantizeLevel=1),
144 scaling=dict(algorithm=ImageScalingOptions.MANUAL, bitpix=32, fuzz=False,
145 bzero=3, bscale=2),
146 addNaN=True,
147 checkAstropy=True
148 )
151class TestMemory(lsst.utils.tests.MemoryTestCase):
152 pass
155def setup_module(module):
156 lsst.utils.tests.init()
159if __name__ == "__main__": 159 ↛ 160line 159 didn't jump to line 160 because the condition on line 159 was never true
160 lsst.utils.tests.init()
161 unittest.main()