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## @addtogroup LSST_task_documentation 

162## @{ 

163## @page sourceDetectionTask 

164## @ref SourceDetectionTask_ "SourceDetectionTask" 

165## @copybrief SourceDetectionTask 

166## @} 

167 

168 

169class SourceDetectionTask(pipeBase.Task): 

170 r"""! 

171@anchor SourceDetectionTask_ 

172 

173@brief Detect positive and negative sources on an exposure and return a new @link table.SourceCatalog@endlink. 

174 

175@section meas_algorithms_detection_Contents Contents 

176 

177 - @ref meas_algorithms_detection_Purpose 

178 - @ref meas_algorithms_detection_Initialize 

179 - @ref meas_algorithms_detection_Invoke 

180 - @ref meas_algorithms_detection_Config 

181 - @ref meas_algorithms_detection_Debug 

182 - @ref meas_algorithms_detection_Example 

183 

184@section meas_algorithms_detection_Purpose Description 

185 

186@copybrief SourceDetectionTask 

187 

188@section meas_algorithms_detection_Initialize Task initialisation 

189 

190@copydoc \_\_init\_\_ 

191 

192@section meas_algorithms_detection_Invoke Invoking the Task 

193 

194@copydoc run 

195 

196@section meas_algorithms_detection_Config Configuration parameters 

197 

198See @ref SourceDetectionConfig 

199 

200@section meas_algorithms_detection_Debug Debug variables 

201 

202The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a 

203flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about @b debug.py files. 

204 

205The available variables in SourceDetectionTask are: 

206<DL> 

207 <DT> @c display 

208 <DD> 

209 - If True, display the exposure on afwDisplay.Display's frame 0. 

210 +ve detections in blue, -ve detections in cyan 

211 - If display > 1, display the convolved exposure on frame 1 

212</DL> 

213 

214@section meas_algorithms_detection_Example A complete example of using SourceDetectionTask 

215 

216This code is in @link measAlgTasks.py@endlink in the examples directory, and can be run as @em e.g. 

217@code 

218examples/measAlgTasks.py --doDisplay 

219@endcode 

220@dontinclude measAlgTasks.py 

221The example also runs the SingleFrameMeasurementTask; see @ref meas_algorithms_measurement_Example for more 

222explanation. 

223 

224Import the task (there are some other standard imports; read the file if you're confused) 

225@skipline SourceDetectionTask 

226 

227We need to create our task before processing any data as the task constructor 

228can add an extra column to the schema, but first we need an almost-empty Schema 

229@skipline makeMinimalSchema 

230after which we can call the constructor: 

231@skip SourceDetectionTask.ConfigClass 

232@until detectionTask 

233 

234We're now ready to process the data (we could loop over multiple exposures/catalogues using the same 

235task objects). First create the output table: 

236@skipline afwTable 

237 

238And process the image 

239@skipline result 

240(You may not be happy that the threshold was set in the config before creating the Task rather than being set 

241separately for each exposure. You @em can reset it just before calling the run method if you must, but we 

242should really implement a better solution). 

243 

244We can then unpack and use the results: 

245@skip sources 

246@until print 

247 

248<HR> 

249To investigate the @ref meas_algorithms_detection_Debug, put something like 

250@code{.py} 

251 import lsstDebug 

252 def DebugInfo(name): 

253 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 

254 if name == "lsst.meas.algorithms.detection": 

255 di.display = 1 

256 

257 return di 

258 

259 lsstDebug.Info = DebugInfo 

260@endcode 

261into your debug.py file and run measAlgTasks.py with the @c --debug flag. 

262 """ 

263 ConfigClass = SourceDetectionConfig 

264 _DefaultName = "sourceDetection" 

265 

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

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

268 

269 @param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog 

270 @param **kwds Keyword arguments passed to lsst.pipe.base.task.Task.__init__. 

271 

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

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

274 negative threshold. 

275 

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

277 these columns are indeed present in the input match list; see @ref Example 

278 """ 

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

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

281 self.negativeFlagKey = schema.addField( 

282 "flags_negative", type="Flag", 

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

284 ) 

285 else: 

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

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

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

289 self.negativeFlagKey = None 

290 if self.config.reEstimateBackground: 

291 self.makeSubtask("background") 

292 if self.config.doTempLocalBackground: 

293 self.makeSubtask("tempLocalBackground") 

294 if self.config.doTempWideBackground: 

295 self.makeSubtask("tempWideBackground") 

296 

297 @pipeBase.timeMethod 

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

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

300 

301 Parameters 

302 ---------- 

303 table : `lsst.afw.table.SourceTable` 

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

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

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

307 doSmooth : `bool` 

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

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

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

311 sigma : `float` 

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

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

314 clearMask : `bool` 

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

316 expId : `int` 

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

318 RNG seed by subclasses. 

319 

320 Returns 

321 ------- 

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

323 ``sources`` 

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

325 ``fpSets`` 

326 The result resturned by `detectFootprints` 

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

328 

329 Raises 

330 ------ 

331 ValueError 

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

333 lsst.pipe.base.TaskError 

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

335 

336 Notes 

337 ----- 

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

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

340 """ 

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

342 raise ValueError("Table has incorrect Schema") 

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

344 clearMask=clearMask, expId=expId) 

345 sources = afwTable.SourceCatalog(table) 

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

347 if results.negative: 

348 results.negative.makeSources(sources) 

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

350 for record in sources: 

351 record.set(self.negativeFlagKey, True) 

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

353 results.positive.makeSources(sources) 

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

355 results.sources = sources 

356 return results 

357 

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

359 category=FutureWarning) 

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

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

362 

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

364 """Display detections if so configured 

365 

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

367 

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

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

370 

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

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

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

374 

375 Parameters 

376 ---------- 

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

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

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

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

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

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

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

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

385 Convolved image used for thresholding. 

386 """ 

387 try: 

388 import lsstDebug 

389 display = lsstDebug.Info(__name__).display 

390 except ImportError: 

391 try: 

392 display 

393 except NameError: 

394 display = False 

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

396 return 

397 

398 afwDisplay.setDefaultMaskTransparency(75) 

399 

400 disp0 = afwDisplay.Display(frame=0) 

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

402 

403 def plotPeaks(fps, ctype): 

404 if fps is None: 

405 return 

406 with disp0.Buffering(): 

407 for fp in fps.getFootprints(): 

408 for pp in fp.getPeaks(): 

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

410 plotPeaks(results.positive, "yellow") 

411 plotPeaks(results.negative, "red") 

412 

413 if convolvedImage and display > 1: 

414 disp1 = afwDisplay.Display(frame=1) 

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

416 

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

418 """Apply a temporary local background subtraction 

419 

420 This temporary local background serves to suppress noise fluctuations 

421 in the wings of bright objects. 

422 

423 Peaks in the footprints will be updated. 

424 

425 Parameters 

426 ---------- 

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

428 Exposure for which to fit local background. 

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

430 Convolved image on which detection will be performed 

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

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

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

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

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

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

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

438 """ 

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

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

441 # it back in. 

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

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

444 self.tempLocalBackground.config.undersampleStyle) 

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

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

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

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

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

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

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

452 

453 def clearMask(self, mask): 

454 """Clear the DETECTED and DETECTED_NEGATIVE mask planes 

455 

456 Removes any previous detection mask in preparation for a new 

457 detection pass. 

458 

459 Parameters 

460 ---------- 

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

462 Mask to be cleared. 

463 """ 

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

465 

466 def calculateKernelSize(self, sigma): 

467 """Calculate size of smoothing kernel 

468 

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

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

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

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

473 

474 Parameters 

475 ---------- 

476 sigma : `float` 

477 Gaussian sigma of smoothing kernel. 

478 

479 Returns 

480 ------- 

481 size : `int` 

482 Size of the smoothing kernel. 

483 """ 

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

485 

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

487 """Retrieve the PSF for an exposure 

488 

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

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

491 

492 Parameters 

493 ---------- 

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

495 Exposure from which to retrieve the PSF. 

496 sigma : `float`, optional 

497 Gaussian sigma to use if provided. 

498 

499 Returns 

500 ------- 

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

502 PSF to use for detection. 

503 """ 

504 if sigma is None: 

505 psf = exposure.getPsf() 

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

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

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

509 size = self.calculateKernelSize(sigma) 

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

511 return psf 

512 

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

514 """Convolve the image with the PSF 

515 

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

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

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

519 Gaussian there's no difference. 

520 

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

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

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

524 because the kernel would extend off the image. 

525 

526 Parameters 

527 ---------- 

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

529 Image to convolve. 

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

531 PSF to convolve with (actually with a Gaussian approximation 

532 to it). 

533 doSmooth : `bool` 

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

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

536 

537 Return Struct contents 

538 ---------------------- 

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

540 Convolved image, without the edges. 

541 sigma : `float` 

542 Gaussian sigma used for the convolution. 

543 """ 

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

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

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

547 

548 if not doSmooth: 

549 middle = maskedImage.Factory(maskedImage) 

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

551 

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

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

554 kWidth = self.calculateKernelSize(sigma) 

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

556 gaussFunc = afwMath.GaussianFunction1D(sigma) 

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

558 

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

560 

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

562 # 

563 # Only search psf-smoothed part of frame 

564 # 

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

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

567 # 

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

569 # 

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

571 

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

573 

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

575 """Apply thresholds to the convolved image 

576 

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

578 

579 The threshold can be modified by the provided multiplication 

580 ``factor``. 

581 

582 Parameters 

583 ---------- 

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

585 Convolved image to threshold. 

586 bbox : `lsst.geom.Box2I` 

587 Bounding box of unconvolved image. 

588 factor : `float` 

589 Multiplier for the configured threshold. 

590 

591 Return Struct contents 

592 ---------------------- 

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

594 Positive detection footprints, if configured. 

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

596 Negative detection footprints, if configured. 

597 factor : `float` 

598 Multiplier for the configured threshold. 

599 """ 

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

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

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

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

604 results.positive = afwDet.FootprintSet( 

605 middle, 

606 threshold, 

607 "DETECTED", 

608 self.config.minPixels 

609 ) 

610 results.positive.setRegion(bbox) 

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

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

613 results.negative = afwDet.FootprintSet( 

614 middle, 

615 threshold, 

616 "DETECTED_NEGATIVE", 

617 self.config.minPixels 

618 ) 

619 results.negative.setRegion(bbox) 

620 

621 return results 

622 

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

624 """Finalize the detected footprints 

625 

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

627 mask planes, and logs the results. 

628 

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

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

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

632 detection results. 

633 

634 Parameters 

635 ---------- 

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

637 Mask image on which to flag detected pixels. 

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

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

640 ``negative`` entries; modified. 

641 sigma : `float` 

642 Gaussian sigma of PSF. 

643 factor : `float` 

644 Multiplier for the configured threshold. 

645 """ 

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

647 fpSet = getattr(results, polarity) 

648 if fpSet is None: 

649 continue 

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

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

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

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

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

655 else: 

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

657 afwGeom.Stencil.MANHATTAN) 

658 for fp in fpSet: 

659 fp.dilate(nGrow, stencil) 

660 fpSet.setMask(mask, maskName) 

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

662 setattr(results, polarity, fpSet) 

663 

664 results.numPos = 0 

665 results.numPosPeaks = 0 

666 results.numNeg = 0 

667 results.numNegPeaks = 0 

668 positive = "" 

669 negative = "" 

670 

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

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

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

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

675 if results.negative is not None: 

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

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

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

679 

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

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

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

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

684 

685 def reEstimateBackground(self, maskedImage, backgrounds): 

686 """Estimate the background after detection 

687 

688 Parameters 

689 ---------- 

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

691 Image on which to estimate the background. 

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

693 List of backgrounds; modified. 

694 

695 Returns 

696 ------- 

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

698 Empirical background model. 

699 """ 

700 bg = self.background.fitBackground(maskedImage) 

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

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

703 bg += self.config.adjustBackground 

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

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

706 self.background.config.undersampleStyle) 

707 

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

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

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

711 actrl.getOrderY(), actrl.getWeighting())) 

712 return bg 

713 

714 def clearUnwantedResults(self, mask, results): 

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

716 

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

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

719 

720 Parameters 

721 ---------- 

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

723 Mask image. 

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

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

726 modified. 

727 """ 

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

729 if self.config.reEstimateBackground: 

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

731 results.negative = None 

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

733 if self.config.reEstimateBackground: 

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

735 results.positive = None 

736 

737 @pipeBase.timeMethod 

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

739 """Detect footprints on an exposure. 

740 

741 Parameters 

742 ---------- 

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

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

745 set in-place. 

746 doSmooth : `bool`, optional 

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

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

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

750 plane. 

751 sigma : `float`, optional 

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

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

754 ``exposure``. 

755 clearMask : `bool`, optional 

756 Clear both DETECTED and DETECTED_NEGATIVE planes before running 

757 detection. 

758 expId : `dict`, optional 

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

760 RNG seed by subclasses. 

761 

762 Return Struct contents 

763 ---------------------- 

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

765 Positive polarity footprints (may be `None`) 

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

767 Negative polarity footprints (may be `None`) 

768 numPos : `int` 

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

770 negative. 

771 numNeg : `int` 

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

773 positive. 

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

775 Re-estimated background. `None` if 

776 ``reEstimateBackground==False``. 

777 factor : `float` 

778 Multiplication factor applied to the configured detection 

779 threshold. 

780 """ 

781 maskedImage = exposure.maskedImage 

782 

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

784 self.clearMask(maskedImage.getMask()) 

785 

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

787 with self.tempWideBackgroundContext(exposure): 

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

789 middle = convolveResults.middle 

790 sigma = convolveResults.sigma 

791 

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

793 results.background = afwMath.BackgroundList() 

794 if self.config.doTempLocalBackground: 

795 self.applyTempLocalBackground(exposure, middle, results) 

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

797 

798 if self.config.reEstimateBackground: 

799 self.reEstimateBackground(maskedImage, results.background) 

800 

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

802 self.display(exposure, results, middle) 

803 

804 return results 

805 

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

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

808 configuration and the statistics of the given image. 

809 

810 Parameters 

811 ---------- 

812 image : `afw.image.MaskedImage` 

813 Image to measure noise statistics from if needed. 

814 thresholdParity: `str` 

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

816 the Threshold will detect. 

817 factor : `float` 

818 Factor by which to multiply the configured detection threshold. 

819 This is useful for tweaking the detection threshold slightly. 

820 

821 Returns 

822 ------- 

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

824 Detection threshold. 

825 """ 

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

827 thresholdValue = self.config.thresholdValue 

828 thresholdType = self.config.thresholdType 

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

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

831 sctrl = afwMath.StatisticsControl() 

832 sctrl.setAndMask(bad) 

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

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

835 thresholdType = 'value' 

836 

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

838 threshold.setIncludeMultiplier(self.config.includeThresholdMultiplier) 

839 return threshold 

840 

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

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

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

844 

845 Parameters 

846 ---------- 

847 fpSet : `afw.detection.FootprintSet` 

848 Set of Footprints whose Peaks should be updated. 

849 image : `afw.image.MaskedImage` 

850 Image to detect new Footprints and Peak in. 

851 threshold : `afw.detection.Threshold` 

852 Threshold object for detection. 

853 

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

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

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

857 """ 

858 for footprint in fpSet.getFootprints(): 

859 oldPeaks = footprint.getPeaks() 

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

861 continue 

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

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

864 # Footprints. 

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

866 fpSetForPeaks = afwDet.FootprintSet( 

867 sub, 

868 threshold, 

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

870 self.config.minPixels 

871 ) 

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

873 for fpForPeaks in fpSetForPeaks.getFootprints(): 

874 for peak in fpForPeaks.getPeaks(): 

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

876 newPeaks.append(peak) 

877 if len(newPeaks) > 0: 

878 del oldPeaks[:] 

879 oldPeaks.extend(newPeaks) 

880 else: 

881 del oldPeaks[1:] 

882 

883 @staticmethod 

884 def setEdgeBits(maskedImage, goodBBox, edgeBitmask): 

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

886 

887 Parameters 

888 ---------- 

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

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

891 goodBBox : `lsst.geom.Box2I` 

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

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

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

895 outside ``goodBBox``. 

896 """ 

897 msk = maskedImage.getMask() 

898 

899 mx0, my0 = maskedImage.getXY0() 

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

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

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

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

904 [0, 0, 

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

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

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

908 ): 

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

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

911 edgeMask |= edgeBitmask 

912 

913 @contextmanager 

914 def tempWideBackgroundContext(self, exposure): 

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

916 

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

918 detection of large footprints that may overwhelm the deblender. 

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

920 

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

922 the context manager. 

923 

924 Parameters 

925 ---------- 

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

927 Exposure on which to remove large-scale background. 

928 

929 Returns 

930 ------- 

931 context : context manager 

932 Context manager that will ensure the temporary wide background 

933 is restored. 

934 """ 

935 doTempWideBackground = self.config.doTempWideBackground 

936 if doTempWideBackground: 

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

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

939 self.tempWideBackground.run(exposure).background 

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

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

942 image = exposure.maskedImage.image 

943 mask = exposure.maskedImage.mask 

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

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

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

947 try: 

948 yield 

949 finally: 

950 if doTempWideBackground: 

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

952 

953 

954def addExposures(exposureList): 

955 """Add a set of exposures together. 

956 

957 Parameters 

958 ---------- 

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

960 Sequence of exposures to add. 

961 

962 Returns 

963 ------- 

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

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

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

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

968 """ 

969 exposure0 = exposureList[0] 

970 image0 = exposure0.getMaskedImage() 

971 

972 addedImage = image0.Factory(image0, True) 

973 addedImage.setXY0(image0.getXY0()) 

974 

975 for exposure in exposureList[1:]: 

976 image = exposure.getMaskedImage() 

977 addedImage += image 

978 

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

980 return addedExposure