Coverage for tests/test_measure.py: 18%

236 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-04-07 01:35 -0700

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 os 

23import unittest 

24import math 

25import warnings 

26 

27import lsst.geom 

28import lsst.afw.detection as afwDetection 

29import lsst.afw.image as afwImage 

30import lsst.afw.math as afwMath 

31import lsst.afw.table as afwTable 

32from lsst.log import Log 

33import lsst.meas.base as measBase 

34import lsst.meas.algorithms as algorithms 

35import lsst.meas.algorithms.testUtils as testUtils 

36import lsst.pex.config as pexConfig 

37import lsst.utils.tests 

38 

39# Change the level to Log.DEBUG or Log.TRACE to see debug messages 

40Log.getLogger("lsst.measurement").setLevel(Log.INFO) 

41 

42try: 

43 type(display) 

44except NameError: 

45 display = False 

46else: 

47 import lsst.afw.display as afwDisplay 

48 afwDisplay.setDefaultMaskTransparency(75) 

49 

50# Determine if we have afwdata 

51try: 

52 afwdataDir = lsst.utils.getPackageDir('afwdata') 

53except Exception: 

54 afwdataDir = None 

55 

56 

57def toString(*args): 

58 """toString written in python""" 

59 if len(args) == 1: 

60 args = args[0] 

61 

62 y, x0, x1 = args 

63 return "%d: %d..%d" % (y, x0, x1) 

64 

65 

66class MeasureTestCase(lsst.utils.tests.TestCase): 

67 """A test case for Measure""" 

68 class Object: 

69 

70 def __init__(self, val, spans): 

71 self.val = val 

72 self.spans = spans 

73 

74 def insert(self, im, dx=0, dy=0): 

75 """Insert self into an image""" 

76 for sp in self.spans: 

77 y, x0, x1 = sp 

78 for x in range(x0, x1 + 1): 

79 im[x + dx, y + dy, afwImage.LOCAL] = self.val 

80 

81 def __eq__(self, other): 

82 for osp, sp in zip(other.getSpans(), self.spans): 

83 if osp.toString() != toString(sp): 

84 return False 

85 

86 return True 

87 

88 def setUp(self): 

89 ms = afwImage.MaskedImageF(lsst.geom.ExtentI(31, 27)) 

90 ms.getVariance().set(1) 

91 bbox = lsst.geom.BoxI(lsst.geom.PointI(1, 1), lsst.geom.ExtentI(24, 20)) 

92 self.mi = afwImage.MaskedImageF(ms, bbox, afwImage.LOCAL) 

93 self.exposure = afwImage.makeExposure(self.mi) 

94 im = self.mi.getImage() 

95 # 

96 # Objects that we should detect. These are coordinates in the subimage 

97 # 

98 self.objects = [] 

99 self.objects += [self.Object(10, [(1, 4, 4), (2, 3, 5), (3, 4, 4)])] 

100 self.objects += [self.Object(20, [(5, 7, 8), (5, 10, 10), (6, 8, 9)])] 

101 self.objects += [self.Object(20, [(8, 3, 3)])] 

102 

103 im.set(0) # clear image 

104 for obj in self.objects: 

105 obj.insert(im, 5, 5) 

106 # 

107 # Add a few more pixels to make peaks that we can centroid around 

108 # 

109 for x, y in [(9, 7), (13, 11)]: 

110 im[x, y, afwImage.LOCAL] += 1 

111 

112 def tearDown(self): 

113 del self.mi 

114 del self.exposure 

115 

116 def testFootprintsMeasure(self): 

117 """Check that we can measure the objects in a detectionSet""" 

118 

119 xcentroid = [10.0, 14.0, 9.0] 

120 ycentroid = [8.0, 11.5061728, 14.0] 

121 flux = [51.0, 101.0, 20.0] 

122 # sqrt of num pixels in aperture; note the second source is offset 

123 # from the pixel grid. 

124 fluxErr = [math.sqrt(29), math.sqrt(26), math.sqrt(29)] 

125 

126 footprints = afwDetection.FootprintSet(self.mi, afwDetection.Threshold(10), "DETECTED") 

127 

128 if display: 

129 disp = afwDisplay.Display(frame=0) 

130 disp.mtv(self.mi, title=self._testMethodName + ": image") 

131 afwDisplay.Display(frame=1).mtv(self.mi.getVariance(), title=self._testMethodName + ": variance") 

132 

133 measureSourcesConfig = measBase.SingleFrameMeasurementConfig() 

134 measureSourcesConfig.algorithms["base_CircularApertureFlux"].radii = [3.0] 

135 # Numerical tests below assumes that we are not using sinc fluxes. 

136 measureSourcesConfig.algorithms["base_CircularApertureFlux"].maxSincRadius = 0.0 

137 measureSourcesConfig.algorithms.names = ["base_NaiveCentroid", "base_SdssShape", "base_PsfFlux", 

138 "base_CircularApertureFlux"] 

139 measureSourcesConfig.slots.centroid = "base_NaiveCentroid" 

140 measureSourcesConfig.slots.psfFlux = "base_PsfFlux" 

141 measureSourcesConfig.slots.apFlux = "base_CircularApertureFlux_3_0" 

142 measureSourcesConfig.slots.modelFlux = None 

143 measureSourcesConfig.slots.gaussianFlux = None 

144 measureSourcesConfig.slots.calibFlux = None 

145 

146 schema = afwTable.SourceTable.makeMinimalSchema() 

147 task = measBase.SingleFrameMeasurementTask(schema, config=measureSourcesConfig) 

148 measCat = afwTable.SourceCatalog(schema) 

149 footprints.makeSources(measCat) 

150 # now run the SFM task with the test plugin 

151 sigma = 1e-10 

152 psf = algorithms.DoubleGaussianPsf(11, 11, sigma) # i.e. a single pixel 

153 self.exposure.setPsf(psf) 

154 task.run(measCat, self.exposure) 

155 

156 self.assertEqual(len(measCat), len(flux)) 

157 for i, source in enumerate(measCat): 

158 

159 xc, yc = source.getX(), source.getY() 

160 if display: 

161 disp.dot("+", xc, yc) 

162 

163 self.assertAlmostEqual(source.getX(), xcentroid[i], 6) 

164 self.assertAlmostEqual(source.getY(), ycentroid[i], 6) 

165 self.assertEqual(source.getApInstFlux(), flux[i]) 

166 self.assertAlmostEqual(source.getApInstFluxErr(), fluxErr[i], 6) 

167 

168 # We're using a delta-function PSF, so the psfFlux should be the 

169 # pixel under the centroid, iff the object's centred in the pixel 

170 if xc == int(xc) and yc == int(yc): 

171 self.assertAlmostEqual(source.getPsfInstFlux(), 

172 self.exposure.getMaskedImage().getImage()[int(xc + 0.5), 

173 int(yc + 0.5)]) 

174 self.assertAlmostEqual(source.getPsfInstFluxErr(), 

175 self.exposure.getMaskedImage().getVariance()[int(xc + 0.5), 

176 int(yc + 0.5)]) 

177 

178 

179class FindAndMeasureTestCase(lsst.utils.tests.TestCase): 

180 """A test case detecting and measuring objects.""" 

181 

182 def setUp(self): 

183 self.mi = afwImage.MaskedImageF(os.path.join(afwdataDir, 

184 "CFHT", "D4", "cal-53535-i-797722_1.fits")) 

185 

186 self.FWHM = 5 

187 self.psf = algorithms.DoubleGaussianPsf(15, 15, self.FWHM/(2*math.sqrt(2*math.log(2)))) 

188 

189 if False: # use full image, trimmed to data section 

190 self.XY0 = lsst.geom.PointI(32, 2) 

191 self.mi = self.mi.Factory(self.mi, lsst.geom.BoxI(self.XY0, lsst.geom.PointI(2079, 4609)), 

192 afwImage.LOCAL) 

193 self.mi.setXY0(lsst.geom.PointI(0, 0)) 

194 else: # use sub-image 

195 self.XY0 = lsst.geom.PointI(824, 140) 

196 self.mi = self.mi.Factory(self.mi, lsst.geom.BoxI(self.XY0, lsst.geom.ExtentI(256, 256)), 

197 afwImage.LOCAL) 

198 

199 self.mi.getMask().addMaskPlane("DETECTED") 

200 self.exposure = afwImage.makeExposure(self.mi) 

201 

202 def tearDown(self): 

203 del self.mi 

204 del self.psf 

205 del self.exposure 

206 

207 @unittest.skipUnless(afwdataDir, "afwdata not available") 

208 def testDetection(self): 

209 """Test object detection""" 

210 # 

211 # Fix defects 

212 # 

213 # Mask known bad pixels 

214 # 

215 badPixels = testUtils.makeDefectList() 

216 algorithms.interpolateOverDefects(self.mi, self.psf, badPixels) 

217 

218 # 

219 # Subtract background 

220 # 

221 bgGridSize = 64 # was 256 ... but that gives only one region and the spline breaks 

222 bctrl = afwMath.BackgroundControl(afwMath.Interpolate.NATURAL_SPLINE) 

223 bctrl.setNxSample(int(self.mi.getWidth()/bgGridSize) + 1) 

224 bctrl.setNySample(int(self.mi.getHeight()/bgGridSize) + 1) 

225 backobj = afwMath.makeBackground(self.mi.getImage(), bctrl) 

226 

227 self.mi.getImage()[:] -= backobj.getImageF() 

228 # 

229 # Remove CRs 

230 # 

231 crConfig = algorithms.FindCosmicRaysConfig() 

232 algorithms.findCosmicRays(self.mi, self.psf, 0, pexConfig.makePropertySet(crConfig)) 

233 # 

234 # We do a pretty good job of interpolating, so don't propagagate the convolved CR/INTRP bits 

235 # (we'll keep them for the original CR/INTRP pixels) 

236 # 

237 savedMask = self.mi.getMask().Factory(self.mi.getMask(), True) 

238 saveBits = savedMask.getPlaneBitMask("CR") | \ 

239 savedMask.getPlaneBitMask("BAD") | \ 

240 savedMask.getPlaneBitMask("INTRP") # Bits to not convolve 

241 savedMask &= saveBits 

242 

243 msk = self.mi.getMask() 

244 msk &= ~saveBits # Clear the saved bits 

245 del msk 

246 # 

247 # Smooth image 

248 # 

249 psf = algorithms.DoubleGaussianPsf(15, 15, self.FWHM/(2*math.sqrt(2*math.log(2)))) 

250 

251 cnvImage = self.mi.Factory(self.mi.getBBox()) 

252 kernel = psf.getKernel() 

253 afwMath.convolve(cnvImage, self.mi, kernel, afwMath.ConvolutionControl()) 

254 

255 msk = cnvImage.getMask() 

256 msk |= savedMask # restore the saved bits 

257 del msk 

258 

259 threshold = afwDetection.Threshold(3, afwDetection.Threshold.STDEV) 

260 # 

261 # Only search the part of the frame that was PSF-smoothed 

262 # 

263 llc = lsst.geom.PointI(psf.getKernel().getWidth()//2, psf.getKernel().getHeight()//2) 

264 urc = lsst.geom.PointI(cnvImage.getWidth() - llc[0] - 1, cnvImage.getHeight() - llc[1] - 1) 

265 middle = cnvImage.Factory(cnvImage, lsst.geom.BoxI(llc, urc), afwImage.LOCAL) 

266 ds = afwDetection.FootprintSet(middle, threshold, "DETECTED") 

267 del middle 

268 # 

269 # Reinstate the saved (e.g. BAD) (and also the DETECTED | EDGE) bits in the unsmoothed image 

270 # 

271 savedMask[:] = cnvImage.getMask() 

272 msk = self.mi.getMask() 

273 msk |= savedMask 

274 del msk 

275 del savedMask 

276 

277 if display: 

278 disp = afwDisplay.Display(frame=2) 

279 disp.mtv(self.mi, title=self._testMethodName + ": image") 

280 afwDisplay.Display(frame=3).mtv(cnvImage, title=self._testMethodName + ": cnvImage") 

281 

282 # 

283 # Time to actually measure 

284 # 

285 schema = afwTable.SourceTable.makeMinimalSchema() 

286 sfm_config = measBase.SingleFrameMeasurementConfig() 

287 sfm_config.plugins = ["base_SdssCentroid", "base_CircularApertureFlux", "base_PsfFlux", 

288 "base_SdssShape", "base_GaussianFlux", 

289 "base_PixelFlags"] 

290 sfm_config.slots.centroid = "base_SdssCentroid" 

291 sfm_config.slots.shape = "base_SdssShape" 

292 sfm_config.slots.psfFlux = "base_PsfFlux" 

293 sfm_config.slots.gaussianFlux = None 

294 sfm_config.slots.apFlux = "base_CircularApertureFlux_3_0" 

295 sfm_config.slots.modelFlux = "base_GaussianFlux" 

296 sfm_config.slots.calibFlux = None 

297 sfm_config.plugins["base_SdssShape"].maxShift = 10.0 

298 sfm_config.plugins["base_CircularApertureFlux"].radii = [3.0] 

299 task = measBase.SingleFrameMeasurementTask(schema, config=sfm_config) 

300 measCat = afwTable.SourceCatalog(schema) 

301 # detect the sources and run with the measurement task 

302 ds.makeSources(measCat) 

303 self.exposure.setPsf(self.psf) 

304 task.run(measCat, self.exposure) 

305 

306 self.assertGreater(len(measCat), 0) 

307 for source in measCat: 

308 if source.get("base_PixelFlags_flag_edge"): 

309 continue 

310 

311 if display: 

312 disp.dot("+", source.getX(), source.getY()) 

313 

314 

315class GaussianPsfTestCase(lsst.utils.tests.TestCase): 

316 """A test case detecting and measuring Gaussian PSFs.""" 

317 

318 def setUp(self): 

319 FWHM = 5 

320 psf = algorithms.DoubleGaussianPsf(15, 15, FWHM/(2*math.sqrt(2*math.log(2)))) 

321 mi = afwImage.MaskedImageF(lsst.geom.ExtentI(100, 100)) 

322 

323 self.xc, self.yc, self.instFlux = 45, 55, 1000.0 

324 mi.image[self.xc, self.yc, afwImage.LOCAL] = self.instFlux 

325 

326 cnvImage = mi.Factory(mi.getDimensions()) 

327 afwMath.convolve(cnvImage, mi, psf.getKernel(), afwMath.ConvolutionControl()) 

328 

329 self.exp = afwImage.makeExposure(cnvImage) 

330 self.exp.setPsf(psf) 

331 

332 if display and False: 

333 afwDisplay.Display(frame=0).mtv(self.exp, title=self._testMethodName + ": image") 

334 

335 def tearDown(self): 

336 del self.exp 

337 

338 def testPsfFlux(self): 

339 """Test that fluxes are measured correctly.""" 

340 # 

341 # Total flux in image 

342 # 

343 flux = afwMath.makeStatistics(self.exp.getMaskedImage(), afwMath.SUM).getValue() 

344 self.assertAlmostEqual(flux/self.instFlux, 1.0) 

345 

346 # 

347 # Various algorithms 

348 # 

349 rad = 10.0 

350 

351 schema = afwTable.SourceTable.makeMinimalSchema() 

352 schema.addField("centroid_x", type=float) 

353 schema.addField("centroid_y", type=float) 

354 schema.addField("centroid_flag", type='Flag') 

355 with warnings.catch_warnings(): 

356 warnings.filterwarnings("ignore", message="ignoreSlotPluginChecks", category=FutureWarning) 

357 sfm_config = measBase.SingleFrameMeasurementConfig(ignoreSlotPluginChecks=True) 

358 sfm_config.doReplaceWithNoise = False 

359 sfm_config.plugins = ["base_CircularApertureFlux", "base_PsfFlux"] 

360 sfm_config.slots.centroid = "centroid" 

361 sfm_config.slots.shape = None 

362 sfm_config.slots.psfFlux = None 

363 sfm_config.slots.gaussianFlux = None 

364 sfm_config.slots.apFlux = None 

365 sfm_config.slots.modelFlux = None 

366 sfm_config.slots.calibFlux = None 

367 sfm_config.plugins["base_SdssShape"].maxShift = 10.0 

368 sfm_config.plugins["base_CircularApertureFlux"].radii = [rad] 

369 task = measBase.SingleFrameMeasurementTask(schema, config=sfm_config) 

370 measCat = afwTable.SourceCatalog(schema) 

371 source = measCat.addNew() 

372 source.set("centroid_x", self.xc) 

373 source.set("centroid_y", self.yc) 

374 task.run(measCat, self.exp) 

375 for algName in ["base_CircularApertureFlux_10_0", "base_PsfFlux"]: 

376 instFlux = source.get(algName + "_instFlux") 

377 flag = source.get(algName + "_flag") 

378 self.assertEqual(flag, False) 

379 self.assertAlmostEqual(instFlux/self.instFlux, 1.0, 4, "Measuring with %s: %g v. %g" % 

380 (algName, instFlux, self.instFlux)) 

381 

382 

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

384 pass 

385 

386 

387def setup_module(module): 

388 lsst.utils.tests.init() 

389 

390 

391if __name__ == "__main__": 391 ↛ 392line 391 didn't jump to line 392, because the condition on line 391 was never true

392 lsst.utils.tests.init() 

393 unittest.main()