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# 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 

38import lsst.pex.exceptions as pexExcept 

39 

40# Set to True to display debug messages and images 

41debugMode = False 

42 

43afwDisplay.setDefaultMaskTransparency(75) 

44try: 

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

46except pexExcept.NotFoundError: 

47 AfwdataDir = None 

48 

49 

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

51 

52 def setUp(self): 

53 np.random.seed(1) 

54 self.val = 10 

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

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

57 self.image.set(self.val) 

58 

59 def tearDown(self): 

60 del self.image 

61 

62 def testOddSize(self): 

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

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

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

66 subimage boundaries are spread more evenly so the last pixels 

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

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

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

70 just puts a limit on the errors. 

71 """ 

72 W, H = 2, 99 

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

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

75 bgCtrl.setNxSample(2) 

76 NY = 10 

77 bgCtrl.setNySample(NY) 

78 for y in range(H): 

79 for x in range(W): 

80 B = 89 

81 if y < B: 

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

83 else: 

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

85 bobj = afwMath.makeBackground(image, bgCtrl) 

86 back = bobj.getImageF() 

87 

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

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

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

91 

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

93 def testBackgroundTestImages(self): 

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

95 imginfolist = [] 

96 # cooked to known value 

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

98 

99 for imginfo in imginfolist: 

100 imgfile, centerValue = imginfo 

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

102 

103 # get the image and header 

104 dimg = afwImage.DecoratedImageF(imgPath) 

105 img = dimg.getImage() 

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

107 

108 # get the True values of the mean and stdev 

109 reqMean = fitsHdr.getAsDouble("MEANREQ") 

110 reqStdev = fitsHdr.getAsDouble("SIGREQ") 

111 naxis1 = img.getWidth() 

112 naxis2 = img.getHeight() 

113 

114 # create a background control object 

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

116 bctrl.setNxSample(5) 

117 bctrl.setNySample(5) 

118 

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

120 backobj = afwMath.makeBackground(img, bctrl) 

121 

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

123 (bctrl.getNxSample()*bctrl.getNySample()) 

124 stdevInterp = reqStdev/math.sqrt(pixPerSubimage) 

125 

126 # test getImage() by checking the center pixel 

127 bimg = backobj.getImageF() 

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

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

130 

131 def testRamp(self): 

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

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

134 # image 

135 nx = 512 

136 ny = 512 

137 x0, y0 = 9876, 54321 

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

139 rampimg = afwImage.ImageF(box) 

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

141 for x in range(nx): 

142 for y in range(ny): 

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

144 

145 # check corner, edge, and center pixels 

146 bctrl = afwMath.BackgroundControl(10, 10) 

147 bctrl.setInterpStyle(afwMath.Interpolate.CUBIC_SPLINE) 

148 bctrl.setNxSample(6) 

149 bctrl.setNySample(6) 

150 # large enough to entirely avoid clipping 

151 bctrl.getStatisticsControl().setNumSigmaClip(20.0) 

152 bctrl.getStatisticsControl().setNumIter(1) 

153 backobj = afwMath.makeBackground(rampimg, bctrl) 

154 

155 if debugMode: 

156 print(rampimg.getArray()) 

157 

158 frame = 1 

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

160 diff = backobj.getImageF(interp) 

161 if debugMode: 

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

163 frame += 1 

164 diff -= rampimg 

165 if debugMode: 

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

167 if debugMode: 

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

169 frame += 1 

170 if debugMode: 

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

172 frame += 1 

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

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

175 frame += 1 

176 

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

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

179 for xpix in xpixels: 

180 for ypix in ypixels: 

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

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

183 

184 # Test pickle 

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

186 self.assertBackgroundEqual(backobj, new) 

187 

188 # Check creation of sub-image 

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

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

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

192 bgImage = backobj.getImageF("AKIMA_SPLINE") 

193 bgSubImage = afwImage.ImageF(bgImage, box) 

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

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

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

197 self.assertImagesEqual(testImage, bgSubImage) 

198 

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

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

201 """ 

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

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

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

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

206 return parabimg 

207 

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

209 def testTicket987(self): 

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

211 imagePath = os.path.join( 

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

213 mimg = afwImage.MaskedImageF(imagePath) 

214 binsize = 512 

215 bctrl = afwMath.BackgroundControl("NATURAL_SPLINE") 

216 

217 # note: by default undersampleStyle is THROW_EXCEPTION 

218 bctrl.setUndersampleStyle(afwMath.REDUCE_INTERP_ORDER) 

219 

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

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

222 

223 bctrl.setNxSample(nx) 

224 bctrl.setNySample(ny) 

225 image = mimg.getImage() 

226 backobj = afwMath.makeBackground(image, bctrl) 

227 image -= backobj.getImageF() 

228 

229 def testTicket1781(self): 

230 """Test an unusual-sized image""" 

231 nx = 526 

232 ny = 154 

233 

234 parabimg = self.getParabolaImage(nx, ny) 

235 

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

237 bctrl.setNxSample(16) 

238 bctrl.setNySample(4) 

239 bctrl.getStatisticsControl().setNumSigmaClip(10.0) 

240 bctrl.getStatisticsControl().setNumIter(1) 

241 afwMath.makeBackground(parabimg, bctrl) 

242 

243 def testParabola(self): 

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

245 nx = 512 

246 ny = 512 

247 

248 parabimg = self.getParabolaImage(nx, ny) 

249 

250 # check corner, edge, and center pixels 

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

252 bctrl.setNxSample(24) 

253 bctrl.setNySample(24) 

254 bctrl.getStatisticsControl().setNumSigmaClip(10.0) 

255 bctrl.getStatisticsControl().setNumIter(1) 

256 backobj = afwMath.makeBackground(parabimg, bctrl) 

257 

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

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

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

261 for xpix in xpixels: 

262 for ypix in ypixels: 

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

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

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

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

267 # is a fair (if arbitrary) test. 

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

269 

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

271 def testCFHT_oldAPI(self): 

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

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

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

275 mi = mi.Factory(mi, 

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

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

278 afwImage.LOCAL) 

279 

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

281 bctrl.setNxSample(16) 

282 bctrl.setNySample(16) 

283 bctrl.getStatisticsControl().setNumSigmaClip(3.0) 

284 bctrl.getStatisticsControl().setNumIter(2) 

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

286 

287 if debugMode: 

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

289 

290 im = mi.getImage() 

291 im -= backobj.getImageF() 

292 

293 if debugMode: 

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

295 

296 def getCfhtImage(self): 

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

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

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

300 imagePath = os.path.join( 

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

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

303 

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

305 def testXY0(self): 

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

307 

308 The statsImage and background image should not vary with xy0 

309 """ 

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

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

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

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

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

315 mi = self.getCfhtImage() 

316 mi.setXY0(xy0) 

317 

318 bctrl = afwMath.BackgroundControl( 

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

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

321 bgImage = backobj.getImageF() 

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

323 bgImageList.append(bgImage) 

324 

325 statsImage = backobj.getStatsImage() 

326 statsImageList.append(statsImage) 

327 

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

329 # so compare pixels using exact equality 

330 for bgImage in bgImageList[1:]: 

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

332 for statsImage in statsImageList[1:]: 

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

334 

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

336 def testSubImage(self): 

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

338 

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

340 """ 

341 mi = self.getCfhtImage() 

342 

343 bctrl = afwMath.BackgroundControl( 

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

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

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

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

348 

349 bgFullImage = backobj.getImageF() 

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

351 

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

353 

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

355 subArr = bgSubImage.getArray() 

356 

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

358 # that; close is good enough 

359 self.assertFloatsEqual(subArr, subFullArr) 

360 

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

362 def testCFHT(self): 

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

364 mi = self.getCfhtImage() 

365 

366 bctrl = afwMath.BackgroundControl( 

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

368 bctrl.getStatisticsControl().setNumSigmaClip(3.0) 

369 bctrl.getStatisticsControl().setNumIter(2) 

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

371 

372 if debugMode: 

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

374 

375 im = mi.getImage() 

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

377 

378 if debugMode: 

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

380 

381 statsImage = backobj.getStatsImage() 

382 

383 if debugMode: 

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

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

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

387 

388 def testUndersample(self): 

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

390 nx = 64 

391 ny = 64 

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

393 

394 # make a background control object 

395 bctrl = afwMath.BackgroundControl(10, 10) 

396 bctrl.setInterpStyle(afwMath.Interpolate.CUBIC_SPLINE) 

397 bctrl.setNxSample(3) 

398 bctrl.setNySample(3) 

399 

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

401 # linear 

402 bctrl.setNxSample(2) 

403 bctrl.setNySample(2) 

404 bctrl.setUndersampleStyle("REDUCE_INTERP_ORDER") 

405 backobj = afwMath.makeBackground(img, bctrl) 

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

407 backobj.getImageF() 

408 self.assertEqual(backobj.getAsUsedInterpStyle(), 

409 afwMath.Interpolate.LINEAR) 

410 

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

412 bctrl.setUndersampleStyle("THROW_EXCEPTION") 

413 

414 def tst(img, bctrl): 

415 backobj = afwMath.makeBackground(img, bctrl) 

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

417 backobj.getImageF("CUBIC_SPLINE") 

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

419 tst, img, bctrl) 

420 

421 def testOnlyOneGridCell(self): 

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

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

424 nx = 64 

425 ny = 64 

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

427 

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

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

430 for x in range(nx): 

431 for y in range(ny): 

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

433 

434 # make a background control object 

435 bctrl = afwMath.BackgroundControl(10, 10) 

436 bctrl.setInterpStyle(afwMath.Interpolate.CONSTANT) 

437 bctrl.setNxSample(1) 

438 bctrl.setNySample(1) 

439 bctrl.setUndersampleStyle(afwMath.THROW_EXCEPTION) 

440 backobj = afwMath.makeBackground(img, bctrl) 

441 

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

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

444 for xpix in xpixels: 

445 for ypix in ypixels: 

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

447 self.assertAlmostEqual(testval/mean, 1) 

448 

449 def testAdjustLevel(self): 

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

451 sky = 100 

452 im = afwImage.ImageF(40, 40) 

453 im.set(sky) 

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

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

456 bkd = afwMath.makeBackground(im, bctrl) 

457 

458 self.assertEqual( 

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

460 sky) 

461 

462 delta = 123 

463 bkd += delta 

464 self.assertEqual( 

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

466 sky + delta) 

467 bkd -= delta 

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

469 sky) 

470 

471 def testNaNFromMaskedImage(self): 

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

473 image = afwImage.MaskedImageF(800, 800) 

474 msk = image.getMask() 

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

476 smsk = msk.Factory(msk, bbox) 

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

478 

479 binSize = 256 

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

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

482 

483 sctrl = afwMath.StatisticsControl() 

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

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

486 

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

488 

489 bkgd = afwMath.makeBackground(image, bctrl) 

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

491 if debugMode: 

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

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

494 

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

496 

497 # Check that the non-string API works too 

498 bkgdImage = bkgd.getImageF( 

499 afwMath.Interpolate.NATURAL_SPLINE, afwMath.THROW_EXCEPTION) 

500 

501 def testBadAreaFailsSpline(self): 

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

503 image = afwImage.ImageF(15, 9) 

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

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

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

507 # to 

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

509 

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

511 # points for a spline interpolator 

512 binSize = 3 

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

514 

515 nx = image.getWidth()//binSize 

516 ny = image.getHeight()//binSize 

517 

518 sctrl = afwMath.StatisticsControl() 

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

520 

521 bkgd = afwMath.makeBackground(image, bctrl) 

522 if debugMode: 

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

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

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

526 # Should throw if we don't permit REDUCE_INTERP_ORDER 

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

528 bkgd.getImageF, afwMath.Interpolate.NATURAL_SPLINE) 

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

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

531 # to be exact) 

532 bkgdImage = bkgd.getImageF( 

533 afwMath.Interpolate.NATURAL_SPLINE, afwMath.REDUCE_INTERP_ORDER) 

534 

535 if debugMode: 

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

537 

538 image -= bkgdImage 

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

540 0.0) 

541 

542 def testBadPatch(self): 

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

544 initialValue = 20 

545 mi = afwImage.MaskedImageF(500, 200) 

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

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

548 badBits = mi.mask.getPlaneBitMask( 

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

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

551 

552 if debugMode: 

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

554 

555 sctrl = afwMath.StatisticsControl() 

556 sctrl.setAndMask(badBits) 

557 nx, ny = 17, 17 

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

559 

560 bkgd = afwMath.makeBackground(mi, bctrl) 

561 statsImage = bkgd.getStatsImage() 

562 if debugMode: 

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

564 

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

566 bkgdImage = bkgd.getImageF( 

567 afwMath.Interpolate.NATURAL_SPLINE, afwMath.REDUCE_INTERP_ORDER) 

568 self.assertEqual( 

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

570 if debugMode: 

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

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

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

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

575 bkgdImage = bkgd.getImageF( 

576 afwMath.Interpolate.NATURAL_SPLINE, afwMath.REDUCE_INTERP_ORDER) 

577 

578 self.assertAlmostEqual( 

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

580 initialValue) 

581 

582 def testBadRows(self): 

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

584 initialValue = 20 

585 mi = afwImage.MaskedImageF(500, 200) 

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

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

588 badBits = mi.mask.getPlaneBitMask( 

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

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

591 

592 if debugMode: 

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

594 

595 sctrl = afwMath.StatisticsControl() 

596 sctrl.setAndMask(badBits) 

597 nx, ny = 17, 17 

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

599 

600 bkgd = afwMath.makeBackground(mi, bctrl) 

601 statsImage = bkgd.getStatsImage() 

602 if debugMode: 

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

604 

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

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

607 afwMath.Interpolate.NATURAL_SPLINE, 

608 afwMath.Interpolate.AKIMA_SPLINE], 2): 

609 bkgdImage = bkgd.getImageF( 

610 interpStyle, afwMath.REDUCE_INTERP_ORDER) 

611 self.assertEqual( 

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

613 if debugMode: 

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

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

616 

617 def testBadImage(self): 

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

619 initialValue = 20 

620 mi = afwImage.MaskedImageF(500, 200) 

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

622 # is enough to redeem the entire image 

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

624 mi.image[:] = np.nan 

625 mi.image[0, 0] = pix00 

626 

627 sctrl = afwMath.StatisticsControl() 

628 nx, ny = 17, 17 

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

630 

631 bkgd = afwMath.makeBackground(mi, bctrl) 

632 

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

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

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

636 # fixed 

637 bkgdImage = bkgd.getImageF( 

638 interpStyle, afwMath.REDUCE_INTERP_ORDER) 

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

640 

641 if np.isfinite(pix00): 

642 self.assertEqual(val, pix00) 

643 else: 

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

645 

646 def testBackgroundFromStatsImage(self): 

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

648 bgCtrl = afwMath.BackgroundControl(10, 10) 

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

650 

651 interpStyle = afwMath.Interpolate.AKIMA_SPLINE 

652 undersampleStyle = afwMath.REDUCE_INTERP_ORDER 

653 bkgdImage = bkgd.getImageF(interpStyle, undersampleStyle) 

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

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

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

657 

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

659 bkgd2 = afwMath.BackgroundMI( 

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

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

662 bkgdImage2 = bkgd2.getImageF(interpStyle) 

663 

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

665 

666 def testBackgroundList(self): 

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

668 bgCtrl = afwMath.BackgroundControl(10, 10) 

669 interpStyle = afwMath.Interpolate.AKIMA_SPLINE 

670 undersampleStyle = afwMath.REDUCE_INTERP_ORDER 

671 approxStyle = afwMath.ApproximateControl.UNKNOWN 

672 approxOrderX = 0 

673 approxOrderY = 0 

674 approxWeighting = False 

675 

676 backgroundList = afwMath.BackgroundList() 

677 

678 for i in range(2): 

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

680 if i == 0: 

681 # no need to call getImage 

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

683 approxStyle, approxOrderX, approxOrderY, approxWeighting)) 

684 else: 

685 backgroundList.append(bkgd) 

686 

687 def assertBackgroundList(bgl): 

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

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

690 pass 

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

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

693 # approxStyle, orderX, orderY, weighting) 

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

695 

696 assertBackgroundList(backgroundList) 

697 

698 # Check pickling 

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

700 assertBackgroundList(new) 

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

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

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

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

705 

706 def assertBackgroundEqual(self, lhs, rhs): 

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

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

709 self.assertMaskedImagesEqual(lhsStats, rhsStats) 

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

711 self.assertImagesEqual(lhsImage, rhsImage) 

712 

713 def testApproximate(self): 

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

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

716 img = self.getParabolaImage(256, 256) 

717 

718 # try regular interpolated image (the default) 

719 interpStyle = afwMath.Interpolate.AKIMA_SPLINE 

720 undersampleStyle = afwMath.REDUCE_INTERP_ORDER 

721 bgCtrl = afwMath.BackgroundControl(6, 6) 

722 bgCtrl.setInterpStyle(interpStyle) 

723 bgCtrl.setUndersampleStyle(undersampleStyle) 

724 bkgd = afwMath.makeBackground(img, bgCtrl) 

725 interpImage = bkgd.getImageF() 

726 

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

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

729 bglInterp = afwMath.BackgroundList() 

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

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

732 bglInterp.writeFits(bgiFile) 

733 

734 # try an approx background 

735 approxStyle = afwMath.ApproximateControl.CHEBYSHEV 

736 approxOrder = 2 

737 actrl = afwMath.ApproximateControl(approxStyle, approxOrder) 

738 bkgd.getBackgroundControl().setApproximateControl(actrl) 

739 approxImage = bkgd.getImageF() 

740 bglApprox = afwMath.BackgroundList() 

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

742 approxStyle, approxOrder, approxOrder, True)) 

743 bglApprox.writeFits(bgaFile) 

744 

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

746 interpNp = interpImage.getArray() 

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

748 

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

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

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

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

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

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

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

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

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

758 

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

760 # we wrote 

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

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

763 

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

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

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

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

768 

769 def testBackgroundListIO(self): 

770 """Test I/O for BackgroundLists""" 

771 bgCtrl = afwMath.BackgroundControl(10, 10) 

772 interpStyle = afwMath.Interpolate.AKIMA_SPLINE 

773 undersampleStyle = afwMath.REDUCE_INTERP_ORDER 

774 approxOrderX = 6 

775 approxOrderY = 6 

776 approxWeighting = True 

777 

778 im = self.image.Factory( 

779 self.image, self.image.getBBox()) 

780 arr = im.getArray() 

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

782 

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

784 actrl = afwMath.ApproximateControl(astyle, approxOrderX) 

785 bgCtrl.setApproximateControl(actrl) 

786 

787 backgroundList = afwMath.BackgroundList() 

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

789 for i in range(2): 

790 bkgd = afwMath.makeBackground(im, bgCtrl) 

791 if i == 0: 

792 # no need to call getImage 

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

794 astyle, approxOrderX, approxOrderY, approxWeighting)) 

795 else: 

796 backgroundList.append(bkgd) 

797 

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

799 

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

801 backgroundList.writeFits(fileName) 

802 

803 backgrounds = afwMath.BackgroundList.readFits(fileName) 

804 

805 img = backgrounds.getImage() 

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

807 # round-tripped to disk 

808 backImage -= img 

809 

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

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

812 

813 

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

815 pass 

816 

817 

818def setup_module(module): 

819 lsst.utils.tests.init() 

820 

821 

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

823 lsst.utils.tests.init() 

824 unittest.main()