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