Skip to content

Commit 742401c

Browse files
authored
Merge pull request #779 from lsst/tickets/DM-49333-v29
DM-49333 (v29): Simplify afwDisplay and pass full FITS header to backend
2 parents 476fa81 + 9633b30 commit 742401c

File tree

9 files changed

+246
-631
lines changed

9 files changed

+246
-631
lines changed

python/lsst/afw/display/SConscript

-3
This file was deleted.

python/lsst/afw/display/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424
from .interface import *
2525
from .utils import Mosaic
2626
from .rgb import *
27-
from ._simpleFits import *
27+
from ._write_fits import *

python/lsst/afw/display/_simpleFits.cc

-72
This file was deleted.
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
from __future__ import annotations
23+
24+
__all__ = ["writeFitsImage"]
25+
26+
import io
27+
import os
28+
import subprocess
29+
30+
import lsst.afw.fits
31+
import lsst.afw.geom
32+
import lsst.afw.image
33+
from lsst.daf.base import PropertyList, PropertySet
34+
from lsst.geom import Extent2D
35+
36+
37+
def _add_wcs(wcs_name: str, ps: PropertyList, x0: int = 0, y0: int = 0) -> None:
38+
ps.setInt(f"CRVAL1{wcs_name}", x0, "(output) Column pixel of Reference Pixel")
39+
ps.setInt(f"CRVAL2{wcs_name}", y0, "(output) Row pixel of Reference Pixel")
40+
ps.setDouble(f"CRPIX1{wcs_name}", 1.0, "Column Pixel Coordinate of Reference")
41+
ps.setDouble(f"CRPIX2{wcs_name}", 1.0, "Row Pixel Coordinate of Reference")
42+
ps.setString(f"CTYPE1{wcs_name}", "LINEAR", "Type of projection")
43+
ps.setString(f"CTYPE1{wcs_name}", "LINEAR", "Type of projection")
44+
ps.setString(f"CUNIT1{wcs_name}", "PIXEL", "Column unit")
45+
ps.setString(f"CUNIT2{wcs_name}", "PIXEL", "Row unit")
46+
47+
48+
def writeFitsImage(
49+
file: str | int | io.BytesIO,
50+
data: lsst.afw.image.Image | lsst.afw.image.Mask,
51+
wcs: lsst.afw.geom.SkyWcs | None = None,
52+
title: str = "",
53+
metadata: PropertySet | None = None,
54+
) -> None:
55+
"""Write a simple FITS file with no extensions.
56+
57+
Parameters
58+
----------
59+
file : `str` or `int`
60+
Path to a file or a file descriptor.
61+
data : `lsst.afw.Image` or `lsst.afw.Mask`
62+
Data to be displayed.
63+
wcs : `lsst.afw.geom.SkyWcs` or `None`, optional
64+
WCS to be written to header to FITS file.
65+
title : `str`, optional
66+
If defined, the value to be stored in the ``OBJECT`` header.
67+
Overrides any value found in ``metadata``.
68+
metadata : `lsst.daf.base.PropertySet` or `None`, optional
69+
Additional information to be written to FITS header.
70+
"""
71+
ps = PropertyList()
72+
73+
# Seed with the external metadata, stripping wcs keywords.
74+
if metadata:
75+
lsst.afw.geom.stripWcsMetadata(metadata)
76+
ps.update(metadata)
77+
78+
# Write WcsB, so that pixel (0,0) is correctly labelled (but ignoring XY0)
79+
_add_wcs("B", ps)
80+
81+
if not wcs:
82+
_add_wcs("", ps) # Works around a ds9 bug that WCSA/B is ignored if no WCS is present.
83+
else:
84+
shift = Extent2D(-data.getX0(), -data.getY0())
85+
new_wcs = wcs.copyAtShiftedPixelOrigin(shift)
86+
wcs_metadata = new_wcs.getFitsMetadata()
87+
ps.update(wcs_metadata)
88+
89+
if title:
90+
ps.set("OBJECT", title, "Image being displayed")
91+
92+
if isinstance(file, str):
93+
data.writeFits(file, metadata=ps)
94+
else:
95+
mem = lsst.afw.fits.MemFileManager()
96+
data.writeFits(manager=mem, metadata=ps)
97+
if isinstance(file, int):
98+
# Duplicate to prevent a double close, assuming the caller
99+
# will close the file descriptor they passed in.
100+
with os.fdopen(os.dup(file), "wb") as fh:
101+
fh.write(mem.getData())
102+
elif isinstance(file, subprocess.Popen):
103+
file.communicate(input=mem.getData())
104+
else:
105+
# Try the write() method directly.
106+
file.write(mem.getData())

python/lsst/afw/display/interface.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ def __addMissingMaskPlanes(self, mask):
535535
if name not in self._defaultMaskPlaneColor:
536536
self.setDefaultMaskPlaneColor(name, next(colorGenerator))
537537

538-
def image(self, data, title="", wcs=None):
538+
def image(self, data, title="", wcs=None, metadata=None):
539539
"""Display an image on a display, with semi-transparent masks
540540
overlaid, if available.
541541
@@ -550,6 +550,8 @@ def image(self, data, title="", wcs=None):
550550
World Coordinate System to align an `~lsst.afw.image.MaskedImage`
551551
or `~lsst.afw.image.Image` to; raise an exception if ``data``
552552
is an `~lsst.afw.image.Exposure`.
553+
metadata : `lsst.daf.base.PropertySet`, optional
554+
Additional FITS metadata to be sent to display.
553555
554556
Raises
555557
------
@@ -568,7 +570,7 @@ def image(self, data, title="", wcs=None):
568570
if isinstance(data, afwImage.Exposure):
569571
if wcs:
570572
raise RuntimeError("You may not specify a wcs with an Exposure")
571-
data, wcs = data.getMaskedImage(), data.wcs
573+
data, wcs, metadata = data.getMaskedImage(), data.wcs, data.metadata
572574
# it's a DecoratedImage; display it
573575
elif isinstance(data, afwImage.DecoratedImage):
574576
try:
@@ -580,21 +582,21 @@ def image(self, data, title="", wcs=None):
580582
self._xy0 = data.getXY0() # DecoratedImage doesn't have getXY0()
581583

582584
if isinstance(data, afwImage.Image): # it's an Image; display it
583-
self._impl._mtv(data, None, wcs, title)
585+
self._impl._mtv(data, None, wcs, title, metadata)
584586
# It's a Mask; display it, bitplane by bitplane.
585587
elif isinstance(data, afwImage.Mask):
586588
self.__addMissingMaskPlanes(data)
587589
# Some displays can't display a Mask without an image; so display
588590
# an Image too, with pixel values set to the mask.
589-
self._impl._mtv(afwImage.ImageI(data.array), data, wcs, title)
591+
self._impl._mtv(afwImage.ImageI(data.array), data, wcs, title, metadata)
590592
# It's a MaskedImage; display Image and overlay Mask.
591593
elif isinstance(data, afwImage.MaskedImage):
592594
self.__addMissingMaskPlanes(data.mask)
593-
self._impl._mtv(data.image, data.mask, wcs, title)
595+
self._impl._mtv(data.image, data.mask, wcs, title, metadata)
594596
else:
595597
raise TypeError(f"Unsupported type {data!r}")
596598

597-
def mtv(self, data, title="", wcs=None):
599+
def mtv(self, data, title="", wcs=None, metadata=None):
598600
"""Display an image on a display, with semi-transparent masks
599601
overlaid, if available.
600602
@@ -603,7 +605,7 @@ def mtv(self, data, title="", wcs=None):
603605
Historical note: the name "mtv" comes from Jim Gunn's forth imageprocessing
604606
system, Mirella (named after Mirella Freni); The "m" stands for Mirella.
605607
"""
606-
self.image(data, title, wcs)
608+
self.image(data, title, wcs, metadata)
607609

608610
class _Buffering:
609611
"""Context manager for buffering repeated display commands.

0 commit comments

Comments
 (0)