Coverage for tests / test_display.py: 25%

162 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-23 01:26 -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""" 

23Tests for displaying devices 

24 

25Run with: 

26 python test_display.py [backend] 

27""" 

28import os 

29import subprocess 

30import sys 

31import tempfile 

32import unittest 

33 

34import lsst.utils.tests 

35import lsst.afw.image as afwImage 

36import lsst.afw.display as afwDisplay 

37import lsst.geom 

38from lsst.daf.base import PropertyList 

39 

40try: 

41 type(backend) 

42except NameError: 

43 backend = "virtualDevice" 

44 oldBackend = None 

45 

46TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

47 

48 

49class DisplayTestCase(unittest.TestCase): 

50 """A test case for Display""" 

51 

52 def setUp(self): 

53 global oldBackend 

54 if backend != oldBackend: 

55 afwDisplay.setDefaultBackend(backend) 

56 afwDisplay.delAllDisplays() # as some may use the old backend 

57 

58 oldBackend = backend 

59 

60 self.fileName = os.path.join(TESTDIR, "data", "HSC-0908120-056-small.fits") 

61 self.display0 = afwDisplay.Display(frame=0, verbose=True) 

62 

63 def testMtv(self): 

64 """Test basic image display""" 

65 exp = afwImage.ExposureF(self.fileName) 

66 self.display0.mtv(exp, title="parent") 

67 

68 def testMaskPlanes(self): 

69 """Test basic image display""" 

70 self.display0.setMaskTransparency(50) 

71 self.display0.setMaskPlaneColor("CROSSTALK", "orange") 

72 

73 def testWith(self): 

74 """Test using displays with with statement""" 

75 with afwDisplay.Display(0) as disp: 

76 self.assertIsNotNone(disp) 

77 

78 def testTwoDisplays(self): 

79 """Test that we can do things with two frames""" 

80 

81 exp = afwImage.ExposureF(self.fileName) 

82 

83 for frame in (0, 1): 

84 with afwDisplay.Display(frame, verbose=False) as disp: 

85 disp.setMaskTransparency(50) 

86 

87 if frame == 1: 

88 disp.setMaskPlaneColor("CROSSTALK", "ignore") 

89 disp.mtv(exp, title="parent") 

90 

91 disp.erase() 

92 disp.dot('o', 205, 180, size=6, ctype=afwDisplay.RED) 

93 

94 def testZoomPan(self): 

95 self.display0.pan(205, 180) 

96 self.display0.zoom(4) 

97 

98 afwDisplay.Display(1).zoom(4, 205, 180) 

99 

100 def testStackingOrder(self): 

101 """ Un-iconise and raise the display to the top of the stacking order if appropriate""" 

102 self.display0.show() 

103 

104 def testDrawing(self): 

105 """Test drawing lines and glyphs""" 

106 self.display0.erase() 

107 

108 exp = afwImage.ExposureF(self.fileName) 

109 # tells display0 about the image's xy0 

110 self.display0.mtv(exp, title="parent") 

111 

112 with self.display0.Buffering(): 

113 self.display0.dot('o', 200, 220) 

114 vertices = [(200, 220), (210, 230), (224, 230), 

115 (214, 220), (200, 220)] 

116 self.display0.line(vertices, ctype=afwDisplay.CYAN) 

117 self.display0.line(vertices[:-1], symbs="+x+x", size=3) 

118 

119 def testStretch(self): 

120 """Test playing with the lookup table""" 

121 self.display0.show() 

122 

123 self.display0.scale("linear", "zscale") 

124 

125 def testMaskColorGeneration(self): 

126 """Demonstrate the utility routine to generate mask plane colours 

127 (used by e.g. the ds9 implementation of _mtv)""" 

128 

129 colorGenerator = self.display0.maskColorGenerator(omitBW=True) 

130 for i in range(10): 

131 print(i, next(colorGenerator), end=' ') 

132 print() 

133 

134 def testImageTypes(self): 

135 """Check that we can display a range of types of image""" 

136 with afwDisplay.Display("dummy", "virtualDevice") as dummy: 

137 for imageType in [afwImage.DecoratedImageF, 

138 afwImage.ExposureF, 

139 afwImage.ImageF, 

140 afwImage.MaskedImageF, 

141 ]: 

142 im = imageType(self.fileName) 

143 dummy.mtv(im) 

144 

145 for imageType in [afwImage.ImageU, afwImage.ImageI]: 

146 im = imageType(self.fileName, hdu=2, allowUnsafe=True) 

147 dummy.mtv(im) 

148 

149 im = afwImage.Mask(self.fileName, hdu=2) 

150 dummy.mtv(im) 

151 

152 def testInteract(self): 

153 r"""Check that interact exits when a q, \c CR, or \c ESC is pressed, or if a callback function 

154 returns a ``True`` value. 

155 If this is run using the virtualDevice a "q" is automatically triggered. 

156 If running the tests using ds9 you will be expected to do this manually. 

157 """ 

158 print("Hit q to exit interactive mode") 

159 self.display0.interact() 

160 

161 def testGetMaskPlaneColor(self): 

162 """Test that we can return mask colours either as a dict or maskplane by maskplane 

163 """ 

164 mpc = self.display0.getMaskPlaneColor() 

165 

166 maskPlane = 'DETECTED' 

167 self.assertEqual(mpc[maskPlane], self.display0.getMaskPlaneColor(maskPlane)) 

168 

169 def testSetDefaultImageColormap(self): 

170 """Test that we can set the default colourmap 

171 """ 

172 self.display0.setDefaultImageColormap("gray") 

173 

174 def testSetImageColormap(self): 

175 """Test that we can set a colourmap 

176 """ 

177 self.display0.setImageColormap("gray") 

178 

179 def testClose(self): 

180 """Test that we can close devices.""" 

181 self.display0.close() 

182 

183 def tearDown(self): 

184 for d in self.display0._displays.values(): 

185 d.verbose = False # ensure that display9.close() call is quiet 

186 

187 del self.display0 

188 afwDisplay.delAllDisplays() 

189 

190 

191class TestFitsWriting(lsst.utils.tests.TestCase): 

192 """Test the FITS file writing used internally by afwDisplay.""" 

193 

194 def setUp(self): 

195 self.fileName = os.path.join(TESTDIR, "data", "HSC-0908120-056-small.fits") 

196 self.exposure = afwImage.ExposureF(self.fileName) 

197 self.unit = "nJy" 

198 self.exposure.metadata["BUNIT"] = self.unit 

199 

200 def read_image(self, filename) -> tuple[afwImage.Image, PropertyList]: 

201 reader = afwImage.ImageFitsReader(filename) 

202 return reader.read(), reader.readMetadata() 

203 

204 def read_mask(self, filename) -> tuple[afwImage.Mask, PropertyList]: 

205 reader = afwImage.MaskFitsReader(filename) 

206 return reader.read(), reader.readMetadata() 

207 

208 def assertFitsEqual( 

209 self, 

210 fits_file: str, 

211 data: afwImage.Image | afwImage.Mask, 

212 wcs: lsst.afw.geom.SkyWcs | None, 

213 title: str | None, 

214 metadata: lsst.daf.base.PropertyList | None, 

215 unit: str | None, 

216 ): 

217 """Compare FITS file with parameters given to writeFitsImage.""" 

218 if isinstance(data, afwImage.Image): 

219 new_data, new_metadata = self.read_image(fits_file) 

220 else: 

221 new_data, new_metadata = self.read_mask(fits_file) 

222 self.assertImagesEqual(new_data, data) 

223 if metadata and "BUNIT" in metadata: 

224 self.assertEqual(new_metadata["BUNIT"], metadata["BUNIT"]) 

225 if metadata and unit: 

226 self.assertEqual(new_metadata["BUNIT"], unit) 

227 if title: 

228 self.assertEqual(new_metadata["OBJECT"], title) 

229 if wcs: 

230 # WCS needs to be shifted back to same reference. 

231 bbox = lsst.geom.Box2D(lsst.geom.Point2D(-100, -100), lsst.geom.Extent2D(300, 300)) 

232 new_wcs = lsst.afw.geom.makeSkyWcs(new_metadata, strip=False) 

233 shift = lsst.geom.Extent2D(data.getX0(), data.getY0()) 

234 unshifted_wcs = new_wcs.copyAtShiftedPixelOrigin(shift) 

235 self.assertWcsAlmostEqualOverBBox(unshifted_wcs, wcs, bbox) 

236 

237 def test_named_file(self): 

238 """Write to a named file.""" 

239 with tempfile.TemporaryDirectory() as tempdir: 

240 filename = os.path.join(tempdir, "test.fits") 

241 for data, wcs, title, metadata in ( 

242 (self.exposure.image, self.exposure.wcs, "Write to file", self.exposure.metadata), 

243 (self.exposure.mask, self.exposure.wcs, "Mask to file", self.exposure.metadata), 

244 (self.exposure.image, None, "Image to file", self.exposure.metadata), 

245 (self.exposure.image, None, None, self.exposure.metadata), 

246 (self.exposure.image, None, None, None), 

247 ): 

248 afwDisplay.writeFitsImage(filename, data, wcs, title, metadata) 

249 self.assertFitsEqual(filename, data, wcs, title, metadata, self.unit) 

250 

251 def test_file_handle(self): 

252 """Write to a file handle. 

253 

254 This is how firefly uses afwDisplay. 

255 """ 

256 with tempfile.NamedTemporaryFile(suffix=".fits") as tmp: 

257 afwDisplay.writeFitsImage( 

258 tmp, self.exposure.image, self.exposure.wcs, "filehdl", self.exposure.metadata 

259 ) 

260 tmp.flush() 

261 tmp.seek(0) 

262 self.assertFitsEqual( 

263 tmp.name, self.exposure.image, self.exposure.wcs, "filehdl", self.exposure.metadata, self.unit 

264 ) 

265 

266 def test_fileno(self): 

267 """Write to a file descriptor. 

268 

269 This is the way that the C++ interface worked. 

270 """ 

271 with tempfile.TemporaryDirectory() as tempdir: 

272 filename = os.path.join(tempdir, "test.fits") 

273 with open(filename, "wb") as fh: 

274 afwDisplay.writeFitsImage( 

275 fh.fileno(), self.exposure.image, self.exposure.wcs, "fileno", self.exposure.metadata 

276 ) 

277 self.assertFitsEqual( 

278 filename, self.exposure.image, self.exposure.wcs, "fileno", self.exposure.metadata, self.unit 

279 ) 

280 

281 def test_subprocess(self): 

282 """Write through a pipe. 

283 

284 This is how display_ds9 works. 

285 """ 

286 with tempfile.TemporaryDirectory() as tempdir: 

287 filename = os.path.join(tempdir, "test.fits") 

288 with subprocess.Popen( 

289 [ 

290 sys.executable, 

291 "-c", 

292 # Minimal command that reads from stdin and writes to the 

293 # named file. 

294 "import sys; fh = open(sys.argv[1], 'wb'); fh.write(sys.stdin.buffer.read()); fh.close()", 

295 filename, 

296 ], 

297 stdin=subprocess.PIPE 

298 ) as pipe: 

299 afwDisplay.writeFitsImage( 

300 pipe, self.exposure.image, self.exposure.wcs, "pipe", self.exposure.metadata 

301 ) 

302 self.assertFitsEqual( 

303 filename, self.exposure.image, self.exposure.wcs, "pipe", self.exposure.metadata, self.unit 

304 ) 

305 

306 

307class MemoryTester(lsst.utils.tests.MemoryTestCase): 

308 pass 

309 

310 

311def setup_module(module): 

312 lsst.utils.tests.init() 

313 

314 

315if __name__ == "__main__": 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true

316 import argparse 

317 

318 parser = argparse.ArgumentParser( 

319 description="Run the image display test suite") 

320 

321 parser.add_argument('backend', type=str, nargs="?", default="virtualDevice", 

322 help="The backend to use, e.g. ds9. You may need to have the device setup") 

323 args = parser.parse_args() 

324 

325 # check that that backend is valid 

326 with afwDisplay.Display("test", backend=args.backend) as disp: 

327 pass 

328 

329 backend = args.backend # backend is just a variable in this file 

330 lsst.utils.tests.init() 

331 del sys.argv[1:] 

332 unittest.main()