Coverage for tests / test_masked_image.py: 25%
73 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-26 12:40 -0800
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-26 12:40 -0800
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 tempfile
16import unittest
18import astropy.io.fits
19import astropy.units as u
20import numpy as np
22from lsst.images import Box, Image, MaskedImage, MaskPlane, MaskSchema, get_legacy_visit_image_mask_planes
23from lsst.images.fits import FitsCompressionOptions
24from lsst.images.tests import RoundtripFits, assert_masked_images_equal, compare_masked_image_to_legacy
26DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None)
29class MaskedImageTestCase(unittest.TestCase):
30 """Tests for the MaskedImage class and the basics of the archive system."""
32 def setUp(self) -> None:
33 self.maxDiff = None
34 self.rng = np.random.default_rng(500)
35 self.masked_image = MaskedImage(
36 Image(self.rng.normal(100.0, 8.0, size=(200, 251)), dtype=np.float64, unit=u.nJy, start=(5, 8)),
37 mask_schema=MaskSchema(
38 [
39 MaskPlane("BAD", "Pixel is very bad, possibly downright evil."),
40 MaskPlane("HUNGRY", "Pixel hasn't had enough to eat today."),
41 ]
42 ),
43 )
44 self.masked_image.mask.array |= np.multiply.outer(
45 self.masked_image.image.array < 102.0,
46 self.masked_image.mask.schema.bitmask("BAD"),
47 )
48 self.masked_image.mask.array |= np.multiply.outer(
49 self.masked_image.image.array > 98.0,
50 self.masked_image.mask.schema.bitmask("HUNGRY"),
51 )
52 self.masked_image.variance.array = self.rng.normal(64.0, 0.5, size=self.masked_image.bbox.shape)
54 def test_construction(self) -> None:
55 """Test that the MaskedImage construction (in setUp) worked."""
56 self.assertEqual(self.masked_image.bbox, Box.factory[5:205, 8:259])
57 self.assertEqual(self.masked_image.mask.bbox, self.masked_image.bbox)
58 self.assertEqual(self.masked_image.variance.bbox, self.masked_image.bbox)
59 self.assertEqual(self.masked_image.image.array.shape, self.masked_image.bbox.shape)
60 self.assertEqual(self.masked_image.mask.array.shape, self.masked_image.bbox.shape + (1,))
61 self.assertEqual(self.masked_image.variance.array.shape, self.masked_image.bbox.shape)
62 self.assertEqual(self.masked_image.unit, u.nJy)
63 self.assertEqual(self.masked_image.variance.unit, u.nJy**2)
64 # The checks below are subject to the vagaries of the RNG, but we want
65 # the seed to be such that they all pass, or other tests will be
66 # weaker.
67 self.assertGreater(
68 np.sum(self.masked_image.mask.array == self.masked_image.mask.schema.bitmask("BAD")), 0
69 )
70 self.assertGreater(
71 np.sum(self.masked_image.mask.array == self.masked_image.mask.schema.bitmask("HUNGRY")), 0
72 )
73 self.assertGreater(
74 np.sum(self.masked_image.mask.array == self.masked_image.mask.schema.bitmask("BAD", "HUNGRY")), 0
75 )
77 def test_fits_roundtrip(self) -> None:
78 """Test that we can round-trip the MaskedImage through FITS, including
79 subimage reads.
80 """
81 subbox = Box.factory[11:20, 25:30]
82 subslices = (slice(6, 15), slice(17, 22))
83 np.testing.assert_array_equal(
84 self.masked_image.image.array[subslices], self.masked_image.image[subbox].array
85 )
86 with RoundtripFits(self, self.masked_image, "MaskedImageV2") as roundtrip:
87 subimage = roundtrip.get(bbox=subbox)
88 # Check that we used lossless compression (the default).
89 fits = roundtrip.inspect()
90 self.assertEqual(fits[1].header["ZCMPTYPE"], "GZIP_2")
91 self.assertEqual(fits[2].header["ZCMPTYPE"], "GZIP_2")
92 self.assertEqual(fits[3].header["ZCMPTYPE"], "GZIP_2")
93 assert_masked_images_equal(self, roundtrip.result, self.masked_image, expect_view=False)
94 assert_masked_images_equal(self, subimage, roundtrip.result[subbox], expect_view=False)
96 def test_fits_roundtrip_lossy(self) -> None:
97 """Test that we can round-trip the MaskedImage through FITS, including
98 subimage reads, with lossy compression.
99 """
100 subbox = Box.factory[11:20, 25:30]
101 subslices = (slice(6, 15), slice(17, 22))
102 np.testing.assert_array_equal(
103 self.masked_image.image.array[subslices], self.masked_image.image[subbox].array
104 )
105 with tempfile.NamedTemporaryFile(suffix=".fits", delete_on_close=False, delete=True) as tmp:
106 tmp.close()
107 self.masked_image.write_fits(
108 tmp.name,
109 image_compression=FitsCompressionOptions.LOSSY,
110 variance_compression=FitsCompressionOptions.LOSSY,
111 compression_seed=50,
112 )
113 roundtripped = MaskedImage.read_fits(tmp.name)
114 subimage = MaskedImage.read_fits(tmp.name, bbox=subbox)
115 with astropy.io.fits.open(tmp.name, disable_image_compression=True) as fits:
116 self.assertEqual(fits[1].header["ZCMPTYPE"], "RICE_1")
117 self.assertEqual(fits[2].header["ZCMPTYPE"], "GZIP_2")
118 self.assertEqual(fits[3].header["ZCMPTYPE"], "RICE_1")
119 assert_masked_images_equal(self, roundtripped, self.masked_image, expect_view=False, rtol=0.01)
120 assert_masked_images_equal(self, subimage, roundtripped[subbox], expect_view=False)
122 @unittest.skipUnless(DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.")
123 def test_legacy(self) -> None:
124 """Test MaskedImage.read_legacy, MaskedImage.to_legacy, and
125 MaskedImage.from_legacy.
126 """
127 assert DATA_DIR is not None, "Guaranteed by decorator."
128 filename = os.path.join(DATA_DIR, "dp2", "legacy", "visit_image.fits")
129 plane_map = get_legacy_visit_image_mask_planes()
130 masked_image = MaskedImage.read_legacy(filename, plane_map=plane_map)
131 try:
132 from lsst.afw.image import MaskedImageFitsReader
133 except ImportError:
134 raise unittest.SkipTest("'lsst.afw.image' could not be imported.") from None
135 reader = MaskedImageFitsReader(filename)
136 legacy_masked_image = reader.read()
137 compare_masked_image_to_legacy(
138 self, masked_image, legacy_masked_image, plane_map=plane_map, expect_view=False
139 )
140 compare_masked_image_to_legacy(
141 self,
142 masked_image,
143 masked_image.to_legacy(plane_map=plane_map),
144 plane_map=plane_map,
145 expect_view=True,
146 )
147 compare_masked_image_to_legacy(
148 self,
149 MaskedImage.from_legacy(legacy_masked_image, plane_map=plane_map),
150 legacy_masked_image,
151 expect_view=True,
152 plane_map=plane_map,
153 )
156if __name__ == "__main__":
157 unittest.main()