Coverage for tests / test_display.py: 25%
162 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 01:29 -0700
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 01:29 -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"""
23Tests for displaying devices
25Run with:
26 python test_display.py [backend]
27"""
28import os
29import subprocess
30import sys
31import tempfile
32import unittest
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
40try:
41 type(backend)
42except NameError:
43 backend = "virtualDevice"
44 oldBackend = None
46TESTDIR = os.path.abspath(os.path.dirname(__file__))
49class DisplayTestCase(unittest.TestCase):
50 """A test case for Display"""
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
58 oldBackend = backend
60 self.fileName = os.path.join(TESTDIR, "data", "HSC-0908120-056-small.fits")
61 self.display0 = afwDisplay.Display(frame=0, verbose=True)
63 def testMtv(self):
64 """Test basic image display"""
65 exp = afwImage.ExposureF(self.fileName)
66 self.display0.mtv(exp, title="parent")
68 def testMaskPlanes(self):
69 """Test basic image display"""
70 self.display0.setMaskTransparency(50)
71 self.display0.setMaskPlaneColor("CROSSTALK", "orange")
73 def testWith(self):
74 """Test using displays with with statement"""
75 with afwDisplay.Display(0) as disp:
76 self.assertIsNotNone(disp)
78 def testTwoDisplays(self):
79 """Test that we can do things with two frames"""
81 exp = afwImage.ExposureF(self.fileName)
83 for frame in (0, 1):
84 with afwDisplay.Display(frame, verbose=False) as disp:
85 disp.setMaskTransparency(50)
87 if frame == 1:
88 disp.setMaskPlaneColor("CROSSTALK", "ignore")
89 disp.mtv(exp, title="parent")
91 disp.erase()
92 disp.dot('o', 205, 180, size=6, ctype=afwDisplay.RED)
94 def testZoomPan(self):
95 self.display0.pan(205, 180)
96 self.display0.zoom(4)
98 afwDisplay.Display(1).zoom(4, 205, 180)
100 def testStackingOrder(self):
101 """ Un-iconise and raise the display to the top of the stacking order if appropriate"""
102 self.display0.show()
104 def testDrawing(self):
105 """Test drawing lines and glyphs"""
106 self.display0.erase()
108 exp = afwImage.ExposureF(self.fileName)
109 # tells display0 about the image's xy0
110 self.display0.mtv(exp, title="parent")
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)
119 def testStretch(self):
120 """Test playing with the lookup table"""
121 self.display0.show()
123 self.display0.scale("linear", "zscale")
125 def testMaskColorGeneration(self):
126 """Demonstrate the utility routine to generate mask plane colours
127 (used by e.g. the ds9 implementation of _mtv)"""
129 colorGenerator = self.display0.maskColorGenerator(omitBW=True)
130 for i in range(10):
131 print(i, next(colorGenerator), end=' ')
132 print()
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)
145 for imageType in [afwImage.ImageU, afwImage.ImageI]:
146 im = imageType(self.fileName, hdu=2, allowUnsafe=True)
147 dummy.mtv(im)
149 im = afwImage.Mask(self.fileName, hdu=2)
150 dummy.mtv(im)
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()
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()
166 maskPlane = 'DETECTED'
167 self.assertEqual(mpc[maskPlane], self.display0.getMaskPlaneColor(maskPlane))
169 def testSetDefaultImageColormap(self):
170 """Test that we can set the default colourmap
171 """
172 self.display0.setDefaultImageColormap("gray")
174 def testSetImageColormap(self):
175 """Test that we can set a colourmap
176 """
177 self.display0.setImageColormap("gray")
179 def testClose(self):
180 """Test that we can close devices."""
181 self.display0.close()
183 def tearDown(self):
184 for d in self.display0._displays.values():
185 d.verbose = False # ensure that display9.close() call is quiet
187 del self.display0
188 afwDisplay.delAllDisplays()
191class TestFitsWriting(lsst.utils.tests.TestCase):
192 """Test the FITS file writing used internally by afwDisplay."""
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
200 def read_image(self, filename) -> tuple[afwImage.Image, PropertyList]:
201 reader = afwImage.ImageFitsReader(filename)
202 return reader.read(), reader.readMetadata()
204 def read_mask(self, filename) -> tuple[afwImage.Mask, PropertyList]:
205 reader = afwImage.MaskFitsReader(filename)
206 return reader.read(), reader.readMetadata()
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)
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)
251 def test_file_handle(self):
252 """Write to a file handle.
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 )
266 def test_fileno(self):
267 """Write to a file descriptor.
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 )
281 def test_subprocess(self):
282 """Write through a pipe.
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 )
307class MemoryTester(lsst.utils.tests.MemoryTestCase):
308 pass
311def setup_module(module):
312 lsst.utils.tests.init()
315if __name__ == "__main__": 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true
316 import argparse
318 parser = argparse.ArgumentParser(
319 description="Run the image display test suite")
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()
325 # check that that backend is valid
326 with afwDisplay.Display("test", backend=args.backend) as disp:
327 pass
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()