Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# 

2# LSST Data Management System 

3# Copyright 2008-2017 LSST/AURA. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23# the asserts are automatically imported so unit tests can find them without special imports; 

24# the other functions are hidden unless explicitly asked for 

25__all__ = ["assertImagesAlmostEqual", "assertImagesEqual", "assertMasksEqual", 

26 "assertMaskedImagesAlmostEqual", "assertMaskedImagesEqual"] 

27 

28import numpy as np 

29 

30import lsst.utils.tests 

31from .image import ImageF 

32from .basicUtils import makeMaskedImageFromArrays 

33 

34 

35def makeGaussianNoiseMaskedImage(dimensions, sigma, variance=1.0): 

36 """Make a gaussian noise MaskedImageF 

37 

38 Inputs: 

39 - dimensions: dimensions of output array (cols, rows) 

40 - sigma; sigma of image plane's noise distribution 

41 - variance: constant value for variance plane 

42 """ 

43 npSize = (dimensions[1], dimensions[0]) 

44 image = np.random.normal(loc=0.0, scale=sigma, 

45 size=npSize).astype(np.float32) 

46 mask = np.zeros(npSize, dtype=np.int32) 

47 variance = np.zeros(npSize, dtype=np.float32) + variance 

48 

49 return makeMaskedImageFromArrays(image, mask, variance) 

50 

51 

52def makeRampImage(bbox, start=0, stop=None, imageClass=ImageF): 

53 """!Make an image whose values are a linear ramp 

54 

55 @param[in] bbox bounding box of image (an lsst.geom.Box2I) 

56 @param[in] start starting ramp value, inclusive 

57 @param[in] stop ending ramp value, inclusive; if None, increase by integer values 

58 @param[in] imageClass type of image (e.g. lsst.afw.image.ImageF) 

59 """ 

60 im = imageClass(bbox) 

61 imDim = im.getDimensions() 

62 numPix = imDim[0]*imDim[1] 

63 imArr = im.getArray() 

64 if stop is None: 

65 # increase by integer values 

66 stop = start + numPix - 1 

67 rampArr = np.linspace(start=start, stop=stop, 

68 endpoint=True, num=numPix, dtype=imArr.dtype) 

69 # numpy arrays are transposed w.r.t. afwImage 

70 imArr[:] = np.reshape(rampArr, (imDim[1], imDim[0])) 

71 return im 

72 

73 

74@lsst.utils.tests.inTestCase 

75def assertImagesAlmostEqual(testCase, image0, image1, skipMask=None, 

76 rtol=1.0e-05, atol=1e-08, msg="Images differ"): 

77 """!Assert that two images are almost equal, including non-finite values 

78 

79 @param[in] testCase unittest.TestCase instance the test is part of; 

80 an object supporting one method: fail(self, msgStr) 

81 @param[in] image0 image 0, an lsst.afw.image.Image, lsst.afw.image.Mask, 

82 or transposed numpy array (see warning) 

83 @param[in] image1 image 1, an lsst.afw.image.Image, lsst.afw.image.Mask, 

84 or transposed numpy array (see warning) 

85 @param[in] skipMask mask of pixels to skip, or None to compare all pixels; 

86 an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array (see warning); 

87 all non-zero pixels are skipped 

88 @param[in] rtol maximum allowed relative tolerance; more info below 

89 @param[in] atol maximum allowed absolute tolerance; more info below 

90 @param[in] msg exception message prefix; details of the error are appended after ": " 

91 

92 The images are nearly equal if all pixels obey: 

93 |val1 - val0| <= rtol*|val1| + atol 

94 or, for float types, if nan/inf/-inf pixels match. 

95 

96 @warning the comparison equation is not symmetric, so in rare cases the assertion 

97 may give different results depending on which image comes first. 

98 

99 @warning the axes of numpy arrays are transposed with respect to Image and Mask data. 

100 Thus for example if image0 and image1 are both lsst.afw.image.ImageD with dimensions (2, 3) 

101 and skipMask is a numpy array, then skipMask must have shape (3, 2). 

102 

103 @throw self.failureException (usually AssertionError) if any of the following are true 

104 for un-skipped pixels: 

105 - non-finite values differ in any way (e.g. one is "nan" and another is not) 

106 - finite values differ by too much, as defined by atol and rtol 

107 

108 @throw TypeError if the dimensions of image0, image1 and skipMask do not match, 

109 or any are not of a numeric data type. 

110 """ 

111 errStr = imagesDiffer( 

112 image0, image1, skipMask=skipMask, rtol=rtol, atol=atol) 

113 if errStr: 

114 testCase.fail(f"{msg}: {errStr}") 

115 

116 

117@lsst.utils.tests.inTestCase 

118def assertImagesEqual(*args, **kwds): 

119 """!Assert that two images are exactly equal, including non-finite values. 

120 

121 All arguments are forwarded to assertAnglesAlmostEqual aside from atol and rtol, 

122 which are set to zero. 

123 """ 

124 return assertImagesAlmostEqual(*args, atol=0, rtol=0, **kwds) 

125 

126 

127@lsst.utils.tests.inTestCase 

128def assertMasksEqual(testCase, mask0, mask1, skipMask=None, msg="Masks differ"): 

129 """!Assert that two masks are equal 

130 

131 @param[in] testCase unittest.TestCase instance the test is part of; 

132 an object supporting one method: fail(self, msgStr) 

133 @param[in] mask0 mask 0, an lsst.afw.image.Mask, lsst.afw.image.Image, 

134 or transposed numpy array (see warning) 

135 @param[in] mask1 mask 1, an lsst.afw.image.Mask, lsst.afw.image.Image, 

136 or transposed numpy array (see warning) 

137 @param[in] skipMask mask of pixels to skip, or None to compare all pixels; 

138 an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array (see warning); 

139 all non-zero pixels are skipped 

140 @param[in] msg exception message prefix; details of the error are appended after ": " 

141 

142 @warning the axes of numpy arrays are transposed with respect to Mask and Image. 

143 Thus for example if mask0 and mask1 are both lsst.afw.image.Mask with dimensions (2, 3) 

144 and skipMask is a numpy array, then skipMask must have shape (3, 2). 

145 

146 @throw self.failureException (usually AssertionError) if any any un-skipped pixels differ 

147 

148 @throw TypeError if the dimensions of mask0, mask1 and skipMask do not match, 

149 or any are not of a numeric data type. 

150 """ 

151 errStr = imagesDiffer(mask0, mask1, skipMask=skipMask, rtol=0, atol=0) 

152 if errStr: 

153 testCase.fail(f"{msg}: {errStr}") 

154 

155 

156@lsst.utils.tests.inTestCase 

157def assertMaskedImagesAlmostEqual( 

158 testCase, maskedImage0, maskedImage1, 

159 doImage=True, doMask=True, doVariance=True, skipMask=None, 

160 rtol=1.0e-05, atol=1e-08, msg="Masked images differ", 

161): 

162 """!Assert that two masked images are nearly equal, including non-finite values 

163 

164 @param[in] testCase unittest.TestCase instance the test is part of; 

165 an object supporting one method: fail(self, msgStr) 

166 @param[in] maskedImage0 masked image 0 (an lsst.afw.image.MaskedImage or 

167 collection of three transposed numpy arrays: image, mask, variance) 

168 @param[in] maskedImage1 masked image 1 (an lsst.afw.image.MaskedImage or 

169 collection of three transposed numpy arrays: image, mask, variance) 

170 @param[in] doImage compare image planes if True 

171 @param[in] doMask compare mask planes if True 

172 @param[in] doVariance compare variance planes if True 

173 @param[in] skipMask mask of pixels to skip, or None to compare all pixels; 

174 an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array; 

175 all non-zero pixels are skipped 

176 @param[in] rtol maximum allowed relative tolerance; more info below 

177 @param[in] atol maximum allowed absolute tolerance; more info below 

178 @param[in] msg exception message prefix; details of the error are appended after ": " 

179 

180 The mask planes must match exactly. The image and variance planes are nearly equal if all pixels obey: 

181 |val1 - val0| <= rtol*|val1| + atol 

182 or, for float types, if nan/inf/-inf pixels match. 

183 

184 @warning the comparison equation is not symmetric, so in rare cases the assertion 

185 may give different results depending on which masked image comes first. 

186 

187 @warning the axes of numpy arrays are transposed with respect to MaskedImage data. 

188 Thus for example if maskedImage0 and maskedImage1 are both lsst.afw.image.MaskedImageD 

189 with dimensions (2, 3) and skipMask is a numpy array, then skipMask must have shape (3, 2). 

190 

191 @throw self.failureException (usually AssertionError) if any of the following are true 

192 for un-skipped pixels: 

193 - non-finite image or variance values differ in any way (e.g. one is "nan" and another is not) 

194 - finite values differ by too much, as defined by atol and rtol 

195 - mask pixels differ at all 

196 

197 @throw TypeError if the dimensions of maskedImage0, maskedImage1 and skipMask do not match, 

198 either image or variance plane is not of a numeric data type, 

199 either mask plane is not of an integer type (unsigned or signed), 

200 or skipMask is not of a numeric data type. 

201 """ 

202 maskedImageArrList0 = maskedImage0.getArrays() if hasattr( 

203 maskedImage0, "getArrays") else maskedImage0 

204 maskedImageArrList1 = maskedImage1.getArrays() if hasattr( 

205 maskedImage1, "getArrays") else maskedImage1 

206 

207 for arrList, arg, name in ( 

208 (maskedImageArrList0, maskedImage0, "maskedImage0"), 

209 (maskedImageArrList1, maskedImage1, "maskedImage1"), 

210 ): 

211 try: 

212 assert len(arrList) == 3 

213 # check that array shapes are all identical 

214 # check that image and variance are float or int of some kind 

215 # and mask is int of some kind 

216 for i in (0, 2): 

217 assert arrList[i].shape == arrList[1].shape 

218 assert arrList[i].dtype.kind in ("b", "i", "u", "f", "c") 

219 assert arrList[1].dtype.kind in ("b", "i", "u") 

220 except Exception: 

221 raise TypeError(f"{name}={arg!r} is not a supported type") 

222 

223 errStrList = [] 

224 for ind, (doPlane, planeName) in enumerate(((doImage, "image"), 

225 (doMask, "mask"), 

226 (doVariance, "variance"))): 

227 if not doPlane: 

228 continue 

229 

230 if planeName == "mask": 

231 errStr = imagesDiffer(maskedImageArrList0[ind], maskedImageArrList1[ind], skipMask=skipMask, 

232 rtol=0, atol=0) 

233 if errStr: 

234 errStrList.append(errStr) 

235 else: 

236 errStr = imagesDiffer(maskedImageArrList0[ind], maskedImageArrList1[ind], 

237 skipMask=skipMask, rtol=rtol, atol=atol) 

238 if errStr: 

239 errStrList.append(f"{planeName} planes differ: {errStr}") 

240 

241 if errStrList: 

242 errStr = "; ".join(errStrList) 

243 testCase.fail(f"{msg}: {errStr}") 

244 

245 

246@lsst.utils.tests.inTestCase 

247def assertMaskedImagesEqual(*args, **kwds): 

248 """!Assert that two masked images are exactly equal, including non-finite values. 

249 

250 All arguments are forwarded to assertMaskedImagesAlmostEqual aside from atol and rtol, 

251 which are set to zero. 

252 """ 

253 return assertMaskedImagesAlmostEqual(*args, atol=0, rtol=0, **kwds) 

254 

255 

256def imagesDiffer(image0, image1, skipMask=None, rtol=1.0e-05, atol=1e-08): 

257 """!Compare the pixels of two image or mask arrays; return True if close, False otherwise 

258 

259 @param[in] image0 image 0, an lsst.afw.image.Image, lsst.afw.image.Mask, 

260 or transposed numpy array (see warning) 

261 @param[in] image1 image 1, an lsst.afw.image.Image, lsst.afw.image.Mask, 

262 or transposed numpy array (see warning) 

263 @param[in] skipMask mask of pixels to skip, or None to compare all pixels; 

264 an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array (see warning); 

265 all non-zero pixels are skipped 

266 @param[in] rtol maximum allowed relative tolerance; more info below 

267 @param[in] atol maximum allowed absolute tolerance; more info below 

268 

269 The images are nearly equal if all pixels obey: 

270 |val1 - val0| <= rtol*|val1| + atol 

271 or, for float types, if nan/inf/-inf pixels match. 

272 

273 @warning the comparison equation is not symmetric, so in rare cases the assertion 

274 may give different results depending on which image comes first. 

275 

276 @warning the axes of numpy arrays are transposed with respect to Image and Mask data. 

277 Thus for example if image0 and image1 are both lsst.afw.image.ImageD with dimensions (2, 3) 

278 and skipMask is a numpy array, then skipMask must have shape (3, 2). 

279 

280 @return a string which is non-empty if the images differ 

281 

282 @throw TypeError if the dimensions of image0, image1 and skipMask do not match, 

283 or any are not of a numeric data type. 

284 """ 

285 errStrList = [] 

286 imageArr0 = image0.getArray() if hasattr(image0, "getArray") else image0 

287 imageArr1 = image1.getArray() if hasattr(image1, "getArray") else image1 

288 skipMaskArr = skipMask.getArray() if hasattr(skipMask, "getArray") else skipMask 

289 

290 # check the inputs 

291 arrArgNameList = [ 

292 (imageArr0, image0, "image0"), 

293 (imageArr1, image1, "image1"), 

294 ] 

295 if skipMask is not None: 

296 arrArgNameList.append((skipMaskArr, skipMask, "skipMask")) 

297 for i, (arr, arg, name) in enumerate(arrArgNameList): 

298 try: 

299 assert arr.dtype.kind in ("b", "i", "u", "f", "c") 

300 except Exception: 

301 raise TypeError(f"{name!r}={arg!r} is not a supported type") 

302 if i != 0: 

303 if arr.shape != imageArr0.shape: 

304 raise TypeError(f"{name} shape = {arr.shape} != {imageArr0.shape} = image0 shape") 

305 

306 # np.allclose mis-handled unsigned ints in numpy 1.8 

307 # and subtraction doesn't give the desired answer in any case 

308 # so cast unsigned arrays into int64 (there may be a simple 

309 # way to safely use a smaller data type but I've not found it) 

310 if imageArr0.dtype.kind == "u": 

311 imageArr0 = imageArr0.astype( 

312 np.promote_types(imageArr0.dtype, np.int8)) 

313 if imageArr1.dtype.kind == "u": 

314 imageArr1 = imageArr1.astype( 

315 np.promote_types(imageArr1.dtype, np.int8)) 

316 

317 if skipMaskArr is not None: 

318 skipMaskArr = np.array(skipMaskArr, dtype=bool) 

319 maskedArr0 = np.ma.array(imageArr0, copy=False, mask=skipMaskArr) 

320 maskedArr1 = np.ma.array(imageArr1, copy=False, mask=skipMaskArr) 

321 filledArr0 = maskedArr0.filled(0.0) 

322 filledArr1 = maskedArr1.filled(0.0) 

323 else: 

324 skipMaskArr = None 

325 filledArr0 = imageArr0 

326 filledArr1 = imageArr1 

327 

328 try: 

329 np.array([np.nan], dtype=imageArr0.dtype) 

330 np.array([np.nan], dtype=imageArr1.dtype) 

331 except Exception: 

332 # one or both images does not support non-finite values (nan, etc.) 

333 # so just use value comparison 

334 valSkipMaskArr = skipMaskArr 

335 else: 

336 # both images support non-finite values, of which numpy has exactly three: nan, +inf and -inf; 

337 # compare those individually in order to give useful diagnostic output 

338 nan0 = np.isnan(filledArr0) 

339 nan1 = np.isnan(filledArr1) 

340 if np.any(nan0 != nan1): 

341 errStrList.append("NaNs differ") 

342 

343 posinf0 = np.isposinf(filledArr0) 

344 posinf1 = np.isposinf(filledArr1) 

345 if np.any(posinf0 != posinf1): 

346 errStrList.append("+infs differ") 

347 

348 neginf0 = np.isneginf(filledArr0) 

349 neginf1 = np.isneginf(filledArr1) 

350 if np.any(neginf0 != neginf1): 

351 errStrList.append("-infs differ") 

352 

353 valSkipMaskArr = nan0 | nan1 | posinf0 | posinf1 | neginf0 | neginf1 

354 if skipMaskArr is not None: 

355 valSkipMaskArr |= skipMaskArr 

356 

357 # compare values that should be comparable (are finite and not masked) 

358 valMaskedArr1 = np.ma.array(imageArr0, copy=False, mask=valSkipMaskArr) 

359 valMaskedArr2 = np.ma.array(imageArr1, copy=False, mask=valSkipMaskArr) 

360 valFilledArr1 = valMaskedArr1.filled(0.0) 

361 valFilledArr2 = valMaskedArr2.filled(0.0) 

362 

363 if not np.allclose(valFilledArr1, valFilledArr2, rtol=rtol, atol=atol): 

364 errArr = np.abs(valFilledArr1 - valFilledArr2) 

365 maxErr = errArr.max() 

366 maxPosInd = np.where(errArr == maxErr) 

367 maxPosTuple = (maxPosInd[1][0], maxPosInd[0][0]) 

368 errStr = f"maxDiff={maxErr} at position {maxPosTuple}; " \ 

369 f"value={valFilledArr1[maxPosInd][0]} vs. {valFilledArr2[maxPosInd][0]}" 

370 errStrList.insert(0, errStr) 

371 

372 return "; ".join(errStrList)