Coverage for tests/test_cloughTocher2DInterpolate.py: 20%
138 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-01-30 11:16 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-01-30 11:16 +0000
1# This file is part of meas_algorithms.
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/>.
23import unittest
25from typing import Iterable
26from itertools import product
27import numpy as np
29import lsst.utils.tests
30import lsst.geom
31import lsst.afw.image as afwImage
32from lsst.meas.algorithms.cloughTocher2DInterpolator import (
33 CloughTocher2DInterpolateTask,
34)
35from lsst.meas.algorithms import CloughTocher2DInterpolatorUtils as ctUtils
38class CloughTocher2DInterpolateTestCase(lsst.utils.tests.TestCase):
39 """Test the CloughTocher2DInterpolateTask."""
41 def setUp(self):
42 super().setUp()
44 self.maskedimage = afwImage.MaskedImageF(100, 121)
45 for x in range(100):
46 for y in range(121):
47 self.maskedimage[x, y] = (3 * y + x * 5, 0, 1.0)
49 # Clone the maskedimage so we can compare it after running the task.
50 self.reference = self.maskedimage.clone()
52 # Set some central pixels as SAT
53 sliceX, sliceY = slice(30, 35), slice(40, 45)
54 self.maskedimage.mask[sliceX, sliceY] = afwImage.Mask.getPlaneBitMask("SAT")
55 self.maskedimage.image[sliceX, sliceY] = np.nan
56 # Put nans here to make sure interp is done ok
58 # Set an entire column as BAD
59 self.maskedimage.mask[54:55, :] = afwImage.Mask.getPlaneBitMask("BAD")
60 self.maskedimage.image[54:55, :] = np.nan
62 # Set an entire row as BAD
63 self.maskedimage.mask[:, 110:111] = afwImage.Mask.getPlaneBitMask("BAD")
64 self.maskedimage.image[:, 110:111] = np.nan
66 # Set a diagonal set of pixels as CR
67 for i in range(74, 78):
68 self.maskedimage.mask[i, i] = afwImage.Mask.getPlaneBitMask("CR")
69 self.maskedimage.image[i, i] = np.nan
71 # Set one of the edges as EDGE
72 self.maskedimage.mask[0:1, :] = afwImage.Mask.getPlaneBitMask("EDGE")
73 self.maskedimage.image[0:1, :] = np.nan
75 # Set a smaller streak at the edge
76 self.maskedimage.mask[25:28, 0:1] = afwImage.Mask.getPlaneBitMask("EDGE")
77 self.maskedimage.image[25:28, 0:1] = np.nan
79 # Update the reference image's mask alone, so we can compare them after
80 # running the task.
81 self.reference.mask.array[:, :] = self.maskedimage.mask.array
83 # Create a noise image
84 self.noise = self.maskedimage.clone()
85 np.random.seed(12345)
86 self.noise.image.array[:, :] = np.random.normal(size=self.noise.image.array.shape)
88 @lsst.utils.tests.methodParameters(n_runs=(1, 2))
89 def test_interpolation(self, n_runs: int):
90 """Test that the interpolation is done correctly.
92 Parameters
93 ----------
94 n_runs : `int`
95 Number of times to run the task. Running the task more than once
96 should have no effect.
97 """
98 config = CloughTocher2DInterpolateTask.ConfigClass()
99 config.badMaskPlanes = (
100 "BAD",
101 "SAT",
102 "CR",
103 "EDGE",
104 )
105 config.fillValue = 0.5
106 task = CloughTocher2DInterpolateTask(config)
107 for n in range(n_runs):
108 task.run(self.maskedimage)
110 # Assert that the mask and the variance planes remain unchanged.
111 self.assertImagesEqual(self.maskedimage.variance, self.reference.variance)
112 self.assertMasksEqual(self.maskedimage.mask, self.reference.mask)
114 # Check that the long streak of bad pixels have been replaced with the
115 # fillValue, but not the short streak.
116 np.testing.assert_array_equal(self.maskedimage.image[0:1, :].array, config.fillValue)
117 with self.assertRaises(AssertionError):
118 np.testing.assert_array_equal(self.maskedimage.image[25:28, 0:1].array, config.fillValue)
120 # Check that interpolated pixels are close to the reference (original),
121 # and that none of them is still NaN.
122 self.assertTrue(np.isfinite(self.maskedimage.image.array).all())
123 self.assertImagesAlmostEqual(
124 self.maskedimage.image[1:, :],
125 self.reference.image[1:, :],
126 rtol=1e-05,
127 atol=1e-08,
128 )
130 @lsst.utils.tests.methodParametersProduct(pass_badpix=(True, False), pass_goodpix=(True, False))
131 def test_interpolation_with_noise(self, pass_badpix: bool = True, pass_goodpix: bool = True):
132 """Test that we can reuse the badpix and goodpix.
134 Parameters
135 ----------
136 pass_badpix : `bool`
137 Whether to pass the badpix to the task?
138 pass_goodpix : `bool`
139 Whether to pass the goodpix to the task?
140 """
142 config = CloughTocher2DInterpolateTask.ConfigClass()
143 config.badMaskPlanes = (
144 "BAD",
145 "SAT",
146 "CR",
147 "EDGE",
148 )
149 task = CloughTocher2DInterpolateTask(config)
151 badpix, goodpix = task.run(self.noise)
152 task.run(
153 self.maskedimage,
154 badpix=(badpix if pass_badpix else None),
155 goodpix=(goodpix if pass_goodpix else None),
156 )
158 # Check that the long streak of bad pixels by the edge have been
159 # replaced with fillValue, but not the short streak.
160 np.testing.assert_array_equal(self.maskedimage.image[0:1, :].array, config.fillValue)
161 with self.assertRaises(AssertionError):
162 np.testing.assert_array_equal(self.maskedimage.image[25:28, 0:1].array, config.fillValue)
164 # Check that interpolated pixels are close to the reference (original),
165 # and that none of them is still NaN.
166 self.assertTrue(np.isfinite(self.maskedimage.image.array).all())
167 self.assertImagesAlmostEqual(
168 self.maskedimage.image[1:, :],
169 self.reference.image[1:, :],
170 rtol=1e-05,
171 atol=1e-08,
172 )
175class CloughTocher2DInterpolatorUtilsTestCase(CloughTocher2DInterpolateTestCase):
176 """Test the CloughTocher2DInterpolatorUtils."""
178 @classmethod
179 def find_good_pixels_around_bad_pixels(
180 cls,
181 image: afwImage.MaskedImage,
182 maskPlanes: Iterable[str],
183 *,
184 max_window_extent: lsst.geom.Extent2I,
185 badpix: set | None = None,
186 goodpix: dict | None = None,
187 ):
188 """Find the location of bad pixels, and neighboring good pixels.
190 Parameters
191 ----------
192 image : `~lsst.afw.image.MaskedImage`
193 Image from which to find the bad and the good pixels.
194 maskPlanes : `list` [`str`]
195 List of mask planes to consider as bad pixels.
196 max_window_extent : `lsst.geom.Extent2I`
197 Maximum extent of the window around a bad pixel to consider when
198 looking for good pixels.
199 badpix : `list` [`tuple` [`int`, `int`]], optional
200 A known list of bad pixels. If provided, the function does not look for
201 any additional bad pixels, but it verifies that the provided
202 coordinates correspond to bad pixels. If an input``badpix`` is not
203 found to be bad as specified by ``maskPlanes``, an exception is raised.
204 goodpix : `dict` [`tuple` [`int`, `int`], `float`], optional
205 A known mapping of the coordinates of good pixels to their values, to
206 which any newly found good pixels locations will be added, and the
207 values (even for existing items) will be updated.
209 Returns
210 -------
211 badpix : `list` [`tuple` [`int`, `int`]]
212 The coordinates of the bad pixels. If ``badpix`` was provided as an
213 input argument, the returned quantity is the same as the input.
214 goodpix : `dict` [`tuple` [`int`, `int`], `float`]
215 Updated mapping of the coordinates of good pixels to their values.
217 Raises
218 ------
219 RuntimeError
220 If a pixel passed in as ``goodpix`` is found to be bad as specified by
221 ``maskPlanes``.
222 ValueError
223 If an input ``badpix`` is not found to be bad as specified by
224 ``maskPlanes``.
225 """
227 bbox = image.getBBox()
228 if badpix is None:
229 iterator = product(range(bbox.minX, bbox.maxX + 1), range(bbox.minY, bbox.maxY + 1))
230 badpix = set()
231 else:
232 iterator = badpix
234 if goodpix is None:
235 goodpix = {}
237 for x, y in iterator:
238 if image.mask[x, y] & afwImage.Mask.getPlaneBitMask(maskPlanes):
239 if (x, y) in goodpix:
240 raise RuntimeError(
241 f"Pixel ({x}, {y}) is bad as specified by maskPlanes {maskPlanes} but "
242 "passed in as goodpix"
243 )
244 badpix.add((x, y))
245 window = lsst.geom.Box2I.makeCenteredBox(
246 center=lsst.geom.Point2D(x, y), # center has to be a Point2D instance.
247 size=max_window_extent,
248 )
249 # Restrict to the bounding box of the image.
250 window.clip(bbox)
252 for xx, yy in product(
253 range(window.minX, window.maxX + 1),
254 range(window.minY, window.maxY + 1),
255 ):
256 if not (image.mask[xx, yy] & afwImage.Mask.getPlaneBitMask(maskPlanes)):
257 goodpix[(xx, yy)] = image.image[xx, yy]
258 elif (x, y) in badpix:
259 # If (x, y) is in badpix, but did not get flagged as bad,
260 # raise an exception.
261 raise ValueError(f"Pixel ({x}, {y}) is not bad as specified by maskPlanes {maskPlanes}")
263 return badpix, goodpix
265 def test_parity(self, buffer=4):
266 """Test that the C++ implementation gives the same results as the
267 pure-Python implementation.
269 Parameters
270 ----------
271 buffer : `int`, optional
272 Same as the buffer parameter in `findGoodPixelsAroundBadPixels`.
273 """
274 bpix, gpix = ctUtils.findGoodPixelsAroundBadPixels(
275 self.maskedimage, ["BAD", "SAT", "CR", "EDGE"], buffer=buffer
276 )
277 badpix, goodpix = self.find_good_pixels_around_bad_pixels(
278 self.maskedimage,
279 ["BAD", "SAT", "CR", "EDGE"],
280 max_window_extent=lsst.geom.Extent2I(2 * buffer + 1, 2 * buffer + 1),
281 )
283 self.assertEqual(len(goodpix), gpix.shape[0])
284 for row in gpix:
285 x, y, val = int(row[0]), int(row[1]), row[2]
286 self.assertEqual(goodpix[(x, y)], val)
288 self.assertEqual(set(zip(bpix[:, 0], bpix[:, 1])), badpix)
290 def test_findGoodPixelsAroundBadPixels(self):
291 """Test the findGoodPixelsAroundBadPixels utility functino."""
292 badpix, goodpix = ctUtils.findGoodPixelsAroundBadPixels(
293 self.maskedimage,
294 ["BAD", "SAT", "CR", "EDGE"],
295 buffer=4,
296 )
298 # Check that badpix and goodpix have no overlaps
299 badSet = set(zip(badpix[:, 0], badpix[:, 1]))
300 goodSet = set(zip(goodpix[:, 0], goodpix[:, 1]))
301 self.assertEqual(len(badSet & goodSet), 0)
303 # buffer = 0 should give no goodpix, but same badpix
304 badpix0, goodpix0 = ctUtils.findGoodPixelsAroundBadPixels(
305 self.maskedimage,
306 ["BAD", "SAT", "CR", "EDGE"],
307 buffer=0,
308 )
310 self.assertEqual(len(goodpix0), 0)
311 np.testing.assert_array_equal(badpix0, badpix)
313 # For large enough buffer, badpix and goodpix should be mutually
314 # exclusive and complete. This also checks that edges are handled.
315 badpix, goodpix = ctUtils.findGoodPixelsAroundBadPixels(
316 self.maskedimage,
317 ["BAD", "SAT", "CR", "EDGE"],
318 buffer=251,
319 )
321 self.assertEqual(
322 len(badpix) + len(goodpix),
323 self.maskedimage.getWidth() * self.maskedimage.getHeight(),
324 )
326 def test_update_functions(self):
327 """Test updateArrayFromImage and updateImageFromArray behave as
328 expected.
329 """
330 badpix, _ = ctUtils.findGoodPixelsAroundBadPixels(
331 self.maskedimage,
332 ["BAD", "SAT", "CR", "EDGE"],
333 buffer=3,
334 )
336 # Ensure that maskedimage and reference are not the same initially.
337 with self.assertRaises(AssertionError):
338 self.assertImagesEqual(self.maskedimage.image, self.reference.image)
340 # Update badpix values from the reference image
341 ctUtils.updateArrayFromImage(badpix, self.reference.image)
343 # Update maskedimage from badpix values
344 ctUtils.updateImageFromArray(self.maskedimage.image, badpix)
346 # maskedimage and reference image should now to be identifical
347 self.assertImagesEqual(self.maskedimage.image, self.reference.image)
349 @lsst.utils.tests.methodParametersProduct(x0=(0, 23, -53), y0=(0, 47, -31))
350 def test_origin(self, x0=23, y0=47):
351 """Test that we get consistent results with arbitrary image origins.
353 Parameters
354 ----------
355 x0 : `int`
356 The origin of the image along the horizontal axis.
357 y0 : `int`
358 The origin of the image along the vertical axis.
359 """
360 # Calling setUp explicitly becomes necessary, as we change in the image
361 # in-place and need to reset to the original state when running with
362 # a different set of parameters.
363 self.setUp()
364 badpix0, goodpix0 = ctUtils.findGoodPixelsAroundBadPixels(
365 self.maskedimage,
366 ["BAD", "SAT", "CR", "EDGE"],
367 buffer=4,
368 )
370 # Check that badpix and goodpix have no overlaps
371 badSet = set(zip(badpix0[:, 0], badpix0[:, 1]))
372 goodSet = set(zip(goodpix0[:, 0], goodpix0[:, 1]))
373 self.assertEqual(len(badSet & goodSet), 0)
375 # Set a non-trivial xy0 for the maskedimage
376 self.maskedimage.setXY0(lsst.geom.Point2I(x0, y0))
377 badpix, goodpix = ctUtils.findGoodPixelsAroundBadPixels(
378 self.maskedimage,
379 ["BAD", "SAT", "CR", "EDGE"],
380 buffer=4,
381 )
383 # Adjust the x and y columns with origin, so we can compare them.
384 badpix0[:, 0] += x0
385 goodpix0[:, 0] += x0
386 badpix0[:, 1] += y0
387 goodpix0[:, 1] += y0
389 # The third column (pixel values) must match exactly if the
390 # corresponding pixel values are read, regardless of the coordinate.
391 np.testing.assert_array_equal(goodpix, goodpix0)
392 np.testing.assert_array_equal(badpix, badpix0)
394 # Update one of the goodpix arrays from image and check that it is
395 # invariant. It would be invariant if it handles the pixel coordinates
396 # consistently.
397 ctUtils.updateArrayFromImage(goodpix0, self.maskedimage.image)
398 np.testing.assert_array_equal(goodpix, goodpix0)
400 # There should be some nan values right now.
401 self.assertFalse(np.isfinite(self.maskedimage.image.array).all())
403 # There should not be any nan values if the image is updated correctly.
404 badpix[:, 2] = -99
405 ctUtils.updateImageFromArray(self.maskedimage.image, badpix)
406 self.assertTrue(np.isfinite(self.maskedimage.image.array).all())
409def setup_module(module):
410 lsst.utils.tests.init()
413class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
414 pass
417if __name__ == "__main__": 417 ↛ 418line 417 didn't jump to line 418, because the condition on line 417 was never true
418 lsst.utils.tests.init()
419 unittest.main()