Coverage for tests / test_1079.py: 18%
137 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 01:50 -0700
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 01:50 -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# test1079
23# @brief Test that the wcs of sub-images are written and read from disk correctly
24# $Id$
25# @author Fergal Mullally
27import os.path
28import unittest
29import numbers
31import lsst.utils
32import lsst.geom
33import lsst.afw.image as afwImage
34from lsst.afw.fits import readMetadata
35import lsst.utils.tests
36import lsst.pex.exceptions
37import lsst.afw.display as afwDisplay
39try:
40 type(display)
41except NameError:
42 display = False
44TESTDIR = os.path.abspath(os.path.dirname(__file__))
47class SavingSubImagesTest(unittest.TestCase):
48 """Tests for changes made for ticket #1079.
50 In the LSST wcs transformations are done in terms of pixel position, which
51 is measured from the lower left hand corner of the parent image from which
52 this sub-image is drawn. However, when saving a sub-image to disk, the fits
53 standards has no concept of parent- and sub- images, and specifies that the
54 wcs is measured relative to the pixel index (i.e. the lower left hand corner
55 of the sub-image). This test makes sure we're saving and reading wcs headers
56 from sub-images correctly.
57 """
59 def setUp(self):
60 self.parentFile = os.path.join(TESTDIR, "data", "parent.fits")
62 self.parent = afwImage.ExposureF(self.parentFile)
63 self.llcParent = self.parent.getMaskedImage().getXY0()
64 self.oParent = self.parent.getWcs().getPixelOrigin()
66 # A list of pixel positions to test
67 self.testPositions = []
68 self.testPositions.append(lsst.geom.Point2D(128, 128))
69 self.testPositions.append(lsst.geom.Point2D(0, 0))
70 self.testPositions.append(lsst.geom.Point2D(20, 30))
71 self.testPositions.append(lsst.geom.Point2D(60, 50))
72 self.testPositions.append(lsst.geom.Point2D(80, 80))
73 self.testPositions.append(lsst.geom.Point2D(255, 255))
75 self.parent.getMaskedImage().set(0)
76 for p in self.testPositions:
77 self.parent.image[lsst.geom.Point2I(p), afwImage.LOCAL] = 10 + p[0]
79 def tearDown(self):
80 del self.parent
81 del self.oParent
82 del self.testPositions
84 def testInvarianceOfCrpix1(self):
85 """Test that crpix is the same for parent and sub-image.
87 Also tests that llc of sub-image saved correctly.
88 """
89 llc = lsst.geom.Point2I(20, 30)
90 bbox = lsst.geom.Box2I(llc, lsst.geom.Extent2I(60, 50))
91 subImg = afwImage.ExposureF(self.parent, bbox, afwImage.LOCAL)
93 subImgLlc = subImg.getMaskedImage().getXY0()
94 oSubImage = subImg.getWcs().getPixelOrigin()
96 # Useful for debugging
97 if False:
98 print(self.parent.getMaskedImage().getXY0())
99 print(subImg.getMaskedImage().getXY0())
100 print(self.oParent, oSubImage)
102 for i in range(2):
103 self.assertEqual(llc[i], subImgLlc[i], "Corner of sub-image not correct")
104 self.assertAlmostEqual(
105 self.oParent[i], oSubImage[i], 6, "Crpix of sub-image not correct")
107 def testInvarianceOfCrpix2(self):
108 """For sub-images loaded from disk, test that crpix is the same for parent and sub-image.
110 Also tests that llc of sub-image saved correctly.
111 """
112 # Load sub-image directly off of disk
113 llc = lsst.geom.Point2I(20, 30)
114 bbox = lsst.geom.Box2I(llc, lsst.geom.Extent2I(60, 50))
115 subImg = afwImage.ExposureF(self.parentFile, bbox, afwImage.LOCAL)
116 oSubImage = subImg.getWcs().getPixelOrigin()
117 subImgLlc = subImg.getMaskedImage().getXY0()
119 # Useful for debugging
120 if False:
121 print(self.parent.getMaskedImage().getXY0())
122 print(subImg.getMaskedImage().getXY0())
123 print(self.oParent, oSubImage)
125 for i in range(2):
126 self.assertEqual(llc[i], subImgLlc[i], "Corner of sub-image not correct")
127 self.assertAlmostEqual(
128 self.oParent[i], oSubImage[i], 6, "Crpix of sub-image not correct")
130 def testInvarianceOfPixelToSky(self):
131 for deep in (True, False):
132 llc = lsst.geom.Point2I(20, 30)
133 bbox = lsst.geom.Box2I(llc, lsst.geom.Extent2I(60, 50))
134 subImg = afwImage.ExposureF(self.parent, bbox, afwImage.LOCAL, deep)
136 xy0 = subImg.getMaskedImage().getXY0()
138 if display:
139 afwDisplay.Display(frame=0).mtv(self.parent, title=self._testMethodName + ": parent")
140 afwDisplay.Display(frame=1).mtv(subImg, title=self._testMethodName + ": subImg")
142 for p in self.testPositions:
143 subP = p - lsst.geom.Extent2D(llc[0], llc[1]) # pixel in subImg
145 if subP[0] < 0 or subP[0] >= bbox.getWidth() or subP[1] < 0 or subP[1] >= bbox.getHeight():
146 continue
148 adParent = self.parent.getWcs().pixelToSky(p)
149 adSub = subImg.getWcs().pixelToSky(subP + lsst.geom.Extent2D(xy0[0], xy0[1]))
150 #
151 # Check that we're talking about the same pixel
152 #
153 self.assertEqual(self.parent.maskedImage[lsst.geom.Point2I(p), afwImage.LOCAL],
154 subImg.maskedImage[lsst.geom.Point2I(subP), afwImage.LOCAL])
156 self.assertEqual(adParent[0], adSub[0], f"RAs are equal; deep = {deep}")
157 self.assertEqual(adParent[1], adSub[1], f"DECs are equal; deep = {deep}")
159 def testSubSubImage(self):
160 """Check that a sub-image of a sub-image is equivalent to a sub image.
162 I.e. that the parent is an invarient.
163 """
165 llc1 = lsst.geom.Point2I(20, 30)
166 bbox = lsst.geom.Box2I(llc1, lsst.geom.Extent2I(60, 50))
167 subImg = afwImage.ExposureF(self.parentFile, bbox, afwImage.LOCAL)
169 llc2 = lsst.geom.Point2I(22, 23)
171 # This subsub image should fail. Although it's big enough to fit in the parent image
172 # it's too small for the sub-image
173 bbox = lsst.geom.Box2I(llc2, lsst.geom.Extent2I(100, 110))
174 self.assertRaises(lsst.pex.exceptions.Exception,
175 afwImage.ExposureF, subImg, bbox, afwImage.LOCAL)
177 bbox = lsst.geom.Box2I(llc2, lsst.geom.Extent2I(10, 11))
178 subSubImg = afwImage.ExposureF(subImg, bbox, afwImage.LOCAL)
180 sub0 = subImg.getMaskedImage().getXY0()
181 subsub0 = subSubImg.getMaskedImage().getXY0()
183 if False:
184 print(sub0)
185 print(subsub0)
187 for i in range(2):
188 self.assertEqual(llc1[i], sub0[i], "XY0 don't match (1)")
189 self.assertEqual(llc1[i] + llc2[i], subsub0[i], "XY0 don't match (2)")
191 subCrpix = subImg.getWcs().getPixelOrigin()
192 subsubCrpix = subSubImg.getWcs().getPixelOrigin()
194 for i in range(2):
195 self.assertAlmostEqual(
196 subCrpix[i], subsubCrpix[i], 6, "crpix don't match")
198 def testRoundTrip(self):
199 """Test that saving and retrieving an image doesn't alter the metadata.
200 """
201 llc = lsst.geom.Point2I(20, 30)
202 bbox = lsst.geom.Box2I(llc, lsst.geom.Extent2I(60, 50))
203 for deep in (False, True):
204 subImg = afwImage.ExposureF(self.parent, bbox, afwImage.LOCAL, deep)
206 with lsst.utils.tests.getTempFilePath(f"_{deep}.fits") as outFile:
207 subImg.writeFits(outFile)
208 newImg = afwImage.ExposureF(outFile)
210 subXY0 = subImg.getMaskedImage().getXY0()
211 newXY0 = newImg.getMaskedImage().getXY0()
213 self.parent.getWcs().getPixelOrigin()
214 subCrpix = subImg.getWcs().getPixelOrigin()
215 newCrpix = newImg.getWcs().getPixelOrigin()
217 for i in range(2):
218 self.assertEqual(
219 subXY0[i], newXY0[i], f"Origin has changed; deep = {deep}")
220 self.assertAlmostEqual(
221 subCrpix[i], newCrpix[i], 6, f"crpix has changed; deep = {deep}")
223 def testFitsHeader(self):
224 """Test that XY0 and crpix are written to the header as expected.
225 """
226 # getPixelOrigin() returns origin in lsst coordinates, so need to add 1 to
227 # compare to values stored in fits headers
228 parentCrpix = self.parent.getWcs().getPixelOrigin()
230 # Make a sub-image
231 x0, y0 = 20, 30
232 llc = lsst.geom.Point2I(x0, y0)
233 bbox = lsst.geom.Box2I(llc, lsst.geom.Extent2I(60, 50))
234 deep = False
235 subImg = afwImage.ExposureF(self.parent, bbox, afwImage.LOCAL, deep)
237 with lsst.utils.tests.getTempFilePath(".fits") as outFile:
238 subImg.writeFits(outFile)
239 hdr = readMetadata(outFile)
241 def checkLtvHeader(hdr, name, value):
242 # Per DM-4133, LTVn headers are required to be floating point
243 self.assertTrue(hdr.exists(name), name + " not saved to FITS header")
244 self.assertIsInstance(
245 hdr.getScalar(name), numbers.Real, name + " is not numeric")
246 self.assertNotIsInstance(
247 hdr.getScalar(name), numbers.Integral, name + " is an int")
248 self.assertEqual(hdr.getScalar(name), value, name + " has wrong value")
250 checkLtvHeader(hdr, "LTV1", -1*x0)
251 checkLtvHeader(hdr, "LTV2", -1*y0)
253 self.assertTrue(hdr.exists("CRPIX1"), "CRPIX1 not saved to fits header")
254 self.assertTrue(hdr.exists("CRPIX2"), "CRPIX2 not saved to fits header")
256 fitsCrpix = [hdr.getScalar("CRPIX1"), hdr.getScalar("CRPIX2")]
257 self.assertAlmostEqual(
258 fitsCrpix[0] - hdr.getScalar("LTV1"), parentCrpix[0] + 1, 6, "CRPIX1 saved wrong")
259 self.assertAlmostEqual(
260 fitsCrpix[1] - hdr.getScalar("LTV2"), parentCrpix[1] + 1, 6, "CRPIX2 saved wrong")
263class TestMemory(lsst.utils.tests.MemoryTestCase):
264 pass
267def setup_module(module):
268 lsst.utils.tests.init()
271if __name__ == "__main__": 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true
272 lsst.utils.tests.init()
273 unittest.main()