2 __all__ = [
"DynamicDetectionConfig", 
"DynamicDetectionTask"]
 
    6 from lsst.pex.config 
import Field, ConfigurableField
 
    9 from .detection 
import SourceDetectionConfig, SourceDetectionTask
 
   10 from .skyObjects 
import SkyObjectsTask
 
   21     """Configuration for DynamicDetectionTask""" 
   22     prelimThresholdFactor = Field(dtype=float, default=0.5,
 
   23                                   doc=
"Fraction of the threshold to use for first pass (to find sky objects)")
 
   24     skyObjects = ConfigurableField(target=SkyObjectsTask, doc=
"Generate sky objects")
 
   25     doBackgroundTweak = Field(dtype=bool, default=
True,
 
   26                               doc=
"Tweak background level so median PSF flux of sky objects is zero?")
 
   27     minNumSources = Field(dtype=int, default=10,
 
   28                           doc=
"Minimum number of sky sources in statistical sample; " 
   29                               "if below this number, we refuse to modify the threshold.")
 
   32         SourceDetectionConfig.setDefaults(self)
 
   37     """Detection of sources on an image with a dynamic threshold 
   39     We first detect sources using a lower threshold than normal (see config 
   40     parameter ``prelimThresholdFactor``) in order to identify good sky regions 
   41     (configurable ``skyObjects``). Then we perform forced PSF photometry on 
   42     those sky regions. Using those PSF flux measurements and estimated errors, 
   43     we set the threshold so that the stdev of the measurements matches the 
   44     median estimated error. 
   46     ConfigClass = DynamicDetectionConfig
 
   47     _DefaultName = 
"dynamicDetection" 
   52         Besides the usual initialisation of configurables, we also set up 
   53         the forced measurement which is deliberately not represented in 
   54         this Task's configuration parameters because we're using it as part 
   55         of the algorithm and we don't want to allow it to be modified. 
   57         SourceDetectionTask.__init__(self, *args, **kwargs)
 
   58         self.makeSubtask(
"skyObjects")
 
   61         config = ForcedMeasurementTask.ConfigClass()
 
   62         config.plugins.names = [
'base_TransformedCentroid', 
'base_PsfFlux', 
'base_LocalBackground']
 
   64         for slot 
in (
"shape", 
"psfShape", 
"apFlux", 
"modelFlux", 
"gaussianFlux", 
"calibFlux"):
 
   65             setattr(config.slots, slot, 
None)
 
   66         config.copyColumns = {}
 
   72         """Calculate new threshold 
   74         This is the main functional addition to the vanilla 
   75         `SourceDetectionTask`. 
   77         We identify sky objects and perform forced PSF photometry on 
   78         them. Using those PSF flux measurements and estimated errors, 
   79         we set the threshold so that the stdev of the measurements 
   80         matches the median estimated error. 
   84         exposure : `lsst.afw.image.Exposure` 
   85             Exposure on which we're detecting sources. 
   87             RNG seed to use for finding sky objects. 
   88         sigma : `float`, optional 
   89             Gaussian sigma of smoothing kernel; if not provided, 
   90             will be deduced from the exposure's PSF. 
   94         result : `lsst.pipe.base.Struct` 
   95             Result struct with components: 
   97             - ``multiplicative``: multiplicative factor to be applied to the 
   98                 configured detection threshold (`float`). 
   99             - ``additive``: additive factor to be applied to the background 
  103         fp = self.skyObjects.
run(exposure.maskedImage.mask, seed)
 
  105         skyFootprints.setFootprints(fp)
 
  108         catalog.reserve(len(skyFootprints.getFootprints()))
 
  109         skyFootprints.makeSources(catalog)
 
  110         key = catalog.getCentroidSlot().getMeasKey()
 
  111         for source 
in catalog:
 
  112             peaks = source.getFootprint().getPeaks()
 
  113             assert len(peaks) == 1
 
  114             source.set(key, peaks[0].getF())
 
  115             source.updateCoord(exposure.getWcs())
 
  121         fluxes = catalog[
"base_PsfFlux_instFlux"]
 
  122         area = catalog[
"base_PsfFlux_area"]
 
  123         bg = catalog[
"base_LocalBackground_instFlux"]
 
  125         good = (~catalog[
"base_PsfFlux_flag"] & ~catalog[
"base_LocalBackground_flag"]
 
  126                 & np.isfinite(fluxes) & np.isfinite(area) & np.isfinite(bg))
 
  128         if good.sum() < self.config.minNumSources:
 
  129             self.log.
warn(
"Insufficient good flux measurements (%d < %d) for dynamic threshold calculation",
 
  130                           good.sum(), self.config.minNumSources)
 
  131             return Struct(multiplicative=1.0, additive=0.0)
 
  133         bgMedian = np.median((fluxes/area)[good])
 
  135         lq, uq = np.percentile((fluxes - bg*area)[good], [25.0, 75.0])
 
  136         stdevMeas = 0.741*(uq - lq)
 
  137         medianError = np.median(catalog[
"base_PsfFlux_instFluxErr"][good])
 
  138         return Struct(multiplicative=medianError/stdevMeas, additive=bgMedian)
 
  140     def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None):
 
  141         """Detect footprints with a dynamic threshold 
  143         This varies from the vanilla ``detectFootprints`` method because we 
  144         do detection twice: one with a low threshold so that we can find 
  145         sky uncontaminated by objects, then one more with the new calculated 
  150         exposure : `lsst.afw.image.Exposure` 
  151             Exposure to process; DETECTED{,_NEGATIVE} mask plane will be 
  153         doSmooth : `bool`, optional 
  154             If True, smooth the image before detection using a Gaussian 
  156         sigma : `float`, optional 
  157             Gaussian Sigma of PSF (pixels); used for smoothing and to grow 
  158             detections; if `None` then measure the sigma of the PSF of the 
  160         clearMask : `bool`, optional 
  161             Clear both DETECTED and DETECTED_NEGATIVE planes before running 
  163         expId : `int`, optional 
  164             Exposure identifier, used as a seed for the random number 
  165             generator. If absent, the seed will be the sum of the image. 
  167         Return Struct contents 
  168         ---------------------- 
  169         positive : `lsst.afw.detection.FootprintSet` 
  170             Positive polarity footprints (may be `None`) 
  171         negative : `lsst.afw.detection.FootprintSet` 
  172             Negative polarity footprints (may be `None`) 
  174             Number of footprints in positive or 0 if detection polarity was 
  177             Number of footprints in negative or 0 if detection polarity was 
  179         background : `lsst.afw.math.BackgroundList` 
  180             Re-estimated background.  `None` if 
  181             ``reEstimateBackground==False``. 
  183             Multiplication factor applied to the configured detection 
  185         prelim : `lsst.pipe.base.Struct` 
  186             Results from preliminary detection pass. 
  188         maskedImage = exposure.maskedImage
 
  193             oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask([
"DETECTED",
 
  194                                                                                      "DETECTED_NEGATIVE"])
 
  199             psf = self.
getPsf(exposure, sigma=sigma)
 
  200             convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
 
  201             middle = convolveResults.middle
 
  202             sigma = convolveResults.sigma
 
  203             prelim = self.
applyThreshold(middle, maskedImage.getBBox(), self.config.prelimThresholdFactor)
 
  204             self.
finalizeFootprints(maskedImage.mask, prelim, sigma, self.config.prelimThresholdFactor)
 
  208             seed = (expId 
if expId 
is not None else int(maskedImage.image.array.sum())) % (2**31 - 1)
 
  210             factor = threshResults.multiplicative
 
  211             self.log.
info(
"Modifying configured detection threshold by factor %f to %f",
 
  212                           factor, factor*self.config.thresholdValue)
 
  217                 maskedImage.mask.array |= oldDetected
 
  220             results = self.
applyThreshold(middle, maskedImage.getBBox(), factor)
 
  221             results.prelim = prelim
 
  223             if self.config.doTempLocalBackground:
 
  229         if self.config.reEstimateBackground:
 
  232         self.
display(exposure, results, middle)
 
  234         if self.config.doBackgroundTweak:
 
  241             originalMask = maskedImage.mask.array.copy()
 
  244                 convolveResults = self.
convolveImage(maskedImage, psf, doSmooth=doSmooth)
 
  245                 tweakDetResults = self.
applyThreshold(convolveResults.middle, maskedImage.getBBox(), factor)
 
  249                 maskedImage.mask.array[:] = originalMask
 
  255         """Modify the background by a constant value 
  259         exposure : `lsst.afw.image.Exposure` 
  260             Exposure for which to tweak background. 
  262             Background level to remove 
  263         bgList : `lsst.afw.math.BackgroundList`, optional 
  264             List of backgrounds to append to. 
  268         bg : `lsst.afw.math.BackgroundMI` 
  269             Constant background model. 
  271         self.log.
info(
"Tweaking background by %f to match sky photometry", bgLevel)
 
  272         exposure.image -= bgLevel
 
  273         bgStats = lsst.afw.image.MaskedImageF(1, 1)
 
  274         bgStats.set(bgLevel, 0, bgLevel)
 
  276         bgData = (bg, lsst.afw.math.Interpolate.LINEAR, lsst.afw.math.REDUCE_INTERP_ORDER,
 
  277                   lsst.afw.math.ApproximateControl.UNKNOWN, 0, 0, 
False)
 
  278         if bgList 
is not None:
 
  279             bgList.append(bgData)