Coverage for tests/test_measure.py: 18%
236 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-04 10:06 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-04 10:06 +0000
1# This file is part of meas_algorithms.
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 os
23import unittest
24import math
25import warnings
27import lsst.geom
28import lsst.afw.detection as afwDetection
29import lsst.afw.image as afwImage
30import lsst.afw.math as afwMath
31import lsst.afw.table as afwTable
32from lsst.log import Log
33import lsst.meas.base as measBase
34import lsst.meas.algorithms as algorithms
35import lsst.meas.algorithms.testUtils as testUtils
36import lsst.pex.config as pexConfig
37import lsst.utils.tests
39# Change the level to Log.DEBUG or Log.TRACE to see debug messages
40Log.getLogger("lsst.measurement").setLevel(Log.INFO)
42try:
43 type(display)
44except NameError:
45 display = False
46else:
47 import lsst.afw.display as afwDisplay
48 afwDisplay.setDefaultMaskTransparency(75)
50# Determine if we have afwdata
51try:
52 afwdataDir = lsst.utils.getPackageDir('afwdata')
53except Exception:
54 afwdataDir = None
57def toString(*args):
58 """toString written in python"""
59 if len(args) == 1:
60 args = args[0]
62 y, x0, x1 = args
63 return "%d: %d..%d" % (y, x0, x1)
66class MeasureTestCase(lsst.utils.tests.TestCase):
67 """A test case for Measure"""
68 class Object:
70 def __init__(self, val, spans):
71 self.val = val
72 self.spans = spans
74 def insert(self, im, dx=0, dy=0):
75 """Insert self into an image"""
76 for sp in self.spans:
77 y, x0, x1 = sp
78 for x in range(x0, x1 + 1):
79 im[x + dx, y + dy, afwImage.LOCAL] = self.val
81 def __eq__(self, other):
82 for osp, sp in zip(other.getSpans(), self.spans):
83 if osp.toString() != toString(sp):
84 return False
86 return True
88 def setUp(self):
89 ms = afwImage.MaskedImageF(lsst.geom.ExtentI(31, 27))
90 ms.getVariance().set(1)
91 bbox = lsst.geom.BoxI(lsst.geom.PointI(1, 1), lsst.geom.ExtentI(24, 20))
92 self.mi = afwImage.MaskedImageF(ms, bbox, afwImage.LOCAL)
93 self.exposure = afwImage.makeExposure(self.mi)
94 im = self.mi.getImage()
95 #
96 # Objects that we should detect. These are coordinates in the subimage
97 #
98 self.objects = []
99 self.objects += [self.Object(10, [(1, 4, 4), (2, 3, 5), (3, 4, 4)])]
100 self.objects += [self.Object(20, [(5, 7, 8), (5, 10, 10), (6, 8, 9)])]
101 self.objects += [self.Object(20, [(8, 3, 3)])]
103 im.set(0) # clear image
104 for obj in self.objects:
105 obj.insert(im, 5, 5)
106 #
107 # Add a few more pixels to make peaks that we can centroid around
108 #
109 for x, y in [(9, 7), (13, 11)]:
110 im[x, y, afwImage.LOCAL] += 1
112 def tearDown(self):
113 del self.mi
114 del self.exposure
116 def testFootprintsMeasure(self):
117 """Check that we can measure the objects in a detectionSet"""
119 xcentroid = [10.0, 14.0, 9.0]
120 ycentroid = [8.0, 11.5061728, 14.0]
121 flux = [51.0, 101.0, 20.0]
122 # sqrt of num pixels in aperture; note the second source is offset
123 # from the pixel grid.
124 fluxErr = [math.sqrt(29), math.sqrt(26), math.sqrt(29)]
126 footprints = afwDetection.FootprintSet(self.mi, afwDetection.Threshold(10), "DETECTED")
128 if display:
129 disp = afwDisplay.Display(frame=0)
130 disp.mtv(self.mi, title=self._testMethodName + ": image")
131 afwDisplay.Display(frame=1).mtv(self.mi.getVariance(), title=self._testMethodName + ": variance")
133 measureSourcesConfig = measBase.SingleFrameMeasurementConfig()
134 measureSourcesConfig.algorithms["base_CircularApertureFlux"].radii = [3.0]
135 # Numerical tests below assumes that we are not using sinc fluxes.
136 measureSourcesConfig.algorithms["base_CircularApertureFlux"].maxSincRadius = 0.0
137 measureSourcesConfig.algorithms.names = ["base_NaiveCentroid", "base_SdssShape", "base_PsfFlux",
138 "base_CircularApertureFlux"]
139 measureSourcesConfig.slots.centroid = "base_NaiveCentroid"
140 measureSourcesConfig.slots.psfFlux = "base_PsfFlux"
141 measureSourcesConfig.slots.apFlux = "base_CircularApertureFlux_3_0"
142 measureSourcesConfig.slots.modelFlux = None
143 measureSourcesConfig.slots.gaussianFlux = None
144 measureSourcesConfig.slots.calibFlux = None
146 schema = afwTable.SourceTable.makeMinimalSchema()
147 task = measBase.SingleFrameMeasurementTask(schema, config=measureSourcesConfig)
148 measCat = afwTable.SourceCatalog(schema)
149 footprints.makeSources(measCat)
150 # now run the SFM task with the test plugin
151 sigma = 1e-10
152 psf = algorithms.DoubleGaussianPsf(11, 11, sigma) # i.e. a single pixel
153 self.exposure.setPsf(psf)
154 task.run(measCat, self.exposure)
156 self.assertEqual(len(measCat), len(flux))
157 for i, source in enumerate(measCat):
159 xc, yc = source.getX(), source.getY()
160 if display:
161 disp.dot("+", xc, yc)
163 self.assertAlmostEqual(source.getX(), xcentroid[i], 6)
164 self.assertAlmostEqual(source.getY(), ycentroid[i], 6)
165 self.assertEqual(source.getApInstFlux(), flux[i])
166 self.assertAlmostEqual(source.getApInstFluxErr(), fluxErr[i], 6)
168 # We're using a delta-function PSF, so the psfFlux should be the
169 # pixel under the centroid, iff the object's centred in the pixel
170 if xc == int(xc) and yc == int(yc):
171 self.assertAlmostEqual(source.getPsfInstFlux(),
172 self.exposure.getMaskedImage().getImage()[int(xc + 0.5),
173 int(yc + 0.5)])
174 self.assertAlmostEqual(source.getPsfInstFluxErr(),
175 self.exposure.getMaskedImage().getVariance()[int(xc + 0.5),
176 int(yc + 0.5)])
179class FindAndMeasureTestCase(lsst.utils.tests.TestCase):
180 """A test case detecting and measuring objects."""
182 def setUp(self):
183 self.mi = afwImage.MaskedImageF(os.path.join(afwdataDir,
184 "CFHT", "D4", "cal-53535-i-797722_1.fits"))
186 self.FWHM = 5
187 self.psf = algorithms.DoubleGaussianPsf(15, 15, self.FWHM/(2*math.sqrt(2*math.log(2))))
189 if False: # use full image, trimmed to data section
190 self.XY0 = lsst.geom.PointI(32, 2)
191 self.mi = self.mi.Factory(self.mi, lsst.geom.BoxI(self.XY0, lsst.geom.PointI(2079, 4609)),
192 afwImage.LOCAL)
193 self.mi.setXY0(lsst.geom.PointI(0, 0))
194 else: # use sub-image
195 self.XY0 = lsst.geom.PointI(824, 140)
196 self.mi = self.mi.Factory(self.mi, lsst.geom.BoxI(self.XY0, lsst.geom.ExtentI(256, 256)),
197 afwImage.LOCAL)
199 self.mi.getMask().addMaskPlane("DETECTED")
200 self.exposure = afwImage.makeExposure(self.mi)
202 def tearDown(self):
203 del self.mi
204 del self.psf
205 del self.exposure
207 @unittest.skipUnless(afwdataDir, "afwdata not available")
208 def testDetection(self):
209 """Test object detection"""
210 #
211 # Fix defects
212 #
213 # Mask known bad pixels
214 #
215 badPixels = testUtils.makeDefectList()
216 algorithms.interpolateOverDefects(self.mi, self.psf, badPixels)
218 #
219 # Subtract background
220 #
221 bgGridSize = 64 # was 256 ... but that gives only one region and the spline breaks
222 bctrl = afwMath.BackgroundControl(afwMath.Interpolate.NATURAL_SPLINE)
223 bctrl.setNxSample(int(self.mi.getWidth()/bgGridSize) + 1)
224 bctrl.setNySample(int(self.mi.getHeight()/bgGridSize) + 1)
225 backobj = afwMath.makeBackground(self.mi.getImage(), bctrl)
227 self.mi.getImage()[:] -= backobj.getImageF()
228 #
229 # Remove CRs
230 #
231 crConfig = algorithms.FindCosmicRaysConfig()
232 algorithms.findCosmicRays(self.mi, self.psf, 0, pexConfig.makePropertySet(crConfig))
233 #
234 # We do a pretty good job of interpolating, so don't propagagate the convolved CR/INTRP bits
235 # (we'll keep them for the original CR/INTRP pixels)
236 #
237 savedMask = self.mi.getMask().Factory(self.mi.getMask(), True)
238 saveBits = savedMask.getPlaneBitMask("CR") | \
239 savedMask.getPlaneBitMask("BAD") | \
240 savedMask.getPlaneBitMask("INTRP") # Bits to not convolve
241 savedMask &= saveBits
243 msk = self.mi.getMask()
244 msk &= ~saveBits # Clear the saved bits
245 del msk
246 #
247 # Smooth image
248 #
249 psf = algorithms.DoubleGaussianPsf(15, 15, self.FWHM/(2*math.sqrt(2*math.log(2))))
251 cnvImage = self.mi.Factory(self.mi.getBBox())
252 kernel = psf.getKernel()
253 afwMath.convolve(cnvImage, self.mi, kernel, afwMath.ConvolutionControl())
255 msk = cnvImage.getMask()
256 msk |= savedMask # restore the saved bits
257 del msk
259 threshold = afwDetection.Threshold(3, afwDetection.Threshold.STDEV)
260 #
261 # Only search the part of the frame that was PSF-smoothed
262 #
263 llc = lsst.geom.PointI(psf.getKernel().getWidth()//2, psf.getKernel().getHeight()//2)
264 urc = lsst.geom.PointI(cnvImage.getWidth() - llc[0] - 1, cnvImage.getHeight() - llc[1] - 1)
265 middle = cnvImage.Factory(cnvImage, lsst.geom.BoxI(llc, urc), afwImage.LOCAL)
266 ds = afwDetection.FootprintSet(middle, threshold, "DETECTED")
267 del middle
268 #
269 # Reinstate the saved (e.g. BAD) (and also the DETECTED | EDGE) bits in the unsmoothed image
270 #
271 savedMask[:] = cnvImage.getMask()
272 msk = self.mi.getMask()
273 msk |= savedMask
274 del msk
275 del savedMask
277 if display:
278 disp = afwDisplay.Display(frame=2)
279 disp.mtv(self.mi, title=self._testMethodName + ": image")
280 afwDisplay.Display(frame=3).mtv(cnvImage, title=self._testMethodName + ": cnvImage")
282 #
283 # Time to actually measure
284 #
285 schema = afwTable.SourceTable.makeMinimalSchema()
286 sfm_config = measBase.SingleFrameMeasurementConfig()
287 sfm_config.plugins = ["base_SdssCentroid", "base_CircularApertureFlux", "base_PsfFlux",
288 "base_SdssShape", "base_GaussianFlux",
289 "base_PixelFlags"]
290 sfm_config.slots.centroid = "base_SdssCentroid"
291 sfm_config.slots.shape = "base_SdssShape"
292 sfm_config.slots.psfFlux = "base_PsfFlux"
293 sfm_config.slots.gaussianFlux = None
294 sfm_config.slots.apFlux = "base_CircularApertureFlux_3_0"
295 sfm_config.slots.modelFlux = "base_GaussianFlux"
296 sfm_config.slots.calibFlux = None
297 sfm_config.plugins["base_SdssShape"].maxShift = 10.0
298 sfm_config.plugins["base_CircularApertureFlux"].radii = [3.0]
299 task = measBase.SingleFrameMeasurementTask(schema, config=sfm_config)
300 measCat = afwTable.SourceCatalog(schema)
301 # detect the sources and run with the measurement task
302 ds.makeSources(measCat)
303 self.exposure.setPsf(self.psf)
304 task.run(measCat, self.exposure)
306 self.assertGreater(len(measCat), 0)
307 for source in measCat:
308 if source.get("base_PixelFlags_flag_edge"):
309 continue
311 if display:
312 disp.dot("+", source.getX(), source.getY())
315class GaussianPsfTestCase(lsst.utils.tests.TestCase):
316 """A test case detecting and measuring Gaussian PSFs."""
318 def setUp(self):
319 FWHM = 5
320 psf = algorithms.DoubleGaussianPsf(15, 15, FWHM/(2*math.sqrt(2*math.log(2))))
321 mi = afwImage.MaskedImageF(lsst.geom.ExtentI(100, 100))
323 self.xc, self.yc, self.instFlux = 45, 55, 1000.0
324 mi.image[self.xc, self.yc, afwImage.LOCAL] = self.instFlux
326 cnvImage = mi.Factory(mi.getDimensions())
327 afwMath.convolve(cnvImage, mi, psf.getKernel(), afwMath.ConvolutionControl())
329 self.exp = afwImage.makeExposure(cnvImage)
330 self.exp.setPsf(psf)
332 if display and False:
333 afwDisplay.Display(frame=0).mtv(self.exp, title=self._testMethodName + ": image")
335 def tearDown(self):
336 del self.exp
338 def testPsfFlux(self):
339 """Test that fluxes are measured correctly."""
340 #
341 # Total flux in image
342 #
343 flux = afwMath.makeStatistics(self.exp.getMaskedImage(), afwMath.SUM).getValue()
344 self.assertAlmostEqual(flux/self.instFlux, 1.0)
346 #
347 # Various algorithms
348 #
349 rad = 10.0
351 schema = afwTable.SourceTable.makeMinimalSchema()
352 schema.addField("centroid_x", type=float)
353 schema.addField("centroid_y", type=float)
354 schema.addField("centroid_flag", type='Flag')
355 with warnings.catch_warnings():
356 warnings.filterwarnings("ignore", message="ignoreSlotPluginChecks", category=FutureWarning)
357 sfm_config = measBase.SingleFrameMeasurementConfig(ignoreSlotPluginChecks=True)
358 sfm_config.doReplaceWithNoise = False
359 sfm_config.plugins = ["base_CircularApertureFlux", "base_PsfFlux"]
360 sfm_config.slots.centroid = "centroid"
361 sfm_config.slots.shape = None
362 sfm_config.slots.psfFlux = None
363 sfm_config.slots.gaussianFlux = None
364 sfm_config.slots.apFlux = None
365 sfm_config.slots.modelFlux = None
366 sfm_config.slots.calibFlux = None
367 sfm_config.plugins["base_SdssShape"].maxShift = 10.0
368 sfm_config.plugins["base_CircularApertureFlux"].radii = [rad]
369 task = measBase.SingleFrameMeasurementTask(schema, config=sfm_config)
370 measCat = afwTable.SourceCatalog(schema)
371 source = measCat.addNew()
372 source.set("centroid_x", self.xc)
373 source.set("centroid_y", self.yc)
374 task.run(measCat, self.exp)
375 for algName in ["base_CircularApertureFlux_10_0", "base_PsfFlux"]:
376 instFlux = source.get(algName + "_instFlux")
377 flag = source.get(algName + "_flag")
378 self.assertEqual(flag, False)
379 self.assertAlmostEqual(instFlux/self.instFlux, 1.0, 4, "Measuring with %s: %g v. %g" %
380 (algName, instFlux, self.instFlux))
383class TestMemory(lsst.utils.tests.MemoryTestCase):
384 pass
387def setup_module(module):
388 lsst.utils.tests.init()
391if __name__ == "__main__": 391 ↛ 392line 391 didn't jump to line 392, because the condition on line 391 was never true
392 lsst.utils.tests.init()
393 unittest.main()