Coverage for tests/test_display_images.py: 31%

143 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-08 01:42 -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/>. 

21 

22"""Tests for displaying lsst.images objects via lsst.afw.display.""" 

23import unittest 

24 

25import numpy as np 

26 

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 

33 

34try: 

35 import lsst.images 

36 import astropy.units as u 

37 HAVE_LSST_IMAGES = True 

38except ImportError: 

39 HAVE_LSST_IMAGES = False 

40 

41 

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.""" 

45 

46 @property 

47 def bbox(self): 

48 return lsst.images.Box.from_shape((1, 1)) 

49 

50 @property 

51 def sky_projection(self): 

52 return None 

53 

54 def __getitem__(self, bbox): 

55 return self 

56 

57 def copy(self): 

58 return self 

59 

60 

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 ) 

68 

69 

70def _make_sky_projection(bbox): 

71 """Return an lsst.images.SkyProjection covering bbox. 

72 

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) 

79 

80 

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 ) 

91 

92 

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 

104 

105 

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) 

111 

112 

113@unittest.skipUnless(HAVE_LSST_IMAGES, "lsst.images is not available") 

114class NormalizeLsstImagesTestCase(lsst.utils.tests.TestCase): 

115 """Direct tests of normalize_lsst_images.""" 

116 

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) 

124 

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) 

132 

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 ) 

143 

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) 

150 

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()) 

155 

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()) 

169 

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) 

181 

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) 

187 

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) 

194 

195 

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.""" 

199 

200 def setUp(self): 

201 afwDisplay.setDefaultBackend("virtualDevice") 

202 afwDisplay.delAllDisplays() 

203 self.display = afwDisplay.Display(frame=0, verbose=True) 

204 

205 def tearDown(self): 

206 afwDisplay.delAllDisplays() 

207 

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)) 

211 

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)) 

215 

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)) 

219 

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()) 

224 

225 def test_mtv_unsupported(self): 

226 with self.assertRaises(TypeError): 

227 self.display.mtv(_UnsupportedGeneralizedImage()) 

228 

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) 

233 

234 

235class TestMemory(lsst.utils.tests.MemoryTestCase): 

236 pass 

237 

238 

239def setup_module(module): 

240 lsst.utils.tests.init() 

241 

242 

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()