Coverage for tests/test_measure.py: 21%

233 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-06-02 04:02 -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 

25 

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.meas.algorithms.testUtils as testUtils 

35import lsst.pex.config as pexConfig 

36import lsst.utils.tests 

37 

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

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

40 

41try: 

42 type(display) 

43except NameError: 

44 display = False 

45else: 

46 import lsst.afw.display as afwDisplay 

47 afwDisplay.setDefaultMaskTransparency(75) 

48 

49# Determine if we have afwdata 

50try: 

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

52except Exception: 

53 afwdataDir = None 

54 

55 

56def toString(*args): 

57 """toString written in python""" 

58 if len(args) == 1: 

59 args = args[0] 

60 

61 y, x0, x1 = args 

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

63 

64 

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

66 """A test case for Measure""" 

67 class Object: 

68 

69 def __init__(self, val, spans): 

70 self.val = val 

71 self.spans = spans 

72 

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

74 """Insert self into an image""" 

75 for sp in self.spans: 

76 y, x0, x1 = sp 

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

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

79 

80 def __eq__(self, other): 

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

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

83 return False 

84 

85 return True 

86 

87 def setUp(self): 

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

89 ms.getVariance().set(1) 

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

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

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

93 im = self.mi.getImage() 

94 # 

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

96 # 

97 self.objects = [] 

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

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

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

101 

102 im.set(0) # clear image 

103 for obj in self.objects: 

104 obj.insert(im, 5, 5) 

105 # 

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

107 # 

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

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

110 

111 def tearDown(self): 

112 del self.mi 

113 del self.exposure 

114 

115 def testFootprintsMeasure(self): 

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

117 

118 xcentroid = [10.0, 14.0, 9.0] 

119 ycentroid = [8.0, 11.5061728, 14.0] 

120 flux = [51.0, 101.0, 20.0] 

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

122 # from the pixel grid. 

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

124 

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

126 

127 if display: 

128 disp = afwDisplay.Display(frame=0) 

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

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

131 

132 measureSourcesConfig = measBase.SingleFrameMeasurementConfig() 

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

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

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

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

137 "base_CircularApertureFlux"] 

138 measureSourcesConfig.slots.centroid = "base_NaiveCentroid" 

139 measureSourcesConfig.slots.psfFlux = "base_PsfFlux" 

140 measureSourcesConfig.slots.apFlux = "base_CircularApertureFlux_3_0" 

141 measureSourcesConfig.slots.modelFlux = None 

142 measureSourcesConfig.slots.gaussianFlux = None 

143 measureSourcesConfig.slots.calibFlux = None 

144 

145 schema = afwTable.SourceTable.makeMinimalSchema() 

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

147 measCat = afwTable.SourceCatalog(schema) 

148 footprints.makeSources(measCat) 

149 # now run the SFM task with the test plugin 

150 sigma = 1e-10 

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

152 self.exposure.setPsf(psf) 

153 task.run(measCat, self.exposure) 

154 

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

156 for i, source in enumerate(measCat): 

157 

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

159 if display: 

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

161 

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

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

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

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

166 

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

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

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

170 self.assertAlmostEqual(source.getPsfInstFlux(), 

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

172 int(yc + 0.5)]) 

173 self.assertAlmostEqual(source.getPsfInstFluxErr(), 

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

175 int(yc + 0.5)]) 

176 

177 

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

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

180 

181 def setUp(self): 

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

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

184 

185 self.FWHM = 5 

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

187 

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

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

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

191 afwImage.LOCAL) 

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

193 else: # use sub-image 

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

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

196 afwImage.LOCAL) 

197 

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

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

200 

201 def tearDown(self): 

202 del self.mi 

203 del self.psf 

204 del self.exposure 

205 

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

207 def testDetection(self): 

208 """Test object detection""" 

209 # 

210 # Fix defects 

211 # 

212 # Mask known bad pixels 

213 # 

214 badPixels = testUtils.makeDefectList() 

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

216 

217 # 

218 # Subtract background 

219 # 

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

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

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

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

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

225 

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

227 # 

228 # Remove CRs 

229 # 

230 crConfig = algorithms.FindCosmicRaysConfig() 

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

232 # 

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

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

235 # 

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

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

238 savedMask.getPlaneBitMask("BAD") | \ 

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

240 savedMask &= saveBits 

241 

242 msk = self.mi.getMask() 

243 msk &= ~saveBits # Clear the saved bits 

244 del msk 

245 # 

246 # Smooth image 

247 # 

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

249 

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

251 kernel = psf.getKernel() 

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

253 

254 msk = cnvImage.getMask() 

255 msk |= savedMask # restore the saved bits 

256 del msk 

257 

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

259 # 

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

261 # 

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

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

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

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

266 del middle 

267 # 

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

269 # 

270 savedMask[:] = cnvImage.getMask() 

271 msk = self.mi.getMask() 

272 msk |= savedMask 

273 del msk 

274 del savedMask 

275 

276 if display: 

277 disp = afwDisplay.Display(frame=2) 

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

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

280 

281 # 

282 # Time to actually measure 

283 # 

284 schema = afwTable.SourceTable.makeMinimalSchema() 

285 sfm_config = measBase.SingleFrameMeasurementConfig() 

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

287 "base_SdssShape", "base_GaussianFlux", 

288 "base_PixelFlags"] 

289 sfm_config.slots.centroid = "base_SdssCentroid" 

290 sfm_config.slots.shape = "base_SdssShape" 

291 sfm_config.slots.psfFlux = "base_PsfFlux" 

292 sfm_config.slots.gaussianFlux = None 

293 sfm_config.slots.apFlux = "base_CircularApertureFlux_3_0" 

294 sfm_config.slots.modelFlux = "base_GaussianFlux" 

295 sfm_config.slots.calibFlux = None 

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

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

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

299 measCat = afwTable.SourceCatalog(schema) 

300 # detect the sources and run with the measurement task 

301 ds.makeSources(measCat) 

302 self.exposure.setPsf(self.psf) 

303 task.run(measCat, self.exposure) 

304 

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

306 for source in measCat: 

307 if source.get("base_PixelFlags_flag_edge"): 

308 continue 

309 

310 if display: 

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

312 

313 

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

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

316 

317 def setUp(self): 

318 FWHM = 5 

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

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

321 

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

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

324 

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

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

327 

328 self.exp = afwImage.makeExposure(cnvImage) 

329 self.exp.setPsf(psf) 

330 

331 if display and False: 

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

333 

334 def tearDown(self): 

335 del self.exp 

336 

337 def testPsfFlux(self): 

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

339 # 

340 # Total flux in image 

341 # 

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

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

344 

345 # 

346 # Various algorithms 

347 # 

348 rad = 10.0 

349 

350 schema = afwTable.SourceTable.makeMinimalSchema() 

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

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

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

354 sfm_config = measBase.SingleFrameMeasurementConfig() 

355 sfm_config.doReplaceWithNoise = False 

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

357 sfm_config.slots.centroid = "centroid" 

358 sfm_config.slots.shape = None 

359 sfm_config.slots.psfFlux = None 

360 sfm_config.slots.gaussianFlux = None 

361 sfm_config.slots.apFlux = None 

362 sfm_config.slots.modelFlux = None 

363 sfm_config.slots.calibFlux = None 

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

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

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

367 measCat = afwTable.SourceCatalog(schema) 

368 source = measCat.addNew() 

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

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

371 task.run(measCat, self.exp) 

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

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

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

375 self.assertEqual(flag, False) 

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

377 (algName, instFlux, self.instFlux)) 

378 

379 

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

381 pass 

382 

383 

384def setup_module(module): 

385 lsst.utils.tests.init() 

386 

387 

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

389 lsst.utils.tests.init() 

390 unittest.main()