30from lsst.ip.diffim.utils
import evaluateMaskFraction
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 deblend = pexConfig.ConfigurableField(
130 doc=
"Task to split blended sources into their components."
132 measurement = pexConfig.ConfigurableField(
133 target=DipoleFitTask,
134 doc=
"Task to measure sources on the difference image.",
139 doc=
"Run subtask to apply aperture corrections"
142 target=ApplyApCorrTask,
143 doc=
"Task to apply aperture corrections"
145 forcedMeasurement = pexConfig.ConfigurableField(
146 target=ForcedMeasurementTask,
147 doc=
"Task to force photometer science image at diaSource locations.",
149 growFootprint = pexConfig.Field(
152 doc=
"Grow positive and negative footprints by this many pixels before merging"
154 diaSourceMatchRadius = pexConfig.Field(
157 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
159 doSkySources = pexConfig.Field(
162 doc=
"Generate sky sources?",
164 skySources = pexConfig.ConfigurableField(
165 target=SkyObjectsTask,
166 doc=
"Generate sky sources",
168 doMaskStreaks = pexConfig.Field(
171 doc=
"Turn on streak masking",
173 maskStreaks = pexConfig.ConfigurableField(
174 target=MaskStreaksTask,
175 doc=
"Subtask for masking streaks. Only used if doMaskStreaks is True. "
176 "Adds a mask plane to an exposure, with the mask plane name set by streakMaskName.",
178 streakBinFactor = pexConfig.Field(
181 doc=
"Bin scale factor to use when rerunning detection for masking streaks. "
182 "Only used if doMaskStreaks is True.",
184 writeStreakInfo = pexConfig.Field(
187 doc=
"Record the parameters of any detected streaks. For LSST, this should be turned off except for "
190 setPrimaryFlags = pexConfig.ConfigurableField(
191 target=SetPrimaryFlagsTask,
192 doc=
"Task to add isPrimary and deblending-related flags to the catalog."
196 doc=
"Sources with any of these flags set are removed before writing the output catalog.",
197 default=(
"base_PixelFlags_flag_offimage",
198 "base_PixelFlags_flag_interpolatedCenterAll",
199 "base_PixelFlags_flag_badCenterAll",
200 "base_PixelFlags_flag_edgeCenterAll",
201 "base_PixelFlags_flag_saturatedCenterAll",
206 doc=
"Mask planes to clear before running detection.",
207 default=(
"DETECTED",
"DETECTED_NEGATIVE",
"NOT_DEBLENDED",
"STREAK"),
209 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
211 def setDefaults(self):
213 self.detection.thresholdPolarity =
"both"
214 self.detection.thresholdValue = 5.0
215 self.detection.reEstimateBackground =
False
216 self.detection.thresholdType =
"pixel_stdev"
217 self.detection.excludeMaskPlanes = [
"EDGE",
225 self.streakDetection.thresholdType = self.detection.thresholdType
226 self.streakDetection.reEstimateBackground = self.detection.reEstimateBackground
227 self.streakDetection.excludeMaskPlanes = self.detection.excludeMaskPlanes
228 self.streakDetection.thresholdValue = self.detection.thresholdValue
230 self.streakDetection.thresholdPolarity =
"positive"
232 self.streakDetection.nSigmaToGrow = 0
235 self.maskStreaks.onlyMaskDetected =
False
237 self.maskStreaks.maxStreakWidth = 100
240 self.maskStreaks.maxFitIter = 10
242 self.maskStreaks.nSigmaMask = 2
245 self.maskStreaks.absMinimumKernelHeight = 2
247 self.measurement.plugins.names |= [
"ext_trailedSources_Naive",
248 "base_LocalPhotoCalib",
250 "ext_shapeHSM_HsmSourceMoments",
251 "ext_shapeHSM_HsmPsfMoments",
252 "base_ClassificationSizeExtendedness",
254 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
255 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
256 self.measurement.plugins[
"base_SdssCentroid"].maxDistToPeak = 5.0
257 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
258 self.forcedMeasurement.copyColumns = {
259 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
260 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
261 self.forcedMeasurement.slots.shape =
None
264 self.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere = [
265 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE"]
266 self.measurement.plugins[
"base_PixelFlags"].masksFpCenter = [
267 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE"]
268 self.skySources.avoidMask = [
"DETECTED",
"DETECTED_NEGATIVE",
"BAD",
"NO_DATA",
"EDGE"]
271class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
272 """Detect and measure sources on a difference image.
274 ConfigClass = DetectAndMeasureConfig
275 _DefaultName =
"detectAndMeasure"
277 def __init__(self, **kwargs):
278 super().__init__(**kwargs)
279 self.schema = afwTable.SourceTable.makeMinimalSchema()
281 afwTable.CoordKey.addErrorFields(self.schema)
284 self.makeSubtask(
"detection", schema=self.schema)
285 self.makeSubtask(
"deblend", schema=self.schema)
286 self.makeSubtask(
"setPrimaryFlags", schema=self.schema, isSingleFrame=
True)
287 self.makeSubtask(
"measurement", schema=self.schema,
288 algMetadata=self.algMetadata)
289 if self.config.doApCorr:
290 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
291 if self.config.doForcedMeasurement:
292 self.schema.addField(
293 "ip_diffim_forced_PsfFlux_instFlux",
"D",
294 "Forced PSF flux measured on the direct image.",
296 self.schema.addField(
297 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
298 "Forced PSF flux error measured on the direct image.",
300 self.schema.addField(
301 "ip_diffim_forced_PsfFlux_area",
"F",
302 "Forced PSF flux effective area of PSF.",
304 self.schema.addField(
305 "ip_diffim_forced_PsfFlux_flag",
"Flag",
306 "Forced PSF flux general failure flag.")
307 self.schema.addField(
308 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
309 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
310 self.schema.addField(
311 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
312 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
313 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
315 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
316 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
317 if self.config.doSkySources:
318 self.makeSubtask(
"skySources", schema=self.schema)
319 if self.config.doMaskStreaks:
320 self.makeSubtask(
"maskStreaks")
321 self.makeSubtask(
"streakDetection")
324 for flag
in self.config.badSourceFlags:
325 if flag
not in self.schema:
326 raise pipeBase.InvalidQuantumError(
"Field %s not in schema" % flag)
330 self.outputSchema.getTable().setMetadata(self.algMetadata)
332 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
333 inputRefs: pipeBase.InputQuantizedConnection,
334 outputRefs: pipeBase.OutputQuantizedConnection):
335 inputs = butlerQC.get(inputRefs)
336 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
337 idFactory = idGenerator.make_table_id_factory()
338 outputs = self.run(**inputs, idFactory=idFactory)
339 butlerQC.put(outputs, outputRefs)
342 def run(self, science, matchedTemplate, difference,
344 """Detect and measure sources on a difference image.
346 The difference image will be convolved with a gaussian approximation of
347 the PSF to form a maximum likelihood image for detection.
348 Close positive and negative detections will optionally be merged into
350 Sky sources, or forced detections in background regions, will optionally
351 be added, and the configured measurement algorithm will be run on all
356 science : `lsst.afw.image.ExposureF`
357 Science exposure that the template was subtracted from.
358 matchedTemplate : `lsst.afw.image.ExposureF`
359 Warped and PSF-matched template that was used produce the
361 difference : `lsst.afw.image.ExposureF`
362 Result of subtracting template from the science image.
363 idFactory : `lsst.afw.table.IdFactory`, optional
364 Generator object used to assign ids to detected sources in the
365 difference image. Ids from this generator are not set until after
366 deblending and merging positive/negative peaks.
370 measurementResults : `lsst.pipe.base.Struct`
372 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
373 Subtracted exposure with detection mask applied.
374 ``diaSources`` : `lsst.afw.table.SourceCatalog`
375 The catalog of detected sources.
377 if idFactory
is None:
380 self._prepareInputs(difference)
385 table = afwTable.SourceTable.make(self.schema)
386 results = self.detection.run(
392 sources, positives, negatives = self._deblend(difference,
396 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
397 positiveFootprints=positives,
398 negativeFootprints=negatives)
400 def _prepareInputs(self, difference):
401 """Ensure that we start with an empty detection and deblended mask.
405 difference : `lsst.afw.image.ExposureF`
406 The difference image that will be used for detecting diaSources.
407 The mask plane will be modified in place.
411 lsst.pipe.base.UpstreamFailureNoWorkFound
412 If the PSF is not usable for measurement.
415 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
417 raise pipeBase.UpstreamFailureNoWorkFound(
"Invalid PSF detected! PSF width evaluates to NaN.")
419 mask = difference.mask
420 for mp
in self.config.clearMaskPlanes:
421 if mp
not in mask.getMaskPlaneDict():
422 mask.addMaskPlane(mp)
423 mask &= ~mask.getPlaneBitMask(self.config.clearMaskPlanes)
425 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
426 positiveFootprints=None, negativeFootprints=None,):
427 """Measure and process the results of source detection.
431 science : `lsst.afw.image.ExposureF`
432 Science exposure that the template was subtracted from.
433 matchedTemplate : `lsst.afw.image.ExposureF`
434 Warped and PSF-matched template that was used produce the
436 difference : `lsst.afw.image.ExposureF`
437 Result of subtracting template from the science image.
438 sources : `lsst.afw.table.SourceCatalog`
439 Detected sources on the difference exposure.
440 idFactory : `lsst.afw.table.IdFactory`
441 Generator object used to assign ids to detected sources in the
443 positiveFootprints : `lsst.afw.detection.FootprintSet`, optional
444 Positive polarity footprints.
445 negativeFootprints : `lsst.afw.detection.FootprintSet`, optional
446 Negative polarity footprints.
450 measurementResults : `lsst.pipe.base.Struct`
452 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
453 Subtracted exposure with detection mask applied.
454 ``diaSources`` : `lsst.afw.table.SourceCatalog`
455 The catalog of detected sources.
457 self.metadata[
"nUnmergedDiaSources"] = len(sources)
458 if self.config.doMerge:
459 fpSet = positiveFootprints
460 fpSet.merge(negativeFootprints, self.config.growFootprint,
461 self.config.growFootprint,
False)
463 fpSet.makeSources(initialDiaSources)
464 self.log.info(
"Merging detections into %d sources", len(initialDiaSources))
466 initialDiaSources = sources
470 for source
in initialDiaSources:
473 initialDiaSources.getTable().setIdFactory(idFactory)
474 initialDiaSources.setMetadata(self.algMetadata)
476 self.metadata[
"nMergedDiaSources"] = len(initialDiaSources)
478 if self.config.doMaskStreaks:
479 streakInfo = self._runStreakMasking(difference)
481 if self.config.doSkySources:
482 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
484 if not initialDiaSources.isContiguous():
485 initialDiaSources = initialDiaSources.copy(deep=
True)
487 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
488 diaSources = self._removeBadSources(initialDiaSources)
490 if self.config.doForcedMeasurement:
491 self.measureForcedSources(diaSources, science, difference.getWcs())
493 self.calculateMetrics(difference)
495 measurementResults = pipeBase.Struct(
496 subtractedMeasuredExposure=difference,
497 diaSources=diaSources,
499 if self.config.doMaskStreaks
and self.config.writeStreakInfo:
500 measurementResults.mergeItems(streakInfo,
'maskedStreaks')
502 return measurementResults
504 def _deblend(self, difference, positiveFootprints, negativeFootprints):
505 """Deblend the positive and negative footprints and return a catalog
506 containing just the children, and the deblended footprints.
510 difference : `lsst.afw.image.Exposure`
511 Result of subtracting template from the science image.
512 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
513 Positive and negative polarity footprints measured on
514 ``difference`` to be deblended separately.
518 sources : `lsst.afw.table.SourceCatalog`
519 Positive and negative deblended children.
520 positives, negatives : `lsst.afw.detection.FootprintSet`
521 Deblended positive and negative polarity footprints measured on
524 def makeFootprints(sources):
525 footprints = afwDetection.FootprintSet(difference.getBBox())
526 footprints.setFootprints([src.getFootprint()
for src
in sources])
530 """Deblend a positive or negative footprint set,
531 and return the deblended children.
534 footprints.makeSources(sources)
535 self.deblend.run(exposure=difference, sources=sources)
536 self.setPrimaryFlags.run(sources)
537 children = sources[
"detect_isDeblendedSource"] == 1
538 sources = sources[children].copy(deep=
True)
540 sources[
'parent'] = 0
541 return sources.copy(deep=
True)
543 positives =
deblend(positiveFootprints)
544 negatives =
deblend(negativeFootprints)
547 sources.reserve(len(positives) + len(negatives))
548 sources.extend(positives, deep=
True)
549 sources.extend(negatives, deep=
True)
550 return sources, makeFootprints(positives), makeFootprints(negatives)
552 def _removeBadSources(self, diaSources):
553 """Remove unphysical diaSources from the catalog.
557 diaSources : `lsst.afw.table.SourceCatalog`
558 The catalog of detected sources.
562 diaSources : `lsst.afw.table.SourceCatalog`
563 The updated catalog of detected sources, with any source that has a
564 flag in ``config.badSourceFlags`` set removed.
566 selector = np.ones(len(diaSources), dtype=bool)
567 for flag
in self.config.badSourceFlags:
568 flags = diaSources[flag]
569 nBad = np.count_nonzero(flags)
571 self.log.debug(
"Found %d unphysical sources with flag %s.", nBad, flag)
573 nBadTotal = np.count_nonzero(~selector)
574 self.metadata[
"nRemovedBadFlaggedSources"] = nBadTotal
575 self.log.info(
"Removed %d unphysical sources.", nBadTotal)
576 return diaSources[selector].copy(deep=
True)
578 def addSkySources(self, diaSources, mask, seed,
580 """Add sources in empty regions of the difference image
581 for measuring the background.
585 diaSources : `lsst.afw.table.SourceCatalog`
586 The catalog of detected sources.
587 mask : `lsst.afw.image.Mask`
588 Mask plane for determining regions where Sky sources can be added.
590 Seed value to initialize the random number generator.
593 subtask = self.skySources
594 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
595 self.metadata[f
"n_{subtask.getName()}"] = len(skySourceFootprints)
597 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
598 """Use (matched) template and science image to constrain dipole fitting.
602 diaSources : `lsst.afw.table.SourceCatalog`
603 The catalog of detected sources.
604 science : `lsst.afw.image.ExposureF`
605 Science exposure that the template was subtracted from.
606 difference : `lsst.afw.image.ExposureF`
607 Result of subtracting template from the science image.
608 matchedTemplate : `lsst.afw.image.ExposureF`
609 Warped and PSF-matched template that was used produce the
613 for mp
in self.config.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere:
614 difference.mask.addMaskPlane(mp)
617 self.measurement.run(diaSources, difference, science, matchedTemplate)
618 if self.config.doApCorr:
619 apCorrMap = difference.getInfo().getApCorrMap()
620 if apCorrMap
is None:
621 self.log.warning(
"Difference image does not have valid aperture correction; skipping.")
623 self.applyApCorr.run(
628 def measureForcedSources(self, diaSources, science, wcs):
629 """Perform forced measurement of the diaSources on the science image.
633 diaSources : `lsst.afw.table.SourceCatalog`
634 The catalog of detected sources.
635 science : `lsst.afw.image.ExposureF`
636 Science exposure that the template was subtracted from.
637 wcs : `lsst.afw.geom.SkyWcs`
638 Coordinate system definition (wcs) for the exposure.
642 forcedSources = self.forcedMeasurement.generateMeasCat(science, diaSources, wcs)
643 self.forcedMeasurement.run(forcedSources, science, diaSources, wcs)
645 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
646 "ip_diffim_forced_PsfFlux_instFlux",
True)
647 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
648 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
649 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
650 "ip_diffim_forced_PsfFlux_area",
True)
651 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
652 "ip_diffim_forced_PsfFlux_flag",
True)
653 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
654 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
655 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
656 "ip_diffim_forced_PsfFlux_flag_edge",
True)
657 for diaSource, forcedSource
in zip(diaSources, forcedSources):
658 diaSource.assign(forcedSource, mapper)
660 def calculateMetrics(self, difference):
661 """Add difference image QA metrics to the Task metadata.
663 This may be used to produce corresponding metrics (see
664 lsst.analysis.tools.tasks.diffimTaskDetectorVisitMetricAnalysis).
668 difference : `lsst.afw.image.Exposure`
669 The target difference image to calculate metrics for.
671 mask = difference.mask
672 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
673 self.metadata[
"nGoodPixels"] = np.sum(~badPix)
674 self.metadata[
"nBadPixels"] = np.sum(badPix)
675 detPosPix = (mask.array & mask.getPlaneBitMask(
"DETECTED")) > 0
676 detNegPix = (mask.array & mask.getPlaneBitMask(
"DETECTED_NEGATIVE")) > 0
677 self.metadata[
"nPixelsDetectedPositive"] = np.sum(detPosPix)
678 self.metadata[
"nPixelsDetectedNegative"] = np.sum(detNegPix)
681 self.metadata[
"nBadPixelsDetectedPositive"] = np.sum(detPosPix)
682 self.metadata[
"nBadPixelsDetectedNegative"] = np.sum(detNegPix)
684 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
685 for maskPlane
in metricsMaskPlanes:
687 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = evaluateMaskFraction(mask, maskPlane)
688 except InvalidParameterError:
689 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = -1
690 self.log.info(
"Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
692 def _runStreakMasking(self, difference):
693 """Do streak masking and optionally save the resulting streak
694 fit parameters in a catalog.
696 Only returns non-empty streakInfo if self.config.writeStreakInfo
697 is set. The difference image is binned by self.config.streakBinFactor
698 (and detection is run a second time) so that regions with lower
699 surface brightness streaks are more likely to fall above the
704 difference: `lsst.afw.image.Exposure`
705 The exposure in which to search for streaks. Must have a detection
710 streakInfo: `lsst.pipe.base.Struct`
711 ``rho`` : `np.ndarray`
712 Angle of detected streak.
713 ``theta`` : `np.ndarray`
714 Distance from center of detected streak.
715 ``sigma`` : `np.ndarray`
716 Width of streak profile.
717 ``reducedChi2`` : `np.ndarray`
718 Reduced chi2 of the best-fit streak profile.
719 ``modelMaximum`` : `np.ndarray`
720 Peak value of the fit line profile.
722 maskedImage = difference.maskedImage
725 self.config.streakBinFactor,
726 self.config.streakBinFactor)
727 binnedExposure = afwImage.ExposureF(binnedMaskedImage.getBBox())
728 binnedExposure.setMaskedImage(binnedMaskedImage)
730 binnedExposure.mask &= ~binnedExposure.mask.getPlaneBitMask(
'DETECTED')
732 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
733 _table = afwTable.SourceTable.make(afwTable.SourceTable.makeMinimalSchema())
734 self.streakDetection.run(table=_table, exposure=binnedExposure, doSmooth=
True,
735 sigma=sigma/self.config.streakBinFactor)
736 binnedDetectedMaskPlane = binnedExposure.mask.array & binnedExposure.mask.getPlaneBitMask(
'DETECTED')
737 rescaledDetectedMaskPlane = binnedDetectedMaskPlane.repeat(self.config.streakBinFactor,
738 axis=0).repeat(self.config.streakBinFactor,
741 streakMaskedImage = maskedImage.clone()
742 ysize, xsize = rescaledDetectedMaskPlane.shape
743 streakMaskedImage.mask.array[:ysize, :xsize] |= rescaledDetectedMaskPlane
745 streaks = self.maskStreaks.run(streakMaskedImage)
746 streakMaskPlane = streakMaskedImage.mask.array & streakMaskedImage.mask.getPlaneBitMask(
'STREAK')
748 maskedImage.mask.array |= streakMaskPlane
750 if self.config.writeStreakInfo:
751 rhos = np.array([line.rho
for line
in streaks.lines])
752 thetas = np.array([line.theta
for line
in streaks.lines])
753 sigmas = np.array([line.sigma
for line
in streaks.lines])
754 chi2s = np.array([line.reducedChi2
for line
in streaks.lines])
755 modelMaximums = np.array([line.modelMaximum
for line
in streaks.lines])
756 streakInfo = {
'rho': rhos,
'theta': thetas,
'sigma': sigmas,
'reducedChi2': chi2s,
757 'modelMaximum': modelMaximums}
759 streakInfo = {
'rho': np.array([]),
'theta': np.array([]),
'sigma': np.array([]),
760 'reducedChi2': np.array([]),
'modelMaximum': np.array([])}
761 return pipeBase.Struct(maskedStreaks=streakInfo)
765 scoreExposure = pipeBase.connectionTypes.Input(
766 doc=
"Maximum likelihood image for detection.",
767 dimensions=(
"instrument",
"visit",
"detector"),
768 storageClass=
"ExposureF",
769 name=
"{fakesType}{coaddName}Diff_scoreExp",
773class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
774 pipelineConnections=DetectAndMeasureScoreConnections):
778class DetectAndMeasureScoreTask(DetectAndMeasureTask):
779 """Detect DIA sources using a score image,
780 and measure the detections on the difference image.
782 Source detection is run on the supplied score, or maximum likelihood,
783 image. Note that no additional convolution will be done in this case.
784 Close positive and negative detections will optionally be merged into
786 Sky sources, or forced detections in background regions, will optionally
787 be added, and the configured measurement algorithm will be run on all
790 ConfigClass = DetectAndMeasureScoreConfig
791 _DefaultName =
"detectAndMeasureScore"
794 def run(self, science, matchedTemplate, difference, scoreExposure,
796 """Detect and measure sources on a score image.
800 science : `lsst.afw.image.ExposureF`
801 Science exposure that the template was subtracted from.
802 matchedTemplate : `lsst.afw.image.ExposureF`
803 Warped and PSF-matched template that was used produce the
805 difference : `lsst.afw.image.ExposureF`
806 Result of subtracting template from the science image.
807 scoreExposure : `lsst.afw.image.ExposureF`
808 Score or maximum likelihood difference image
809 idFactory : `lsst.afw.table.IdFactory`, optional
810 Generator object used to assign ids to detected sources in the
811 difference image. Ids from this generator are not set until after
812 deblending and merging positive/negative peaks.
816 measurementResults : `lsst.pipe.base.Struct`
818 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
819 Subtracted exposure with detection mask applied.
820 ``diaSources`` : `lsst.afw.table.SourceCatalog`
821 The catalog of detected sources.
823 if idFactory
is None:
826 self._prepareInputs(scoreExposure)
831 table = afwTable.SourceTable.make(self.schema)
832 results = self.detection.run(
834 exposure=scoreExposure,
838 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
840 sources, positives, negatives = self._deblend(difference,
844 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
845 positiveFootprints=positives, negativeFootprints=negatives)
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, coaddExposures, bbox, wcs, dataIds, physical_filter)