Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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.pex.config as pexConfig 

35import lsst.utils.tests 

36 

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

38Log.getLogger("measurement").setLevel(Log.INFO) 

39 

40try: 

41 type(display) 

42except NameError: 

43 display = False 

44else: 

45 import lsst.afw.display as afwDisplay 

46 afwDisplay.setDefaultMaskTransparency(75) 

47 

48# Determine if we have afwdata 

49try: 

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

51except Exception: 

52 afwdataDir = None 

53 

54 

55def toString(*args): 

56 """toString written in python""" 

57 if len(args) == 1: 

58 args = args[0] 

59 

60 y, x0, x1 = args 

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

62 

63 

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

65 """A test case for Measure""" 

66 class Object: 

67 

68 def __init__(self, val, spans): 

69 self.val = val 

70 self.spans = spans 

71 

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

73 """Insert self into an image""" 

74 for sp in self.spans: 

75 y, x0, x1 = sp 

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

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

78 

79 def __eq__(self, other): 

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

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

82 return False 

83 

84 return True 

85 

86 def setUp(self): 

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

88 ms.getVariance().set(1) 

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

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

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

92 im = self.mi.getImage() 

93 # 

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

95 # 

96 self.objects = [] 

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

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

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

100 

101 im.set(0) # clear image 

102 for obj in self.objects: 

103 obj.insert(im, 5, 5) 

104 # 

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

106 # 

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

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

109 

110 def tearDown(self): 

111 del self.mi 

112 del self.exposure 

113 

114 def testFootprintsMeasure(self): 

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

116 

117 xcentroid = [10.0, 14.0, 9.0] 

118 ycentroid = [8.0, 11.5061728, 14.0] 

119 flux = [51.0, 101.0, 20.0] 

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

121 # from the pixel grid. 

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

123 

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

125 

126 if display: 126 ↛ 127line 126 didn't jump to line 127, because the condition on line 126 was never true

127 disp = afwDisplay.Display(frame=0) 

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

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

130 

131 measureSourcesConfig = measBase.SingleFrameMeasurementConfig() 

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

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

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

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

136 "base_CircularApertureFlux"] 

137 measureSourcesConfig.slots.centroid = "base_NaiveCentroid" 

138 measureSourcesConfig.slots.psfFlux = "base_PsfFlux" 

139 measureSourcesConfig.slots.apFlux = "base_CircularApertureFlux_3_0" 

140 measureSourcesConfig.slots.modelFlux = None 

141 measureSourcesConfig.slots.gaussianFlux = None 

142 measureSourcesConfig.slots.calibFlux = None 

143 

144 schema = afwTable.SourceTable.makeMinimalSchema() 

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

146 measCat = afwTable.SourceCatalog(schema) 

147 footprints.makeSources(measCat) 

148 # now run the SFM task with the test plugin 

149 sigma = 1e-10 

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

151 self.exposure.setPsf(psf) 

152 task.run(measCat, self.exposure) 

153 

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

155 for i, source in enumerate(measCat): 

156 

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

158 if display: 158 ↛ 159line 158 didn't jump to line 159, because the condition on line 158 was never true

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

160 

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

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

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

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

165 

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

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

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

169 self.assertAlmostEqual(source.getPsfInstFlux(), 

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

171 int(yc + 0.5)]) 

172 self.assertAlmostEqual(source.getPsfInstFluxErr(), 

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

174 int(yc + 0.5)]) 

175 

176 

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

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

179 

180 def setUp(self): 

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

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

183 

184 self.FWHM = 5 

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

186 

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

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

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

190 afwImage.LOCAL) 

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

192 else: # use sub-image 

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

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

195 afwImage.LOCAL) 

196 

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

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

199 

200 def tearDown(self): 

201 del self.mi 

202 del self.psf 

203 del self.exposure 

204 

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

206 def testDetection(self): 

207 """Test object detection""" 

208 # 

209 # Fix defects 

210 # 

211 # Mask known bad pixels 

212 # 

213 measAlgorithmsDir = lsst.utils.getPackageDir('meas_algorithms') 

214 badPixels = algorithms.Defects.readText(os.path.join(measAlgorithmsDir, 

215 "policy", "BadPixels.ecsv")) 

216 # did someone lie about the origin of the maskedImage? If so, adjust bad pixel list 

217 if self.XY0.getX() != self.mi.getX0() or self.XY0.getY() != self.mi.getY0(): 

218 dx = self.XY0.getX() - self.mi.getX0() 

219 dy = self.XY0.getY() - self.mi.getY0() 

220 for bp in badPixels: 

221 bp.shift(-dx, -dy) 

222 

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

224 # 

225 # Subtract background 

226 # 

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

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

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

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

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

232 

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

234 # 

235 # Remove CRs 

236 # 

237 crConfig = algorithms.FindCosmicRaysConfig() 

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

239 # 

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

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

242 # 

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

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

245 savedMask.getPlaneBitMask("BAD") | \ 

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

247 savedMask &= saveBits 

248 

249 msk = self.mi.getMask() 

250 msk &= ~saveBits # Clear the saved bits 

251 del msk 

252 # 

253 # Smooth image 

254 # 

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

256 

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

258 kernel = psf.getKernel() 

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

260 

261 msk = cnvImage.getMask() 

262 msk |= savedMask # restore the saved bits 

263 del msk 

264 

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

266 # 

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

268 # 

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

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

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

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

273 del middle 

274 # 

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

276 # 

277 savedMask[:] = cnvImage.getMask() 

278 msk = self.mi.getMask() 

279 msk |= savedMask 

280 del msk 

281 del savedMask 

282 

283 if display: 

284 disp = afwDisplay.Display(frame=2) 

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

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

287 

288 # 

289 # Time to actually measure 

290 # 

291 schema = afwTable.SourceTable.makeMinimalSchema() 

292 sfm_config = measBase.SingleFrameMeasurementConfig() 

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

294 "base_SdssShape", "base_GaussianFlux", 

295 "base_PixelFlags"] 

296 sfm_config.slots.centroid = "base_SdssCentroid" 

297 sfm_config.slots.shape = "base_SdssShape" 

298 sfm_config.slots.psfFlux = "base_PsfFlux" 

299 sfm_config.slots.gaussianFlux = None 

300 sfm_config.slots.apFlux = "base_CircularApertureFlux_3_0" 

301 sfm_config.slots.modelFlux = "base_GaussianFlux" 

302 sfm_config.slots.calibFlux = None 

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

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

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

306 measCat = afwTable.SourceCatalog(schema) 

307 # detect the sources and run with the measurement task 

308 ds.makeSources(measCat) 

309 self.exposure.setPsf(self.psf) 

310 task.run(measCat, self.exposure) 

311 

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

313 for source in measCat: 

314 if source.get("base_PixelFlags_flag_edge"): 

315 continue 

316 

317 if display: 

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

319 

320 

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

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

323 

324 def setUp(self): 

325 FWHM = 5 

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

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

328 

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

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

331 

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

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

334 

335 self.exp = afwImage.makeExposure(cnvImage) 

336 self.exp.setPsf(psf) 

337 

338 if display and False: 338 ↛ 339line 338 didn't jump to line 339, because the condition on line 338 was never true

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

340 

341 def tearDown(self): 

342 del self.exp 

343 

344 def testPsfFlux(self): 

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

346 # 

347 # Total flux in image 

348 # 

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

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

351 

352 # 

353 # Various algorithms 

354 # 

355 rad = 10.0 

356 

357 schema = afwTable.SourceTable.makeMinimalSchema() 

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

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

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

361 sfm_config = measBase.SingleFrameMeasurementConfig() 

362 sfm_config.doReplaceWithNoise = False 

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

364 sfm_config.slots.centroid = "centroid" 

365 sfm_config.slots.shape = None 

366 sfm_config.slots.psfFlux = None 

367 sfm_config.slots.gaussianFlux = None 

368 sfm_config.slots.apFlux = None 

369 sfm_config.slots.modelFlux = None 

370 sfm_config.slots.calibFlux = None 

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

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

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

374 measCat = afwTable.SourceCatalog(schema) 

375 source = measCat.addNew() 

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

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

378 task.run(measCat, self.exp) 

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

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

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

382 self.assertEqual(flag, False) 

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

384 (algName, instFlux, self.instFlux)) 

385 

386 

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

388 pass 

389 

390 

391def setup_module(module): 

392 lsst.utils.tests.init() 

393 

394 

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

396 lsst.utils.tests.init() 

397 unittest.main()