Coverage for tests/test_measure.py : 58%

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