Coverage for tests/test_image.py: 19%
136 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-29 08:43 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-29 08:43 +0000
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.
12from __future__ import annotations
14import os
15import unittest
17import astropy.io.fits
18import astropy.units as u
19import numpy as np
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)
34try:
35 import h5py # noqa: F401
37 HAVE_H5PY = True
38except ImportError:
39 HAVE_H5PY = False
41DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None)
44class ImageTestCase(unittest.TestCase):
45 """Tests for the Image class."""
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)
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)
59 assert_images_equal(self, image.copy(), image, expect_view=False)
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]]))
72 # Test __eq__ behavior.
73 self.assertEqual(image[...], image)
74 self.assertEqual(image.__eq__(data), NotImplemented)
75 self.assertNotEqual(image, list(data))
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])
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])
85 with self.assertRaises(TypeError):
86 # shape and bbox both None.
87 Image()
89 with self.assertRaises(ValueError):
90 # Shape mismatch.
91 Image(shape=[3, 6], bbox=Box.factory[-5:10, 0:10])
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)
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)
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)
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)
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)
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])
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)
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 )
178 def test_read_write(self):
179 """Round trip through file.
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])
190 image = Image(
191 data,
192 unit=u.dn,
193 metadata=md,
194 bbox=Box.factory[-2:1, 3:7],
195 projection=projection,
196 )
198 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile:
199 image.write_fits(tmpFile)
201 new = Image.read_fits(tmpFile)
202 self.assertEqual(new, image)
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)
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 )
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")
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)
263if __name__ == "__main__":
264 unittest.main()