lsst.afw g2603b601e3+f394777a51
testUtils.py
Go to the documentation of this file.
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# the asserts are automatically imported so unit tests can find them without special imports;
23# the other functions are hidden unless explicitly asked for
24__all__ = ["assertImagesAlmostEqual", "assertImagesEqual", "assertMasksEqual",
25 "assertMaskedImagesAlmostEqual", "assertMaskedImagesEqual"]
26
27import numpy as np
28
30from .image import ImageF
31from .basicUtils import makeMaskedImageFromArrays
32
33
34def makeGaussianNoiseMaskedImage(dimensions, sigma, variance=1.0):
35 """Make a gaussian noise MaskedImageF
36
37 Inputs:
38 - dimensions: dimensions of output array (cols, rows)
39 - sigma; sigma of image plane's noise distribution
40 - variance: constant value for variance plane
41 """
42 npSize = (dimensions[1], dimensions[0])
43 image = np.random.normal(loc=0.0, scale=sigma,
44 size=npSize).astype(np.float32)
45 mask = np.zeros(npSize, dtype=np.int32)
46 variance = np.zeros(npSize, dtype=np.float32) + variance
47
48 return makeMaskedImageFromArrays(image, mask, variance)
49
50
51def makeRampImage(bbox, start=0, stop=None, imageClass=ImageF):
52 """!Make an image whose values are a linear ramp
53
54 @param[in] bbox bounding box of image (an lsst.geom.Box2I)
55 @param[in] start starting ramp value, inclusive
56 @param[in] stop ending ramp value, inclusive; if None, increase by integer values
57 @param[in] imageClass type of image (e.g. lsst.afw.image.ImageF)
58 """
59 im = imageClass(bbox)
60 imDim = im.getDimensions()
61 numPix = imDim[0]*imDim[1]
62 imArr = im.getArray()
63 if stop is None:
64 # increase by integer values
65 stop = start + numPix - 1
66 rampArr = np.linspace(start=start, stop=stop,
67 endpoint=True, num=numPix, dtype=imArr.dtype)
68 # numpy arrays are transposed w.r.t. afwImage
69 imArr[:] = np.reshape(rampArr, (imDim[1], imDim[0]))
70 return im
71
72
73@lsst.utils.tests.inTestCase
74def assertImagesAlmostEqual(testCase, image0, image1, skipMask=None,
75 rtol=1.0e-05, atol=1e-08, msg="Images differ"):
76 """!Assert that two images are almost equal, including non-finite values
77
78 @param[in] testCase unittest.TestCase instance the test is part of;
79 an object supporting one method: fail(self, msgStr)
80 @param[in] image0 image 0, an lsst.afw.image.Image, lsst.afw.image.Mask,
81 or transposed numpy array (see warning)
82 @param[in] image1 image 1, an lsst.afw.image.Image, lsst.afw.image.Mask,
83 or transposed numpy array (see warning)
84 @param[in] skipMask mask of pixels to skip, or None to compare all pixels;
85 an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array (see warning);
86 all non-zero pixels are skipped
87 @param[in] rtol maximum allowed relative tolerance; more info below
88 @param[in] atol maximum allowed absolute tolerance; more info below
89 @param[in] msg exception message prefix; details of the error are appended after ": "
90
91 The images are nearly equal if all pixels obey:
92 |val1 - val0| <= rtol*|val1| + atol
93 or, for float types, if nan/inf/-inf pixels match.
94
95 @warning the comparison equation is not symmetric, so in rare cases the assertion
96 may give different results depending on which image comes first.
97
98 @warning the axes of numpy arrays are transposed with respect to Image and Mask data.
99 Thus for example if image0 and image1 are both lsst.afw.image.ImageD with dimensions (2, 3)
100 and skipMask is a numpy array, then skipMask must have shape (3, 2).
101
102 @throw self.failureException (usually AssertionError) if any of the following are true
103 for un-skipped pixels:
104 - non-finite values differ in any way (e.g. one is "nan" and another is not)
105 - finite values differ by too much, as defined by atol and rtol
106
107 @throw TypeError if the dimensions of image0, image1 and skipMask do not match,
108 or any are not of a numeric data type.
109 """
110 errStr = imagesDiffer(
111 image0, image1, skipMask=skipMask, rtol=rtol, atol=atol)
112 if errStr:
113 testCase.fail(f"{msg}: {errStr}")
114
115
116@lsst.utils.tests.inTestCase
117def assertImagesEqual(*args, **kwds):
118 """!Assert that two images are exactly equal, including non-finite values.
119
120 All arguments are forwarded to assertAnglesAlmostEqual aside from atol and rtol,
121 which are set to zero.
122 """
123 return assertImagesAlmostEqual(*args, atol=0, rtol=0, **kwds)
124
125
126@lsst.utils.tests.inTestCase
127def assertMasksEqual(testCase, mask0, mask1, skipMask=None, msg="Masks differ"):
128 """!Assert that two masks are equal
129
130 @param[in] testCase unittest.TestCase instance the test is part of;
131 an object supporting one method: fail(self, msgStr)
132 @param[in] mask0 mask 0, an lsst.afw.image.Mask, lsst.afw.image.Image,
133 or transposed numpy array (see warning)
134 @param[in] mask1 mask 1, an lsst.afw.image.Mask, lsst.afw.image.Image,
135 or transposed numpy array (see warning)
136 @param[in] skipMask mask of pixels to skip, or None to compare all pixels;
137 an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array (see warning);
138 all non-zero pixels are skipped
139 @param[in] msg exception message prefix; details of the error are appended after ": "
140
141 @warning the axes of numpy arrays are transposed with respect to Mask and Image.
142 Thus for example if mask0 and mask1 are both lsst.afw.image.Mask with dimensions (2, 3)
143 and skipMask is a numpy array, then skipMask must have shape (3, 2).
144
145 @throw self.failureException (usually AssertionError) if any any un-skipped pixels differ
146
147 @throw TypeError if the dimensions of mask0, mask1 and skipMask do not match,
148 or any are not of a numeric data type.
149 """
150 errStr = imagesDiffer(mask0, mask1, skipMask=skipMask, rtol=0, atol=0)
151 if errStr:
152 testCase.fail(f"{msg}: {errStr}")
153
154
155@lsst.utils.tests.inTestCase
157 testCase, maskedImage0, maskedImage1,
158 doImage=True, doMask=True, doVariance=True, skipMask=None,
159 rtol=1.0e-05, atol=1e-08, msg="Masked images differ",
160):
161 """!Assert that two masked images are nearly equal, including non-finite values
162
163 @param[in] testCase unittest.TestCase instance the test is part of;
164 an object supporting one method: fail(self, msgStr)
165 @param[in] maskedImage0 masked image 0 (an lsst.afw.image.MaskedImage or
166 collection of three transposed numpy arrays: image, mask, variance)
167 @param[in] maskedImage1 masked image 1 (an lsst.afw.image.MaskedImage or
168 collection of three transposed numpy arrays: image, mask, variance)
169 @param[in] doImage compare image planes if True
170 @param[in] doMask compare mask planes if True
171 @param[in] doVariance compare variance planes if True
172 @param[in] skipMask mask of pixels to skip, or None to compare all pixels;
173 an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array;
174 all non-zero pixels are skipped
175 @param[in] rtol maximum allowed relative tolerance; more info below
176 @param[in] atol maximum allowed absolute tolerance; more info below
177 @param[in] msg exception message prefix; details of the error are appended after ": "
178
179 The mask planes must match exactly. The image and variance planes are nearly equal if all pixels obey:
180 |val1 - val0| <= rtol*|val1| + atol
181 or, for float types, if nan/inf/-inf pixels match.
182
183 @warning the comparison equation is not symmetric, so in rare cases the assertion
184 may give different results depending on which masked image comes first.
185
186 @warning the axes of numpy arrays are transposed with respect to MaskedImage data.
187 Thus for example if maskedImage0 and maskedImage1 are both lsst.afw.image.MaskedImageD
188 with dimensions (2, 3) and skipMask is a numpy array, then skipMask must have shape (3, 2).
189
190 @throw self.failureException (usually AssertionError) if any of the following are true
191 for un-skipped pixels:
192 - non-finite image or variance values differ in any way (e.g. one is "nan" and another is not)
193 - finite values differ by too much, as defined by atol and rtol
194 - mask pixels differ at all
195
196 @throw TypeError if the dimensions of maskedImage0, maskedImage1 and skipMask do not match,
197 either image or variance plane is not of a numeric data type,
198 either mask plane is not of an integer type (unsigned or signed),
199 or skipMask is not of a numeric data type.
200 """
201 maskedImageArrList0 = maskedImage0.getArrays() if hasattr(
202 maskedImage0, "getArrays") else maskedImage0
203 maskedImageArrList1 = maskedImage1.getArrays() if hasattr(
204 maskedImage1, "getArrays") else maskedImage1
205
206 for arrList, arg, name in (
207 (maskedImageArrList0, maskedImage0, "maskedImage0"),
208 (maskedImageArrList1, maskedImage1, "maskedImage1"),
209 ):
210 try:
211 assert len(arrList) == 3
212 # check that array shapes are all identical
213 # check that image and variance are float or int of some kind
214 # and mask is int of some kind
215 for i in (0, 2):
216 assert arrList[i].shape == arrList[1].shape
217 assert arrList[i].dtype.kind in ("b", "i", "u", "f", "c")
218 assert arrList[1].dtype.kind in ("b", "i", "u")
219 except Exception:
220 raise TypeError(f"{name}={arg!r} is not a supported type")
221
222 errStrList = []
223 for ind, (doPlane, planeName) in enumerate(((doImage, "image"),
224 (doMask, "mask"),
225 (doVariance, "variance"))):
226 if not doPlane:
227 continue
228
229 if planeName == "mask":
230 errStr = imagesDiffer(maskedImageArrList0[ind], maskedImageArrList1[ind], skipMask=skipMask,
231 rtol=0, atol=0)
232 if errStr:
233 errStrList.append(errStr)
234 else:
235 errStr = imagesDiffer(maskedImageArrList0[ind], maskedImageArrList1[ind],
236 skipMask=skipMask, rtol=rtol, atol=atol)
237 if errStr:
238 errStrList.append(f"{planeName} planes differ: {errStr}")
239
240 if errStrList:
241 errStr = "; ".join(errStrList)
242 testCase.fail(f"{msg}: {errStr}")
243
244
245@lsst.utils.tests.inTestCase
246def assertMaskedImagesEqual(*args, **kwds):
247 """!Assert that two masked images are exactly equal, including non-finite values.
248
249 All arguments are forwarded to assertMaskedImagesAlmostEqual aside from atol and rtol,
250 which are set to zero.
251 """
252 return assertMaskedImagesAlmostEqual(*args, atol=0, rtol=0, **kwds)
253
254
255def imagesDiffer(image0, image1, skipMask=None, rtol=1.0e-05, atol=1e-08):
256 """!Compare the pixels of two image or mask arrays; return True if close, False otherwise
257
258 @param[in] image0 image 0, an lsst.afw.image.Image, lsst.afw.image.Mask,
259 or transposed numpy array (see warning)
260 @param[in] image1 image 1, an lsst.afw.image.Image, lsst.afw.image.Mask,
261 or transposed numpy array (see warning)
262 @param[in] skipMask mask of pixels to skip, or None to compare all pixels;
263 an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array (see warning);
264 all non-zero pixels are skipped
265 @param[in] rtol maximum allowed relative tolerance; more info below
266 @param[in] atol maximum allowed absolute tolerance; more info below
267
268 The images are nearly equal if all pixels obey:
269 |val1 - val0| <= rtol*|val1| + atol
270 or, for float types, if nan/inf/-inf pixels match.
271
272 @warning the comparison equation is not symmetric, so in rare cases the assertion
273 may give different results depending on which image comes first.
274
275 @warning the axes of numpy arrays are transposed with respect to Image and Mask data.
276 Thus for example if image0 and image1 are both lsst.afw.image.ImageD with dimensions (2, 3)
277 and skipMask is a numpy array, then skipMask must have shape (3, 2).
278
279 @return a string which is non-empty if the images differ
280
281 @throw TypeError if the dimensions of image0, image1 and skipMask do not match,
282 or any are not of a numeric data type.
283 """
284 errStrList = []
285 imageArr0 = image0.getArray() if hasattr(image0, "getArray") else image0
286 imageArr1 = image1.getArray() if hasattr(image1, "getArray") else image1
287 skipMaskArr = skipMask.getArray() if hasattr(skipMask, "getArray") else skipMask
288
289 # check the inputs
290 arrArgNameList = [
291 (imageArr0, image0, "image0"),
292 (imageArr1, image1, "image1"),
293 ]
294 if skipMask is not None:
295 arrArgNameList.append((skipMaskArr, skipMask, "skipMask"))
296 for i, (arr, arg, name) in enumerate(arrArgNameList):
297 try:
298 assert arr.dtype.kind in ("b", "i", "u", "f", "c")
299 except Exception:
300 raise TypeError(f"{name!r}={arg!r} is not a supported type")
301 if i != 0:
302 if arr.shape != imageArr0.shape:
303 raise TypeError(f"{name} shape = {arr.shape} != {imageArr0.shape} = image0 shape")
304
305 # np.allclose mis-handled unsigned ints in numpy 1.8
306 # and subtraction doesn't give the desired answer in any case
307 # so cast unsigned arrays into int64 (there may be a simple
308 # way to safely use a smaller data type but I've not found it)
309 if imageArr0.dtype.kind == "u":
310 imageArr0 = imageArr0.astype(
311 np.promote_types(imageArr0.dtype, np.int8))
312 if imageArr1.dtype.kind == "u":
313 imageArr1 = imageArr1.astype(
314 np.promote_types(imageArr1.dtype, np.int8))
315
316 if skipMaskArr is not None:
317 skipMaskArr = np.array(skipMaskArr, dtype=bool)
318 maskedArr0 = np.ma.array(imageArr0, copy=False, mask=skipMaskArr)
319 maskedArr1 = np.ma.array(imageArr1, copy=False, mask=skipMaskArr)
320 filledArr0 = maskedArr0.filled(0.0)
321 filledArr1 = maskedArr1.filled(0.0)
322 else:
323 skipMaskArr = None
324 filledArr0 = imageArr0
325 filledArr1 = imageArr1
326
327 try:
328 np.array([np.nan], dtype=imageArr0.dtype)
329 np.array([np.nan], dtype=imageArr1.dtype)
330 except Exception:
331 # one or both images does not support non-finite values (nan, etc.)
332 # so just use value comparison
333 valSkipMaskArr = skipMaskArr
334 else:
335 # both images support non-finite values, of which numpy has exactly three: nan, +inf and -inf;
336 # compare those individually in order to give useful diagnostic output
337 nan0 = np.isnan(filledArr0)
338 nan1 = np.isnan(filledArr1)
339 if np.any(nan0 != nan1):
340 errStrList.append("NaNs differ")
341
342 posinf0 = np.isposinf(filledArr0)
343 posinf1 = np.isposinf(filledArr1)
344 if np.any(posinf0 != posinf1):
345 errStrList.append("+infs differ")
346
347 neginf0 = np.isneginf(filledArr0)
348 neginf1 = np.isneginf(filledArr1)
349 if np.any(neginf0 != neginf1):
350 errStrList.append("-infs differ")
351
352 valSkipMaskArr = nan0 | nan1 | posinf0 | posinf1 | neginf0 | neginf1
353 if skipMaskArr is not None:
354 valSkipMaskArr |= skipMaskArr
355
356 # compare values that should be comparable (are finite and not masked)
357 valMaskedArr1 = np.ma.array(imageArr0, copy=False, mask=valSkipMaskArr)
358 valMaskedArr2 = np.ma.array(imageArr1, copy=False, mask=valSkipMaskArr)
359 valFilledArr1 = valMaskedArr1.filled(0.0)
360 valFilledArr2 = valMaskedArr2.filled(0.0)
361
362 if not np.allclose(valFilledArr1, valFilledArr2, rtol=rtol, atol=atol):
363 errArr = np.abs(valFilledArr1 - valFilledArr2)
364 maxErr = errArr.max()
365 maxPosInd = np.where(errArr == maxErr)
366 maxPosTuple = (maxPosInd[1][0], maxPosInd[0][0])
367 errStr = f"maxDiff={maxErr} at position {maxPosTuple}; " \
368 f"value={valFilledArr1[maxPosInd][0]} vs. {valFilledArr2[maxPosInd][0]}"
369 errStrList.insert(0, errStr)
370
371 return "; ".join(errStrList)
A class to represent a 2-dimensional array of pixels.
Definition: Image.h:51
Represent a 2-dimensional array of bitmask pixels.
Definition: Mask.h:77
A class to manipulate images, masks, and variance as a single object.
Definition: MaskedImage.h:73
def makeMaskedImageFromArrays(image, mask=None, variance=None)
Definition: basicUtils.py:46
def imagesDiffer(image0, image1, skipMask=None, rtol=1.0e-05, atol=1e-08)
Compare the pixels of two image or mask arrays; return True if close, False otherwise.
Definition: testUtils.py:255
def assertImagesAlmostEqual(testCase, image0, image1, skipMask=None, rtol=1.0e-05, atol=1e-08, msg="Images differ")
Assert that two images are almost equal, including non-finite values.
Definition: testUtils.py:75
def assertMasksEqual(testCase, mask0, mask1, skipMask=None, msg="Masks differ")
Assert that two masks are equal.
Definition: testUtils.py:127
def assertMaskedImagesAlmostEqual(testCase, maskedImage0, maskedImage1, doImage=True, doMask=True, doVariance=True, skipMask=None, rtol=1.0e-05, atol=1e-08, msg="Masked images differ")
Assert that two masked images are nearly equal, including non-finite values.
Definition: testUtils.py:160
def makeRampImage(bbox, start=0, stop=None, imageClass=ImageF)
Make an image whose values are a linear ramp.
Definition: testUtils.py:51
def assertMaskedImagesEqual(*args, **kwds)
Assert that two masked images are exactly equal, including non-finite values.
Definition: testUtils.py:246
def assertImagesEqual(*args, **kwds)
Assert that two images are exactly equal, including non-finite values.
Definition: testUtils.py:117
def makeGaussianNoiseMaskedImage(dimensions, sigma, variance=1.0)
Definition: testUtils.py:34