Coverage for tests/test_background.py: 10%
505 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-03 02:47 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-03 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/>.
22import math
23import os.path
24import unittest
25import pickle
26from functools import reduce
28import numpy as np
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
39# Set to True to display debug messages and images
40debugMode = False
42afwDisplay.setDefaultMaskTransparency(75)
43try:
44 AfwdataDir = lsst.utils.getPackageDir("afwdata")
45except LookupError:
46 AfwdataDir = None
49class BackgroundTestCase(lsst.utils.tests.TestCase):
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)
58 def tearDown(self):
59 del self.image
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()
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)
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])
98 for imginfo in imginfolist:
99 imgfile, centerValue = imginfo
100 imgPath = os.path.join(AfwdataDir, "Statistics", imgfile)
102 # get the image and header
103 dimg = afwImage.DecoratedImageF(imgPath)
104 img = dimg.getImage()
105 fitsHdr = dimg.getMetadata() # the FITS header
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()
113 # create a background control object
114 bctrl = afwMath.BackgroundControl(afwMath.Interpolate.AKIMA_SPLINE)
115 bctrl.setNxSample(5)
116 bctrl.setNySample(5)
118 # run the background constructor and call the getImage() function.
119 backobj = afwMath.makeBackground(img, bctrl)
121 pixPerSubimage = img.getWidth()*img.getHeight() / \
122 (bctrl.getNxSample()*bctrl.getNySample())
123 stdevInterp = reqStdev/math.sqrt(pixPerSubimage)
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)
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
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)
154 if debugMode:
155 print(rampimg.getArray())
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
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)
183 # Test pickle
184 new = pickle.loads(pickle.dumps(backobj))
185 self.assertBackgroundEqual(backobj, new)
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)
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
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")
216 # note: by default undersampleStyle is THROW_EXCEPTION
217 bctrl.setUndersampleStyle(afwMath.REDUCE_INTERP_ORDER)
219 nx = int(mimg.getWidth()/binsize) + 1
220 ny = int(mimg.getHeight()/binsize) + 1
222 bctrl.setNxSample(nx)
223 bctrl.setNySample(ny)
224 image = mimg.getImage()
225 backobj = afwMath.makeBackground(image, bctrl)
226 image -= backobj.getImageF()
228 def testTicket1781(self):
229 """Test an unusual-sized image"""
230 nx = 526
231 ny = 154
233 parabimg = self.getParabolaImage(nx, ny)
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)
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
247 parabimg = self.getParabolaImage(nx, ny)
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)
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)
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)
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)
286 if debugMode:
287 afwDisplay.Display(frame=0).mtv(mi, title=self._testMethodName + " image")
289 im = mi.getImage()
290 im -= backobj.getImageF()
292 if debugMode:
293 afwDisplay.Display(frame=1).mtv(mi, title=self._testMethodName + " image-back")
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)
303 @unittest.skipIf(AfwdataDir is None, "afwdata not setup")
304 def testXY0(self):
305 """Test fitting the background to an image with nonzero xy0
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)
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)
324 statsImage = backobj.getStatsImage()
325 statsImageList.append(statsImage)
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])
334 @unittest.skipIf(AfwdataDir is None, "afwdata not setup")
335 def testSubImage(self):
336 """Test getImage on a subregion of the full background image
338 Using real image data is a cheap way to get a variable background
339 """
340 mi = self.getCfhtImage()
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))
348 bgFullImage = backobj.getImageF()
349 self.assertEqual(bgFullImage.getBBox(), mi.getBBox())
351 subFullArr = afwImage.ImageF(bgFullImage, subBBox).getArray()
353 bgSubImage = backobj.getImageF(subBBox, bctrl.getInterpStyle())
354 subArr = bgSubImage.getArray()
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)
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()
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)
371 if debugMode:
372 afwDisplay.Display(frame=0).mtv(mi, title=self._testMethodName + " image")
374 im = mi.getImage()
375 im -= backobj.getImageF("AKIMA_SPLINE")
377 if debugMode:
378 afwDisplay.Display(frame=1).mtv(mi, title=self._testMethodName + " image-back")
380 statsImage = backobj.getStatsImage()
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")
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))
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)
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)
410 # put interp style back up to cspline and see if it throws an exception
411 bctrl.setUndersampleStyle("THROW_EXCEPTION")
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)
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)
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
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)
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)
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)
457 self.assertEqual(
458 afwMath.makeStatistics(bkd.getImageF(), afwMath.MEAN).getValue(),
459 sky)
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)
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"))
478 binSize = 256
479 nx = image.getWidth()//binSize + 1
480 ny = image.getHeight()//binSize + 1
482 sctrl = afwMath.StatisticsControl()
483 sctrl.setAndMask(reduce(lambda x, y: x | image.getMask().getPlaneBitMask(y),
484 ['EDGE', 'DETECTED', 'DETECTED_NEGATIVE'], 0x0))
486 bctrl = afwMath.BackgroundControl(nx, ny, sctrl, "MEANCLIP")
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")
494 self.assertFalse(np.isnan(bkgdImage[0, 0, afwImage.LOCAL]))
496 # Check that the non-string API works too
497 bkgdImage = bkgd.getImageF(
498 afwMath.Interpolate.NATURAL_SPLINE, afwMath.THROW_EXCEPTION)
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
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
514 nx = image.getWidth()//binSize
515 ny = image.getHeight()//binSize
517 sctrl = afwMath.StatisticsControl()
518 bctrl = afwMath.BackgroundControl(nx, ny, sctrl, afwMath.MEANCLIP)
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)
534 if debugMode:
535 afwDisplay.Display(frame=2).mtv(bkgdImage, title=self._testMethodName + " bkgdImage")
537 image -= bkgdImage
538 self.assertEqual(afwMath.makeStatistics(image, afwMath.MEAN).getValue(),
539 0.0)
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
551 if debugMode:
552 afwDisplay.Display(frame=0).mtv(mi, title=self._testMethodName + " image")
554 sctrl = afwMath.StatisticsControl()
555 sctrl.setAndMask(badBits)
556 nx, ny = 17, 17
557 bctrl = afwMath.BackgroundControl(nx, ny, sctrl, afwMath.MEANCLIP)
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")
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)
577 self.assertAlmostEqual(
578 np.mean(bkgdImage[0:100, 0:100].array, dtype=np.float64),
579 initialValue)
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
591 if debugMode:
592 afwDisplay.Display(frame=0).mtv(mi, title=self._testMethodName + " image")
594 sctrl = afwMath.StatisticsControl()
595 sctrl.setAndMask(badBits)
596 nx, ny = 17, 17
597 bctrl = afwMath.BackgroundControl(nx, ny, sctrl, afwMath.MEANCLIP)
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")
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}")
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
626 sctrl = afwMath.StatisticsControl()
627 nx, ny = 17, 17
628 bctrl = afwMath.BackgroundControl(nx, ny, sctrl, afwMath.MEANCLIP)
630 bkgd = afwMath.makeBackground(mi, bctrl)
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)
640 if np.isfinite(pix00):
641 self.assertEqual(val, pix00)
642 else:
643 self.assertTrue(np.isnan(val))
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)
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())
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)
663 self.assertEqual(np.mean(bkgdImage2.getArray()), self.val)
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
675 backgroundList = afwMath.BackgroundList()
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)
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)
695 assertBackgroundList(backgroundList)
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:])
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)
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)
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()
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)
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)
744 # take a difference and make sure the two are very similar
745 interpNp = interpImage.getArray()
746 diff = np.abs(interpNp - approxImage.getArray())/interpNp
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)
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()
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)
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
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()))
782 for astyle in afwMath.ApproximateControl.UNKNOWN, afwMath.ApproximateControl.CHEBYSHEV:
783 actrl = afwMath.ApproximateControl(astyle, approxOrderX)
784 bgCtrl.setApproximateControl(actrl)
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)
797 backImage += bkgd.getImageF(interpStyle, undersampleStyle)
799 with lsst.utils.tests.getTempFilePath(".fits") as fileName:
800 backgroundList.writeFits(fileName)
802 backgrounds = afwMath.BackgroundList.readFits(fileName)
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
809 self.assertEqual(np.min(backImage.getArray()), 0.0)
810 self.assertEqual(np.max(backImage.getArray()), 0.0)
813class MemoryTester(lsst.utils.tests.MemoryTestCase):
814 pass
817def setup_module(module):
818 lsst.utils.tests.init()
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()