Coverage for tests/test_psfCandidate.py: 20%
122 statements
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-22 09:58 +0000
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-22 09:58 +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 unittest
24import lsst.geom
25import lsst.afw.detection as afwDet
26import lsst.afw.image as afwImage
27import lsst.afw.table as afwTable
28import lsst.meas.algorithms as measAlg
29import lsst.utils.tests
31try:
32 display
33except NameError:
34 display = False
35else:
36 import lsst.afw.display as afwDisplay
37 afwDisplay.setDefaultMaskTransparency(75)
40def makeEmptyCatalog(psfCandidateField=None):
41 """Return an empty catalog with a useful schema for psfCandidate testing.
43 Parameters
44 ----------
45 psfCandidateField : `str` or None
46 The name of a flag field to add to the schema.
48 Returns
49 -------
50 catalog : `lsst.afw.table.SourceCatalog`
51 The created (empty) catalog.
52 """
53 schema = afwTable.SourceTable.makeMinimalSchema()
54 lsst.afw.table.Point2DKey.addFields(schema, "centroid", "centroid", "pixels")
55 schema.addField("psfFlux_instFlux", type="D", doc="a place to link psf slot to")
56 if psfCandidateField is not None:
57 schema.addField(psfCandidateField, type="Flag", doc="Is a psfCandidate?")
58 catalog = afwTable.SourceCatalog(schema)
59 catalog.defineCentroid('centroid')
60 catalog.definePsfFlux("psfFlux")
61 return catalog
64def createFakeSource(x, y, catalog, exposure, threshold=0.1):
65 """Create a fake source at the given x/y centroid location.
67 Parameters
68 ----------
69 x,y : `int`
70 The x and y centroid coordinates to place the image at.
71 catalog : `lsst.afw.table.SourceCatalog`
72 The catalog to add the new source to.
73 exposure : `lsst.afw.image.Exposure`
74 The exposure to add the source to.
75 threshold : `float`, optional
76 The footprint threshold for identifying the source.
78 Returns
79 -------
80 source : `lsst.afw.table.SourceRecord`
81 The created source record that was added to ``catalog``.
82 """
83 source = catalog.addNew()
84 source['centroid_x'] = x
85 source['centroid_y'] = y
87 exposure.image[x, y, afwImage.LOCAL] = 1.0
88 fpSet = afwDet.FootprintSet(exposure.getMaskedImage(), afwDet.Threshold(threshold), "DETECTED")
89 if display:
90 disp = afwDisplay.Display(frame=1)
91 disp.mtv(exposure, title="createFakeSource: image")
92 for fp in fpSet.getFootprints():
93 for peak in fp.getPeaks():
94 disp.dot("x", peak.getIx(), peak.getIy())
96 # There might be multiple footprints; only the one around x,y should go in the source
97 found = False
98 for fp in fpSet.getFootprints():
99 if fp.contains(lsst.geom.Point2I(x, y)):
100 found = True
101 break
102 # We cannot continue if the the created source wasn't found.
103 assert found, "Unable to find central peak in footprint: faulty test"
105 source.setFootprint(fp)
106 source["psfFlux_instFlux"] = exposure.image[x, y, afwImage.LOCAL]
107 return source
110class CandidateMaskingTestCase(lsst.utils.tests.TestCase):
111 """Testing masking around PSF candidates.
112 """
113 def setUp(self):
114 self.catalog = makeEmptyCatalog()
116 self.x, self.y = 123, 45
117 self.exposure = afwImage.ExposureF(256, 256)
118 self.exposure.variance.set(0.01)
120 def tearDown(self):
121 del self.exposure
122 del self.catalog
124 def createCandidate(self, threshold=0.1):
125 """Create a PSF candidate from self.exposure.
127 Parameters
128 ----------
129 threshold : `float`, optional
130 Threshold for creating footprints on image.
131 """
132 source = createFakeSource(self.x, self.y, self.catalog, self.exposure, threshold)
134 return measAlg.makePsfCandidate(source, self.exposure)
136 def checkCandidateMasking(self, badPixels, extraPixels=[], size=25, threshold=0.1, pixelThreshold=0.0):
137 """Check that candidates are masked properly.
139 We add various pixels to the image and investigate the masking.
141 Parameters
142 ----------
143 badPixels : `list` of `tuple` of `float`
144 The (x,y,flux) triplet of pixels that should be masked.
145 extraPixels : `tuple` of `int`, optional
146 The (x,y,flux) triplet of additional pixels to add to image.
147 size : `int`, optional
148 Size of candidate.
149 threshold : `float`, optional
150 Threshold for creating footprints on image.
151 pixelThreshold : `float`, optional
152 Threshold for masking pixels on candidate.
153 """
154 image = self.exposure.getMaskedImage().getImage()
155 for x, y, f in badPixels + extraPixels:
156 image[x, y, afwImage.LOCAL] = f
157 cand = self.createCandidate(threshold=threshold)
158 oldPixelThreshold = cand.getPixelThreshold()
159 try:
160 cand.setPixelThreshold(pixelThreshold)
161 candImage = cand.getMaskedImage(size, size)
162 mask = candImage.getMask()
163 if display:
164 afwDisplay.Display(frame=2).mtv(candImage, title=self._testMethodName + ": candImage")
165 afwDisplay.Display(frame=3).mtv(mask, title=self._testMethodName + ": mask")
167 detected = mask.getPlaneBitMask("DETECTED")
168 intrp = mask.getPlaneBitMask("INTRP")
169 for x, y, f in badPixels:
170 x -= self.x - size//2
171 y -= self.y - size//2
172 self.assertTrue(mask[x, y, afwImage.LOCAL] & intrp)
173 self.assertFalse(mask[x, y, afwImage.LOCAL] & detected)
174 finally:
175 # Ensure this static variable is reset
176 cand.setPixelThreshold(oldPixelThreshold)
178 def testBlends(self):
179 """Test that blended objects are masked.
181 We create another object next to the one of interest,
182 joined by a bridge so that they're part of the same
183 footprint. The extra object should be masked.
184 """
185 self.checkCandidateMasking([(self.x + 2, self.y, 1.0)], [(self.x + 1, self.y, 0.5)])
187 def testNeighborMasking(self):
188 """Test that neighbours are masked.
190 We create another object separated from the one of
191 interest, which should be masked.
192 """
193 self.checkCandidateMasking([(self.x + 5, self.y, 1.0)])
195 def testFaintNeighborMasking(self):
196 """Test that faint neighbours are masked.
198 We create another faint (i.e., undetected) object separated
199 from the one of interest, which should be masked.
200 """
201 self.checkCandidateMasking([(self.x + 5, self.y, 0.5)], threshold=0.9, pixelThreshold=1.0)
203 def testStr(self):
204 candidate = self.createCandidate()
205 candidate.setChi2(2.0)
206 expect = "center=(123.0,45.0), status=UNKNOWN, rating=1.0, size=(0, 0), chi2=2.0, amplitude=0.0"
207 self.assertEqual(str(candidate), expect)
210class MakePsfCandidatesTaskTest(lsst.utils.tests.TestCase):
211 """Test MakePsfCandidatesTask on a handful of fake sources.
213 Notes
214 -----
215 Does not test sources with NaN/Inf in their footprint. Also does not test
216 any properties of the resulting PsfCandidates: those are assumed to be tested
217 in ``CandidateMaskingTestCase`` above.
218 """
219 def setUp(self):
220 self.psfCandidateField = "psfCandidate"
221 self.catalog = makeEmptyCatalog(self.psfCandidateField)
223 # id=0 is bad because it's on the edge, so fails with a WARN: LengthError.
224 self.badIds = [1, ]
225 self.goodIds = [2, 3]
226 # x and y coordinate: keep these in sync with the above good/bad list.
227 self.xCoords = [0, 100, 200]
228 self.yCoords = [0, 100, 20]
229 self.exposure = afwImage.ExposureF(256, 256)
230 self.exposure.variance.set(0.01)
231 for x, y in zip(self.xCoords, self.yCoords):
232 createFakeSource(x, y, self.catalog, self.exposure, 0.1)
234 self.makePsfCandidates = measAlg.MakePsfCandidatesTask()
236 def testMakePsfCandidates(self):
237 result = self.makePsfCandidates.run(self.catalog, self.exposure)
238 self.assertEqual(len(result.psfCandidates), len(self.goodIds))
240 for goodId in self.goodIds:
241 self.assertIn(goodId, result.goodStarCat['id'])
243 for badId in self.badIds:
244 self.assertNotIn(badId, result.goodStarCat['id'])
246 def testMakePsfCandidatesStarSelectedField(self):
247 """Test MakePsfCandidatesTask setting a selected field.
248 """
249 result = self.makePsfCandidates.run(self.catalog,
250 self.exposure,
251 psfCandidateField=self.psfCandidateField)
252 self.assertEqual(len(result.psfCandidates), len(self.goodIds))
254 for goodId in self.goodIds:
255 self.assertTrue(self.catalog.find(goodId).get(self.psfCandidateField))
257 for badId in self.badIds:
258 self.assertFalse(self.catalog.find(badId).get(self.psfCandidateField))
261class TestMemory(lsst.utils.tests.MemoryTestCase):
262 pass
265def setup_module(module):
266 lsst.utils.tests.init()
269if __name__ == "__main__": 269 ↛ 270line 269 didn't jump to line 270, because the condition on line 269 was never true
270 lsst.utils.tests.init()
271 unittest.main()