Coverage for tests/test_imageIo2.py: 26%

77 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-14 01:39 -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 

24 

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 

31 

32 

33class ImageIoTestCase(lsst.utils.tests.TestCase): 

34 """A test case for Image FITS I/O""" 

35 

36 def assertImagesEqual(self, image, original): 

37 self.assertEqual(image.getBBox(), original.getBBox()) 

38 super().assertImagesEqual(image, original) 

39 

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)) 

45 

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 

65 

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 

71 

72 readImage, hdu = self.doRoundTrip(original, compression=compression, scaling=scaling) 

73 self.assertImagesAlmostEqual(original, readImage, rtol=rtol, atol=0) 

74 

75 # Integer LSST images never have missing pixels; FITS floating-point images always use NaN 

76 self.assertNotIn("BLANK", hdu.header.keys()) 

77 

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) 

82 

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) 

89 

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)) 

96 

97 def testUInt64(self): 

98 """Test round-tripping uint64 images (ImageL). 

99 

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) 

104 

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) 

110 

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 ) 

120 

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 ) 

134 

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 ) 

149 

150 

151class TestMemory(lsst.utils.tests.MemoryTestCase): 

152 pass 

153 

154 

155def setup_module(module): 

156 lsst.utils.tests.init() 

157 

158 

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()