Coverage for tests/test_detection.py: 15%
128 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-06 13:13 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-06 13:13 -0800
1#
2# LSST Data Management System
3#
4# Copyright 2008-2016 AURA/LSST.
5#
6# This product includes software developed by the
7# LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20# the GNU General Public License along with this program. If not,
21# see <https://www.lsstcorp.org/LegalNotices/>.
22#
23import unittest
24import numpy as np
26import lsst.geom
27import lsst.afw.table as afwTable
28import lsst.afw.image as afwImage
29import lsst.afw.math as afwMath
30from lsst.meas.algorithms import SourceDetectionTask
31from lsst.meas.algorithms.testUtils import plantSources
32import lsst.utils.tests
34# To plot in ds9, `setup display_ds9` first, and open a ds9 window.
35# import lsstDebug
36# def DebugInfo(name):
37# debug = lsstDebug.getInfo(name)
38# if name == "lsst.meas.algorithms.detection":
39# debug.display = 2
40# return debug
41# lsstDebug.Info = DebugInfo
44class SourceDetectionTaskTestCase(lsst.utils.tests.TestCase):
46 def _create_exposure(self):
47 """Return a simulated exposure (and relevant parameters) with Gaussian
48 stars.
49 """
50 bbox = lsst.geom.Box2I(lsst.geom.Point2I(256, 100), lsst.geom.Extent2I(128, 127))
51 minCounts = 5000
52 maxCounts = 50000
53 starSigma = 1.5
54 numX = 5
55 numY = 5
56 coordList = self.makeCoordList(
57 bbox=bbox,
58 numX=numX,
59 numY=numY,
60 minCounts=minCounts,
61 maxCounts=maxCounts,
62 sigma=starSigma,
63 )
64 kwid = 11
65 sky = 2000
66 addPoissonNoise = True
67 exposure = plantSources(bbox=bbox, kwid=kwid, sky=sky, coordList=coordList,
68 addPoissonNoise=addPoissonNoise)
69 schema = afwTable.SourceTable.makeMinimalSchema()
71 return exposure, schema, numX, numY, starSigma
73 def _check_detectFootprints(self, exposure, numX, numY, starSigma, task, config, doSmooth=False):
74 """Run detectFootprints and check that the output is reasonable,
75 for either value of doSmooth.
76 """
77 taskSigma = 2.2
78 res = task.detectFootprints(exposure, doSmooth=doSmooth, sigma=taskSigma)
79 self.assertEqual(res.numPos, numX * numY)
80 self.assertEqual(res.numNeg, 0)
81 self.assertEqual(task.metadata.getScalar("sigma"), taskSigma)
82 self.assertEqual(task.metadata.getScalar("doSmooth"), doSmooth)
83 self.assertEqual(task.metadata.getScalar("nGrow"), int(taskSigma * config.nSigmaToGrow + 0.5))
85 res = task.detectFootprints(exposure, doSmooth=doSmooth, sigma=None)
86 taskSigma = task.metadata.getScalar("sigma")
87 self.assertLess(abs(taskSigma - starSigma), 0.1)
88 self.assertEqual(res.numPos, numX * numY)
89 self.assertEqual(res.numNeg, 0)
90 return res
92 def test_stdev(self):
93 """Test that sources are detected on a simulated image with
94 thresholdType='stdev'.
95 """
96 exposure, schema, numX, numY, starSigma = self._create_exposure()
98 config = SourceDetectionTask.ConfigClass()
99 # don't modify the image after detection.
100 config.reEstimateBackground = False
101 config.thresholdType = "stdev"
102 task = SourceDetectionTask(config=config, schema=schema)
104 self._check_detectFootprints(exposure, numX, numY, starSigma, task, config, doSmooth=True)
105 self._check_detectFootprints(exposure, numX, numY, starSigma, task, config, doSmooth=False)
107 def test_significance_stdev(self):
108 """Check the non-smoothed, non-background updated peak significance
109 values with thresholdType="stddev".
110 """
111 exposure, schema, numX, numY, starSigma = self._create_exposure()
113 config = SourceDetectionTask.ConfigClass()
114 # don't modify the image after detection.
115 config.reEstimateBackground = False
116 config.doTempLocalBackground = False
117 config.thresholdType = "stdev"
118 task = SourceDetectionTask(config=config, schema=schema)
120 result = self._check_detectFootprints(exposure, numX, numY, starSigma, task, config, doSmooth=False)
122 bad = exposure.mask.getPlaneBitMask(config.statsMask)
123 sctrl = afwMath.StatisticsControl()
124 sctrl.setAndMask(bad)
125 stats = afwMath.makeStatistics(exposure.maskedImage, afwMath.STDEVCLIP, sctrl)
126 stddev = stats.getValue(afwMath.STDEVCLIP)
127 for footprint in result.positive.getFootprints():
128 for peak in footprint.peaks:
129 point = lsst.geom.Point2I(peak.getIx(), peak.getIy())
130 value = exposure.image[point]
131 with self.subTest(str(point)):
132 self.assertFloatsAlmostEqual(peak["significance"],
133 value/stddev,
134 rtol=1e-7,
135 msg=str(point))
137 def test_pixel_stdev(self):
138 """Test that sources are detected on a simulated image with
139 thresholdType='pixel_stdev', and that they have the right significance.
140 """
141 exposure, schema, numX, numY, starSigma = self._create_exposure()
143 config = SourceDetectionTask.ConfigClass()
144 config.thresholdType = "pixel_stdev"
145 config.reEstimateBackground = False
146 # TempLocalBackground changes the peak value of the faintest peak,
147 # so disable it for this test so that we can calculate an expected
148 # answer without having to try to deal with backgrounds.
149 config.doTempLocalBackground = False
150 task = SourceDetectionTask(config=config, schema=schema)
151 # Don't smooth, so that we can directly calculate the s/n from the exposure.
152 result = task.detectFootprints(exposure, doSmooth=False)
153 self.assertEqual(result.numPos, numX * numY)
154 self.assertEqual(result.numNeg, 0)
155 # Significance values for `pixel_stdev` should match image/sqrt(variance).
156 for footprint in result.positive.getFootprints():
157 for peak in footprint.peaks:
158 point = lsst.geom.Point2I(peak.getIx(), peak.getIy())
159 value = exposure.image[point]
160 stddev = np.sqrt(exposure.variance[point])
161 with self.subTest(str(point)):
162 self.assertFloatsAlmostEqual(peak["significance"],
163 value/stddev,
164 rtol=1e-7,
165 msg=str(point))
167 def makeCoordList(self, bbox, numX, numY, minCounts, maxCounts, sigma):
168 """Make a coordList for plantSources."""
169 """
170 Coords are uniformly spaced in a rectangular grid, with linearly increasing counts
171 """
172 dX = bbox.getWidth() / float(numX)
173 dY = bbox.getHeight() / float(numY)
174 minX = bbox.getMinX() + (dX / 2.0)
175 minY = bbox.getMinY() + (dY / 2.0)
176 dCounts = (maxCounts - minCounts) / (numX * numY - 1)
178 coordList = []
179 counts = minCounts
180 for i in range(numX):
181 x = minX + (dX * i)
182 for j in range(numY):
183 y = minY + (dY * j)
184 coordList.append([x, y, counts, sigma])
185 counts += dCounts
186 return coordList
188 def testTempBackgrounds(self):
189 """Test that the temporary backgrounds we remove are properly restored"""
190 bbox = lsst.geom.Box2I(lsst.geom.Point2I(12345, 67890), lsst.geom.Extent2I(128, 127))
191 original = afwImage.ExposureF(bbox)
192 rng = np.random.RandomState(123)
193 original.image.array[:] = rng.normal(size=original.image.array.shape)
194 original.mask.set(0)
195 original.variance.set(1.0)
197 def checkExposure(original, doTempLocalBackground, doTempWideBackground):
198 """Check that the original exposure is unmodified."""
199 config = SourceDetectionTask.ConfigClass()
200 config.reEstimateBackground = False
201 config.thresholdType = "pixel_stdev"
202 config.doTempLocalBackground = doTempLocalBackground
203 config.doTempWideBackground = doTempWideBackground
204 schema = afwTable.SourceTable.makeMinimalSchema()
205 task = SourceDetectionTask(config=config, schema=schema)
207 exposure = original.clone()
208 task.detectFootprints(exposure, sigma=3.21)
210 self.assertFloatsEqual(exposure.image.array, original.image.array)
211 # Mask is permitted to vary: DETECTED bit gets set
212 self.assertFloatsEqual(exposure.variance.array, original.variance.array)
214 checkExposure(original, False, False)
215 checkExposure(original, True, False)
216 checkExposure(original, False, True)
217 checkExposure(original, True, True)
220class TestMemory(lsst.utils.tests.MemoryTestCase):
221 pass
224def setup_module(module):
225 lsst.utils.tests.init()
228if __name__ == "__main__": 228 ↛ 229line 228 didn't jump to line 229, because the condition on line 228 was never true
229 lsst.utils.tests.init()
230 unittest.main()