doMerge = pexConfig.Field(
dtype=bool,
default=True,
doc="Merge positive and negative diaSources with grow radius "
"set by growFootprint"
)
doForcedMeasurement = pexConfig.Field(
dtype=bool,
default=True,
doc="Force photometer diaSource locations on PVI?"
)
doAddMetrics = pexConfig.Field(
dtype=bool,
default=False,
doc="Add columns to the source table to hold analysis metrics?"
)
doSubtractBackground = pexConfig.Field(
dtype=bool,
doc="Subtract a background model from the image before detection?",
default=True,
)
doWriteBackground = pexConfig.Field(
dtype=bool,
doc="Persist the fitted background model?",
default=False,
)
doCalculateResidualMetics = pexConfig.Field(
dtype=bool,
doc="Calculate metrics to assess image subtraction quality for the task"
"metadata?",
default=True,
)
subtractInitialBackground = pexConfig.ConfigurableField(
target=lsst.meas.algorithms.SubtractBackgroundTask,
doc="Task to perform intial background subtraction, before first detection pass.",
)
subtractFinalBackground = pexConfig.ConfigurableField(
target=lsst.meas.algorithms.SubtractBackgroundTask,
doc="Task to perform final background subtraction, after first detection pass.",
)
doFindCosmicRays = pexConfig.Field(
dtype=bool,
doc="Detect and mask cosmic rays on the difference image?"
"CRs can be interpolated over by setting cosmicray.keepCRs=False",
default=True,
)
cosmicray = pexConfig.ConfigField(
dtype=FindCosmicRaysConfig,
doc="Options for finding and masking cosmic rays",
)
detection = pexConfig.ConfigurableField(
target=SourceDetectionTask,
doc="Final source detection for diaSource measurement",
)
streakDetection = pexConfig.ConfigurableField(
target=SourceDetectionTask,
doc="Separate source detection used only for streak masking",
)
doDeblend = pexConfig.Field(
dtype=bool,
default=False,
doc="Deblend DIASources after detection?"
)
deblend = pexConfig.ConfigurableField(
target=lsst.meas.deblender.SourceDeblendTask,
doc="Task to split blended sources into their components."
)
measurement = pexConfig.ConfigurableField(
target=DipoleFitTask,
doc="Task to measure sources on the difference image.",
)
doApCorr = lsst.pex.config.Field(
dtype=bool,
default=True,
doc="Run subtask to apply aperture corrections"
)
applyApCorr = lsst.pex.config.ConfigurableField(
target=ApplyApCorrTask,
doc="Task to apply aperture corrections"
)
forcedMeasurement = pexConfig.ConfigurableField(
target=ForcedMeasurementTask,
doc="Task to force photometer science image at diaSource locations.",
)
growFootprint = pexConfig.Field(
dtype=int,
default=2,
doc="Grow positive and negative footprints by this many pixels before merging"
)
diaSourceMatchRadius = pexConfig.Field(
dtype=float,
default=0.5,
doc="Match radius (in arcseconds) for DiaSource to Source association"
)
doSkySources = pexConfig.Field(
dtype=bool,
default=False,
doc="Generate sky sources?",
)
skySources = pexConfig.ConfigurableField(
target=SkyObjectsTask,
doc="Generate sky sources",
)
doMaskStreaks = pexConfig.Field(
dtype=bool,
default=True,
doc="Turn on streak masking",
)
maskStreaks = pexConfig.ConfigurableField(
target=MaskStreaksTask,
doc="Subtask for masking streaks. Only used if doMaskStreaks is True. "
"Adds a mask plane to an exposure, with the mask plane name set by streakMaskName.",
)
streakBinFactor = pexConfig.Field(
dtype=int,
default=4,
doc="Bin scale factor to use when rerunning detection for masking streaks. "
"Only used if doMaskStreaks is True.",
)
writeStreakInfo = pexConfig.Field(
dtype=bool,
default=False,
doc="Record the parameters of any detected streaks. For LSST, this should be turned off except for "
"development work."
)
findGlints = pexConfig.ConfigurableField(
target=FindGlintTrailsTask,
doc="Subtask for finding glint trails, usually caused by satellites or debris."
)
writeGlintInfo = pexConfig.Field(
dtype=bool,
default=True,
doc="Record the parameters of any detected glint trails."
)
setPrimaryFlags = pexConfig.ConfigurableField(
target=SetPrimaryFlagsTask,
doc="Task to add isPrimary and deblending-related flags to the catalog."
)
badSourceFlags = lsst.pex.config.ListField(
dtype=str,
doc="Sources with any of these flags set are removed before writing the output catalog.",
default=("base_PixelFlags_flag_offimage",
"base_PixelFlags_flag_interpolatedCenterAll",
"base_PixelFlags_flag_badCenter",
"base_PixelFlags_flag_edgeCenter",
"base_PixelFlags_flag_nodataCenter",
"base_PixelFlags_flag_saturatedCenter",
"base_PixelFlags_flag_sat_templateCenter",
"base_PixelFlags_flag_spikeCenter",
),
)
clearMaskPlanes = lsst.pex.config.ListField(
dtype=str,
doc="Mask planes to clear before running detection.",
default=("DETECTED", "DETECTED_NEGATIVE", "NOT_DEBLENDED", "STREAK"),
)
doRejectBadMaskPlaneDetections = pexConfig.Field(
dtype=bool,
default=True,
doc="Reject any peaks detected on ``badMaskPlanes`` before measurement."
"These should all be rejected downstream by ``badSourceFlags``, "
"but filtering earlier can save time."
)
badMaskPlanes = lsst.pex.config.ListField(
dtype=str,
doc="Detections whose footprint peak lies on a pixel with any of these"
" mask planes set will be rejected before measurement."
" Any missing mask planes will be silently ignored.",
default=("NO_DATA", "BAD", "SAT", "EDGE"),
)
raiseOnBadSubtractionRatio = pexConfig.Field(
dtype=bool,
default=True,
doc="Raise an error if the ratio of power in detected footprints"
" on the difference image to the power in footprints on the science"
" image exceeds ``badSubtractionRatioThreshold``",
)
badSubtractionRatioThreshold = pexConfig.Field(
dtype=float,
default=0.2,
doc="Maximum ratio of power in footprints on the difference image to"
" the same footprints on the science image."
"Only used if ``raiseOnBadSubtractionRatio`` is set",
)
badSubtractionVariationThreshold = pexConfig.Field(
dtype=float,
default=0.4,
doc="Maximum standard deviation of the ratio of power in footprints on"
" the difference image to the same footprints on the science image."
"Only used if ``raiseOnBadSubtractionRatio`` is set",
)
raiseOnNoDiaSources = pexConfig.Field(
dtype=bool,
default=True,
doc="Raise an algorithm error if no diaSources are detected.",
)
run_sattle = pexConfig.Field(
dtype=bool,
default=False,
doc="If true, dia source bounding boxes will be sent for verification"
"to the sattle service."
)
sattle_historical = pexConfig.Field(
dtype=bool,
default=False,
doc="If re-running a pipeline that requires sattle, this should be set "
"to True. This will populate sattle's cache with the historic data "
"closest in time to the exposure."
)
idGenerator = DetectorVisitIdGeneratorConfig.make_field()
def setDefaults(self):
# Background subtraction
# Use a small binsize for the first pass to reduce detections on glints
# and extended structures. Should not affect the detectability of
# faint diaSources
self.subtractInitialBackground.binSize = 8
self.subtractInitialBackground.useApprox = False
self.subtractInitialBackground.statisticsProperty = "MEDIAN"
self.subtractInitialBackground.doFilterSuperPixels = True
self.subtractInitialBackground.ignoredPixelMask = ["BAD",
"EDGE",
"DETECTED",
"DETECTED_NEGATIVE",
"NO_DATA",
]
# Use a larger binsize for the final background subtraction, to reduce
# over-subtraction of bright objects.
self.subtractFinalBackground.binSize = 40
self.subtractFinalBackground.useApprox = False
self.subtractFinalBackground.statisticsProperty = "MEDIAN"
self.subtractFinalBackground.doFilterSuperPixels = True
self.subtractFinalBackground.ignoredPixelMask = ["BAD",
"EDGE",
"DETECTED",
"DETECTED_NEGATIVE",
"NO_DATA",
]
# Cosmic ray detection
self.cosmicray.keepCRs = True # do not interpolate over detected CRs
# DiaSource Detection
self.detection.thresholdPolarity = "both"
self.detection.thresholdValue = 5.0
self.detection.reEstimateBackground = False
self.detection.thresholdType = "pixel_stdev"
self.detection.excludeMaskPlanes = []
# Copy configs for binned streak detection from the base detection task
self.streakDetection.thresholdType = self.detection.thresholdType
self.streakDetection.reEstimateBackground = False
self.streakDetection.excludeMaskPlanes = self.detection.excludeMaskPlanes
self.streakDetection.thresholdValue = self.detection.thresholdValue
# Only detect positive streaks
self.streakDetection.thresholdPolarity = "positive"
# Do not grow detected mask for streaks
self.streakDetection.nSigmaToGrow = 0
# Set the streak mask along the entire fit line, not only where the
# detected mask is set.
self.maskStreaks.onlyMaskDetected = False
# Restrict streak masking from growing too large
self.maskStreaks.maxStreakWidth = 100
# Restrict the number of iterations allowed for fitting streaks
# When the fit is good it should solve quickly, and exit a bad fit quickly
self.maskStreaks.maxFitIter = 10
# Only mask to 2 sigma in width
self.maskStreaks.nSigmaMask = 2
# Threshold for including streaks after the Hough Transform.
# A lower value will detect more features that are less linear.
self.maskStreaks.absMinimumKernelHeight = 2
self.measurement.plugins.names |= ["ext_trailedSources_Naive",
"base_LocalPhotoCalib",
"base_LocalWcs",
"ext_shapeHSM_HsmSourceMoments",
"ext_shapeHSM_HsmPsfMoments",
"base_ClassificationSizeExtendedness",
]
self.measurement.slots.psfShape = "ext_shapeHSM_HsmPsfMoments"
self.measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
self.measurement.plugins["base_SdssCentroid"].maxDistToPeak = 5.0
self.forcedMeasurement.plugins = ["base_TransformedCentroid", "base_PsfFlux"]
self.forcedMeasurement.copyColumns = {
"id": "objectId", "parent": "parentObjectId", "coord_ra": "coord_ra", "coord_dec": "coord_dec"}
self.forcedMeasurement.slots.centroid = "base_TransformedCentroid"
self.forcedMeasurement.slots.shape = None
# Keep track of which footprints contain streaks
self.measurement.plugins["base_PixelFlags"].masksFpAnywhere = [
"STREAK", "INJECTED", "INJECTED_TEMPLATE", "HIGH_VARIANCE", "SAT_TEMPLATE", "SPIKE"]
self.measurement.plugins["base_PixelFlags"].masksFpCenter = [
"STREAK", "INJECTED", "INJECTED_TEMPLATE", "HIGH_VARIANCE", "SAT_TEMPLATE", "SPIKE"]
self.skySources.avoidMask = ["DETECTED", "DETECTED_NEGATIVE", "BAD", "NO_DATA", "EDGE"]
def validate(self):
super().validate()
if self.run_sattle:
if not os.getenv("SATTLE_URI_BASE"):
raise pexConfig.FieldValidationError(DetectAndMeasureConfig.run_sattle, self,
"Sattle requested but SATTLE_URI_BASE "
"environment variable not set.")
class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
ConfigClass = DetectAndMeasureConfig
_DefaultName = "detectAndMeasure"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.schema = afwTable.SourceTable.makeMinimalSchema()
self.algMetadata = dafBase.PropertyList()
if self.config.doSubtractBackground:
self.makeSubtask("subtractInitialBackground")
self.makeSubtask("subtractFinalBackground")
self.makeSubtask("detection", schema=self.schema)
if self.config.doDeblend:
self.makeSubtask("deblend", schema=self.schema)
self.makeSubtask("setPrimaryFlags", schema=self.schema, isSingleFrame=True)
self.makeSubtask("measurement", schema=self.schema,
algMetadata=self.algMetadata)
if self.config.doApCorr:
self.makeSubtask("applyApCorr", schema=self.measurement.schema)
if self.config.doForcedMeasurement:
self.schema.addField(
"ip_diffim_forced_PsfFlux_instFlux", "D",
"Forced PSF flux measured on the direct image.",
units="count")
self.schema.addField(
"ip_diffim_forced_PsfFlux_instFluxErr", "D",
"Forced PSF flux error measured on the direct image.",
units="count")
self.schema.addField(
"ip_diffim_forced_PsfFlux_area", "F",
"Forced PSF flux effective area of PSF.",
units="pixel")
self.schema.addField(
"ip_diffim_forced_PsfFlux_flag", "Flag",
"Forced PSF flux general failure flag.")
self.schema.addField(
"ip_diffim_forced_PsfFlux_flag_noGoodPixels", "Flag",
"Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
self.schema.addField(
"ip_diffim_forced_PsfFlux_flag_edge", "Flag",
"Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
self.schema.addField(
"ip_diffim_forced_template_PsfFlux_instFlux", "D",
"Forced PSF flux measured on the template image.",
units="count")
self.schema.addField(
"ip_diffim_forced_template_PsfFlux_instFluxErr", "D",
"Forced PSF flux error measured on the template image.",
units="count")
self.schema.addField(
"ip_diffim_forced_template_PsfFlux_area", "F",
"Forced template PSF flux effective area of PSF.",
units="pixel")
self.schema.addField(
"ip_diffim_forced_template_PsfFlux_flag", "Flag",
"Forced template PSF flux general failure flag.")
self.schema.addField(
"ip_diffim_forced_template_PsfFlux_flag_noGoodPixels", "Flag",
"Forced template PSF flux not enough non-rejected pixels in data to attempt the fit.")
self.schema.addField(
"ip_diffim_forced_template_PsfFlux_flag_edge", "Flag",
)
self.makeSubtask("forcedMeasurement", refSchema=self.schema)
self.schema.addField("refMatchId", "L", "unique id of reference catalog match")
self.schema.addField("srcMatchId", "L", "unique id of source match")
# Create the sky source task for use by metrics,
# even if sky sources are not added to the diaSource catalog
self.makeSubtask("skySources", schema=self.schema)
if self.config.doMaskStreaks:
self.makeSubtask("maskStreaks")
self.makeSubtask("streakDetection")
self.makeSubtask("findGlints")
self.schema.addField("glint_trail", "Flag", "DiaSource is part of a glint trail.")
self.schema.addField("reliability", type="F", doc="Reliability score of the DiaSource")
self.schema.addField("reliabilityVersion", type=str, size=7, doc="Version of the reliability model")
# To get the "merge_*" fields in the schema; have to re-initialize
# this later, once we have a peak schema post-detection.
lsst.afw.detection.FootprintMergeList(self.schema, ["positive", "negative"])
# Check that the schema and config are consistent
for flag in self.config.badSourceFlags:
if flag not in self.schema:
raise pipeBase.InvalidQuantumError("Field %s not in schema" % flag)
# initialize InitOutputs
self.outputSchema = afwTable.SourceCatalog(self.schema)
self.outputSchema.getTable().setMetadata(self.algMetadata)
def runQuantum(self, butlerQC: pipeBase.QuantumContext,
inputRefs: pipeBase.InputQuantizedConnection,
outputRefs: pipeBase.OutputQuantizedConnection):
inputs = butlerQC.get(inputRefs)
idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
idFactory = idGenerator.make_table_id_factory()
# Specify the fields that `annotate` needs below, to ensure they
# exist, even as None.
measurementResults = pipeBase.Struct(
subtractedMeasuredExposure=None,
diaSources=None,
maskedStreaks=None,
differenceBackground=None,
)
try:
self.run(**inputs, idFactory=idFactory, measurementResults=measurementResults)
except pipeBase.AlgorithmError as e:
error = pipeBase.AnnotatedPartialOutputsError.annotate(
e,
self,
measurementResults.subtractedMeasuredExposure,
measurementResults.diaSources,
measurementResults.maskedStreaks,
log=self.log
)
butlerQC.put(measurementResults, outputRefs)
raise error from e
butlerQC.put(measurementResults, outputRefs)
@timeMethod
def run(self, science, matchedTemplate, difference, kernelSources=None,
idFactory=None, measurementResults=None):
if scoreExposure is None:
detectionExposure = differenceExposure.clone()
background = self.subtractInitialBackground.run(detectionExposure).background
doSmooth = True
else:
# Use a clone of differenceExposure because the background is
# subtracted in place.
background = self.subtractInitialBackground.run(differenceExposure.clone()).background
detectionExposure = scoreExposure.clone()
detectionExposure.image -= background.getImage()
doSmooth = False
table = afwTable.SourceTable.make(self.schema)
self.detection.run(
table=table,
exposure=detectionExposure,
doSmooth=doSmooth,
background=background,
clearMask=True,
)
# Use the temporary detection mask for the final background subtraction.
# The detection mask planes will be cleared before the final detection
# step, so it is OK if they get set for differenceExposure.
detectedBit = differenceExposure.mask.getPlaneBitMask(["DETECTED"])
detectedPix = detectionExposure.mask.array & detectedBit > 0
detectedNegativeBit = differenceExposure.mask.getPlaneBitMask(["DETECTED_NEGATIVE"])
detectedNegativePix = detectionExposure.mask.array & detectedNegativeBit > 0
differenceExposure.mask.array[detectedPix] |= detectedBit
differenceExposure.mask.array[detectedNegativePix] |= detectedNegativeBit
background = self.subtractFinalBackground.run(differenceExposure).background
if scoreExposure is not None:
# The preconvolution kernel is normalized to 1, so the same
# background level applies to the difference and score images.
scoreExposure.image -= background.getImage()
return background
def processResults(self, science, matchedTemplate, difference, sources, idFactory,
kernelSources=None, positives=None, negatives=None, measurementResults=None):
Definition at line 633 of file detectAndMeasure.py.