Coverage for tests/test_background.py: 10%

505 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-15 02:47 -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/>. 

21 

22import math 

23import os.path 

24import unittest 

25import pickle 

26from functools import reduce 

27 

28import numpy as np 

29 

30import lsst.utils 

31import lsst.utils.tests 

32import lsst.pex.exceptions 

33from lsst.daf.base import PropertySet 

34import lsst.geom 

35import lsst.afw.image as afwImage 

36import lsst.afw.math as afwMath 

37import lsst.afw.display as afwDisplay 

38 

39# Set to True to display debug messages and images 

40debugMode = False 

41 

42afwDisplay.setDefaultMaskTransparency(75) 

43try: 

44 AfwdataDir = lsst.utils.getPackageDir("afwdata") 

45except LookupError: 

46 AfwdataDir = None 

47 

48 

49class BackgroundTestCase(lsst.utils.tests.TestCase): 

50 

51 def setUp(self): 

52 np.random.seed(1) 

53 self.val = 10 

54 self.image = afwImage.ImageF(lsst.geom.Box2I( 

55 lsst.geom.Point2I(1000, 500), lsst.geom.Extent2I(100, 200))) 

56 self.image.set(self.val) 

57 

58 def tearDown(self): 

59 del self.image 

60 

61 def testOddSize(self): 

62 """Test for ticket #1781 -- without it, in oddly-sized images 

63 there is a chunk of pixels on the right/bottom that do not go 

64 into the fit and are extrapolated. After this ticket, the 

65 subimage boundaries are spread more evenly so the last pixels 

66 get fit as well. This slightly strange test case checks that 

67 the interpolant is close to the function at the end. I could 

68 not think of an interpolant that would fit exactly, so this 

69 just puts a limit on the errors. 

70 """ 

71 W, H = 2, 99 

72 image = afwImage.ImageF(lsst.geom.Extent2I(W, H)) 

73 bgCtrl = afwMath.BackgroundControl(afwMath.Interpolate.LINEAR) 

74 bgCtrl.setNxSample(2) 

75 NY = 10 

76 bgCtrl.setNySample(NY) 

77 for y in range(H): 

78 for x in range(W): 

79 B = 89 

80 if y < B: 

81 image[x, y, afwImage.LOCAL] = y 

82 else: 

83 image[x, y, afwImage.LOCAL] = B + (y-B)*-1. 

84 bobj = afwMath.makeBackground(image, bgCtrl) 

85 back = bobj.getImageF() 

86 

87 for iy, by in zip([image[0, y, afwImage.LOCAL] for y in range(H)], 

88 [back[0, y, afwImage.LOCAL] for y in range(H)]): 

89 self.assertLess(abs(iy - by), 5) 

90 

91 @unittest.skipIf(AfwdataDir is None, "afwdata not setup") 

92 def testBackgroundTestImages(self): 

93 """Tests Laher's afwdata/Statistics/*.fits images (doubles)""" 

94 imginfolist = [] 

95 # cooked to known value 

96 imginfolist.append(["v1_i1_g_m400_s20_f.fits", 399.9912966583894]) 

97 

98 for imginfo in imginfolist: 

99 imgfile, centerValue = imginfo 

100 imgPath = os.path.join(AfwdataDir, "Statistics", imgfile) 

101 

102 # get the image and header 

103 dimg = afwImage.DecoratedImageF(imgPath) 

104 img = dimg.getImage() 

105 fitsHdr = dimg.getMetadata() # the FITS header 

106 

107 # get the True values of the mean and stdev 

108 reqMean = fitsHdr.getAsDouble("MEANREQ") 

109 reqStdev = fitsHdr.getAsDouble("SIGREQ") 

110 naxis1 = img.getWidth() 

111 naxis2 = img.getHeight() 

112 

113 # create a background control object 

114 bctrl = afwMath.BackgroundControl(afwMath.Interpolate.AKIMA_SPLINE) 

115 bctrl.setNxSample(5) 

116 bctrl.setNySample(5) 

117 

118 # run the background constructor and call the getImage() function. 

119 backobj = afwMath.makeBackground(img, bctrl) 

120 

121 pixPerSubimage = img.getWidth()*img.getHeight() / \ 

122 (bctrl.getNxSample()*bctrl.getNySample()) 

123 stdevInterp = reqStdev/math.sqrt(pixPerSubimage) 

124 

125 # test getImage() by checking the center pixel 

126 bimg = backobj.getImageF() 

127 testImgval = bimg[naxis1//2, naxis2//2, afwImage.LOCAL] 

128 self.assertLess(abs(testImgval - reqMean), 2*stdevInterp) 

129 

130 def testRamp(self): 

131 """tests Laher's afwdata/Statistics/*.fits images (doubles)""" 

132 # make a ramping image (spline should be exact for linear increasing 

133 # image 

134 nx = 512 

135 ny = 512 

136 x0, y0 = 9876, 54321 

137 box = lsst.geom.Box2I(lsst.geom.Point2I(x0, y0), lsst.geom.Extent2I(nx, ny)) 

138 rampimg = afwImage.ImageF(box) 

139 dzdx, dzdy, z0 = 0.1, 0.2, 10000.0 

140 for x in range(nx): 

141 for y in range(ny): 

142 rampimg[x, y, afwImage.LOCAL] = dzdx*x + dzdy*y + z0 

143 

144 # check corner, edge, and center pixels 

145 bctrl = afwMath.BackgroundControl(10, 10) 

146 bctrl.setInterpStyle(afwMath.Interpolate.CUBIC_SPLINE) 

147 bctrl.setNxSample(6) 

148 bctrl.setNySample(6) 

149 # large enough to entirely avoid clipping 

150 bctrl.getStatisticsControl().setNumSigmaClip(20.0) 

151 bctrl.getStatisticsControl().setNumIter(1) 

152 backobj = afwMath.makeBackground(rampimg, bctrl) 

153 

154 if debugMode: 

155 print(rampimg.getArray()) 

156 

157 frame = 1 

158 for interp in ("CONSTANT", "LINEAR", "NATURAL_SPLINE", "AKIMA_SPLINE"): 

159 diff = backobj.getImageF(interp) 

160 if debugMode: 

161 afwDisplay.Display(frame=frame).mtv(diff, title=self._testMethodName + " diff") 

162 frame += 1 

163 diff -= rampimg 

164 if debugMode: 

165 print(interp, diff.getArray().mean(), diff.getArray().std()) 

166 if debugMode: 

167 afwDisplay.Display(frame=frame).mtv(diff, title=self._testMethodName + " diff-ramping") 

168 frame += 1 

169 if debugMode: 

170 afwDisplay.Display(frame=frame).mtv(rampimg, title=self._testMethodName + " ramping") 

171 frame += 1 

172 afwDisplay.Display(frame=frame).mtv(backobj.getStatsImage(), 

173 title=self._testMethodName + " bkgd StatsImage") 

174 frame += 1 

175 

176 xpixels = [0, nx//2, nx - 1] 

177 ypixels = [0, ny//2, ny - 1] 

178 for xpix in xpixels: 

179 for ypix in ypixels: 

180 testval = backobj.getImageF()[xpix, ypix, afwImage.LOCAL] 

181 self.assertAlmostEqual(testval/rampimg[xpix, ypix, afwImage.LOCAL], 1, 6) 

182 

183 # Test pickle 

184 new = pickle.loads(pickle.dumps(backobj)) 

185 self.assertBackgroundEqual(backobj, new) 

186 

187 # Check creation of sub-image 

188 box = lsst.geom.Box2I(lsst.geom.Point2I(123, 45), 

189 lsst.geom.Extent2I(45, 123)) 

190 box.shift(lsst.geom.Extent2I(x0, y0)) 

191 bgImage = backobj.getImageF("AKIMA_SPLINE") 

192 bgSubImage = afwImage.ImageF(bgImage, box) 

193 testImage = backobj.getImageF(box, "AKIMA_SPLINE") 

194 self.assertEqual(testImage.getXY0(), bgSubImage.getXY0()) 

195 self.assertEqual(testImage.getDimensions(), bgSubImage.getDimensions()) 

196 self.assertImagesEqual(testImage, bgSubImage) 

197 

198 def getParabolaImage(self, nx, ny, pars=(1.0e-4, 1.0e-4, 0.1, 0.2, 10.0)): 

199 """Make sure a quadratic map is *well* reproduced by the spline model 

200 """ 

201 parabimg = afwImage.ImageF(lsst.geom.Extent2I(nx, ny)) 

202 d2zdx2, d2zdy2, dzdx, dzdy, z0 = pars # no cross-terms 

203 x, y = np.meshgrid(np.arange(nx, dtype=int), np.arange(ny, dtype=int)) 

204 parabimg.array[:, :] = d2zdx2*x**2 + d2zdy2*y**2 + dzdx*x + dzdy*y + z0 

205 return parabimg 

206 

207 @unittest.skipIf(AfwdataDir is None, "afwdata not setup") 

208 def testTicket987(self): 

209 """This code used to abort; so the test is that it doesn't""" 

210 imagePath = os.path.join( 

211 AfwdataDir, "DC3a-Sim", "sci", "v5-e0", "v5-e0-c011-a00.sci.fits") 

212 mimg = afwImage.MaskedImageF(imagePath) 

213 binsize = 512 

214 bctrl = afwMath.BackgroundControl("NATURAL_SPLINE") 

215 

216 # note: by default undersampleStyle is THROW_EXCEPTION 

217 bctrl.setUndersampleStyle(afwMath.REDUCE_INTERP_ORDER) 

218 

219 nx = int(mimg.getWidth()/binsize) + 1 

220 ny = int(mimg.getHeight()/binsize) + 1 

221 

222 bctrl.setNxSample(nx) 

223 bctrl.setNySample(ny) 

224 image = mimg.getImage() 

225 backobj = afwMath.makeBackground(image, bctrl) 

226 image -= backobj.getImageF() 

227 

228 def testTicket1781(self): 

229 """Test an unusual-sized image""" 

230 nx = 526 

231 ny = 154 

232 

233 parabimg = self.getParabolaImage(nx, ny) 

234 

235 bctrl = afwMath.BackgroundControl(afwMath.Interpolate.CUBIC_SPLINE) 

236 bctrl.setNxSample(16) 

237 bctrl.setNySample(4) 

238 bctrl.getStatisticsControl().setNumSigmaClip(10.0) 

239 bctrl.getStatisticsControl().setNumIter(1) 

240 afwMath.makeBackground(parabimg, bctrl) 

241 

242 def testParabola(self): 

243 """Test an image which varies parabolicly (spline should be exact for 2rd order polynomial)""" 

244 nx = 512 

245 ny = 512 

246 

247 parabimg = self.getParabolaImage(nx, ny) 

248 

249 # check corner, edge, and center pixels 

250 bctrl = afwMath.BackgroundControl(afwMath.Interpolate.CUBIC_SPLINE) 

251 bctrl.setNxSample(24) 

252 bctrl.setNySample(24) 

253 bctrl.getStatisticsControl().setNumSigmaClip(10.0) 

254 bctrl.getStatisticsControl().setNumIter(1) 

255 backobj = afwMath.makeBackground(parabimg, bctrl) 

256 

257 segmentCenter = int(0.5*nx/bctrl.getNxSample()) 

258 xpixels = [segmentCenter, nx//2, nx - segmentCenter] 

259 ypixels = [segmentCenter, ny//2, ny - segmentCenter] 

260 for xpix in xpixels: 

261 for ypix in ypixels: 

262 testval = backobj.getImageF(bctrl.getInterpStyle())[xpix, ypix, afwImage.LOCAL] 

263 realval = parabimg[xpix, ypix, afwImage.LOCAL] 

264 # quadratic terms skew the averages of the subimages and the clipped mean for 

265 # a subimage != value of center pixel. 1/20 counts on a 10000 count sky 

266 # is a fair (if arbitrary) test. 

267 self.assertLess(abs(testval - realval), 0.5) 

268 

269 @unittest.skipIf(AfwdataDir is None, "afwdata not setup") 

270 def testCFHT_oldAPI(self): 

271 """Test background subtraction on some real CFHT data""" 

272 mi = afwImage.MaskedImageF(os.path.join( 

273 AfwdataDir, "CFHT", "D4", "cal-53535-i-797722_1.fits")) 

274 mi = mi.Factory(mi, 

275 lsst.geom.Box2I(lsst.geom.Point2I(32, 2), 

276 lsst.geom.Point2I(2079, 4609)), 

277 afwImage.LOCAL) 

278 

279 bctrl = afwMath.BackgroundControl(afwMath.Interpolate.AKIMA_SPLINE) 

280 bctrl.setNxSample(16) 

281 bctrl.setNySample(16) 

282 bctrl.getStatisticsControl().setNumSigmaClip(3.0) 

283 bctrl.getStatisticsControl().setNumIter(2) 

284 backobj = afwMath.makeBackground(mi.getImage(), bctrl) 

285 

286 if debugMode: 

287 afwDisplay.Display(frame=0).mtv(mi, title=self._testMethodName + " image") 

288 

289 im = mi.getImage() 

290 im -= backobj.getImageF() 

291 

292 if debugMode: 

293 afwDisplay.Display(frame=1).mtv(mi, title=self._testMethodName + " image-back") 

294 

295 def getCfhtImage(self): 

296 """Get a portion of a CFHT image as a MaskedImageF""" 

297 bbox = lsst.geom.Box2I(lsst.geom.Point2I(500, 2000), 

298 lsst.geom.Point2I(2079, 4609)) 

299 imagePath = os.path.join( 

300 AfwdataDir, "CFHT", "D4", "cal-53535-i-797722_1.fits") 

301 return afwImage.MaskedImageF(imagePath, PropertySet(), bbox) 

302 

303 @unittest.skipIf(AfwdataDir is None, "afwdata not setup") 

304 def testXY0(self): 

305 """Test fitting the background to an image with nonzero xy0 

306 

307 The statsImage and background image should not vary with xy0 

308 """ 

309 bgImageList = [] # list of background images, one per xy0 

310 statsImageList = [] # list of stats images, one per xy0 

311 for xy0 in (lsst.geom.Point2I(0, 0), 

312 lsst.geom.Point2I(-100, -999), 

313 lsst.geom.Point2I(1000, 500)): 

314 mi = self.getCfhtImage() 

315 mi.setXY0(xy0) 

316 

317 bctrl = afwMath.BackgroundControl( 

318 mi.getWidth()//128, mi.getHeight()//128) 

319 backobj = afwMath.makeBackground(mi.getImage(), bctrl) 

320 bgImage = backobj.getImageF() 

321 self.assertEqual(bgImage.getBBox(), mi.getBBox()) 

322 bgImageList.append(bgImage) 

323 

324 statsImage = backobj.getStatsImage() 

325 statsImageList.append(statsImage) 

326 

327 # changing the bounding box should make no difference to the pixel values, 

328 # so compare pixels using exact equality 

329 for bgImage in bgImageList[1:]: 

330 self.assertImagesEqual(bgImage, bgImageList[0]) 

331 for statsImage in statsImageList[1:]: 

332 self.assertMaskedImagesEqual(statsImage, statsImageList[0]) 

333 

334 @unittest.skipIf(AfwdataDir is None, "afwdata not setup") 

335 def testSubImage(self): 

336 """Test getImage on a subregion of the full background image 

337 

338 Using real image data is a cheap way to get a variable background 

339 """ 

340 mi = self.getCfhtImage() 

341 

342 bctrl = afwMath.BackgroundControl( 

343 mi.getWidth()//128, mi.getHeight()//128) 

344 backobj = afwMath.makeBackground(mi.getImage(), bctrl) 

345 subBBox = lsst.geom.Box2I(lsst.geom.Point2I(1000, 3000), 

346 lsst.geom.Extent2I(100, 100)) 

347 

348 bgFullImage = backobj.getImageF() 

349 self.assertEqual(bgFullImage.getBBox(), mi.getBBox()) 

350 

351 subFullArr = afwImage.ImageF(bgFullImage, subBBox).getArray() 

352 

353 bgSubImage = backobj.getImageF(subBBox, bctrl.getInterpStyle()) 

354 subArr = bgSubImage.getArray() 

355 

356 # the pixels happen to be identical but it is safer not to rely on 

357 # that; close is good enough 

358 self.assertFloatsEqual(subArr, subFullArr) 

359 

360 @unittest.skipIf(AfwdataDir is None, "afwdata not setup") 

361 def testCFHT(self): 

362 """Test background subtraction on some real CFHT data""" 

363 mi = self.getCfhtImage() 

364 

365 bctrl = afwMath.BackgroundControl( 

366 mi.getWidth()//128, mi.getHeight()//128) 

367 bctrl.getStatisticsControl().setNumSigmaClip(3.0) 

368 bctrl.getStatisticsControl().setNumIter(2) 

369 backobj = afwMath.makeBackground(mi.getImage(), bctrl) 

370 

371 if debugMode: 

372 afwDisplay.Display(frame=0).mtv(mi, title=self._testMethodName + " image") 

373 

374 im = mi.getImage() 

375 im -= backobj.getImageF("AKIMA_SPLINE") 

376 

377 if debugMode: 

378 afwDisplay.Display(frame=1).mtv(mi, title=self._testMethodName + " image-back") 

379 

380 statsImage = backobj.getStatsImage() 

381 

382 if debugMode: 

383 afwDisplay.Display(frame=2).mtv(statsImage, title=self._testMethodName + " bkgd StatsImage") 

384 afwDisplay.Display(frame=3).mtv(statsImage.getVariance(), 

385 title=self._testMethodName + " bkgd Variance") 

386 

387 def testUndersample(self): 

388 """Test how the program handles nx,ny being too small for requested interp style.""" 

389 nx = 64 

390 ny = 64 

391 img = afwImage.ImageF(lsst.geom.Extent2I(nx, ny)) 

392 

393 # make a background control object 

394 bctrl = afwMath.BackgroundControl(10, 10) 

395 bctrl.setInterpStyle(afwMath.Interpolate.CUBIC_SPLINE) 

396 bctrl.setNxSample(3) 

397 bctrl.setNySample(3) 

398 

399 # put nx,ny back to 2 and see if it adjusts the interp style down to 

400 # linear 

401 bctrl.setNxSample(2) 

402 bctrl.setNySample(2) 

403 bctrl.setUndersampleStyle("REDUCE_INTERP_ORDER") 

404 backobj = afwMath.makeBackground(img, bctrl) 

405 # Need to interpolate background to discover what we actually needed 

406 backobj.getImageF() 

407 self.assertEqual(backobj.getAsUsedInterpStyle(), 

408 afwMath.Interpolate.LINEAR) 

409 

410 # put interp style back up to cspline and see if it throws an exception 

411 bctrl.setUndersampleStyle("THROW_EXCEPTION") 

412 

413 def tst(img, bctrl): 

414 backobj = afwMath.makeBackground(img, bctrl) 

415 # only now do we see that we have too few points 

416 backobj.getImageF("CUBIC_SPLINE") 

417 self.assertRaises(lsst.pex.exceptions.InvalidParameterError, 

418 tst, img, bctrl) 

419 

420 def testOnlyOneGridCell(self): 

421 """Test how the program handles nxSample,nySample being 1x1.""" 

422 # try a ramping image ... has an easy analytic solution 

423 nx = 64 

424 ny = 64 

425 img = afwImage.ImageF(lsst.geom.Extent2I(nx, ny), 10) 

426 

427 dzdx, dzdy, z0 = 0.1, 0.2, 10000.0 

428 mean = z0 + dzdx*(nx - 1)/2 + dzdy*(ny - 1)/2 # the analytic solution 

429 for x in range(nx): 

430 for y in range(ny): 

431 img[x, y, afwImage.LOCAL] = dzdx*x + dzdy*y + z0 

432 

433 # make a background control object 

434 bctrl = afwMath.BackgroundControl(10, 10) 

435 bctrl.setInterpStyle(afwMath.Interpolate.CONSTANT) 

436 bctrl.setNxSample(1) 

437 bctrl.setNySample(1) 

438 bctrl.setUndersampleStyle(afwMath.THROW_EXCEPTION) 

439 backobj = afwMath.makeBackground(img, bctrl) 

440 

441 xpixels = [0, nx//2, nx - 1] 

442 ypixels = [0, ny//2, ny - 1] 

443 for xpix in xpixels: 

444 for ypix in ypixels: 

445 testval = backobj.getImageF(bctrl.getInterpStyle())[xpix, ypix] 

446 self.assertAlmostEqual(testval/mean, 1) 

447 

448 def testAdjustLevel(self): 

449 """Test that we can adjust a background level""" 

450 sky = 100 

451 im = afwImage.ImageF(40, 40) 

452 im.set(sky) 

453 nx, ny = im.getWidth()//2, im.getHeight()//2 

454 bctrl = afwMath.BackgroundControl("LINEAR", nx, ny) 

455 bkd = afwMath.makeBackground(im, bctrl) 

456 

457 self.assertEqual( 

458 afwMath.makeStatistics(bkd.getImageF(), afwMath.MEAN).getValue(), 

459 sky) 

460 

461 delta = 123 

462 bkd += delta 

463 self.assertEqual( 

464 afwMath.makeStatistics(bkd.getImageF(), afwMath.MEAN).getValue(), 

465 sky + delta) 

466 bkd -= delta 

467 self.assertEqual(afwMath.makeStatistics(bkd.getImageF(), afwMath.MEAN).getValue(), 

468 sky) 

469 

470 def testNaNFromMaskedImage(self): 

471 """Check that an extensively masked image doesn't lead to NaNs in the background estimation""" 

472 image = afwImage.MaskedImageF(800, 800) 

473 msk = image.getMask() 

474 bbox = lsst.geom.BoxI(lsst.geom.PointI(560, 0), lsst.geom.PointI(799, 335)) 

475 smsk = msk.Factory(msk, bbox) 

476 smsk.set(msk.getPlaneBitMask("DETECTED")) 

477 

478 binSize = 256 

479 nx = image.getWidth()//binSize + 1 

480 ny = image.getHeight()//binSize + 1 

481 

482 sctrl = afwMath.StatisticsControl() 

483 sctrl.setAndMask(reduce(lambda x, y: x | image.getMask().getPlaneBitMask(y), 

484 ['EDGE', 'DETECTED', 'DETECTED_NEGATIVE'], 0x0)) 

485 

486 bctrl = afwMath.BackgroundControl(nx, ny, sctrl, "MEANCLIP") 

487 

488 bkgd = afwMath.makeBackground(image, bctrl) 

489 bkgdImage = bkgd.getImageF("NATURAL_SPLINE", "THROW_EXCEPTION") 

490 if debugMode: 

491 afwDisplay.Display(frame=0).mtv(image, title=self._testMethodName + " image") 

492 afwDisplay.Display(frame=1).mtv(bkgdImage, title=self._testMethodName + " bkgdImage") 

493 

494 self.assertFalse(np.isnan(bkgdImage[0, 0, afwImage.LOCAL])) 

495 

496 # Check that the non-string API works too 

497 bkgdImage = bkgd.getImageF( 

498 afwMath.Interpolate.NATURAL_SPLINE, afwMath.THROW_EXCEPTION) 

499 

500 def testBadAreaFailsSpline(self): 

501 """Check that a NaN in the stats image doesn't cause spline interpolation to fail (#2734)""" 

502 image = afwImage.ImageF(15, 9) 

503 for y in range(image.getHeight()): 

504 for x in range(image.getWidth()): 

505 # n.b. linear, which is what the interpolation will fall back 

506 # to 

507 image[x, y, afwImage.LOCAL] = 1 + 2*y 

508 

509 # Set the right corner to NaN. This will mean that we have too few 

510 # points for a spline interpolator 

511 binSize = 3 

512 image[-binSize:, -binSize:, afwImage.LOCAL] = np.nan 

513 

514 nx = image.getWidth()//binSize 

515 ny = image.getHeight()//binSize 

516 

517 sctrl = afwMath.StatisticsControl() 

518 bctrl = afwMath.BackgroundControl(nx, ny, sctrl, afwMath.MEANCLIP) 

519 

520 bkgd = afwMath.makeBackground(image, bctrl) 

521 if debugMode: 

522 afwDisplay.Display(frame=0).mtv(image, title=self._testMethodName + " image") 

523 afwDisplay.Display(frame=1).mtv(bkgd.getStatsImage(), 

524 title=self._testMethodName + " bkgd StatsImage") 

525 # Should throw if we don't permit REDUCE_INTERP_ORDER 

526 self.assertRaises(lsst.pex.exceptions.OutOfRangeError, 

527 bkgd.getImageF, afwMath.Interpolate.NATURAL_SPLINE) 

528 # The interpolation should fall back to linear for the right part of the image 

529 # where the NaNs don't permit spline interpolation (n.b. this happens 

530 # to be exact) 

531 bkgdImage = bkgd.getImageF( 

532 afwMath.Interpolate.NATURAL_SPLINE, afwMath.REDUCE_INTERP_ORDER) 

533 

534 if debugMode: 

535 afwDisplay.Display(frame=2).mtv(bkgdImage, title=self._testMethodName + " bkgdImage") 

536 

537 image -= bkgdImage 

538 self.assertEqual(afwMath.makeStatistics(image, afwMath.MEAN).getValue(), 

539 0.0) 

540 

541 def testBadPatch(self): 

542 """Test that a large bad patch of an image doesn't cause an absolute failure""" 

543 initialValue = 20 

544 mi = afwImage.MaskedImageF(500, 200) 

545 mi.set((initialValue, 0x0, 1.0)) 

546 mi.image[0:200, :] = np.nan 

547 badBits = mi.mask.getPlaneBitMask( 

548 ['EDGE', 'DETECTED', 'DETECTED_NEGATIVE']) 

549 mi.mask[0:400, :] |= badBits 

550 

551 if debugMode: 

552 afwDisplay.Display(frame=0).mtv(mi, title=self._testMethodName + " image") 

553 

554 sctrl = afwMath.StatisticsControl() 

555 sctrl.setAndMask(badBits) 

556 nx, ny = 17, 17 

557 bctrl = afwMath.BackgroundControl(nx, ny, sctrl, afwMath.MEANCLIP) 

558 

559 bkgd = afwMath.makeBackground(mi, bctrl) 

560 statsImage = bkgd.getStatsImage() 

561 if debugMode: 

562 afwDisplay.Display(frame=1).mtv(statsImage, title=self._testMethodName + " bkgd StatsImage") 

563 

564 # the test is that this doesn't fail if the bug (#2297) is fixed 

565 bkgdImage = bkgd.getImageF( 

566 afwMath.Interpolate.NATURAL_SPLINE, afwMath.REDUCE_INTERP_ORDER) 

567 self.assertEqual( 

568 np.mean(bkgdImage[0:100, 0:100].array), initialValue) 

569 if debugMode: 

570 afwDisplay.Display(frame=2).mtv(bkgdImage, title=self._testMethodName + " bkgdImage") 

571 # Check that we can fix the NaNs in the statsImage 

572 sim = statsImage.getImage().getArray() 

573 sim[np.isnan(sim)] = initialValue # replace NaN by initialValue 

574 bkgdImage = bkgd.getImageF( 

575 afwMath.Interpolate.NATURAL_SPLINE, afwMath.REDUCE_INTERP_ORDER) 

576 

577 self.assertAlmostEqual( 

578 np.mean(bkgdImage[0:100, 0:100].array, dtype=np.float64), 

579 initialValue) 

580 

581 def testBadRows(self): 

582 """Test that a bad set of rows in an image doesn't cause a failure""" 

583 initialValue = 20 

584 mi = afwImage.MaskedImageF(500, 200) 

585 mi.set((initialValue, 0x0, 1.0)) 

586 mi.image[:, 0:100] = np.nan 

587 badBits = mi.mask.getPlaneBitMask( 

588 ['EDGE', 'DETECTED', 'DETECTED_NEGATIVE']) 

589 mi.mask[0:400, :] |= badBits 

590 

591 if debugMode: 

592 afwDisplay.Display(frame=0).mtv(mi, title=self._testMethodName + " image") 

593 

594 sctrl = afwMath.StatisticsControl() 

595 sctrl.setAndMask(badBits) 

596 nx, ny = 17, 17 

597 bctrl = afwMath.BackgroundControl(nx, ny, sctrl, afwMath.MEANCLIP) 

598 

599 bkgd = afwMath.makeBackground(mi, bctrl) 

600 statsImage = bkgd.getStatsImage() 

601 if debugMode: 

602 afwDisplay.Display(frame=1).mtv(statsImage, title=self._testMethodName + " bkgd StatsImage") 

603 

604 # the test is that this doesn't fail if the bug (#2297) is fixed 

605 for frame, interpStyle in enumerate([afwMath.Interpolate.CONSTANT, afwMath.Interpolate.LINEAR, 

606 afwMath.Interpolate.NATURAL_SPLINE, 

607 afwMath.Interpolate.AKIMA_SPLINE], 2): 

608 bkgdImage = bkgd.getImageF( 

609 interpStyle, afwMath.REDUCE_INTERP_ORDER) 

610 self.assertEqual( 

611 np.mean(bkgdImage[0:100, 0:100].array), initialValue) 

612 if debugMode: 

613 afwDisplay.Display(frame=frame).mtv(bkgdImage, 

614 title=f"{self._testMethodName} bkgdImage: {interpStyle}") 

615 

616 def testBadImage(self): 

617 """Test that an entirely bad image doesn't cause an absolute failure""" 

618 initialValue = 20 

619 mi = afwImage.MaskedImageF(500, 200) 

620 # Check that no good values don't crash (they return NaN), and that a single good value 

621 # is enough to redeem the entire image 

622 for pix00 in [np.nan, initialValue]: 

623 mi.image[:] = np.nan 

624 mi.image[0, 0] = pix00 

625 

626 sctrl = afwMath.StatisticsControl() 

627 nx, ny = 17, 17 

628 bctrl = afwMath.BackgroundControl(nx, ny, sctrl, afwMath.MEANCLIP) 

629 

630 bkgd = afwMath.makeBackground(mi, bctrl) 

631 

632 for interpStyle in [afwMath.Interpolate.CONSTANT, afwMath.Interpolate.LINEAR, 

633 afwMath.Interpolate.NATURAL_SPLINE, afwMath.Interpolate.AKIMA_SPLINE]: 

634 # the test is that this doesn't fail if the bug (#2297) is 

635 # fixed 

636 bkgdImage = bkgd.getImageF( 

637 interpStyle, afwMath.REDUCE_INTERP_ORDER) 

638 val = np.mean(bkgdImage[0:100, 0:100].array) 

639 

640 if np.isfinite(pix00): 

641 self.assertEqual(val, pix00) 

642 else: 

643 self.assertTrue(np.isnan(val)) 

644 

645 def testBackgroundFromStatsImage(self): 

646 """Check that we can rebuild a Background from a BackgroundMI.getStatsImage()""" 

647 bgCtrl = afwMath.BackgroundControl(10, 10) 

648 bkgd = afwMath.makeBackground(self.image, bgCtrl) 

649 

650 interpStyle = afwMath.Interpolate.AKIMA_SPLINE 

651 undersampleStyle = afwMath.REDUCE_INTERP_ORDER 

652 bkgdImage = bkgd.getImageF(interpStyle, undersampleStyle) 

653 self.assertEqual(np.mean(bkgdImage.getArray()), self.val) 

654 self.assertEqual(interpStyle, bkgd.getAsUsedInterpStyle()) 

655 self.assertEqual(undersampleStyle, bkgd.getAsUsedUndersampleStyle()) 

656 

657 # OK, we have our background. Make a copy 

658 bkgd2 = afwMath.BackgroundMI( 

659 self.image.getBBox(), bkgd.getStatsImage()) 

660 del bkgd # we should be handling the memory correctly, but let's check 

661 bkgdImage2 = bkgd2.getImageF(interpStyle) 

662 

663 self.assertEqual(np.mean(bkgdImage2.getArray()), self.val) 

664 

665 def testBackgroundList(self): 

666 """Test that a BackgroundLists behaves like a list""" 

667 bgCtrl = afwMath.BackgroundControl(10, 10) 

668 interpStyle = afwMath.Interpolate.AKIMA_SPLINE 

669 undersampleStyle = afwMath.REDUCE_INTERP_ORDER 

670 approxStyle = afwMath.ApproximateControl.UNKNOWN 

671 approxOrderX = 0 

672 approxOrderY = 0 

673 approxWeighting = False 

674 

675 backgroundList = afwMath.BackgroundList() 

676 

677 for i in range(2): 

678 bkgd = afwMath.makeBackground(self.image, bgCtrl) 

679 if i == 0: 

680 # no need to call getImage 

681 backgroundList.append((bkgd, interpStyle, undersampleStyle, 

682 approxStyle, approxOrderX, approxOrderY, approxWeighting)) 

683 else: 

684 backgroundList.append(bkgd) 

685 

686 def assertBackgroundList(bgl): 

687 self.assertEqual(len(bgl), 2) # check that len() works 

688 for a in bgl: # check that we can iterate 

689 pass 

690 self.assertEqual(len(bgl[0]), 7) # check that we can index 

691 # check that we always have a tuple (bkgd, interp, under, 

692 # approxStyle, orderX, orderY, weighting) 

693 self.assertEqual(len(bgl[1]), 7) 

694 

695 assertBackgroundList(backgroundList) 

696 

697 # Check pickling 

698 new = pickle.loads(pickle.dumps(backgroundList)) 

699 assertBackgroundList(new) 

700 self.assertEqual(len(new), len(backgroundList)) 

701 for i, j in zip(new, backgroundList): 

702 self.assertBackgroundEqual(i[0], j[0]) 

703 self.assertEqual(i[1:], j[1:]) 

704 

705 def assertBackgroundEqual(self, lhs, rhs): 

706 lhsStats, rhsStats = lhs.getStatsImage(), rhs.getStatsImage() 

707 self.assertEqual(lhs.getImageBBox(), rhs.getImageBBox()) 

708 self.assertMaskedImagesEqual(lhsStats, rhsStats) 

709 lhsImage, rhsImage = lhs.getImageF("LINEAR"), rhs.getImageF("LINEAR") 

710 self.assertImagesEqual(lhsImage, rhsImage) 

711 

712 def testApproximate(self): 

713 """Test I/O for BackgroundLists with Approximate""" 

714 # approx and interp should be very close, but not the same 

715 img = self.getParabolaImage(256, 256) 

716 

717 # try regular interpolated image (the default) 

718 interpStyle = afwMath.Interpolate.AKIMA_SPLINE 

719 undersampleStyle = afwMath.REDUCE_INTERP_ORDER 

720 bgCtrl = afwMath.BackgroundControl(6, 6) 

721 bgCtrl.setInterpStyle(interpStyle) 

722 bgCtrl.setUndersampleStyle(undersampleStyle) 

723 bkgd = afwMath.makeBackground(img, bgCtrl) 

724 interpImage = bkgd.getImageF() 

725 

726 with lsst.utils.tests.getTempFilePath("_bgi.fits") as bgiFile, \ 

727 lsst.utils.tests.getTempFilePath("_bga.fits") as bgaFile: 

728 bglInterp = afwMath.BackgroundList() 

729 bglInterp.append((bkgd, interpStyle, undersampleStyle, 

730 afwMath.ApproximateControl.UNKNOWN, 0, 0, True)) 

731 bglInterp.writeFits(bgiFile) 

732 

733 # try an approx background 

734 approxStyle = afwMath.ApproximateControl.CHEBYSHEV 

735 approxOrder = 2 

736 actrl = afwMath.ApproximateControl(approxStyle, approxOrder) 

737 bkgd.getBackgroundControl().setApproximateControl(actrl) 

738 approxImage = bkgd.getImageF() 

739 bglApprox = afwMath.BackgroundList() 

740 bglApprox.append((bkgd, interpStyle, undersampleStyle, 

741 approxStyle, approxOrder, approxOrder, True)) 

742 bglApprox.writeFits(bgaFile) 

743 

744 # take a difference and make sure the two are very similar 

745 interpNp = interpImage.getArray() 

746 diff = np.abs(interpNp - approxImage.getArray())/interpNp 

747 

748 # the image and interp/approx parameters are chosen so these limits 

749 # will be greater than machine precision for float. The two methods 

750 # should be measurably different (so we know we're not just getting the 

751 # same thing from the getImage() method. But they should be very close 

752 # since they're both doing the same sort of thing. 

753 tolSame = 1.0e-3 # should be the same to this order 

754 tolDiff = 1.0e-4 # should be different here 

755 self.assertLess(diff.max(), tolSame) 

756 self.assertGreater(diff.max(), tolDiff) 

757 

758 # now see if we can reload them from files and get the same images 

759 # we wrote 

760 interpImage2 = afwMath.BackgroundList().readFits(bgiFile).getImage() 

761 approxImage2 = afwMath.BackgroundList().readFits(bgaFile).getImage() 

762 

763 idiff = interpImage.getArray() - interpImage2.getArray() 

764 adiff = approxImage.getArray() - approxImage2.getArray() 

765 self.assertEqual(idiff.max(), 0.0) 

766 self.assertEqual(adiff.max(), 0.0) 

767 

768 def testBackgroundListIO(self): 

769 """Test I/O for BackgroundLists""" 

770 bgCtrl = afwMath.BackgroundControl(10, 10) 

771 interpStyle = afwMath.Interpolate.AKIMA_SPLINE 

772 undersampleStyle = afwMath.REDUCE_INTERP_ORDER 

773 approxOrderX = 6 

774 approxOrderY = 6 

775 approxWeighting = True 

776 

777 im = self.image.Factory( 

778 self.image, self.image.getBBox()) 

779 arr = im.getArray() 

780 arr += np.random.normal(size=(im.getHeight(), im.getWidth())) 

781 

782 for astyle in afwMath.ApproximateControl.UNKNOWN, afwMath.ApproximateControl.CHEBYSHEV: 

783 actrl = afwMath.ApproximateControl(astyle, approxOrderX) 

784 bgCtrl.setApproximateControl(actrl) 

785 

786 backgroundList = afwMath.BackgroundList() 

787 backImage = afwImage.ImageF(im.getDimensions()) 

788 for i in range(2): 

789 bkgd = afwMath.makeBackground(im, bgCtrl) 

790 if i == 0: 

791 # no need to call getImage 

792 backgroundList.append((bkgd, interpStyle, undersampleStyle, 

793 astyle, approxOrderX, approxOrderY, approxWeighting)) 

794 else: 

795 backgroundList.append(bkgd) 

796 

797 backImage += bkgd.getImageF(interpStyle, undersampleStyle) 

798 

799 with lsst.utils.tests.getTempFilePath(".fits") as fileName: 

800 backgroundList.writeFits(fileName) 

801 

802 backgrounds = afwMath.BackgroundList.readFits(fileName) 

803 

804 img = backgrounds.getImage() 

805 # Check that the read-back image is identical to that generated from the backgroundList 

806 # round-tripped to disk 

807 backImage -= img 

808 

809 self.assertEqual(np.min(backImage.getArray()), 0.0) 

810 self.assertEqual(np.max(backImage.getArray()), 0.0) 

811 

812 

813class MemoryTester(lsst.utils.tests.MemoryTestCase): 

814 pass 

815 

816 

817def setup_module(module): 

818 lsst.utils.tests.init() 

819 

820 

821if __name__ == "__main__": 821 ↛ 822line 821 didn't jump to line 822, because the condition on line 821 was never true

822 lsst.utils.tests.init() 

823 unittest.main()