Coverage for tests/test_image.py: 19%

136 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-29 01:48 -0700

1# This file is part of lsst-images. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14import os 

15import unittest 

16 

17import astropy.io.fits 

18import astropy.units as u 

19import numpy as np 

20 

21import lsst.utils.tests 

22from lsst.images import Box, DetectorFrame, Image 

23from lsst.images.tests import ( 

24 RoundtripFits, 

25 RoundtripJson, 

26 RoundtripNdf, 

27 assert_close, 

28 assert_images_equal, 

29 assert_projections_equal, 

30 compare_image_to_legacy, 

31 make_random_projection, 

32) 

33 

34try: 

35 import h5py # noqa: F401 

36 

37 HAVE_H5PY = True 

38except ImportError: 

39 HAVE_H5PY = False 

40 

41DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None) 

42 

43 

44class ImageTestCase(unittest.TestCase): 

45 """Tests for the Image class.""" 

46 

47 def test_basics(self): 

48 """Test basic constructor patterns.""" 

49 image = Image(42, shape=(5, 5), metadata={"three": 3}) 

50 assert_close(self, image.array, np.zeros([5, 5], dtype=np.int64) + 42) 

51 self.assertEqual(image.metadata["three"], 3) 

52 

53 data = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) 

54 image = Image(data) 

55 subset = image[Box.factory[:3, 1:3]] 

56 subset2 = image.absolute[:3, 1:3] 

57 assert_images_equal(self, subset2, subset, expect_view=True) 

58 

59 assert_images_equal(self, image.copy(), image, expect_view=False) 

60 

61 # Add an explicit bounding box and then slice it. 

62 image = Image(data, bbox=Box.factory[-2:1, 10:14]) 

63 with self.assertRaises(IndexError): 

64 # Same slice no longer works in absolute slicing because we have 

65 # moved origin. 

66 image.absolute[:3, 1:3] 

67 # That slice does still work in local coordinates. 

68 assert_close(self, image.local[:3, 1:3].array, subset2.array) 

69 # And we can write an equivalent slice in absolute coordinates. 

70 assert_close(self, image.absolute[:0, 11:13].array, np.array([[2, 3], [6, 7]])) 

71 

72 # Test __eq__ behavior. 

73 self.assertEqual(image[...], image) 

74 self.assertEqual(image.__eq__(data), NotImplemented) 

75 self.assertNotEqual(image, list(data)) 

76 

77 with self.assertRaises(ValueError): 

78 # bbox does not match array shape. 

79 Image(np.array([[1, 2, 3], [4, 5, 6]]), bbox=Box.factory[0:2, 0:4]) 

80 

81 with self.assertRaises(ValueError): 

82 # shape does not match array shape. 

83 Image(np.array([[2, 3, 4], [6, 7, 8]]), shape=[5, 2]) 

84 

85 with self.assertRaises(TypeError): 

86 # shape and bbox both None. 

87 Image() 

88 

89 with self.assertRaises(ValueError): 

90 # Shape mismatch. 

91 Image(shape=[3, 6], bbox=Box.factory[-5:10, 0:10]) 

92 

93 def test_json_roundtrip(self) -> None: 

94 """Test saving a tiny image to pure JSON.""" 

95 image = Image( 

96 np.arange(15).reshape(5, 3), 

97 start=(2, -1), 

98 ) 

99 with RoundtripJson(self, image, "ImageV2") as roundtrip: 

100 pass 

101 assert_images_equal(self, image, roundtrip.result) 

102 

103 def test_fits_roundtrip(self) -> None: 

104 """Test saving a tiny image to FITS generically.""" 

105 image = Image( 

106 np.arange(15).reshape(5, 3), 

107 start=(2, -1), 

108 ) 

109 with RoundtripFits(self, image, "ImageV2") as roundtrip: 

110 subbox = Box.factory[3:5, 0:1] 

111 assert_images_equal(self, image[subbox], roundtrip.get(bbox=subbox)) 

112 assert_images_equal(self, image, roundtrip.result) 

113 

114 @unittest.skipUnless(HAVE_H5PY, "h5py is not installed") 

115 def test_ndf_roundtrip(self) -> None: 

116 """Test saving a tiny image to NDF.""" 

117 image = Image( 

118 np.arange(15).reshape(5, 3), 

119 start=(2, -1), 

120 ) 

121 with RoundtripNdf(self, image, "ImageV2") as roundtrip: 

122 pass 

123 assert_images_equal(self, image, roundtrip.result) 

124 

125 @unittest.skipUnless(HAVE_H5PY, "h5py is not installed") 

126 def test_fits_ndf_consistency(self): 

127 """Writing via FITS and via NDF, then reading back, produces equal 

128 Images. 

129 """ 

130 rng = np.random.default_rng(321) 

131 image = Image( 

132 rng.normal(100.0, 8.0, size=(60, 80)), 

133 dtype=np.float64, 

134 unit=u.nJy, 

135 start=(0, 0), 

136 ) 

137 with RoundtripFits(self, image) as fits_rt, RoundtripNdf(self, image) as ndf_rt: 

138 assert_images_equal(self, image, fits_rt.result) 

139 assert_images_equal(self, image, ndf_rt.result) 

140 assert_images_equal(self, fits_rt.result, ndf_rt.result) 

141 

142 def test_fits_json_consistency(self): 

143 """Writing via FITS and via JSON, then reading back, produces equal 

144 Images. 

145 """ 

146 rng = np.random.default_rng(321) 

147 image = Image( 

148 rng.normal(100.0, 8.0, size=(60, 80)), 

149 dtype=np.float64, 

150 unit=u.nJy, 

151 start=(0, 0), 

152 ) 

153 with RoundtripFits(self, image) as fits_rt, RoundtripJson(self, image) as json_rt: 

154 assert_images_equal(self, image, fits_rt.result) 

155 assert_images_equal(self, image, json_rt.result) 

156 assert_images_equal(self, fits_rt.result, json_rt.result) 

157 

158 def test_quantity(self): 

159 """Test quantities.""" 

160 data = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]]) 

161 data2 = data.copy() * 2.0 

162 image = Image(data, unit=u.mJy, bbox=Box.factory[-2:1, 3:7]) 

163 

164 q = image.quantity 

165 self.assertEqual(q[1, 0], 5.0 * u.mJy) 

166 image.quantity = image.array * 10.0 * u.uJy 

167 q = image.quantity 

168 self.assertEqual(q[1, 0], 0.05 * u.mJy) 

169 

170 image2 = Image(data2, unit=u.Jy) 

171 image[Box.factory[-1:0, 5:7]] = image2.local[1:2, 2:4] 

172 assert_close( 

173 self, 

174 image.array, 

175 np.array([[0.01, 0.02, 0.03, 0.04], [0.05, 0.06, 14000.0, 16000.0], [0.09, 0.1, 0.11, 0.12]]), 

176 ) 

177 

178 def test_read_write(self): 

179 """Round trip through file. 

180 

181 This uses the read_fits and write_fits methods (which RoundtripFits 

182 does not use). 

183 """ 

184 data = np.array([[1.0, 2.0, np.nan, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]]) 

185 md = {"int": 1, "float": 42.0, "bool": False, "long string header": "This is a string"} 

186 det_frame = DetectorFrame(instrument="Inst", visit=1234, detector=1, bbox=Box.factory[1:4096, 1:4096]) 

187 rng = np.random.default_rng(500) 

188 projection = make_random_projection(rng, det_frame, Box.factory[1:4096, 1:4096]) 

189 

190 image = Image( 

191 data, 

192 unit=u.dn, 

193 metadata=md, 

194 bbox=Box.factory[-2:1, 3:7], 

195 projection=projection, 

196 ) 

197 

198 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

199 image.write_fits(tmpFile) 

200 

201 new = Image.read_fits(tmpFile) 

202 self.assertEqual(new, image) 

203 

204 # __eq__ does not test all components. 

205 self.assertEqual(new.metadata, image.metadata) 

206 self.maxDiff = None 

207 assert_projections_equal(self, new.projection, image.projection, expect_identity=False) 

208 

209 # Read subset. 

210 subset = Image.read_fits(tmpFile, bbox=Box.factory[-2:0, 5:7]) 

211 self.assertEqual(subset, image.absolute[-2:0, 5:7]) 

212 self.assertEqual(subset, image.local[0:2, 2:4]) 

213 self.assertEqual(str(subset), "Image([y=-2:0, x=5:7], float64)") 

214 self.assertEqual( 

215 repr(subset), 

216 "Image(..., bbox=Box(y=Interval(start=-2, stop=0), x=Interval(start=5, stop=7)), " 

217 "dtype=dtype('float64'))", 

218 ) 

219 

220 # Check that WCS headers were written out. 

221 with astropy.io.fits.open(tmpFile) as hdul: 

222 hdu1 = hdul[1] 

223 hdr1 = hdu1.header 

224 self.assertEqual(hdr1["CTYPE1"], "RA---TAN") 

225 

226 @unittest.skipUnless(DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.") 

227 def test_legacy(self) -> None: 

228 """Test Image.read_legacy, Image.to_legacy, and Image.from_legacy.""" 

229 assert DATA_DIR is not None, "Guaranteed by decorator." 

230 filename = os.path.join(DATA_DIR, "dp2", "legacy", "visit_image.fits") 

231 det_frame = DetectorFrame(instrument="Inst", visit=1234, detector=1, bbox=Box.factory[1:4096, 1:4096]) 

232 image = Image.read_legacy(filename, preserve_quantization=True, fits_wcs_frame=det_frame) 

233 try: 

234 from lsst.afw.image import MaskedImageFitsReader 

235 except ImportError: 

236 raise unittest.SkipTest("'lsst.afw.image' could not be imported.") from None 

237 reader = MaskedImageFitsReader(filename) 

238 legacy_image = reader.readImage() 

239 compare_image_to_legacy(self, image, legacy_image, expect_view=False) 

240 # Converting back to afw will not share memory, because 

241 # preserve_quantization=True makes the array read-only and to_legacy 

242 # has to copy in that case. 

243 compare_image_to_legacy(self, image, image.to_legacy(), expect_view=False) 

244 # Converting from afw will always share memory. 

245 image_view = Image.from_legacy(legacy_image) 

246 compare_image_to_legacy(self, image_view, legacy_image, expect_view=True) 

247 # Converting back to afw from the in-memory view will be another view. 

248 compare_image_to_legacy(self, image_view, image_view.to_legacy(), expect_view=True) 

249 # Write the image out in the new format, and test that we can read it 

250 # back either way. 

251 with RoundtripFits(self, image, storage_class="ImageV2") as roundtrip: 

252 with self.subTest(): 

253 try: 

254 import lsst.afw.image 

255 except ImportError: 

256 raise unittest.SkipTest("afw could not be imported") from None 

257 legacy_image = roundtrip.get(storageClass="Image") 

258 self.assertIsInstance(legacy_image, lsst.afw.image.Image) 

259 compare_image_to_legacy(self, image, legacy_image, expect_view=False) 

260 assert_images_equal(self, roundtrip.result, image, expect_view=False) 

261 

262 

263if __name__ == "__main__": 

264 unittest.main()