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)
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())
176 source.updateCoord(exposure.getWcs(), include_covariance=
False)
179 self.
skyMeasurement.run(catalog, exposure, catalog, exposure.getWcs())
182 fluxes = catalog[
"base_PsfFlux_instFlux"]
183 area = catalog[
"base_PsfFlux_area"]
184 bg = catalog[
"base_LocalBackground_instFlux"]
186 good = (~catalog[
"base_PsfFlux_flag"] & ~catalog[
"base_LocalBackground_flag"]
187 & np.isfinite(fluxes) & np.isfinite(area) & np.isfinite(bg))
189 minNumSources = int(self.config.minFractionSources*self.skyObjects.config.nSources)
192 if minFractionSourcesFactor != 1.0:
193 minNumSources =
max(3, int(minNumSources*minFractionSourcesFactor))
194 if good.sum() < minNumSources:
196 msg = (f
"Insufficient good sky source flux measurements ({good.sum()} < "
197 f
"{minNumSources}) for dynamic threshold calculation.")
199 msg = (f
"Insufficient good sky source flux measurements ({good.sum()} < "
200 f
"{minNumSources}) for background tweak calculation.")
202 nPix = exposure.mask.array.size
204 nGoodPix = np.sum(exposure.mask.array & badPixelMask == 0)
205 if nGoodPix/nPix > 0.2:
207 nDetectedPix = np.sum(exposure.mask.array & detectedPixelMask != 0)
208 msg += (f
" However, {nGoodPix}/{nPix} pixels are not marked NO_DATA or BAD, "
209 "so there should be sufficient area to locate suitable sky sources. "
210 f
"Note that {nDetectedPix} of {nGoodPix} \"good\" pixels were marked "
211 "as DETECTED or DETECTED_NEGATIVE.")
212 raise RuntimeError(msg)
213 raise NoWorkFound(msg)
216 self.log.info(
"Number of good sky sources used for dynamic detection: %d (of %d requested).",
217 good.sum(), self.skyObjects.config.nSources)
219 self.log.info(
"Number of good sky sources used for dynamic detection background tweak:"
220 " %d (of %d requested).", good.sum(), self.skyObjects.config.nSources)
221 bgMedian = np.median((fluxes/area)[good])
223 lq, uq = np.percentile((fluxes - bg*area)[good], [25.0, 75.0])
224 stdevMeas = 0.741*(uq - lq)
225 medianError = np.median(catalog[
"base_PsfFlux_instFluxErr"][good])
227 exposure.setWcs(
None)
228 return Struct(multiplicative=medianError/stdevMeas, additive=bgMedian)
230 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None,
232 """Detect footprints with a dynamic threshold
234 This varies from the vanilla ``detectFootprints`` method because we
235 do detection three times: first with a high threshold to detect
236 "bright" (both positive and negative, the latter to identify very
237 over-subtracted regions) sources for which we grow the DETECTED and
238 DETECTED_NEGATIVE masks significantly to account for wings. Second,
239 with a low threshold to mask all non-empty regions of the image. These
240 two masks are combined and used to identify regions of sky
241 uncontaminated by objects. A final round of detection is then done
242 with the new calculated threshold.
246 exposure : `lsst.afw.image.Exposure`
247 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be
249 doSmooth : `bool`, optional
250 If True, smooth the image before detection using a Gaussian
252 sigma : `float`, optional
253 Gaussian Sigma of PSF (pixels); used for smoothing and to grow
254 detections; if `None` then measure the sigma of the PSF of the
256 clearMask : `bool`, optional
257 Clear both DETECTED and DETECTED_NEGATIVE planes before running
259 expId : `int`, optional
260 Exposure identifier, used as a seed for the random number
261 generator. If absent, the seed will be the sum of the image.
262 background : `lsst.afw.math.BackgroundList`, optional
263 Background that was already subtracted from the exposure; will be
264 modified in-place if ``reEstimateBackground=True``.
268 resutls : `lsst.pipe.base.Struct`
269 The results `~lsst.pipe.base.Struct` contains:
272 Positive polarity footprints.
273 (`lsst.afw.detection.FootprintSet` or `None`)
275 Negative polarity footprints.
276 (`lsst.afw.detection.FootprintSet` or `None`)
278 Number of footprints in positive or 0 if detection polarity was
281 Number of footprints in negative or 0 if detection polarity was
284 Re-estimated background. `None` or the input ``background``
285 if ``reEstimateBackground==False``.
286 (`lsst.afw.math.BackgroundList`)
288 Multiplication factor applied to the configured detection
291 Results from preliminary detection pass.
292 (`lsst.pipe.base.Struct`)
294 maskedImage = exposure.maskedImage
299 oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask([
"DETECTED",
300 "DETECTED_NEGATIVE"])
301 nPix = maskedImage.mask.array.size
303 nGoodPix = np.sum(maskedImage.mask.array & badPixelMask == 0)
304 self.log.info(
"Number of good data pixels (i.e. not NO_DATA or BAD): {} ({:.1f}% of total)".
305 format(nGoodPix, 100*nGoodPix/nPix))
311 psf = self.
getPsf(exposure, sigma=sigma)
312 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
314 if self.config.doBrightPrelimDetection:
317 middle = convolveResults.middle
318 sigma = convolveResults.sigma
320 middle, maskedImage.getBBox(), factor=self.config.prelimThresholdFactor,
321 factorNeg=self.config.prelimNegMultiplier*self.config.prelimThresholdFactor
324 maskedImage.mask, prelim, sigma, factor=self.config.prelimThresholdFactor,
325 factorNeg=self.config.prelimNegMultiplier*self.config.prelimThresholdFactor
327 if self.config.doBrightPrelimDetection:
330 maskedImage.mask.array |= brightDetectedMask
334 seed = (expId
if expId
is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
336 factor = threshResults.multiplicative
337 self.log.info(
"Modifying configured detection threshold by factor %f to %f",
338 factor, factor*self.config.thresholdValue)
343 maskedImage.mask.array |= oldDetected
346 results = self.
applyThreshold(middle, maskedImage.getBBox(), factor)
347 results.prelim = prelim
349 if self.config.doTempLocalBackground:
355 if self.config.reEstimateBackground:
358 self.
display(exposure, results, middle)
360 if self.config.doBackgroundTweak:
369 originalMask = maskedImage.mask.array.copy()
372 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
373 tweakDetResults = self.
applyThreshold(convolveResults.middle, maskedImage.getBBox(), factor)
375 bgLevel = self.
calculateThreshold(exposure, seed, sigma=sigma, minFractionSourcesFactor=0.5,
376 isBgTweak=
True).additive
378 maskedImage.mask.array[:] = originalMask
384 """Modify the background by a constant value
388 exposure : `lsst.afw.image.Exposure`
389 Exposure for which to tweak background.
391 Background level to remove
392 bgList : `lsst.afw.math.BackgroundList`, optional
393 List of backgrounds to append to.
397 bg : `lsst.afw.math.BackgroundMI`
398 Constant background model.
400 self.log.info(
"Tweaking background by %f to match sky photometry", bgLevel)
401 exposure.image -= bgLevel
402 bgStats = lsst.afw.image.MaskedImageF(1, 1)
403 bgStats.set(bgLevel, 0, bgLevel)
405 bgData = (bg, lsst.afw.math.Interpolate.LINEAR, lsst.afw.math.REDUCE_INTERP_ORDER,
406 lsst.afw.math.ApproximateControl.UNKNOWN, 0, 0,
False)
407 if bgList
is not None:
408 bgList.append(bgData)
412 """Perform an initial bright source detection pass.
414 Perform an initial bright object detection pass using a high detection
415 threshold. The footprints in this pass are grown significantly more
416 than is typical to account for wings around bright sources. The
417 negative polarity detections in this pass help in masking severely
418 over-subtracted regions.
420 A maximum fraction of masked pixel from this pass is ensured via
421 the config ``brightMaskFractionMax``. If the masked pixel fraction is
422 above this value, the detection thresholds here are increased by
423 ``bisectFactor`` in a while loop until the detected masked fraction
424 falls below this value.
428 maskedImage : `lsst.afw.image.MaskedImage`
429 Masked image on which to run the detection.
430 convolveResults : `lsst.pipe.base.Struct`
431 The results of the self.convolveImage function with attributes:
434 Convolved image, without the edges
435 (`lsst.afw.image.MaskedImage`).
437 Gaussian sigma used for the convolution (`float`).
441 brightDetectedMask : `numpy.ndarray`
442 Boolean array representing the union of the bright detection pass
443 DETECTED and DETECTED_NEGATIVE masks.
447 self.config.prelimThresholdFactor*self.config.brightMultiplier/self.config.bisectFactor
449 brightNegFactor = self.config.brightNegFactor/self.config.bisectFactor
453 brightMaskFractionMax = self.config.brightMaskFractionMax
459 while nPixDetNeg/nPix > brightMaskFractionMax
or nPixDet/nPix > brightMaskFractionMax:
461 brightPosFactor *= self.config.bisectFactor
462 brightNegFactor *= self.config.bisectFactor
463 prelimBright = self.
applyThreshold(convolveResults.middle, maskedImage.getBBox(),
464 factor=brightPosFactor, factorNeg=brightNegFactor)
466 maskedImage.mask, prelimBright, convolveResults.sigma*self.config.brightGrowFactor,
467 factor=brightPosFactor, factorNeg=brightNegFactor
470 nPix = maskedImage.mask.array.size
472 self.log.info(
"Number (%) of bright DETECTED pix: {} ({:.1f}%)".
473 format(nPixDet, 100*nPixDet/nPix))
475 self.log.info(
"Number (%) of bright DETECTED_NEGATIVE pix: {} ({:.1f}%)".
476 format(nPixDetNeg, 100*nPixDetNeg/nPix))
477 if nPixDetNeg/nPix > brightMaskFractionMax
or nPixDet/nPix > brightMaskFractionMax:
478 self.log.warn(
"Too high a fraction (%.1f > %.1f) of pixels were masked with current "
479 "\"bright\" detection round thresholds. Increasing by a factor of %f "
480 "and trying again.",
max(nPixDetNeg, nPixDet)/nPix,
481 brightMaskFractionMax, self.config.bisectFactor)
485 brightDetectedMask = (maskedImage.mask.array
486 & maskedImage.mask.getPlaneBitMask([
"DETECTED",
"DETECTED_NEGATIVE"]))
488 return brightDetectedMask