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# 

2# LSST Data Management System 

3# 

4# Copyright 2008-2017 AURA/LSST. 

5# 

6# This product includes software developed by the 

7# LSST Project (http://www.lsst.org/). 

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 LSST License Statement and 

20# the GNU General Public License along with this program. If not, 

21# see <https://www.lsstcorp.org/LegalNotices/>. 

22# 

23 

24__all__ = ("SourceDetectionConfig", "SourceDetectionTask", "addExposures") 

25 

26from contextlib import contextmanager 

27from deprecated.sphinx import deprecated 

28 

29import numpy as np 

30 

31import lsst.geom 

32import lsst.afw.display as afwDisplay 

33import lsst.afw.detection as afwDet 

34import lsst.afw.geom as afwGeom 

35import lsst.afw.image as afwImage 

36import lsst.afw.math as afwMath 

37import lsst.afw.table as afwTable 

38import lsst.pex.config as pexConfig 

39import lsst.pipe.base as pipeBase 

40from .subtractBackground import SubtractBackgroundTask 

41 

42 

43class SourceDetectionConfig(pexConfig.Config): 

44 """Configuration parameters for the SourceDetectionTask 

45 """ 

46 minPixels = pexConfig.RangeField( 

47 doc="detected sources with fewer than the specified number of pixels will be ignored", 

48 dtype=int, optional=False, default=1, min=0, 

49 ) 

50 isotropicGrow = pexConfig.Field( 

51 doc="Pixels should be grown as isotropically as possible (slower)", 

52 dtype=bool, optional=False, default=False, 

53 ) 

54 combinedGrow = pexConfig.Field( 

55 doc="Grow all footprints at the same time? This allows disconnected footprints to merge.", 

56 dtype=bool, default=True, 

57 ) 

58 nSigmaToGrow = pexConfig.Field( 

59 doc="Grow detections by nSigmaToGrow * [PSF RMS width]; if 0 then do not grow", 

60 dtype=float, default=2.4, # 2.4 pixels/sigma is roughly one pixel/FWHM 

61 ) 

62 returnOriginalFootprints = pexConfig.Field( 

63 doc="Grow detections to set the image mask bits, but return the original (not-grown) footprints", 

64 dtype=bool, optional=False, default=False, 

65 ) 

66 thresholdValue = pexConfig.RangeField( 

67 doc="Threshold for footprints; exact meaning and units depend on thresholdType.", 

68 dtype=float, optional=False, default=5.0, min=0.0, 

69 ) 

70 includeThresholdMultiplier = pexConfig.RangeField( 

71 doc="Include threshold relative to thresholdValue", 

72 dtype=float, default=1.0, min=0.0, 

73 ) 

74 thresholdType = pexConfig.ChoiceField( 

75 doc="specifies the desired flavor of Threshold", 

76 dtype=str, optional=False, default="stdev", 

77 allowed={ 

78 "variance": "threshold applied to image variance", 

79 "stdev": "threshold applied to image std deviation", 

80 "value": "threshold applied to image value", 

81 "pixel_stdev": "threshold applied to per-pixel std deviation", 

82 }, 

83 ) 

84 thresholdPolarity = pexConfig.ChoiceField( 

85 doc="specifies whether to detect positive, or negative sources, or both", 

86 dtype=str, optional=False, default="positive", 

87 allowed={ 

88 "positive": "detect only positive sources", 

89 "negative": "detect only negative sources", 

90 "both": "detect both positive and negative sources", 

91 }, 

92 ) 

93 adjustBackground = pexConfig.Field( 

94 dtype=float, 

95 doc="Fiddle factor to add to the background; debugging only", 

96 default=0.0, 

97 ) 

98 reEstimateBackground = pexConfig.Field( 

99 dtype=bool, 

100 doc="Estimate the background again after final source detection?", 

101 default=True, optional=False, 

102 ) 

103 background = pexConfig.ConfigurableField( 

104 doc="Background re-estimation; ignored if reEstimateBackground false", 

105 target=SubtractBackgroundTask, 

106 ) 

107 tempLocalBackground = pexConfig.ConfigurableField( 

108 doc=("A local (small-scale), temporary background estimation step run between " 

109 "detecting above-threshold regions and detecting the peaks within " 

110 "them; used to avoid detecting spuerious peaks in the wings."), 

111 target=SubtractBackgroundTask, 

112 ) 

113 doTempLocalBackground = pexConfig.Field( 

114 dtype=bool, 

115 doc="Enable temporary local background subtraction? (see tempLocalBackground)", 

116 default=True, 

117 ) 

118 tempWideBackground = pexConfig.ConfigurableField( 

119 doc=("A wide (large-scale) background estimation and removal before footprint and peak detection. " 

120 "It is added back into the image after detection. The purpose is to suppress very large " 

121 "footprints (e.g., from large artifacts) that the deblender may choke on."), 

122 target=SubtractBackgroundTask, 

123 ) 

124 doTempWideBackground = pexConfig.Field( 

125 dtype=bool, 

126 doc="Do temporary wide (large-scale) background subtraction before footprint detection?", 

127 default=False, 

128 ) 

129 nPeaksMaxSimple = pexConfig.Field( 

130 dtype=int, 

131 doc=("The maximum number of peaks in a Footprint before trying to " 

132 "replace its peaks using the temporary local background"), 

133 default=1, 

134 ) 

135 nSigmaForKernel = pexConfig.Field( 

136 dtype=float, 

137 doc=("Multiple of PSF RMS size to use for convolution kernel bounding box size; " 

138 "note that this is not a half-size. The size will be rounded up to the nearest odd integer"), 

139 default=7.0, 

140 ) 

141 statsMask = pexConfig.ListField( 

142 dtype=str, 

143 doc="Mask planes to ignore when calculating statistics of image (for thresholdType=stdev)", 

144 default=['BAD', 'SAT', 'EDGE', 'NO_DATA'], 

145 ) 

146 

147 def setDefaults(self): 

148 self.tempLocalBackground.binSize = 64 

149 self.tempLocalBackground.algorithm = "AKIMA_SPLINE" 

150 self.tempLocalBackground.useApprox = False 

151 # Background subtraction to remove a large-scale background (e.g., scattered light); restored later. 

152 # Want to keep it from exceeding the deblender size limit of 1 Mpix, so half that is reasonable. 

153 self.tempWideBackground.binSize = 512 

154 self.tempWideBackground.algorithm = "AKIMA_SPLINE" 

155 self.tempWideBackground.useApprox = False 

156 # Ensure we can remove even bright scattered light that is DETECTED 

157 for maskPlane in ("DETECTED", "DETECTED_NEGATIVE"): 

158 if maskPlane in self.tempWideBackground.ignoredPixelMask: 158 ↛ 157line 158 didn't jump to line 157, because the condition on line 158 was never false

159 self.tempWideBackground.ignoredPixelMask.remove(maskPlane) 

160 

161 

162class SourceDetectionTask(pipeBase.Task): 

163 """Create the detection task. Most arguments are simply passed onto pipe.base.Task. 

164 

165 Parameters 

166 ---------- 

167 schema : `lsst.afw.table.Schema` 

168 Schema object used to create the output `lsst.afw.table.SourceCatalog` 

169 **kwds 

170 Keyword arguments passed to `lsst.pipe.base.task.Task.__init__` 

171 

172 If schema is not None and configured for 'both' detections, 

173 a 'flags.negative' field will be added to label detections made with a 

174 negative threshold. 

175 

176 Notes 

177 ----- 

178 This task can add fields to the schema, so any code calling this task must ensure that 

179 these columns are indeed present in the input match list. 

180 """ 

181 

182 ConfigClass = SourceDetectionConfig 

183 _DefaultName = "sourceDetection" 

184 

185 def __init__(self, schema=None, **kwds): 

186 pipeBase.Task.__init__(self, **kwds) 

187 if schema is not None and self.config.thresholdPolarity == "both": 

188 self.negativeFlagKey = schema.addField( 

189 "flags_negative", type="Flag", 

190 doc="set if source was detected as significantly negative" 

191 ) 

192 else: 

193 if self.config.thresholdPolarity == "both": 193 ↛ 194line 193 didn't jump to line 194, because the condition on line 193 was never true

194 self.log.warn("Detection polarity set to 'both', but no flag will be " 

195 "set to distinguish between positive and negative detections") 

196 self.negativeFlagKey = None 

197 if self.config.reEstimateBackground: 

198 self.makeSubtask("background") 

199 if self.config.doTempLocalBackground: 

200 self.makeSubtask("tempLocalBackground") 

201 if self.config.doTempWideBackground: 

202 self.makeSubtask("tempWideBackground") 

203 

204 @pipeBase.timeMethod 

205 def run(self, table, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None): 

206 """Run source detection and create a SourceCatalog of detections. 

207 

208 Parameters 

209 ---------- 

210 table : `lsst.afw.table.SourceTable` 

211 Table object that will be used to create the SourceCatalog. 

212 exposure : `lsst.afw.image.Exposure` 

213 Exposure to process; DETECTED mask plane will be set in-place. 

214 doSmooth : `bool` 

215 If True, smooth the image before detection using a Gaussian of width 

216 ``sigma``, or the measured PSF width. Set to False when running on 

217 e.g. a pre-convolved image, or a mask plane. 

218 sigma : `float` 

219 Sigma of PSF (pixels); used for smoothing and to grow detections; 

220 if None then measure the sigma of the PSF of the exposure 

221 clearMask : `bool` 

222 Clear DETECTED{,_NEGATIVE} planes before running detection. 

223 expId : `int` 

224 Exposure identifier; unused by this implementation, but used for 

225 RNG seed by subclasses. 

226 

227 Returns 

228 ------- 

229 result : `lsst.pipe.base.Struct` 

230 ``sources`` 

231 The detected sources (`lsst.afw.table.SourceCatalog`) 

232 ``fpSets`` 

233 The result resturned by `detectFootprints` 

234 (`lsst.pipe.base.Struct`). 

235 

236 Raises 

237 ------ 

238 ValueError 

239 If flags.negative is needed, but isn't in table's schema. 

240 lsst.pipe.base.TaskError 

241 If sigma=None, doSmooth=True and the exposure has no PSF. 

242 

243 Notes 

244 ----- 

245 If you want to avoid dealing with Sources and Tables, you can use 

246 detectFootprints() to just get the `lsst.afw.detection.FootprintSet`s. 

247 """ 

248 if self.negativeFlagKey is not None and self.negativeFlagKey not in table.getSchema(): 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true

249 raise ValueError("Table has incorrect Schema") 

250 results = self.detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma, 

251 clearMask=clearMask, expId=expId) 

252 sources = afwTable.SourceCatalog(table) 

253 sources.reserve(results.numPos + results.numNeg) 

254 if results.negative: 

255 results.negative.makeSources(sources) 

256 if self.negativeFlagKey: 256 ↛ 259line 256 didn't jump to line 259, because the condition on line 256 was never false

257 for record in sources: 

258 record.set(self.negativeFlagKey, True) 

259 if results.positive: 259 ↛ 261line 259 didn't jump to line 261, because the condition on line 259 was never false

260 results.positive.makeSources(sources) 

261 results.fpSets = results.copy() # Backward compatibility 

262 results.sources = sources 

263 return results 

264 

265 @deprecated(reason="Replaced by SourceDetectionTask.run(). Will be removed after v20.", 

266 category=FutureWarning) 

267 def makeSourceCatalog(self, *args, **kwargs): 

268 return self.run(*args, **kwargs) 

269 

270 def display(self, exposure, results, convolvedImage=None): 

271 """Display detections if so configured 

272 

273 Displays the ``exposure`` in frame 0, overlays the detection peaks. 

274 

275 Requires that ``lsstDebug`` has been set up correctly, so that 

276 ``lsstDebug.Info("lsst.meas.algorithms.detection")`` evaluates `True`. 

277 

278 If the ``convolvedImage`` is non-`None` and 

279 ``lsstDebug.Info("lsst.meas.algorithms.detection") > 1``, the 

280 ``convolvedImage`` will be displayed in frame 1. 

281 

282 Parameters 

283 ---------- 

284 exposure : `lsst.afw.image.Exposure` 

285 Exposure to display, on which will be plotted the detections. 

286 results : `lsst.pipe.base.Struct` 

287 Results of the 'detectFootprints' method, containing positive and 

288 negative footprints (which contain the peak positions that we will 

289 plot). This is a `Struct` with ``positive`` and ``negative`` 

290 elements that are of type `lsst.afw.detection.FootprintSet`. 

291 convolvedImage : `lsst.afw.image.Image`, optional 

292 Convolved image used for thresholding. 

293 """ 

294 try: 

295 import lsstDebug 

296 display = lsstDebug.Info(__name__).display 

297 except ImportError: 

298 try: 

299 display 

300 except NameError: 

301 display = False 

302 if not display: 302 ↛ 305line 302 didn't jump to line 305, because the condition on line 302 was never false

303 return 

304 

305 afwDisplay.setDefaultMaskTransparency(75) 

306 

307 disp0 = afwDisplay.Display(frame=0) 

308 disp0.mtv(exposure, title="detection") 

309 

310 def plotPeaks(fps, ctype): 

311 if fps is None: 

312 return 

313 with disp0.Buffering(): 

314 for fp in fps.getFootprints(): 

315 for pp in fp.getPeaks(): 

316 disp0.dot("+", pp.getFx(), pp.getFy(), ctype=ctype) 

317 plotPeaks(results.positive, "yellow") 

318 plotPeaks(results.negative, "red") 

319 

320 if convolvedImage and display > 1: 

321 disp1 = afwDisplay.Display(frame=1) 

322 disp1.mtv(convolvedImage, title="PSF smoothed") 

323 

324 def applyTempLocalBackground(self, exposure, middle, results): 

325 """Apply a temporary local background subtraction 

326 

327 This temporary local background serves to suppress noise fluctuations 

328 in the wings of bright objects. 

329 

330 Peaks in the footprints will be updated. 

331 

332 Parameters 

333 ---------- 

334 exposure : `lsst.afw.image.Exposure` 

335 Exposure for which to fit local background. 

336 middle : `lsst.afw.image.MaskedImage` 

337 Convolved image on which detection will be performed 

338 (typically smaller than ``exposure`` because the 

339 half-kernel has been removed around the edges). 

340 results : `lsst.pipe.base.Struct` 

341 Results of the 'detectFootprints' method, containing positive and 

342 negative footprints (which contain the peak positions that we will 

343 plot). This is a `Struct` with ``positive`` and ``negative`` 

344 elements that are of type `lsst.afw.detection.FootprintSet`. 

345 """ 

346 # Subtract the local background from the smoothed image. Since we 

347 # never use the smoothed again we don't need to worry about adding 

348 # it back in. 

349 bg = self.tempLocalBackground.fitBackground(exposure.getMaskedImage()) 

350 bgImage = bg.getImageF(self.tempLocalBackground.config.algorithm, 

351 self.tempLocalBackground.config.undersampleStyle) 

352 middle -= bgImage.Factory(bgImage, middle.getBBox()) 

353 thresholdPos = self.makeThreshold(middle, "positive") 

354 thresholdNeg = self.makeThreshold(middle, "negative") 

355 if self.config.thresholdPolarity != "negative": 355 ↛ 357line 355 didn't jump to line 357, because the condition on line 355 was never false

356 self.updatePeaks(results.positive, middle, thresholdPos) 

357 if self.config.thresholdPolarity != "positive": 

358 self.updatePeaks(results.negative, middle, thresholdNeg) 

359 

360 def clearMask(self, mask): 

361 """Clear the DETECTED and DETECTED_NEGATIVE mask planes 

362 

363 Removes any previous detection mask in preparation for a new 

364 detection pass. 

365 

366 Parameters 

367 ---------- 

368 mask : `lsst.afw.image.Mask` 

369 Mask to be cleared. 

370 """ 

371 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")) 

372 

373 def calculateKernelSize(self, sigma): 

374 """Calculate size of smoothing kernel 

375 

376 Uses the ``nSigmaForKernel`` configuration parameter. Note 

377 that that is the full width of the kernel bounding box 

378 (so a value of 7 means 3.5 sigma on either side of center). 

379 The value will be rounded up to the nearest odd integer. 

380 

381 Parameters 

382 ---------- 

383 sigma : `float` 

384 Gaussian sigma of smoothing kernel. 

385 

386 Returns 

387 ------- 

388 size : `int` 

389 Size of the smoothing kernel. 

390 """ 

391 return (int(sigma * self.config.nSigmaForKernel + 0.5)//2)*2 + 1 # make sure it is odd 

392 

393 def getPsf(self, exposure, sigma=None): 

394 """Retrieve the PSF for an exposure 

395 

396 If ``sigma`` is provided, we make a ``GaussianPsf`` with that, 

397 otherwise use the one from the ``exposure``. 

398 

399 Parameters 

400 ---------- 

401 exposure : `lsst.afw.image.Exposure` 

402 Exposure from which to retrieve the PSF. 

403 sigma : `float`, optional 

404 Gaussian sigma to use if provided. 

405 

406 Returns 

407 ------- 

408 psf : `lsst.afw.detection.Psf` 

409 PSF to use for detection. 

410 """ 

411 if sigma is None: 

412 psf = exposure.getPsf() 

413 if psf is None: 413 ↛ 414line 413 didn't jump to line 414, because the condition on line 413 was never true

414 raise RuntimeError("Unable to determine PSF to use for detection: no sigma provided") 

415 sigma = psf.computeShape().getDeterminantRadius() 

416 size = self.calculateKernelSize(sigma) 

417 psf = afwDet.GaussianPsf(size, size, sigma) 

418 return psf 

419 

420 def convolveImage(self, maskedImage, psf, doSmooth=True): 

421 """Convolve the image with the PSF 

422 

423 We convolve the image with a Gaussian approximation to the PSF, 

424 because this is separable and therefore fast. It's technically a 

425 correlation rather than a convolution, but since we use a symmetric 

426 Gaussian there's no difference. 

427 

428 The convolution can be disabled with ``doSmooth=False``. If we do 

429 convolve, we mask the edges as ``EDGE`` and return the convolved image 

430 with the edges removed. This is because we can't convolve the edges 

431 because the kernel would extend off the image. 

432 

433 Parameters 

434 ---------- 

435 maskedImage : `lsst.afw.image.MaskedImage` 

436 Image to convolve. 

437 psf : `lsst.afw.detection.Psf` 

438 PSF to convolve with (actually with a Gaussian approximation 

439 to it). 

440 doSmooth : `bool` 

441 Actually do the convolution? Set to False when running on 

442 e.g. a pre-convolved image, or a mask plane. 

443 

444 Return Struct contents 

445 ---------------------- 

446 middle : `lsst.afw.image.MaskedImage` 

447 Convolved image, without the edges. 

448 sigma : `float` 

449 Gaussian sigma used for the convolution. 

450 """ 

451 self.metadata.set("doSmooth", doSmooth) 

452 sigma = psf.computeShape().getDeterminantRadius() 

453 self.metadata.set("sigma", sigma) 

454 

455 if not doSmooth: 

456 middle = maskedImage.Factory(maskedImage, deep=True) 

457 return pipeBase.Struct(middle=middle, sigma=sigma) 

458 

459 # Smooth using a Gaussian (which is separable, hence fast) of width sigma 

460 # Make a SingleGaussian (separable) kernel with the 'sigma' 

461 kWidth = self.calculateKernelSize(sigma) 

462 self.metadata.set("smoothingKernelWidth", kWidth) 

463 gaussFunc = afwMath.GaussianFunction1D(sigma) 

464 gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc) 

465 

466 convolvedImage = maskedImage.Factory(maskedImage.getBBox()) 

467 

468 afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl()) 

469 # 

470 # Only search psf-smoothed part of frame 

471 # 

472 goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox()) 

473 middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False) 

474 # 

475 # Mark the parts of the image outside goodBBox as EDGE 

476 # 

477 self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE")) 

478 

479 return pipeBase.Struct(middle=middle, sigma=sigma) 

480 

481 def applyThreshold(self, middle, bbox, factor=1.0): 

482 """Apply thresholds to the convolved image 

483 

484 Identifies ``Footprint``s, both positive and negative. 

485 

486 The threshold can be modified by the provided multiplication 

487 ``factor``. 

488 

489 Parameters 

490 ---------- 

491 middle : `lsst.afw.image.MaskedImage` 

492 Convolved image to threshold. 

493 bbox : `lsst.geom.Box2I` 

494 Bounding box of unconvolved image. 

495 factor : `float` 

496 Multiplier for the configured threshold. 

497 

498 Return Struct contents 

499 ---------------------- 

500 positive : `lsst.afw.detection.FootprintSet` or `None` 

501 Positive detection footprints, if configured. 

502 negative : `lsst.afw.detection.FootprintSet` or `None` 

503 Negative detection footprints, if configured. 

504 factor : `float` 

505 Multiplier for the configured threshold. 

506 """ 

507 results = pipeBase.Struct(positive=None, negative=None, factor=factor) 

508 # Detect the Footprints (peaks may be replaced if doTempLocalBackground) 

509 if self.config.reEstimateBackground or self.config.thresholdPolarity != "negative": 509 ↛ 518line 509 didn't jump to line 518, because the condition on line 509 was never false

510 threshold = self.makeThreshold(middle, "positive", factor=factor) 

511 results.positive = afwDet.FootprintSet( 

512 middle, 

513 threshold, 

514 "DETECTED", 

515 self.config.minPixels 

516 ) 

517 results.positive.setRegion(bbox) 

518 if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive": 

519 threshold = self.makeThreshold(middle, "negative", factor=factor) 

520 results.negative = afwDet.FootprintSet( 

521 middle, 

522 threshold, 

523 "DETECTED_NEGATIVE", 

524 self.config.minPixels 

525 ) 

526 results.negative.setRegion(bbox) 

527 

528 return results 

529 

530 def finalizeFootprints(self, mask, results, sigma, factor=1.0): 

531 """Finalize the detected footprints 

532 

533 Grows the footprints, sets the ``DETECTED`` and ``DETECTED_NEGATIVE`` 

534 mask planes, and logs the results. 

535 

536 ``numPos`` (number of positive footprints), ``numPosPeaks`` (number 

537 of positive peaks), ``numNeg`` (number of negative footprints), 

538 ``numNegPeaks`` (number of negative peaks) entries are added to the 

539 detection results. 

540 

541 Parameters 

542 ---------- 

543 mask : `lsst.afw.image.Mask` 

544 Mask image on which to flag detected pixels. 

545 results : `lsst.pipe.base.Struct` 

546 Struct of detection results, including ``positive`` and 

547 ``negative`` entries; modified. 

548 sigma : `float` 

549 Gaussian sigma of PSF. 

550 factor : `float` 

551 Multiplier for the configured threshold. 

552 """ 

553 for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")): 

554 fpSet = getattr(results, polarity) 

555 if fpSet is None: 

556 continue 

557 if self.config.nSigmaToGrow > 0: 557 ↛ 567line 557 didn't jump to line 567, because the condition on line 557 was never false

558 nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5) 

559 self.metadata.set("nGrow", nGrow) 

560 if self.config.combinedGrow: 560 ↛ 563line 560 didn't jump to line 563, because the condition on line 560 was never false

561 fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow) 

562 else: 

563 stencil = (afwGeom.Stencil.CIRCLE if self.config.isotropicGrow else 

564 afwGeom.Stencil.MANHATTAN) 

565 for fp in fpSet: 

566 fp.dilate(nGrow, stencil) 

567 fpSet.setMask(mask, maskName) 

568 if not self.config.returnOriginalFootprints: 568 ↛ 553line 568 didn't jump to line 553, because the condition on line 568 was never false

569 setattr(results, polarity, fpSet) 

570 

571 results.numPos = 0 

572 results.numPosPeaks = 0 

573 results.numNeg = 0 

574 results.numNegPeaks = 0 

575 positive = "" 

576 negative = "" 

577 

578 if results.positive is not None: 578 ↛ 582line 578 didn't jump to line 582, because the condition on line 578 was never false

579 results.numPos = len(results.positive.getFootprints()) 

580 results.numPosPeaks = sum(len(fp.getPeaks()) for fp in results.positive.getFootprints()) 

581 positive = " %d positive peaks in %d footprints" % (results.numPosPeaks, results.numPos) 

582 if results.negative is not None: 

583 results.numNeg = len(results.negative.getFootprints()) 

584 results.numNegPeaks = sum(len(fp.getPeaks()) for fp in results.negative.getFootprints()) 

585 negative = " %d negative peaks in %d footprints" % (results.numNegPeaks, results.numNeg) 

586 

587 self.log.info("Detected%s%s%s to %g %s" % 

588 (positive, " and" if positive and negative else "", negative, 

589 self.config.thresholdValue*self.config.includeThresholdMultiplier*factor, 

590 "DN" if self.config.thresholdType == "value" else "sigma")) 

591 

592 def reEstimateBackground(self, maskedImage, backgrounds): 

593 """Estimate the background after detection 

594 

595 Parameters 

596 ---------- 

597 maskedImage : `lsst.afw.image.MaskedImage` 

598 Image on which to estimate the background. 

599 backgrounds : `lsst.afw.math.BackgroundList` 

600 List of backgrounds; modified. 

601 

602 Returns 

603 ------- 

604 bg : `lsst.afw.math.backgroundMI` 

605 Empirical background model. 

606 """ 

607 bg = self.background.fitBackground(maskedImage) 

608 if self.config.adjustBackground: 608 ↛ 609line 608 didn't jump to line 609, because the condition on line 608 was never true

609 self.log.warn("Fiddling the background by %g", self.config.adjustBackground) 

610 bg += self.config.adjustBackground 

611 self.log.info("Resubtracting the background after object detection") 

612 maskedImage -= bg.getImageF(self.background.config.algorithm, 

613 self.background.config.undersampleStyle) 

614 

615 actrl = bg.getBackgroundControl().getApproximateControl() 

616 backgrounds.append((bg, getattr(afwMath.Interpolate, self.background.config.algorithm), 

617 bg.getAsUsedUndersampleStyle(), actrl.getStyle(), actrl.getOrderX(), 

618 actrl.getOrderY(), actrl.getWeighting())) 

619 return bg 

620 

621 def clearUnwantedResults(self, mask, results): 

622 """Clear unwanted results from the Struct of results 

623 

624 If we specifically want only positive or only negative detections, 

625 drop the ones we don't want, and its associated mask plane. 

626 

627 Parameters 

628 ---------- 

629 mask : `lsst.afw.image.Mask` 

630 Mask image. 

631 results : `lsst.pipe.base.Struct` 

632 Detection results, with ``positive`` and ``negative`` elements; 

633 modified. 

634 """ 

635 if self.config.thresholdPolarity == "positive": 

636 if self.config.reEstimateBackground: 

637 mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE") 

638 results.negative = None 

639 elif self.config.thresholdPolarity == "negative": 639 ↛ 640line 639 didn't jump to line 640, because the condition on line 639 was never true

640 if self.config.reEstimateBackground: 

641 mask &= ~mask.getPlaneBitMask("DETECTED") 

642 results.positive = None 

643 

644 @pipeBase.timeMethod 

645 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None): 

646 """Detect footprints on an exposure. 

647 

648 Parameters 

649 ---------- 

650 exposure : `lsst.afw.image.Exposure` 

651 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be 

652 set in-place. 

653 doSmooth : `bool`, optional 

654 If True, smooth the image before detection using a Gaussian 

655 of width ``sigma``, or the measured PSF width of ``exposure``. 

656 Set to False when running on e.g. a pre-convolved image, or a mask 

657 plane. 

658 sigma : `float`, optional 

659 Gaussian Sigma of PSF (pixels); used for smoothing and to grow 

660 detections; if `None` then measure the sigma of the PSF of the 

661 ``exposure``. 

662 clearMask : `bool`, optional 

663 Clear both DETECTED and DETECTED_NEGATIVE planes before running 

664 detection. 

665 expId : `dict`, optional 

666 Exposure identifier; unused by this implementation, but used for 

667 RNG seed by subclasses. 

668 

669 Return Struct contents 

670 ---------------------- 

671 positive : `lsst.afw.detection.FootprintSet` 

672 Positive polarity footprints (may be `None`) 

673 negative : `lsst.afw.detection.FootprintSet` 

674 Negative polarity footprints (may be `None`) 

675 numPos : `int` 

676 Number of footprints in positive or 0 if detection polarity was 

677 negative. 

678 numNeg : `int` 

679 Number of footprints in negative or 0 if detection polarity was 

680 positive. 

681 background : `lsst.afw.math.BackgroundList` 

682 Re-estimated background. `None` if 

683 ``reEstimateBackground==False``. 

684 factor : `float` 

685 Multiplication factor applied to the configured detection 

686 threshold. 

687 """ 

688 maskedImage = exposure.maskedImage 

689 

690 if clearMask: 690 ↛ 693line 690 didn't jump to line 693, because the condition on line 690 was never false

691 self.clearMask(maskedImage.getMask()) 

692 

693 psf = self.getPsf(exposure, sigma=sigma) 

694 with self.tempWideBackgroundContext(exposure): 

695 convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth) 

696 middle = convolveResults.middle 

697 sigma = convolveResults.sigma 

698 

699 results = self.applyThreshold(middle, maskedImage.getBBox()) 

700 results.background = afwMath.BackgroundList() 

701 if self.config.doTempLocalBackground: 

702 self.applyTempLocalBackground(exposure, middle, results) 

703 self.finalizeFootprints(maskedImage.mask, results, sigma) 

704 

705 if self.config.reEstimateBackground: 

706 self.reEstimateBackground(maskedImage, results.background) 

707 

708 self.clearUnwantedResults(maskedImage.getMask(), results) 

709 self.display(exposure, results, middle) 

710 

711 return results 

712 

713 def makeThreshold(self, image, thresholdParity, factor=1.0): 

714 """Make an afw.detection.Threshold object corresponding to the task's 

715 configuration and the statistics of the given image. 

716 

717 Parameters 

718 ---------- 

719 image : `afw.image.MaskedImage` 

720 Image to measure noise statistics from if needed. 

721 thresholdParity: `str` 

722 One of "positive" or "negative", to set the kind of fluctuations 

723 the Threshold will detect. 

724 factor : `float` 

725 Factor by which to multiply the configured detection threshold. 

726 This is useful for tweaking the detection threshold slightly. 

727 

728 Returns 

729 ------- 

730 threshold : `lsst.afw.detection.Threshold` 

731 Detection threshold. 

732 """ 

733 parity = False if thresholdParity == "negative" else True 

734 thresholdValue = self.config.thresholdValue 

735 thresholdType = self.config.thresholdType 

736 if self.config.thresholdType == 'stdev': 

737 bad = image.getMask().getPlaneBitMask(self.config.statsMask) 

738 sctrl = afwMath.StatisticsControl() 

739 sctrl.setAndMask(bad) 

740 stats = afwMath.makeStatistics(image, afwMath.STDEVCLIP, sctrl) 

741 thresholdValue *= stats.getValue(afwMath.STDEVCLIP) 

742 thresholdType = 'value' 

743 

744 threshold = afwDet.createThreshold(thresholdValue*factor, thresholdType, parity) 

745 threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier) 

746 return threshold 

747 

748 def updatePeaks(self, fpSet, image, threshold): 

749 """Update the Peaks in a FootprintSet by detecting new Footprints and 

750 Peaks in an image and using the new Peaks instead of the old ones. 

751 

752 Parameters 

753 ---------- 

754 fpSet : `afw.detection.FootprintSet` 

755 Set of Footprints whose Peaks should be updated. 

756 image : `afw.image.MaskedImage` 

757 Image to detect new Footprints and Peak in. 

758 threshold : `afw.detection.Threshold` 

759 Threshold object for detection. 

760 

761 Input Footprints with fewer Peaks than self.config.nPeaksMaxSimple 

762 are not modified, and if no new Peaks are detected in an input 

763 Footprint, the brightest original Peak in that Footprint is kept. 

764 """ 

765 for footprint in fpSet.getFootprints(): 

766 oldPeaks = footprint.getPeaks() 

767 if len(oldPeaks) <= self.config.nPeaksMaxSimple: 

768 continue 

769 # We detect a new FootprintSet within each non-simple Footprint's 

770 # bbox to avoid a big O(N^2) comparison between the two sets of 

771 # Footprints. 

772 sub = image.Factory(image, footprint.getBBox()) 

773 fpSetForPeaks = afwDet.FootprintSet( 

774 sub, 

775 threshold, 

776 "", # don't set a mask plane 

777 self.config.minPixels 

778 ) 

779 newPeaks = afwDet.PeakCatalog(oldPeaks.getTable()) 

780 for fpForPeaks in fpSetForPeaks.getFootprints(): 

781 for peak in fpForPeaks.getPeaks(): 

782 if footprint.contains(peak.getI()): 

783 newPeaks.append(peak) 

784 if len(newPeaks) > 0: 

785 del oldPeaks[:] 

786 oldPeaks.extend(newPeaks) 

787 else: 

788 del oldPeaks[1:] 

789 

790 @staticmethod 

791 def setEdgeBits(maskedImage, goodBBox, edgeBitmask): 

792 """Set the edgeBitmask bits for all of maskedImage outside goodBBox 

793 

794 Parameters 

795 ---------- 

796 maskedImage : `lsst.afw.image.MaskedImage` 

797 Image on which to set edge bits in the mask. 

798 goodBBox : `lsst.geom.Box2I` 

799 Bounding box of good pixels, in ``LOCAL`` coordinates. 

800 edgeBitmask : `lsst.afw.image.MaskPixel` 

801 Bit mask to OR with the existing mask bits in the region 

802 outside ``goodBBox``. 

803 """ 

804 msk = maskedImage.getMask() 

805 

806 mx0, my0 = maskedImage.getXY0() 

807 for x0, y0, w, h in ([0, 0, 

808 msk.getWidth(), goodBBox.getBeginY() - my0], 

809 [0, goodBBox.getEndY() - my0, msk.getWidth(), 

810 maskedImage.getHeight() - (goodBBox.getEndY() - my0)], 

811 [0, 0, 

812 goodBBox.getBeginX() - mx0, msk.getHeight()], 

813 [goodBBox.getEndX() - mx0, 0, 

814 maskedImage.getWidth() - (goodBBox.getEndX() - mx0), msk.getHeight()], 

815 ): 

816 edgeMask = msk.Factory(msk, lsst.geom.BoxI(lsst.geom.PointI(x0, y0), 

817 lsst.geom.ExtentI(w, h)), afwImage.LOCAL) 

818 edgeMask |= edgeBitmask 

819 

820 @contextmanager 

821 def tempWideBackgroundContext(self, exposure): 

822 """Context manager for removing wide (large-scale) background 

823 

824 Removing a wide (large-scale) background helps to suppress the 

825 detection of large footprints that may overwhelm the deblender. 

826 It does, however, set a limit on the maximum scale of objects. 

827 

828 The background that we remove will be restored upon exit from 

829 the context manager. 

830 

831 Parameters 

832 ---------- 

833 exposure : `lsst.afw.image.Exposure` 

834 Exposure on which to remove large-scale background. 

835 

836 Returns 

837 ------- 

838 context : context manager 

839 Context manager that will ensure the temporary wide background 

840 is restored. 

841 """ 

842 doTempWideBackground = self.config.doTempWideBackground 

843 if doTempWideBackground: 

844 self.log.info("Applying temporary wide background subtraction") 

845 original = exposure.maskedImage.image.array[:].copy() 

846 self.tempWideBackground.run(exposure).background 

847 # Remove NO_DATA regions (e.g., edge of the field-of-view); these can cause detections after 

848 # subtraction because of extrapolation of the background model into areas with no constraints. 

849 image = exposure.maskedImage.image 

850 mask = exposure.maskedImage.mask 

851 noData = mask.array & mask.getPlaneBitMask("NO_DATA") > 0 

852 isGood = mask.array & mask.getPlaneBitMask(self.config.statsMask) == 0 

853 image.array[noData] = np.median(image.array[~noData & isGood]) 

854 try: 

855 yield 

856 finally: 

857 if doTempWideBackground: 

858 exposure.maskedImage.image.array[:] = original 

859 

860 

861def addExposures(exposureList): 

862 """Add a set of exposures together. 

863 

864 Parameters 

865 ---------- 

866 exposureList : `list` of `lsst.afw.image.Exposure` 

867 Sequence of exposures to add. 

868 

869 Returns 

870 ------- 

871 addedExposure : `lsst.afw.image.Exposure` 

872 An exposure of the same size as each exposure in ``exposureList``, 

873 with the metadata from ``exposureList[0]`` and a masked image equal 

874 to the sum of all the exposure's masked images. 

875 """ 

876 exposure0 = exposureList[0] 

877 image0 = exposure0.getMaskedImage() 

878 

879 addedImage = image0.Factory(image0, True) 

880 addedImage.setXY0(image0.getXY0()) 

881 

882 for exposure in exposureList[1:]: 

883 image = exposure.getMaskedImage() 

884 addedImage += image 

885 

886 addedExposure = exposure0.Factory(addedImage, exposure0.getWcs()) 

887 return addedExposure