Coverage for tests/test_psfCandidate.py: 20%

122 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-11 02:22 -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/>. 

21 

22import unittest 

23 

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 

30 

31try: 

32 display 

33except NameError: 

34 display = False 

35else: 

36 import lsst.afw.display as afwDisplay 

37 afwDisplay.setDefaultMaskTransparency(75) 

38 

39 

40def makeEmptyCatalog(psfCandidateField=None): 

41 """Return an empty catalog with a useful schema for psfCandidate testing. 

42 

43 Parameters 

44 ---------- 

45 psfCandidateField : `str` or None 

46 The name of a flag field to add to the schema. 

47 

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 

62 

63 

64def createFakeSource(x, y, catalog, exposure, threshold=0.1): 

65 """Create a fake source at the given x/y centroid location. 

66 

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. 

77 

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 

86 

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()) 

95 

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" 

104 

105 source.setFootprint(fp) 

106 source["psfFlux_instFlux"] = exposure.image[x, y, afwImage.LOCAL] 

107 return source 

108 

109 

110class CandidateMaskingTestCase(lsst.utils.tests.TestCase): 

111 """Testing masking around PSF candidates. 

112 """ 

113 def setUp(self): 

114 self.catalog = makeEmptyCatalog() 

115 

116 self.x, self.y = 123, 45 

117 self.exposure = afwImage.ExposureF(256, 256) 

118 self.exposure.variance.set(0.01) 

119 

120 def tearDown(self): 

121 del self.exposure 

122 del self.catalog 

123 

124 def createCandidate(self, threshold=0.1): 

125 """Create a PSF candidate from self.exposure. 

126 

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) 

133 

134 return measAlg.makePsfCandidate(source, self.exposure) 

135 

136 def checkCandidateMasking(self, badPixels, extraPixels=[], size=25, threshold=0.1, pixelThreshold=0.0): 

137 """Check that candidates are masked properly. 

138 

139 We add various pixels to the image and investigate the masking. 

140 

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") 

166 

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) 

177 

178 def testBlends(self): 

179 """Test that blended objects are masked. 

180 

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)]) 

186 

187 def testNeighborMasking(self): 

188 """Test that neighbours are masked. 

189 

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)]) 

194 

195 def testFaintNeighborMasking(self): 

196 """Test that faint neighbours are masked. 

197 

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) 

202 

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) 

208 

209 

210class MakePsfCandidatesTaskTest(lsst.utils.tests.TestCase): 

211 """Test MakePsfCandidatesTask on a handful of fake sources. 

212 

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) 

222 

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) 

233 

234 self.makePsfCandidates = measAlg.MakePsfCandidatesTask() 

235 

236 def testMakePsfCandidates(self): 

237 result = self.makePsfCandidates.run(self.catalog, self.exposure) 

238 self.assertEqual(len(result.psfCandidates), len(self.goodIds)) 

239 

240 for goodId in self.goodIds: 

241 self.assertIn(goodId, result.goodStarCat['id']) 

242 

243 for badId in self.badIds: 

244 self.assertNotIn(badId, result.goodStarCat['id']) 

245 

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)) 

253 

254 for goodId in self.goodIds: 

255 self.assertTrue(self.catalog.find(goodId).get(self.psfCandidateField)) 

256 

257 for badId in self.badIds: 

258 self.assertFalse(self.catalog.find(badId).get(self.psfCandidateField)) 

259 

260 

261class TestMemory(lsst.utils.tests.MemoryTestCase): 

262 pass 

263 

264 

265def setup_module(module): 

266 lsst.utils.tests.init() 

267 

268 

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()