25import lsst.afw.image
as afwImage
31from lsst.meas.algorithms import SkyObjectsTask, SourceDetectionTask, SetPrimaryFlagsTask, MaskStreaksTask
32from lsst.meas.base import ForcedMeasurementTask, ApplyApCorrTask, DetectorVisitIdGeneratorConfig
35import lsst.meas.extensions.shapeHSM
40from lsst.utils.timer
import timeMethod
42from .
import DipoleFitTask
44__all__ = [
"DetectAndMeasureConfig",
"DetectAndMeasureTask",
45 "DetectAndMeasureScoreConfig",
"DetectAndMeasureScoreTask"]
49 dimensions=(
"instrument",
"visit",
"detector"),
50 defaultTemplates={
"coaddName":
"deep",
53 science = pipeBase.connectionTypes.Input(
54 doc=
"Input science exposure.",
55 dimensions=(
"instrument",
"visit",
"detector"),
56 storageClass=
"ExposureF",
57 name=
"{fakesType}calexp"
59 matchedTemplate = pipeBase.connectionTypes.Input(
60 doc=
"Warped and PSF-matched template used to create the difference image.",
61 dimensions=(
"instrument",
"visit",
"detector"),
62 storageClass=
"ExposureF",
63 name=
"{fakesType}{coaddName}Diff_matchedExp",
65 difference = pipeBase.connectionTypes.Input(
66 doc=
"Result of subtracting template from science.",
67 dimensions=(
"instrument",
"visit",
"detector"),
68 storageClass=
"ExposureF",
69 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
71 outputSchema = pipeBase.connectionTypes.InitOutput(
72 doc=
"Schema (as an example catalog) for output DIASource catalog.",
73 storageClass=
"SourceCatalog",
74 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
76 diaSources = pipeBase.connectionTypes.Output(
77 doc=
"Detected diaSources on the difference image.",
78 dimensions=(
"instrument",
"visit",
"detector"),
79 storageClass=
"SourceCatalog",
80 name=
"{fakesType}{coaddName}Diff_diaSrc",
82 subtractedMeasuredExposure = pipeBase.connectionTypes.Output(
83 doc=
"Difference image with detection mask plane filled in.",
84 dimensions=(
"instrument",
"visit",
"detector"),
85 storageClass=
"ExposureF",
86 name=
"{fakesType}{coaddName}Diff_differenceExp",
88 maskedStreaks = pipeBase.connectionTypes.Output(
89 doc=
'Catalog of streak fit parameters for the difference image.',
90 storageClass=
"ArrowNumpyDict",
91 dimensions=(
"instrument",
"visit",
"detector"),
92 name=
"{fakesType}{coaddName}Diff_streaks",
95 def __init__(self, *, config):
96 super().__init__(config=config)
97 if not (self.config.writeStreakInfo
and self.config.doMaskStreaks):
98 self.outputs.remove(
"maskedStreaks")
101class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
102 pipelineConnections=DetectAndMeasureConnections):
103 """Config for DetectAndMeasureTask
105 doMerge = pexConfig.Field(
108 doc=
"Merge positive and negative diaSources with grow radius "
109 "set by growFootprint"
111 doForcedMeasurement = pexConfig.Field(
114 doc=
"Force photometer diaSource locations on PVI?")
115 doAddMetrics = pexConfig.Field(
118 doc=
"Add columns to the source table to hold analysis metrics?"
120 detection = pexConfig.ConfigurableField(
121 target=SourceDetectionTask,
122 doc=
"Final source detection for diaSource measurement",
124 streakDetection = pexConfig.ConfigurableField(
125 target=SourceDetectionTask,
126 doc=
"Separate source detection used only for streak masking",
128 doDeblend = pexConfig.Field(
131 doc=
"Deblend DIASources after detection?"
133 deblend = pexConfig.ConfigurableField(
135 doc=
"Task to split blended sources into their components."
137 measurement = pexConfig.ConfigurableField(
138 target=DipoleFitTask,
139 doc=
"Task to measure sources on the difference image.",
144 doc=
"Run subtask to apply aperture corrections"
147 target=ApplyApCorrTask,
148 doc=
"Task to apply aperture corrections"
150 forcedMeasurement = pexConfig.ConfigurableField(
151 target=ForcedMeasurementTask,
152 doc=
"Task to force photometer science image at diaSource locations.",
154 growFootprint = pexConfig.Field(
157 doc=
"Grow positive and negative footprints by this many pixels before merging"
159 diaSourceMatchRadius = pexConfig.Field(
162 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
164 doSkySources = pexConfig.Field(
167 doc=
"Generate sky sources?",
169 skySources = pexConfig.ConfigurableField(
170 target=SkyObjectsTask,
171 doc=
"Generate sky sources",
173 doMaskStreaks = pexConfig.Field(
176 doc=
"Turn on streak masking",
178 maskStreaks = pexConfig.ConfigurableField(
179 target=MaskStreaksTask,
180 doc=
"Subtask for masking streaks. Only used if doMaskStreaks is True. "
181 "Adds a mask plane to an exposure, with the mask plane name set by streakMaskName.",
183 streakBinFactor = pexConfig.Field(
186 doc=
"Bin scale factor to use when rerunning detection for masking streaks. "
187 "Only used if doMaskStreaks is True.",
189 writeStreakInfo = pexConfig.Field(
192 doc=
"Record the parameters of any detected streaks. For LSST, this should be turned off except for "
195 setPrimaryFlags = pexConfig.ConfigurableField(
196 target=SetPrimaryFlagsTask,
197 doc=
"Task to add isPrimary and deblending-related flags to the catalog."
201 doc=
"Sources with any of these flags set are removed before writing the output catalog.",
202 default=(
"base_PixelFlags_flag_offimage",
203 "base_PixelFlags_flag_interpolatedCenterAll",
204 "base_PixelFlags_flag_badCenterAll",
205 "base_PixelFlags_flag_edgeCenterAll",
206 "base_PixelFlags_flag_nodataCenterAll",
207 "base_PixelFlags_flag_saturatedCenterAll",
212 doc=
"Mask planes to clear before running detection.",
213 default=(
"DETECTED",
"DETECTED_NEGATIVE",
"NOT_DEBLENDED",
"STREAK"),
215 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
217 def setDefaults(self):
219 self.detection.thresholdPolarity =
"both"
220 self.detection.thresholdValue = 5.0
221 self.detection.reEstimateBackground =
False
222 self.detection.thresholdType =
"pixel_stdev"
223 self.detection.excludeMaskPlanes = [
"EDGE",
228 self.streakDetection.thresholdType = self.detection.thresholdType
229 self.streakDetection.reEstimateBackground = self.detection.reEstimateBackground
230 self.streakDetection.excludeMaskPlanes = self.detection.excludeMaskPlanes
231 self.streakDetection.thresholdValue = self.detection.thresholdValue
233 self.streakDetection.thresholdPolarity =
"positive"
235 self.streakDetection.nSigmaToGrow = 0
238 self.maskStreaks.onlyMaskDetected =
False
240 self.maskStreaks.maxStreakWidth = 100
243 self.maskStreaks.maxFitIter = 10
245 self.maskStreaks.nSigmaMask = 2
248 self.maskStreaks.absMinimumKernelHeight = 2
250 self.measurement.plugins.names |= [
"ext_trailedSources_Naive",
251 "base_LocalPhotoCalib",
253 "ext_shapeHSM_HsmSourceMoments",
254 "ext_shapeHSM_HsmPsfMoments",
255 "base_ClassificationSizeExtendedness",
257 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
258 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
259 self.measurement.plugins[
"base_SdssCentroid"].maxDistToPeak = 5.0
260 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
261 self.forcedMeasurement.copyColumns = {
262 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
263 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
264 self.forcedMeasurement.slots.shape =
None
267 self.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere = [
268 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE"]
269 self.measurement.plugins[
"base_PixelFlags"].masksFpCenter = [
270 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE"]
271 self.skySources.avoidMask = [
"DETECTED",
"DETECTED_NEGATIVE",
"BAD",
"NO_DATA",
"EDGE"]
274class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
275 """Detect and measure sources on a difference image.
277 ConfigClass = DetectAndMeasureConfig
278 _DefaultName =
"detectAndMeasure"
280 def __init__(self, **kwargs):
281 super().__init__(**kwargs)
282 self.schema = afwTable.SourceTable.makeMinimalSchema()
284 afwTable.CoordKey.addErrorFields(self.schema)
287 self.makeSubtask(
"detection", schema=self.schema)
288 if self.config.doDeblend:
289 self.makeSubtask(
"deblend", schema=self.schema)
290 self.makeSubtask(
"setPrimaryFlags", schema=self.schema, isSingleFrame=
True)
291 self.makeSubtask(
"measurement", schema=self.schema,
292 algMetadata=self.algMetadata)
293 if self.config.doApCorr:
294 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
295 if self.config.doForcedMeasurement:
296 self.schema.addField(
297 "ip_diffim_forced_PsfFlux_instFlux",
"D",
298 "Forced PSF flux measured on the direct image.",
300 self.schema.addField(
301 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
302 "Forced PSF flux error measured on the direct image.",
304 self.schema.addField(
305 "ip_diffim_forced_PsfFlux_area",
"F",
306 "Forced PSF flux effective area of PSF.",
308 self.schema.addField(
309 "ip_diffim_forced_PsfFlux_flag",
"Flag",
310 "Forced PSF flux general failure flag.")
311 self.schema.addField(
312 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
313 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
314 self.schema.addField(
315 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
316 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
317 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
319 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
320 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
321 if self.config.doSkySources:
322 self.makeSubtask(
"skySources", schema=self.schema)
323 if self.config.doMaskStreaks:
324 self.makeSubtask(
"maskStreaks")
325 self.makeSubtask(
"streakDetection")
332 for flag
in self.config.badSourceFlags:
333 if flag
not in self.schema:
334 raise pipeBase.InvalidQuantumError(
"Field %s not in schema" % flag)
338 self.outputSchema.getTable().setMetadata(self.algMetadata)
340 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
341 inputRefs: pipeBase.InputQuantizedConnection,
342 outputRefs: pipeBase.OutputQuantizedConnection):
343 inputs = butlerQC.get(inputRefs)
344 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
345 idFactory = idGenerator.make_table_id_factory()
346 outputs = self.run(**inputs, idFactory=idFactory)
347 butlerQC.put(outputs, outputRefs)
350 def run(self, science, matchedTemplate, difference,
352 """Detect and measure sources on a difference image.
354 The difference image will be convolved with a gaussian approximation of
355 the PSF to form a maximum likelihood image for detection.
356 Close positive and negative detections will optionally be merged into
358 Sky sources, or forced detections in background regions, will optionally
359 be added, and the configured measurement algorithm will be run on all
364 science : `lsst.afw.image.ExposureF`
365 Science exposure that the template was subtracted from.
366 matchedTemplate : `lsst.afw.image.ExposureF`
367 Warped and PSF-matched template that was used produce the
369 difference : `lsst.afw.image.ExposureF`
370 Result of subtracting template from the science image.
371 idFactory : `lsst.afw.table.IdFactory`, optional
372 Generator object used to assign ids to detected sources in the
373 difference image. Ids from this generator are not set until after
374 deblending and merging positive/negative peaks.
378 measurementResults : `lsst.pipe.base.Struct`
380 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
381 Subtracted exposure with detection mask applied.
382 ``diaSources`` : `lsst.afw.table.SourceCatalog`
383 The catalog of detected sources.
385 if idFactory
is None:
388 self._prepareInputs(difference)
393 table = afwTable.SourceTable.make(self.schema)
394 results = self.detection.run(
400 if self.config.doDeblend:
401 sources, positives, negatives = self._deblend(difference,
405 return self.processResults(science, matchedTemplate, difference,
412 results.positive.makeSources(positives)
414 results.negative.makeSources(negatives)
415 return self.processResults(science, matchedTemplate, difference,
416 results.sources, idFactory,
420 def _prepareInputs(self, difference):
421 """Ensure that we start with an empty detection and deblended mask.
425 difference : `lsst.afw.image.ExposureF`
426 The difference image that will be used for detecting diaSources.
427 The mask plane will be modified in place.
431 lsst.pipe.base.UpstreamFailureNoWorkFound
432 If the PSF is not usable for measurement.
435 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
437 raise pipeBase.UpstreamFailureNoWorkFound(
"Invalid PSF detected! PSF width evaluates to NaN.")
439 mask = difference.mask
440 for mp
in self.config.clearMaskPlanes:
441 if mp
not in mask.getMaskPlaneDict():
442 mask.addMaskPlane(mp)
443 mask &= ~mask.getPlaneBitMask(self.config.clearMaskPlanes)
445 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
446 positives=None, negatives=None,):
447 """Measure and process the results of source detection.
451 science : `lsst.afw.image.ExposureF`
452 Science exposure that the template was subtracted from.
453 matchedTemplate : `lsst.afw.image.ExposureF`
454 Warped and PSF-matched template that was used produce the
456 difference : `lsst.afw.image.ExposureF`
457 Result of subtracting template from the science image.
458 sources : `lsst.afw.table.SourceCatalog`
459 Detected sources on the difference exposure.
460 idFactory : `lsst.afw.table.IdFactory`
461 Generator object used to assign ids to detected sources in the
463 positives : `lsst.afw.table.SourceCatalog`, optional
464 Positive polarity footprints.
465 negatives : `lsst.afw.table.SourceCatalog`, optional
466 Negative polarity footprints.
470 measurementResults : `lsst.pipe.base.Struct`
472 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
473 Subtracted exposure with detection mask applied.
474 ``diaSources`` : `lsst.afw.table.SourceCatalog`
475 The catalog of detected sources.
477 self.metadata[
"nUnmergedDiaSources"] = len(sources)
478 if self.config.doMerge:
480 if len(positives) > 0:
481 peakSchema = positives[0].getFootprint().peaks.schema
482 elif len(negatives) > 0:
483 peakSchema = negatives[0].getFootprint().peaks.schema
485 peakSchema = afwDetection.PeakTable.makeMinimalSchema()
486 mergeList = afwDetection.FootprintMergeList(self.schema,
487 [
"positive",
"negative"], peakSchema)
492 mergeList.addCatalog(initialDiaSources.table, positives,
"positive", minNewPeakDist=0)
493 mergeList.addCatalog(initialDiaSources.table, negatives,
"negative", minNewPeakDist=0)
494 mergeList.getFinalSources(initialDiaSources)
497 initialDiaSources[
"is_negative"] = initialDiaSources[
"merge_footprint_negative"] & \
498 ~initialDiaSources[
"merge_footprint_positive"]
499 self.log.info(
"Merging detections into %d sources", len(initialDiaSources))
501 initialDiaSources = sources
505 for source
in initialDiaSources:
508 initialDiaSources.getTable().setIdFactory(idFactory)
509 initialDiaSources.setMetadata(self.algMetadata)
511 self.metadata[
"nMergedDiaSources"] = len(initialDiaSources)
513 if self.config.doMaskStreaks:
514 streakInfo = self._runStreakMasking(difference)
516 if self.config.doSkySources:
517 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
519 if not initialDiaSources.isContiguous():
520 initialDiaSources = initialDiaSources.copy(deep=
True)
522 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
523 diaSources = self._removeBadSources(initialDiaSources)
525 if self.config.doForcedMeasurement:
526 self.measureForcedSources(diaSources, science, difference.getWcs())
528 self.calculateMetrics(difference)
530 measurementResults = pipeBase.Struct(
531 subtractedMeasuredExposure=difference,
534 if len(diaSources) > 0:
535 measurementResults.diaSources = diaSources
537 if self.config.doMaskStreaks
and self.config.writeStreakInfo:
538 measurementResults.mergeItems(streakInfo,
'maskedStreaks')
540 return measurementResults
542 def _deblend(self, difference, positiveFootprints, negativeFootprints):
543 """Deblend the positive and negative footprints and return a catalog
544 containing just the children, and the deblended footprints.
548 difference : `lsst.afw.image.Exposure`
549 Result of subtracting template from the science image.
550 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
551 Positive and negative polarity footprints measured on
552 ``difference`` to be deblended separately.
556 sources : `lsst.afw.table.SourceCatalog`
557 Positive and negative deblended children.
558 positives, negatives : `lsst.afw.table.SourceCatalog`
559 Deblended positive and negative polarity sources with footprints
560 detected on ``difference``.
563 def deblend(footprints, negative=False):
564 """Deblend a positive or negative footprint set,
565 and return the deblended children.
569 footprints : `lsst.afw.detection.FootprintSet`
571 Set True if the footprints contain negative fluxes
575 sources : `lsst.afw.table.SourceCatalog`
578 footprints.makeSources(sources)
581 difference_inverted = difference.clone()
582 difference_inverted.image *= -1
583 self.deblend.run(exposure=difference_inverted, sources=sources)
584 children = sources[sources[
"parent"] != 0]
586 for child
in children:
587 footprint = child.getFootprint()
588 array = footprint.getImageArray()
591 self.deblend.run(exposure=difference, sources=sources)
592 self.setPrimaryFlags.run(sources)
593 children = sources[
"detect_isDeblendedSource"] == 1
594 sources = sources[children].copy(deep=
True)
596 sources[
'parent'] = 0
597 return sources.copy(deep=
True)
599 positives =
deblend(positiveFootprints)
600 negatives =
deblend(negativeFootprints, negative=
True)
603 sources.reserve(len(positives) + len(negatives))
604 sources.extend(positives, deep=
True)
605 sources.extend(negatives, deep=
True)
606 if len(negatives) > 0:
607 sources[-len(negatives):][
"is_negative"] =
True
608 return sources, positives, negatives
610 def _removeBadSources(self, diaSources):
611 """Remove unphysical diaSources from the catalog.
615 diaSources : `lsst.afw.table.SourceCatalog`
616 The catalog of detected sources.
620 diaSources : `lsst.afw.table.SourceCatalog`
621 The updated catalog of detected sources, with any source that has a
622 flag in ``config.badSourceFlags`` set removed.
624 selector = np.ones(len(diaSources), dtype=bool)
625 for flag
in self.config.badSourceFlags:
626 flags = diaSources[flag]
627 nBad = np.count_nonzero(flags)
629 self.log.debug(
"Found %d unphysical sources with flag %s.", nBad, flag)
631 nBadTotal = np.count_nonzero(~selector)
632 self.metadata[
"nRemovedBadFlaggedSources"] = nBadTotal
633 self.log.info(
"Removed %d unphysical sources.", nBadTotal)
634 return diaSources[selector].copy(deep=
True)
636 def addSkySources(self, diaSources, mask, seed,
638 """Add sources in empty regions of the difference image
639 for measuring the background.
643 diaSources : `lsst.afw.table.SourceCatalog`
644 The catalog of detected sources.
645 mask : `lsst.afw.image.Mask`
646 Mask plane for determining regions where Sky sources can be added.
648 Seed value to initialize the random number generator.
651 subtask = self.skySources
652 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
653 self.metadata[f
"n_{subtask.getName()}"] = len(skySourceFootprints)
655 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
656 """Use (matched) template and science image to constrain dipole fitting.
660 diaSources : `lsst.afw.table.SourceCatalog`
661 The catalog of detected sources.
662 science : `lsst.afw.image.ExposureF`
663 Science exposure that the template was subtracted from.
664 difference : `lsst.afw.image.ExposureF`
665 Result of subtracting template from the science image.
666 matchedTemplate : `lsst.afw.image.ExposureF`
667 Warped and PSF-matched template that was used produce the
671 for mp
in self.config.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere:
672 difference.mask.addMaskPlane(mp)
675 self.measurement.run(diaSources, difference, science, matchedTemplate)
676 if self.config.doApCorr:
677 apCorrMap = difference.getInfo().getApCorrMap()
678 if apCorrMap
is None:
679 self.log.warning(
"Difference image does not have valid aperture correction; skipping.")
681 self.applyApCorr.run(
686 def measureForcedSources(self, diaSources, science, wcs):
687 """Perform forced measurement of the diaSources on the science image.
691 diaSources : `lsst.afw.table.SourceCatalog`
692 The catalog of detected sources.
693 science : `lsst.afw.image.ExposureF`
694 Science exposure that the template was subtracted from.
695 wcs : `lsst.afw.geom.SkyWcs`
696 Coordinate system definition (wcs) for the exposure.
700 forcedSources = self.forcedMeasurement.generateMeasCat(science, diaSources, wcs)
701 self.forcedMeasurement.run(forcedSources, science, diaSources, wcs)
703 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
704 "ip_diffim_forced_PsfFlux_instFlux",
True)
705 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
706 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
707 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
708 "ip_diffim_forced_PsfFlux_area",
True)
709 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
710 "ip_diffim_forced_PsfFlux_flag",
True)
711 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
712 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
713 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
714 "ip_diffim_forced_PsfFlux_flag_edge",
True)
715 for diaSource, forcedSource
in zip(diaSources, forcedSources):
716 diaSource.assign(forcedSource, mapper)
718 def calculateMetrics(self, difference):
719 """Add difference image QA metrics to the Task metadata.
721 This may be used to produce corresponding metrics (see
722 lsst.analysis.tools.tasks.diffimTaskDetectorVisitMetricAnalysis).
726 difference : `lsst.afw.image.Exposure`
727 The target difference image to calculate metrics for.
729 mask = difference.mask
730 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
731 self.metadata[
"nGoodPixels"] = np.sum(~badPix)
732 self.metadata[
"nBadPixels"] = np.sum(badPix)
733 detPosPix = (mask.array & mask.getPlaneBitMask(
"DETECTED")) > 0
734 detNegPix = (mask.array & mask.getPlaneBitMask(
"DETECTED_NEGATIVE")) > 0
735 self.metadata[
"nPixelsDetectedPositive"] = np.sum(detPosPix)
736 self.metadata[
"nPixelsDetectedNegative"] = np.sum(detNegPix)
739 self.metadata[
"nBadPixelsDetectedPositive"] = np.sum(detPosPix)
740 self.metadata[
"nBadPixelsDetectedNegative"] = np.sum(detNegPix)
742 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
743 for maskPlane
in metricsMaskPlanes:
745 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = evaluateMaskFraction(mask, maskPlane)
746 except InvalidParameterError:
747 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = -1
748 self.log.info(
"Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
750 def _runStreakMasking(self, difference):
751 """Do streak masking and optionally save the resulting streak
752 fit parameters in a catalog.
754 Only returns non-empty streakInfo if self.config.writeStreakInfo
755 is set. The difference image is binned by self.config.streakBinFactor
756 (and detection is run a second time) so that regions with lower
757 surface brightness streaks are more likely to fall above the
762 difference: `lsst.afw.image.Exposure`
763 The exposure in which to search for streaks. Must have a detection
768 streakInfo: `lsst.pipe.base.Struct`
769 ``rho`` : `np.ndarray`
770 Angle of detected streak.
771 ``theta`` : `np.ndarray`
772 Distance from center of detected streak.
773 ``sigma`` : `np.ndarray`
774 Width of streak profile.
775 ``reducedChi2`` : `np.ndarray`
776 Reduced chi2 of the best-fit streak profile.
777 ``modelMaximum`` : `np.ndarray`
778 Peak value of the fit line profile.
780 maskedImage = difference.maskedImage
783 self.config.streakBinFactor,
784 self.config.streakBinFactor)
785 binnedExposure = afwImage.ExposureF(binnedMaskedImage.getBBox())
786 binnedExposure.setMaskedImage(binnedMaskedImage)
788 binnedExposure.mask &= ~binnedExposure.mask.getPlaneBitMask(
'DETECTED')
790 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
791 _table = afwTable.SourceTable.make(afwTable.SourceTable.makeMinimalSchema())
792 self.streakDetection.run(table=_table, exposure=binnedExposure, doSmooth=
True,
793 sigma=sigma/self.config.streakBinFactor)
794 binnedDetectedMaskPlane = binnedExposure.mask.array & binnedExposure.mask.getPlaneBitMask(
'DETECTED')
795 rescaledDetectedMaskPlane = binnedDetectedMaskPlane.repeat(self.config.streakBinFactor,
796 axis=0).repeat(self.config.streakBinFactor,
799 streakMaskedImage = maskedImage.clone()
800 ysize, xsize = rescaledDetectedMaskPlane.shape
801 streakMaskedImage.mask.array[:ysize, :xsize] |= rescaledDetectedMaskPlane
803 streaks = self.maskStreaks.run(streakMaskedImage)
804 streakMaskPlane = streakMaskedImage.mask.array & streakMaskedImage.mask.getPlaneBitMask(
'STREAK')
806 maskedImage.mask.array |= streakMaskPlane
808 if self.config.writeStreakInfo:
809 rhos = np.array([line.rho
for line
in streaks.lines])
810 thetas = np.array([line.theta
for line
in streaks.lines])
811 sigmas = np.array([line.sigma
for line
in streaks.lines])
812 chi2s = np.array([line.reducedChi2
for line
in streaks.lines])
813 modelMaximums = np.array([line.modelMaximum
for line
in streaks.lines])
814 streakInfo = {
'rho': rhos,
'theta': thetas,
'sigma': sigmas,
'reducedChi2': chi2s,
815 'modelMaximum': modelMaximums}
817 streakInfo = {
'rho': np.array([]),
'theta': np.array([]),
'sigma': np.array([]),
818 'reducedChi2': np.array([]),
'modelMaximum': np.array([])}
819 return pipeBase.Struct(maskedStreaks=streakInfo)
823 scoreExposure = pipeBase.connectionTypes.Input(
824 doc=
"Maximum likelihood image for detection.",
825 dimensions=(
"instrument",
"visit",
"detector"),
826 storageClass=
"ExposureF",
827 name=
"{fakesType}{coaddName}Diff_scoreExp",
831class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
832 pipelineConnections=DetectAndMeasureScoreConnections):
836class DetectAndMeasureScoreTask(DetectAndMeasureTask):
837 """Detect DIA sources using a score image,
838 and measure the detections on the difference image.
840 Source detection is run on the supplied score, or maximum likelihood,
841 image. Note that no additional convolution will be done in this case.
842 Close positive and negative detections will optionally be merged into
844 Sky sources, or forced detections in background regions, will optionally
845 be added, and the configured measurement algorithm will be run on all
848 ConfigClass = DetectAndMeasureScoreConfig
849 _DefaultName =
"detectAndMeasureScore"
852 def run(self, science, matchedTemplate, difference, scoreExposure,
854 """Detect and measure sources on a score image.
858 science : `lsst.afw.image.ExposureF`
859 Science exposure that the template was subtracted from.
860 matchedTemplate : `lsst.afw.image.ExposureF`
861 Warped and PSF-matched template that was used produce the
863 difference : `lsst.afw.image.ExposureF`
864 Result of subtracting template from the science image.
865 scoreExposure : `lsst.afw.image.ExposureF`
866 Score or maximum likelihood difference image
867 idFactory : `lsst.afw.table.IdFactory`, optional
868 Generator object used to assign ids to detected sources in the
869 difference image. Ids from this generator are not set until after
870 deblending and merging positive/negative peaks.
874 measurementResults : `lsst.pipe.base.Struct`
876 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
877 Subtracted exposure with detection mask applied.
878 ``diaSources`` : `lsst.afw.table.SourceCatalog`
879 The catalog of detected sources.
881 if idFactory
is None:
884 self._prepareInputs(scoreExposure)
889 table = afwTable.SourceTable.make(self.schema)
890 results = self.detection.run(
892 exposure=scoreExposure,
896 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
898 if self.config.doDeblend:
899 sources, positives, negatives = self._deblend(difference,
903 return self.processResults(science, matchedTemplate, difference,
910 results.positive.makeSources(positives)
912 results.negative.makeSources(negatives)
913 return self.processResults(science, matchedTemplate, difference,
914 results.sources, idFactory,
A mapping between the keys of two Schemas, used to copy data between them.
Class for storing ordered metadata with comments.
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binX, int const binY, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter)