106 def calculateThreshold(self, exposure, seed, sigma=None, minFractionSourcesFactor=1.0, isBgTweak=False):
107 """Calculate new threshold
109 This is the main functional addition to the vanilla
110 `SourceDetectionTask`.
112 We identify sky objects and perform forced PSF photometry on
113 them. Using those PSF flux measurements and estimated errors,
114 we set the threshold so that the stdev of the measurements
115 matches the median estimated error.
119 exposure : `lsst.afw.image.Exposure`
120 Exposure on which we're detecting sources.
122 RNG seed to use for finding sky objects.
123 sigma : `float`, optional
124 Gaussian sigma of smoothing kernel; if not provided,
125 will be deduced from the exposure's PSF.
126 minFractionSourcesFactor : `float`
127 Change the fraction of required sky sources from that set in
128 ``self.config.minFractionSources`` by this factor. NOTE: this
129 is intended for use in the background tweak pass (the detection
130 threshold is much lower there, so many more pixels end up marked
131 as DETECTED or DETECTED_NEGATIVE, leaving less room for sky
134 Set to ``True`` for the background tweak pass (for more helpful
139 result : `lsst.pipe.base.Struct`
140 Result struct with components:
143 Multiplicative factor to be applied to the
144 configured detection threshold (`float`).
146 Additive factor to be applied to the background
152 Raised if the number of good sky sources found is less than the
154 (``self.config.minFractionSources``*``minFractionSourcesFactor``)
155 of the number requested (``self.skyObjects.config.nSources``).
157 wcsIsNone = exposure.getWcs()
is None
159 self.log.info(
"WCS for exposure is None. Setting a dummy WCS for dynamic detection.")
162 cdMatrix=makeCdMatrix(scale=1e-5*geom.degrees)))
163 fp = self.skyObjects.
run(exposure.maskedImage.mask, seed)
165 skyFootprints.setFootprints(fp)
167 catalog = SourceCatalog(table)
168 catalog.reserve(len(skyFootprints.getFootprints()))
169 skyFootprints.makeSources(catalog)
170 key = catalog.getCentroidSlot().getMeasKey()
171 for source
in catalog:
172 peaks = source.getFootprint().getPeaks()
173 assert len(peaks) == 1
174 source.set(key, peaks[0].getF())
175 source.updateCoord(exposure.getWcs())
181 fluxes = catalog[
"base_PsfFlux_instFlux"]
182 area = catalog[
"base_PsfFlux_area"]
183 bg = catalog[
"base_LocalBackground_instFlux"]
185 good = (~catalog[
"base_PsfFlux_flag"] & ~catalog[
"base_LocalBackground_flag"]
186 & np.isfinite(fluxes) & np.isfinite(area) & np.isfinite(bg))
188 minNumSources = int(self.config.minFractionSources*self.skyObjects.config.nSources)
191 if minFractionSourcesFactor != 1.0:
192 minNumSources = max(3, int(minNumSources*minFractionSourcesFactor))
193 if good.sum() < minNumSources:
195 msg = (f
"Insufficient good sky source flux measurements ({good.sum()} < "
196 f
"{minNumSources}) for dynamic threshold calculation.")
198 msg = (f
"Insufficient good sky source flux measurements ({good.sum()} < "
199 f
"{minNumSources}) for background tweak calculation.")
201 nPix = exposure.mask.array.size
203 nGoodPix = np.sum(exposure.mask.array & badPixelMask == 0)
204 if nGoodPix/nPix > 0.2:
206 nDetectedPix = np.sum(exposure.mask.array & detectedPixelMask != 0)
207 msg += (f
" However, {nGoodPix}/{nPix} pixels are not marked NO_DATA or BAD, "
208 "so there should be sufficient area to locate suitable sky sources. "
209 f
"Note that {nDetectedPix} of {nGoodPix} \"good\" pixels were marked "
210 "as DETECTED or DETECTED_NEGATIVE.")
211 raise RuntimeError(msg)
212 raise NoWorkFound(msg)
215 self.log.info(
"Number of good sky sources used for dynamic detection: %d (of %d requested).",
216 good.sum(), self.skyObjects.config.nSources)
218 self.log.info(
"Number of good sky sources used for dynamic detection background tweak:"
219 " %d (of %d requested).", good.sum(), self.skyObjects.config.nSources)
220 bgMedian = np.median((fluxes/area)[good])
222 lq, uq = np.percentile((fluxes - bg*area)[good], [25.0, 75.0])
223 stdevMeas = 0.741*(uq - lq)
224 medianError = np.median(catalog[
"base_PsfFlux_instFluxErr"][good])
226 exposure.setWcs(
None)
227 return Struct(multiplicative=medianError/stdevMeas, additive=bgMedian)
229 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None,
231 """Detect footprints with a dynamic threshold
233 This varies from the vanilla ``detectFootprints`` method because we
234 do detection three times: first with a high threshold to detect
235 "bright" (both positive and negative, the latter to identify very
236 over-subtracted regions) sources for which we grow the DETECTED and
237 DETECTED_NEGATIVE masks significantly to account for wings. Second,
238 with a low threshold to mask all non-empty regions of the image. These
239 two masks are combined and used to identify regions of sky
240 uncontaminated by objects. A final round of detection is then done
241 with the new calculated threshold.
245 exposure : `lsst.afw.image.Exposure`
246 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
248 doSmooth : `bool`, optional
249 If True, smooth the image before detection using a Gaussian
251 sigma : `float`, optional
252 Gaussian Sigma of PSF (pixels); used for smoothing and to grow
253 detections; if `None` then measure the sigma of the PSF of the
255 clearMask : `bool`, optional
256 Clear both DETECTED and DETECTED_NEGATIVE planes before running
258 expId : `int`, optional
259 Exposure identifier, used as a seed for the random number
260 generator. If absent, the seed will be the sum of the image.
261 background : `lsst.afw.math.BackgroundList`, optional
262 Background that was already subtracted from the exposure; will be
263 modified in-place if ``reEstimateBackground=True``.
267 resutls : `lsst.pipe.base.Struct`
268 The results `~lsst.pipe.base.Struct` contains:
271 Positive polarity footprints.
272 (`lsst.afw.detection.FootprintSet` or `None`)
274 Negative polarity footprints.
275 (`lsst.afw.detection.FootprintSet` or `None`)
277 Number of footprints in positive or 0 if detection polarity was
280 Number of footprints in negative or 0 if detection polarity was
283 Re-estimated background. `None` or the input ``background``
284 if ``reEstimateBackground==False``.
285 (`lsst.afw.math.BackgroundList`)
287 Multiplication factor applied to the configured detection
290 Results from preliminary detection pass.
291 (`lsst.pipe.base.Struct`)
293 maskedImage = exposure.maskedImage
298 oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask([
"DETECTED",
299 "DETECTED_NEGATIVE"])
300 nPix = maskedImage.mask.array.size
302 nGoodPix = np.sum(maskedImage.mask.array & badPixelMask == 0)
303 self.log.info(
"Number of good data pixels (i.e. not NO_DATA or BAD): {} ({:.1f}% of total)".
304 format(nGoodPix, 100*nGoodPix/nPix))
310 psf = self.
getPsf(exposure, sigma=sigma)
311 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
313 if self.config.doBrightPrelimDetection:
316 middle = convolveResults.middle
317 sigma = convolveResults.sigma
319 middle, maskedImage.getBBox(), factor=self.config.prelimThresholdFactor,
320 factorNeg=self.config.prelimNegMultiplier*self.config.prelimThresholdFactor
323 maskedImage.mask, prelim, sigma, factor=self.config.prelimThresholdFactor,
324 factorNeg=self.config.prelimNegMultiplier*self.config.prelimThresholdFactor
326 if self.config.doBrightPrelimDetection:
329 maskedImage.mask.array |= brightDetectedMask
333 seed = (expId
if expId
is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
335 factor = threshResults.multiplicative
336 self.log.info(
"Modifying configured detection threshold by factor %f to %f",
337 factor, factor*self.config.thresholdValue)
342 maskedImage.mask.array |= oldDetected
345 results = self.
applyThreshold(middle, maskedImage.getBBox(), factor)
346 results.prelim = prelim
347 results.background = background
if background
is not None else lsst.afw.math.BackgroundList()
348 if self.config.doTempLocalBackground:
354 if self.config.reEstimateBackground:
357 self.
display(exposure, results, middle)
359 if self.config.doBackgroundTweak:
368 originalMask = maskedImage.mask.array.copy()
371 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
372 tweakDetResults = self.
applyThreshold(convolveResults.middle, maskedImage.getBBox(), factor)
374 bgLevel = self.
calculateThreshold(exposure, seed, sigma=sigma, minFractionSourcesFactor=0.5,
375 isBgTweak=
True).additive
377 maskedImage.mask.array[:] = originalMask
383 """Modify the background by a constant value
387 exposure : `lsst.afw.image.Exposure`
388 Exposure for which to tweak background.
390 Background level to remove
391 bgList : `lsst.afw.math.BackgroundList`, optional
392 List of backgrounds to append to.
396 bg : `lsst.afw.math.BackgroundMI`
397 Constant background model.
399 self.log.info(
"Tweaking background by %f to match sky photometry", bgLevel)
400 exposure.image -= bgLevel
401 bgStats = lsst.afw.image.MaskedImageF(1, 1)
402 bgStats.set(bgLevel, 0, bgLevel)
404 bgData = (bg, lsst.afw.math.Interpolate.LINEAR, lsst.afw.math.REDUCE_INTERP_ORDER,
405 lsst.afw.math.ApproximateControl.UNKNOWN, 0, 0,
False)
406 if bgList
is not None:
407 bgList.append(bgData)
411 """Perform an initial bright source detection pass.
413 Perform an initial bright object detection pass using a high detection
414 threshold. The footprints in this pass are grown significantly more
415 than is typical to account for wings around bright sources. The
416 negative polarity detections in this pass help in masking severely
417 over-subtracted regions.
419 A maximum fraction of masked pixel from this pass is ensured via
420 the config ``brightMaskFractionMax``. If the masked pixel fraction is
421 above this value, the detection thresholds here are increased by
422 ``bisectFactor`` in a while loop until the detected masked fraction
423 falls below this value.
427 maskedImage : `lsst.afw.image.MaskedImage`
428 Masked image on which to run the detection.
429 convolveResults : `lsst.pipe.base.Struct`
430 The results of the self.convolveImage function with attributes:
433 Convolved image, without the edges
434 (`lsst.afw.image.MaskedImage`).
436 Gaussian sigma used for the convolution (`float`).
440 brightDetectedMask : `numpy.ndarray`
441 Boolean array representing the union of the bright detection pass
442 DETECTED and DETECTED_NEGATIVE masks.
446 self.config.prelimThresholdFactor*self.config.brightMultiplier/self.config.bisectFactor
448 brightNegFactor = self.config.brightNegFactor/self.config.bisectFactor
452 brightMaskFractionMax = self.config.brightMaskFractionMax
458 while nPixDetNeg/nPix > brightMaskFractionMax
or nPixDet/nPix > brightMaskFractionMax:
460 brightPosFactor *= self.config.bisectFactor
461 brightNegFactor *= self.config.bisectFactor
462 prelimBright = self.
applyThreshold(convolveResults.middle, maskedImage.getBBox(),
463 factor=brightPosFactor, factorNeg=brightNegFactor)
465 maskedImage.mask, prelimBright, convolveResults.sigma*self.config.brightGrowFactor,
466 factor=brightPosFactor, factorNeg=brightNegFactor
469 nPix = maskedImage.mask.array.size
471 self.log.info(
"Number (%) of bright DETECTED pix: {} ({:.1f}%)".
472 format(nPixDet, 100*nPixDet/nPix))
474 self.log.info(
"Number (%) of bright DETECTED_NEGATIVE pix: {} ({:.1f}%)".
475 format(nPixDetNeg, 100*nPixDetNeg/nPix))
476 if nPixDetNeg/nPix > brightMaskFractionMax
or nPixDet/nPix > brightMaskFractionMax:
477 self.log.warn(
"Too high a fraction (%.1f > %.1f) of pixels were masked with current "
478 "\"bright\" detection round thresholds. Increasing by a factor of %f "
479 "and trying again.", max(nPixDetNeg, nPixDet)/nPix,
480 brightMaskFractionMax, self.config.bisectFactor)
484 brightDetectedMask = (maskedImage.mask.array
485 & maskedImage.mask.getPlaneBitMask([
"DETECTED",
"DETECTED_NEGATIVE"]))
487 return brightDetectedMask