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

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 def __repr__(self): 

61 # GeneralizedImage's repr calls _describe, which is abstract. 

62 return "_UnsupportedGeneralizedImage()" 

63 

64 

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 ) 

72 

73 

74def _make_sky_projection(bbox): 

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

76 

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) 

83 

84 

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 ) 

95 

96 

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 

108 

109 

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) 

115 

116 

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

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

119 """Direct tests of normalize_lsst_images.""" 

120 

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) 

128 

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) 

136 

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 ) 

147 

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) 

154 

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

159 

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

173 

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) 

185 

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) 

191 

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) 

198 

199 

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

203 

204 def setUp(self): 

205 afwDisplay.setDefaultBackend("virtualDevice") 

206 afwDisplay.delAllDisplays() 

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

208 

209 def tearDown(self): 

210 afwDisplay.delAllDisplays() 

211 

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

215 

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

219 

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

223 

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

228 

229 def test_mtv_unsupported(self): 

230 with self.assertRaises(TypeError): 

231 self.display.mtv(_UnsupportedGeneralizedImage()) 

232 

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) 

237 

238 

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

240 pass 

241 

242 

243def setup_module(module): 

244 lsst.utils.tests.init() 

245 

246 

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