import lsst.afw.math as afwMath
import lsst.afw.table as afwTable
import lsst.pipe.base.connectionTypes as cT
from lsst.meas.algorithms import DynamicDetectionTask
from lsst.meas.base import SkyMapIdGeneratorConfig
from lsst.pex.config import ConfigurableField
from lsst.pipe.base import (
PipelineTask,
PipelineTaskConfig,
PipelineTaskConnections,
Struct,
)
[docs]
class DetectCoaddSourcesConnections(
PipelineTaskConnections,
dimensions=("tract", "patch", "band", "skymap"),
defaultTemplates={"inputCoaddName": "deep", "outputCoaddName": "deep"},
):
[docs]
detectionSchema = cT.InitOutput(
doc="Schema of the detection catalog",
name="{outputCoaddName}Coadd_det_schema",
storageClass="SourceCatalog",
)
[docs]
exposure = cT.Input(
doc="Exposure post detection",
name="{outputCoaddName}Coadd_calexp",
storageClass="ExposureF",
dimensions=("tract", "patch", "band", "skymap"),
)
[docs]
outputBackgrounds = cT.Output(
doc="Output Backgrounds used in detection",
name="{outputCoaddName}Coadd_calexp_background",
storageClass="Background",
dimensions=("tract", "patch", "band", "skymap"),
)
[docs]
outputSources = cT.Output(
doc="Detected sources catalog",
name="{outputCoaddName}Coadd_det",
storageClass="SourceCatalog",
dimensions=("tract", "patch", "band", "skymap"),
)
[docs]
class DetectCoaddSourcesConfig(
PipelineTaskConfig, pipelineConnections=DetectCoaddSourcesConnections
):
"""Configuration parameters for the DetectCoaddSourcesTask"""
[docs]
detection = ConfigurableField(
target=DynamicDetectionTask,
doc="Source detection",
)
[docs]
idGenerator = SkyMapIdGeneratorConfig.make_field()
[docs]
def setDefaults(self):
super().setDefaults()
self.detection.thresholdType = "pixel_stdev"
self.detection.isotropicGrow = True
# Coadds are made from background-subtracted CCDs, so any background
# subtraction should be very basic
self.detection.reEstimateBackground = False
self.detection.background.useApprox = False
self.detection.background.binSize = 4096
self.detection.background.undersampleStyle = "REDUCE_INTERP_ORDER"
# Suppress large footprints that overwhelm the deblender
self.detection.doTempWideBackground = True
# Include band in packed data IDs that go into object IDs (None -> "as
# many bands as are defined", rather than the default of zero).
self.idGenerator.packer.n_bands = None
[docs]
class DetectCoaddSourcesTask(PipelineTask):
"""Detect sources on a single filter coadd.
Coadding individual visits requires each exposure to be warped. This
introduces covariance in the noise properties across pixels. Before
detection, we correct the coadd variance by scaling the variance plane in
the coadd to match the observed variance. This is an approximate
approach -- strictly, we should propagate the full covariance matrix --
but it is simple and works well in practice.
After scaling the variance plane, we detect sources and generate footprints
by delegating to the @ref SourceDetectionTask_ "detection" subtask.
DetectCoaddSourcesTask is meant to be run after assembling a coadded image
in a given band. The purpose of the task is to update the background,
detect all sources in a single band and generate a set of parent
footprints. Subsequent tasks in the multi-band processing procedure will
merge sources across bands and, eventually, perform forced photometry.
Parameters
----------
schema : `lsst.afw.table.Schema`, optional
Initial schema for the output catalog, modified-in place to include all
fields set by this task. If None, the source minimal schema will be
used.
**kwargs
Additional keyword arguments.
"""
[docs]
_DefaultName = "detectCoaddSources"
[docs]
ConfigClass = DetectCoaddSourcesConfig
def __init__(self, schema=None, **kwargs):
super().__init__(**kwargs)
if schema is None:
schema = afwTable.SourceTable.makeMinimalSchema()
self.makeSubtask("detection", schema=self.schema)
[docs]
self.detectionSchema = afwTable.SourceCatalog(self.schema)
[docs]
def runQuantum(self, butlerQC, inputRefs, outputRefs):
inputs = butlerQC.get(inputRefs)
idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
inputs["idFactory"] = idGenerator.make_table_id_factory()
inputs["expId"] = idGenerator.catalog_id
outputs = self.run(**inputs)
butlerQC.put(outputs, outputRefs)
return
[docs]
def run(self, exposure, idFactory, expId):
"""Run detection on an exposure.
First scale the variance plane to match the observed variance
using ``ScaleVarianceTask``. Then invoke the ``SourceDetectionTask_``
"detection" subtask to detect sources.
Parameters
----------
exposure : `lsst.afw.image.Exposure`
Exposure on which to detect (may be backround-subtracted and
scaled, depending on configuration).
idFactory : `lsst.afw.table.IdFactory`
IdFactory to set source identifiers.
expId : `int`
Exposure identifier (integer) for RNG seed.
Returns
-------
result : `lsst.pipe.base.Struct`
Results as a struct with attributes:
``sources``
Catalog of detections (`lsst.afw.table.SourceCatalog`).
``backgrounds``
List of backgrounds (`list`).
"""
backgrounds = afwMath.BackgroundList()
table = afwTable.SourceTable.make(self.schema, idFactory)
detections = self.detection.run(table, exposure, expId=expId)
sources = detections.sources
if hasattr(detections, "background") and detections.background:
for bg in detections.background:
backgrounds.append(bg)
return Struct(outputSources=sources, outputBackgrounds=backgrounds)