Coverage for tests/test_display_images.py: 31%
145 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 02:10 -0700
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 02:10 -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
60 def __repr__(self):
61 # GeneralizedImage's repr calls _describe, which is abstract.
62 return "_UnsupportedGeneralizedImage()"
65def _make_legacy_wcs():
66 """Return a simple lsst.afw.geom.SkyWcs."""
67 return afwGeom.makeSkyWcs(
68 lsst.geom.Point2D(5, 5),
69 lsst.geom.SpherePoint(30.0, -10.0, lsst.geom.degrees),
70 afwGeom.makeCdMatrix(scale=0.2 * lsst.geom.arcseconds),
71 )
74def _make_sky_projection(bbox):
75 """Return an lsst.images.SkyProjection covering bbox.
77 Built with from_legacy because projections created by from_fits_wcs
78 cannot currently be converted back to SkyWcs (PIXEL vs PIXELS AST
79 domain mismatch in lsst.images).
80 """
81 frame = lsst.images.DetectorFrame(instrument="TestCam", detector=0, bbox=bbox)
82 return lsst.images.SkyProjection.from_legacy(_make_legacy_wcs(), frame, pixel_bounds=bbox)
85def _make_image(with_projection=False):
86 """Return an lsst.images.Image with a non-zero origin."""
87 bbox = lsst.images.Box.from_shape((10, 20), start=(100, 200))
88 sky_projection = _make_sky_projection(bbox) if with_projection else None
89 return lsst.images.Image(
90 np.arange(200, dtype=np.float32).reshape(10, 20),
91 bbox=bbox,
92 unit=u.nJy,
93 sky_projection=sky_projection,
94 )
97def _make_mask():
98 """Return an lsst.images.Mask with the COSMIC_RAY plane set everywhere."""
99 schema = lsst.images.MaskSchema(
100 [
101 lsst.images.MaskPlane("SATURATED", "Saturated pixel."),
102 lsst.images.MaskPlane("COSMIC_RAY", "Cosmic ray hit."),
103 ]
104 )
105 mask = lsst.images.Mask(0, schema=schema, yx0=(100, 200), shape=(10, 20))
106 mask.set("COSMIC_RAY", np.ones((10, 20), dtype=bool))
107 return mask
110def _make_masked_image(with_projection=False):
111 """Return an lsst.images.MaskedImage, optionally with a sky projection."""
112 image = _make_image()
113 sky_projection = _make_sky_projection(image.bbox) if with_projection else None
114 return lsst.images.MaskedImage(image, mask=_make_mask(), sky_projection=sky_projection)
117@unittest.skipUnless(HAVE_LSST_IMAGES, "lsst.images is not available")
118class NormalizeLsstImagesTestCase(lsst.utils.tests.TestCase):
119 """Direct tests of normalize_lsst_images."""
121 def test_afw_passthrough(self):
122 """afw objects and their wcs must pass through untouched."""
123 image = afwImage.ImageF(3, 4)
124 wcs = _make_legacy_wcs()
125 data, outwcs = normalize_lsst_images(image, wcs)
126 self.assertIs(data, image)
127 self.assertIs(outwcs, wcs)
129 def test_image(self):
130 image = _make_image()
131 data, wcs = normalize_lsst_images(image, None)
132 self.assertIsInstance(data, afwImage.ImageF)
133 self.assertEqual(data.getXY0(), lsst.geom.Point2I(200, 100))
134 np.testing.assert_array_equal(data.array, image.array)
135 self.assertIsNone(wcs)
137 def test_image_with_projection(self):
138 image = _make_image(with_projection=True)
139 data, wcs = normalize_lsst_images(image, None)
140 self.assertIsInstance(data, afwImage.ImageF)
141 self.assertIsInstance(wcs, afwGeom.SkyWcs)
142 # The converted WCS must agree with the source projection.
143 expected = _make_legacy_wcs()
144 self.assertSpherePointsAlmostEqual(
145 wcs.pixelToSky(205.0, 105.0), expected.pixelToSky(205.0, 105.0)
146 )
148 def test_image_wcs_kwarg(self):
149 """A caller-supplied wcs is kept when the image has no projection."""
150 image = _make_image()
151 wcs = _make_legacy_wcs()
152 data, outwcs = normalize_lsst_images(image, wcs)
153 self.assertIs(outwcs, wcs)
155 def test_wcs_conflict(self):
156 image = _make_image(with_projection=True)
157 with self.assertRaises(RuntimeError):
158 normalize_lsst_images(image, _make_legacy_wcs())
160 def test_mask(self):
161 mask = _make_mask()
162 data, wcs = normalize_lsst_images(mask, None)
163 self.assertIsInstance(data, afwImage.Mask)
164 self.assertEqual(data.getXY0(), lsst.geom.Point2I(200, 100))
165 self.assertIsNone(wcs)
166 planes = data.getMaskPlaneDict()
167 self.assertIn("COSMIC_RAY", planes)
168 self.assertIn("SATURATED", planes)
169 cr = data.getPlaneBitMask("COSMIC_RAY")
170 self.assertTrue(((data.array & cr) != 0).all())
171 sat = data.getPlaneBitMask("SATURATED")
172 self.assertTrue(((data.array & sat) == 0).all())
174 def test_masked_image(self):
175 masked_image = _make_masked_image(with_projection=True)
176 data, wcs = normalize_lsst_images(masked_image, None)
177 self.assertIsInstance(data, afwImage.MaskedImageF)
178 self.assertEqual(data.getXY0(), lsst.geom.Point2I(200, 100))
179 np.testing.assert_array_equal(data.image.array, masked_image.image.array)
180 np.testing.assert_array_equal(data.variance.array, masked_image.variance.array)
181 self.assertIn("COSMIC_RAY", data.mask.getMaskPlaneDict())
182 cr = data.mask.getPlaneBitMask("COSMIC_RAY")
183 self.assertTrue(((data.mask.array & cr) != 0).all())
184 self.assertIsInstance(wcs, afwGeom.SkyWcs)
186 def test_masked_image_without_projection(self):
187 masked_image = _make_masked_image()
188 data, wcs = normalize_lsst_images(masked_image, None)
189 self.assertIsInstance(data, afwImage.MaskedImageF)
190 self.assertIsNone(wcs)
192 def test_unsupported_generalized_image(self):
193 """Unsupported subclasses pass through for Display to reject."""
194 unsupported = _UnsupportedGeneralizedImage()
195 data, wcs = normalize_lsst_images(unsupported, None)
196 self.assertIs(data, unsupported)
197 self.assertIsNone(wcs)
200@unittest.skipUnless(HAVE_LSST_IMAGES, "lsst.images is not available")
201class DisplayLsstImagesTestCase(lsst.utils.tests.TestCase):
202 """End-to-end mtv tests through the virtualDevice backend."""
204 def setUp(self):
205 afwDisplay.setDefaultBackend("virtualDevice")
206 afwDisplay.delAllDisplays()
207 self.display = afwDisplay.Display(frame=0, verbose=True)
209 def tearDown(self):
210 afwDisplay.delAllDisplays()
212 def test_mtv_image(self):
213 self.display.mtv(_make_image(), title="image")
214 self.assertEqual(self.display._xy0, lsst.geom.Point2I(200, 100))
216 def test_mtv_mask(self):
217 self.display.mtv(_make_mask(), title="mask")
218 self.assertEqual(self.display._xy0, lsst.geom.Point2I(200, 100))
220 def test_mtv_masked_image(self):
221 self.display.mtv(_make_masked_image(with_projection=True), title="masked image")
222 self.assertEqual(self.display._xy0, lsst.geom.Point2I(200, 100))
224 def test_mtv_wcs_conflict(self):
225 masked_image = _make_masked_image(with_projection=True)
226 with self.assertRaises(RuntimeError):
227 self.display.mtv(masked_image, wcs=_make_legacy_wcs())
229 def test_mtv_unsupported(self):
230 with self.assertRaises(TypeError):
231 self.display.mtv(_UnsupportedGeneralizedImage())
233 def test_default_mask_plane_colors(self):
234 """Renamed planes keep their traditional colors."""
235 self.assertEqual(self.display.getMaskPlaneColor("COSMIC_RAY"), afwDisplay.MAGENTA)
236 self.assertEqual(self.display.getMaskPlaneColor("DETECTION_EDGE"), afwDisplay.YELLOW)
239class TestMemory(lsst.utils.tests.MemoryTestCase):
240 pass
243def setup_module(module):
244 lsst.utils.tests.init()
247if __name__ == "__main__": 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true
248 lsst.utils.tests.init()
249 unittest.main()