Coverage for tests/test_display_images.py: 31%
143 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 02:01 -0700
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 02:01 -0700
1# This file is part of afw.
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# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22"""Tests for displaying lsst.images objects via lsst.afw.display."""
23import unittest
25import numpy as np
27import lsst.utils.tests
28import lsst.geom
29import lsst.afw.display as afwDisplay
30import lsst.afw.geom as afwGeom
31import lsst.afw.image as afwImage
32from lsst.afw.display._images_compat import normalize_lsst_images
34try:
35 import lsst.images
36 import astropy.units as u
37 HAVE_LSST_IMAGES = True
38except ImportError:
39 HAVE_LSST_IMAGES = False
42if HAVE_LSST_IMAGES: 42 ↛ 43line 42 didn't jump to line 43 because the condition on line 42 was never true
43 class _UnsupportedGeneralizedImage(lsst.images.GeneralizedImage):
44 """A GeneralizedImage subclass that display does not support."""
46 @property
47 def bbox(self):
48 return lsst.images.Box.from_shape((1, 1))
50 @property
51 def sky_projection(self):
52 return None
54 def __getitem__(self, bbox):
55 return self
57 def copy(self):
58 return self
61def _make_legacy_wcs():
62 """Return a simple lsst.afw.geom.SkyWcs."""
63 return afwGeom.makeSkyWcs(
64 lsst.geom.Point2D(5, 5),
65 lsst.geom.SpherePoint(30.0, -10.0, lsst.geom.degrees),
66 afwGeom.makeCdMatrix(scale=0.2 * lsst.geom.arcseconds),
67 )
70def _make_sky_projection(bbox):
71 """Return an lsst.images.SkyProjection covering bbox.
73 Built with from_legacy because projections created by from_fits_wcs
74 cannot currently be converted back to SkyWcs (PIXEL vs PIXELS AST
75 domain mismatch in lsst.images).
76 """
77 frame = lsst.images.DetectorFrame(instrument="TestCam", detector=0, bbox=bbox)
78 return lsst.images.SkyProjection.from_legacy(_make_legacy_wcs(), frame, pixel_bounds=bbox)
81def _make_image(with_projection=False):
82 """Return an lsst.images.Image with a non-zero origin."""
83 bbox = lsst.images.Box.from_shape((10, 20), start=(100, 200))
84 sky_projection = _make_sky_projection(bbox) if with_projection else None
85 return lsst.images.Image(
86 np.arange(200, dtype=np.float32).reshape(10, 20),
87 bbox=bbox,
88 unit=u.nJy,
89 sky_projection=sky_projection,
90 )
93def _make_mask():
94 """Return an lsst.images.Mask with the COSMIC_RAY plane set everywhere."""
95 schema = lsst.images.MaskSchema(
96 [
97 lsst.images.MaskPlane("SATURATED", "Saturated pixel."),
98 lsst.images.MaskPlane("COSMIC_RAY", "Cosmic ray hit."),
99 ]
100 )
101 mask = lsst.images.Mask(0, schema=schema, yx0=(100, 200), shape=(10, 20))
102 mask.set("COSMIC_RAY", np.ones((10, 20), dtype=bool))
103 return mask
106def _make_masked_image(with_projection=False):
107 """Return an lsst.images.MaskedImage, optionally with a sky projection."""
108 image = _make_image()
109 sky_projection = _make_sky_projection(image.bbox) if with_projection else None
110 return lsst.images.MaskedImage(image, mask=_make_mask(), sky_projection=sky_projection)
113@unittest.skipUnless(HAVE_LSST_IMAGES, "lsst.images is not available")
114class NormalizeLsstImagesTestCase(lsst.utils.tests.TestCase):
115 """Direct tests of normalize_lsst_images."""
117 def test_afw_passthrough(self):
118 """afw objects and their wcs must pass through untouched."""
119 image = afwImage.ImageF(3, 4)
120 wcs = _make_legacy_wcs()
121 data, outwcs = normalize_lsst_images(image, wcs)
122 self.assertIs(data, image)
123 self.assertIs(outwcs, wcs)
125 def test_image(self):
126 image = _make_image()
127 data, wcs = normalize_lsst_images(image, None)
128 self.assertIsInstance(data, afwImage.ImageF)
129 self.assertEqual(data.getXY0(), lsst.geom.Point2I(200, 100))
130 np.testing.assert_array_equal(data.array, image.array)
131 self.assertIsNone(wcs)
133 def test_image_with_projection(self):
134 image = _make_image(with_projection=True)
135 data, wcs = normalize_lsst_images(image, None)
136 self.assertIsInstance(data, afwImage.ImageF)
137 self.assertIsInstance(wcs, afwGeom.SkyWcs)
138 # The converted WCS must agree with the source projection.
139 expected = _make_legacy_wcs()
140 self.assertSpherePointsAlmostEqual(
141 wcs.pixelToSky(205.0, 105.0), expected.pixelToSky(205.0, 105.0)
142 )
144 def test_image_wcs_kwarg(self):
145 """A caller-supplied wcs is kept when the image has no projection."""
146 image = _make_image()
147 wcs = _make_legacy_wcs()
148 data, outwcs = normalize_lsst_images(image, wcs)
149 self.assertIs(outwcs, wcs)
151 def test_wcs_conflict(self):
152 image = _make_image(with_projection=True)
153 with self.assertRaises(RuntimeError):
154 normalize_lsst_images(image, _make_legacy_wcs())
156 def test_mask(self):
157 mask = _make_mask()
158 data, wcs = normalize_lsst_images(mask, None)
159 self.assertIsInstance(data, afwImage.Mask)
160 self.assertEqual(data.getXY0(), lsst.geom.Point2I(200, 100))
161 self.assertIsNone(wcs)
162 planes = data.getMaskPlaneDict()
163 self.assertIn("COSMIC_RAY", planes)
164 self.assertIn("SATURATED", planes)
165 cr = data.getPlaneBitMask("COSMIC_RAY")
166 self.assertTrue(((data.array & cr) != 0).all())
167 sat = data.getPlaneBitMask("SATURATED")
168 self.assertTrue(((data.array & sat) == 0).all())
170 def test_masked_image(self):
171 masked_image = _make_masked_image(with_projection=True)
172 data, wcs = normalize_lsst_images(masked_image, None)
173 self.assertIsInstance(data, afwImage.MaskedImageF)
174 self.assertEqual(data.getXY0(), lsst.geom.Point2I(200, 100))
175 np.testing.assert_array_equal(data.image.array, masked_image.image.array)
176 np.testing.assert_array_equal(data.variance.array, masked_image.variance.array)
177 self.assertIn("COSMIC_RAY", data.mask.getMaskPlaneDict())
178 cr = data.mask.getPlaneBitMask("COSMIC_RAY")
179 self.assertTrue(((data.mask.array & cr) != 0).all())
180 self.assertIsInstance(wcs, afwGeom.SkyWcs)
182 def test_masked_image_without_projection(self):
183 masked_image = _make_masked_image()
184 data, wcs = normalize_lsst_images(masked_image, None)
185 self.assertIsInstance(data, afwImage.MaskedImageF)
186 self.assertIsNone(wcs)
188 def test_unsupported_generalized_image(self):
189 """Unsupported subclasses pass through for Display to reject."""
190 unsupported = _UnsupportedGeneralizedImage()
191 data, wcs = normalize_lsst_images(unsupported, None)
192 self.assertIs(data, unsupported)
193 self.assertIsNone(wcs)
196@unittest.skipUnless(HAVE_LSST_IMAGES, "lsst.images is not available")
197class DisplayLsstImagesTestCase(lsst.utils.tests.TestCase):
198 """End-to-end mtv tests through the virtualDevice backend."""
200 def setUp(self):
201 afwDisplay.setDefaultBackend("virtualDevice")
202 afwDisplay.delAllDisplays()
203 self.display = afwDisplay.Display(frame=0, verbose=True)
205 def tearDown(self):
206 afwDisplay.delAllDisplays()
208 def test_mtv_image(self):
209 self.display.mtv(_make_image(), title="image")
210 self.assertEqual(self.display._xy0, lsst.geom.Point2I(200, 100))
212 def test_mtv_mask(self):
213 self.display.mtv(_make_mask(), title="mask")
214 self.assertEqual(self.display._xy0, lsst.geom.Point2I(200, 100))
216 def test_mtv_masked_image(self):
217 self.display.mtv(_make_masked_image(with_projection=True), title="masked image")
218 self.assertEqual(self.display._xy0, lsst.geom.Point2I(200, 100))
220 def test_mtv_wcs_conflict(self):
221 masked_image = _make_masked_image(with_projection=True)
222 with self.assertRaises(RuntimeError):
223 self.display.mtv(masked_image, wcs=_make_legacy_wcs())
225 def test_mtv_unsupported(self):
226 with self.assertRaises(TypeError):
227 self.display.mtv(_UnsupportedGeneralizedImage())
229 def test_default_mask_plane_colors(self):
230 """Renamed planes keep their traditional colors."""
231 self.assertEqual(self.display.getMaskPlaneColor("COSMIC_RAY"), afwDisplay.MAGENTA)
232 self.assertEqual(self.display.getMaskPlaneColor("DETECTION_EDGE"), afwDisplay.YELLOW)
235class TestMemory(lsst.utils.tests.MemoryTestCase):
236 pass
239def setup_module(module):
240 lsst.utils.tests.init()
243if __name__ == "__main__": 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true
244 lsst.utils.tests.init()
245 unittest.main()