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 deblend = pexConfig.ConfigurableField(
126 doc=
"Task to split blended sources into their components."
128 measurement = pexConfig.ConfigurableField(
129 target=DipoleFitTask,
130 doc=
"Task to measure sources on the difference image.",
135 doc=
"Run subtask to apply aperture corrections"
138 target=ApplyApCorrTask,
139 doc=
"Task to apply aperture corrections"
141 forcedMeasurement = pexConfig.ConfigurableField(
142 target=ForcedMeasurementTask,
143 doc=
"Task to force photometer science image at diaSource locations.",
145 growFootprint = pexConfig.Field(
148 doc=
"Grow positive and negative footprints by this many pixels before merging"
150 diaSourceMatchRadius = pexConfig.Field(
153 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
155 doSkySources = pexConfig.Field(
158 doc=
"Generate sky sources?",
160 skySources = pexConfig.ConfigurableField(
161 target=SkyObjectsTask,
162 doc=
"Generate sky sources",
164 doMaskStreaks = pexConfig.Field(
167 doc=
"Turn on streak masking",
169 maskStreaks = pexConfig.ConfigurableField(
170 target=MaskStreaksTask,
171 doc=
"Subtask for masking streaks. Only used if doMaskStreaks is True. "
172 "Adds a mask plane to an exposure, with the mask plane name set by streakMaskName.",
174 streakBinFactor = pexConfig.Field(
177 doc=
"Bin scale factor to use when rerunning detection for masking streaks. "
178 "Only used if doMaskStreaks is True.",
180 writeStreakInfo = pexConfig.Field(
183 doc=
"Record the parameters of any detected streaks. For LSST, this should be turned off except for "
186 setPrimaryFlags = pexConfig.ConfigurableField(
187 target=SetPrimaryFlagsTask,
188 doc=
"Task to add isPrimary and deblending-related flags to the catalog."
192 doc=
"Sources with any of these flags set are removed before writing the output catalog.",
193 default=(
"base_PixelFlags_flag_offimage",
194 "base_PixelFlags_flag_interpolatedCenterAll",
195 "base_PixelFlags_flag_badCenterAll",
196 "base_PixelFlags_flag_edgeCenterAll",
197 "base_PixelFlags_flag_saturatedCenterAll",
202 doc=
"Mask planes to clear before running detection.",
203 default=(
"DETECTED",
"DETECTED_NEGATIVE",
"NOT_DEBLENDED",
"STREAK"),
205 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
207 def setDefaults(self):
209 self.detection.thresholdPolarity =
"both"
210 self.detection.thresholdValue = 5.0
211 self.detection.reEstimateBackground =
False
212 self.detection.thresholdType =
"pixel_stdev"
213 self.detection.excludeMaskPlanes = [
"EDGE",
220 self.measurement.plugins.names |= [
"ext_trailedSources_Naive",
221 "base_LocalPhotoCalib",
223 "ext_shapeHSM_HsmSourceMoments",
224 "ext_shapeHSM_HsmPsfMoments",
225 "base_ClassificationSizeExtendedness",
227 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
228 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
229 self.measurement.plugins[
"base_SdssCentroid"].maxDistToPeak = 5.0
230 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
231 self.forcedMeasurement.copyColumns = {
232 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
233 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
234 self.forcedMeasurement.slots.shape =
None
237 self.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere = [
238 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE"]
239 self.measurement.plugins[
"base_PixelFlags"].masksFpCenter = [
240 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE"]
241 self.skySources.avoidMask = [
"DETECTED",
"DETECTED_NEGATIVE",
"BAD",
"NO_DATA",
"EDGE"]
245 self.maskStreaks.onlyMaskDetected =
False
248class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
249 """Detect and measure sources on a difference image.
251 ConfigClass = DetectAndMeasureConfig
252 _DefaultName =
"detectAndMeasure"
254 def __init__(self, **kwargs):
255 super().__init__(**kwargs)
256 self.schema = afwTable.SourceTable.makeMinimalSchema()
258 afwTable.CoordKey.addErrorFields(self.schema)
261 self.makeSubtask(
"detection", schema=self.schema)
262 self.makeSubtask(
"deblend", schema=self.schema)
263 self.makeSubtask(
"setPrimaryFlags", schema=self.schema, isSingleFrame=
True)
264 self.makeSubtask(
"measurement", schema=self.schema,
265 algMetadata=self.algMetadata)
266 if self.config.doApCorr:
267 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
268 if self.config.doForcedMeasurement:
269 self.schema.addField(
270 "ip_diffim_forced_PsfFlux_instFlux",
"D",
271 "Forced PSF flux measured on the direct image.",
273 self.schema.addField(
274 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
275 "Forced PSF flux error measured on the direct image.",
277 self.schema.addField(
278 "ip_diffim_forced_PsfFlux_area",
"F",
279 "Forced PSF flux effective area of PSF.",
281 self.schema.addField(
282 "ip_diffim_forced_PsfFlux_flag",
"Flag",
283 "Forced PSF flux general failure flag.")
284 self.schema.addField(
285 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
286 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
287 self.schema.addField(
288 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
289 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
290 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
292 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
293 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
294 if self.config.doSkySources:
295 self.makeSubtask(
"skySources", schema=self.schema)
296 if self.config.doMaskStreaks:
297 self.makeSubtask(
"maskStreaks")
300 for flag
in self.config.badSourceFlags:
301 if flag
not in self.schema:
302 raise pipeBase.InvalidQuantumError(
"Field %s not in schema" % flag)
306 self.outputSchema.getTable().setMetadata(self.algMetadata)
308 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
309 inputRefs: pipeBase.InputQuantizedConnection,
310 outputRefs: pipeBase.OutputQuantizedConnection):
311 inputs = butlerQC.get(inputRefs)
312 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
313 idFactory = idGenerator.make_table_id_factory()
314 outputs = self.run(**inputs, idFactory=idFactory)
315 butlerQC.put(outputs, outputRefs)
318 def run(self, science, matchedTemplate, difference,
320 """Detect and measure sources on a difference image.
322 The difference image will be convolved with a gaussian approximation of
323 the PSF to form a maximum likelihood image for detection.
324 Close positive and negative detections will optionally be merged into
326 Sky sources, or forced detections in background regions, will optionally
327 be added, and the configured measurement algorithm will be run on all
332 science : `lsst.afw.image.ExposureF`
333 Science exposure that the template was subtracted from.
334 matchedTemplate : `lsst.afw.image.ExposureF`
335 Warped and PSF-matched template that was used produce the
337 difference : `lsst.afw.image.ExposureF`
338 Result of subtracting template from the science image.
339 idFactory : `lsst.afw.table.IdFactory`, optional
340 Generator object used to assign ids to detected sources in the
341 difference image. Ids from this generator are not set until after
342 deblending and merging positive/negative peaks.
346 measurementResults : `lsst.pipe.base.Struct`
348 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
349 Subtracted exposure with detection mask applied.
350 ``diaSources`` : `lsst.afw.table.SourceCatalog`
351 The catalog of detected sources.
353 if idFactory
is None:
356 self._prepareInputs(difference)
361 table = afwTable.SourceTable.make(self.schema)
362 results = self.detection.run(
368 sources, positives, negatives = self._deblend(difference,
372 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
373 positiveFootprints=positives,
374 negativeFootprints=negatives)
376 def _prepareInputs(self, difference):
377 """Ensure that we start with an empty detection and deblended mask.
381 difference : `lsst.afw.image.ExposureF`
382 The difference image that will be used for detecting diaSources.
383 The mask plane will be modified in place.
387 lsst.pipe.base.UpstreamFailureNoWorkFound
388 If the PSF is not usable for measurement.
391 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
393 raise pipeBase.UpstreamFailureNoWorkFound(
"Invalid PSF detected! PSF width evaluates to NaN.")
395 mask = difference.mask
396 for mp
in self.config.clearMaskPlanes:
397 if mp
not in mask.getMaskPlaneDict():
398 mask.addMaskPlane(mp)
399 mask &= ~mask.getPlaneBitMask(self.config.clearMaskPlanes)
401 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
402 positiveFootprints=None, negativeFootprints=None,):
403 """Measure and process the results of source detection.
407 science : `lsst.afw.image.ExposureF`
408 Science exposure that the template was subtracted from.
409 matchedTemplate : `lsst.afw.image.ExposureF`
410 Warped and PSF-matched template that was used produce the
412 difference : `lsst.afw.image.ExposureF`
413 Result of subtracting template from the science image.
414 sources : `lsst.afw.table.SourceCatalog`
415 Detected sources on the difference exposure.
416 idFactory : `lsst.afw.table.IdFactory`
417 Generator object used to assign ids to detected sources in the
419 positiveFootprints : `lsst.afw.detection.FootprintSet`, optional
420 Positive polarity footprints.
421 negativeFootprints : `lsst.afw.detection.FootprintSet`, optional
422 Negative polarity footprints.
426 measurementResults : `lsst.pipe.base.Struct`
428 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
429 Subtracted exposure with detection mask applied.
430 ``diaSources`` : `lsst.afw.table.SourceCatalog`
431 The catalog of detected sources.
433 self.metadata[
"nUnmergedDiaSources"] = len(sources)
434 if self.config.doMerge:
435 fpSet = positiveFootprints
436 fpSet.merge(negativeFootprints, self.config.growFootprint,
437 self.config.growFootprint,
False)
439 fpSet.makeSources(initialDiaSources)
440 self.log.info(
"Merging detections into %d sources", len(initialDiaSources))
442 initialDiaSources = sources
446 for source
in initialDiaSources:
449 initialDiaSources.getTable().setIdFactory(idFactory)
450 initialDiaSources.setMetadata(self.algMetadata)
452 self.metadata[
"nMergedDiaSources"] = len(initialDiaSources)
454 if self.config.doMaskStreaks:
455 streakInfo = self._runStreakMasking(difference)
457 if self.config.doSkySources:
458 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
460 if not initialDiaSources.isContiguous():
461 initialDiaSources = initialDiaSources.copy(deep=
True)
463 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
464 diaSources = self._removeBadSources(initialDiaSources)
466 if self.config.doForcedMeasurement:
467 self.measureForcedSources(diaSources, science, difference.getWcs())
469 self.calculateMetrics(difference)
471 measurementResults = pipeBase.Struct(
472 subtractedMeasuredExposure=difference,
473 diaSources=diaSources,
475 if self.config.doMaskStreaks
and self.config.writeStreakInfo:
476 measurementResults.mergeItems(streakInfo,
'maskedStreaks')
478 return measurementResults
480 def _deblend(self, difference, positiveFootprints, negativeFootprints):
481 """Deblend the positive and negative footprints and return a catalog
482 containing just the children, and the deblended footprints.
486 difference : `lsst.afw.image.Exposure`
487 Result of subtracting template from the science image.
488 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
489 Positive and negative polarity footprints measured on
490 ``difference`` to be deblended separately.
494 sources : `lsst.afw.table.SourceCatalog`
495 Positive and negative deblended children.
496 positives, negatives : `lsst.afw.detection.FootprintSet`
497 Deblended positive and negative polarity footprints measured on
500 def makeFootprints(sources):
501 footprints = afwDetection.FootprintSet(difference.getBBox())
502 footprints.setFootprints([src.getFootprint()
for src
in sources])
506 """Deblend a positive or negative footprint set,
507 and return the deblended children.
510 footprints.makeSources(sources)
511 self.deblend.run(exposure=difference, sources=sources)
512 self.setPrimaryFlags.run(sources)
513 children = sources[
"detect_isDeblendedSource"] == 1
514 sources = sources[children].copy(deep=
True)
516 sources[
'parent'] = 0
517 return sources.copy(deep=
True)
519 positives =
deblend(positiveFootprints)
520 negatives =
deblend(negativeFootprints)
523 sources.reserve(len(positives) + len(negatives))
524 sources.extend(positives, deep=
True)
525 sources.extend(negatives, deep=
True)
526 return sources, makeFootprints(positives), makeFootprints(negatives)
528 def _removeBadSources(self, diaSources):
529 """Remove unphysical diaSources from the catalog.
533 diaSources : `lsst.afw.table.SourceCatalog`
534 The catalog of detected sources.
538 diaSources : `lsst.afw.table.SourceCatalog`
539 The updated catalog of detected sources, with any source that has a
540 flag in ``config.badSourceFlags`` set removed.
542 selector = np.ones(len(diaSources), dtype=bool)
543 for flag
in self.config.badSourceFlags:
544 flags = diaSources[flag]
545 nBad = np.count_nonzero(flags)
547 self.log.debug(
"Found %d unphysical sources with flag %s.", nBad, flag)
549 nBadTotal = np.count_nonzero(~selector)
550 self.metadata[
"nRemovedBadFlaggedSources"] = nBadTotal
551 self.log.info(
"Removed %d unphysical sources.", nBadTotal)
552 return diaSources[selector].copy(deep=
True)
554 def addSkySources(self, diaSources, mask, seed,
556 """Add sources in empty regions of the difference image
557 for measuring the background.
561 diaSources : `lsst.afw.table.SourceCatalog`
562 The catalog of detected sources.
563 mask : `lsst.afw.image.Mask`
564 Mask plane for determining regions where Sky sources can be added.
566 Seed value to initialize the random number generator.
569 subtask = self.skySources
570 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
571 self.metadata[f
"n_{subtask.getName()}"] = len(skySourceFootprints)
573 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
574 """Use (matched) template and science image to constrain dipole fitting.
578 diaSources : `lsst.afw.table.SourceCatalog`
579 The catalog of detected sources.
580 science : `lsst.afw.image.ExposureF`
581 Science exposure that the template was subtracted from.
582 difference : `lsst.afw.image.ExposureF`
583 Result of subtracting template from the science image.
584 matchedTemplate : `lsst.afw.image.ExposureF`
585 Warped and PSF-matched template that was used produce the
589 for mp
in self.config.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere:
590 difference.mask.addMaskPlane(mp)
593 self.measurement.run(diaSources, difference, science, matchedTemplate)
594 if self.config.doApCorr:
595 apCorrMap = difference.getInfo().getApCorrMap()
596 if apCorrMap
is None:
597 self.log.warning(
"Difference image does not have valid aperture correction; skipping.")
599 self.applyApCorr.run(
604 def measureForcedSources(self, diaSources, science, wcs):
605 """Perform forced measurement of the diaSources on the science image.
609 diaSources : `lsst.afw.table.SourceCatalog`
610 The catalog of detected sources.
611 science : `lsst.afw.image.ExposureF`
612 Science exposure that the template was subtracted from.
613 wcs : `lsst.afw.geom.SkyWcs`
614 Coordinate system definition (wcs) for the exposure.
618 forcedSources = self.forcedMeasurement.generateMeasCat(science, diaSources, wcs)
619 self.forcedMeasurement.run(forcedSources, science, diaSources, wcs)
621 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
622 "ip_diffim_forced_PsfFlux_instFlux",
True)
623 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
624 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
625 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
626 "ip_diffim_forced_PsfFlux_area",
True)
627 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
628 "ip_diffim_forced_PsfFlux_flag",
True)
629 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
630 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
631 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
632 "ip_diffim_forced_PsfFlux_flag_edge",
True)
633 for diaSource, forcedSource
in zip(diaSources, forcedSources):
634 diaSource.assign(forcedSource, mapper)
636 def calculateMetrics(self, difference):
637 """Add difference image QA metrics to the Task metadata.
639 This may be used to produce corresponding metrics (see
640 lsst.analysis.tools.tasks.diffimTaskDetectorVisitMetricAnalysis).
644 difference : `lsst.afw.image.Exposure`
645 The target difference image to calculate metrics for.
647 mask = difference.mask
648 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
649 self.metadata[
"nGoodPixels"] = np.sum(~badPix)
650 self.metadata[
"nBadPixels"] = np.sum(badPix)
651 detPosPix = (mask.array & mask.getPlaneBitMask(
"DETECTED")) > 0
652 detNegPix = (mask.array & mask.getPlaneBitMask(
"DETECTED_NEGATIVE")) > 0
653 self.metadata[
"nPixelsDetectedPositive"] = np.sum(detPosPix)
654 self.metadata[
"nPixelsDetectedNegative"] = np.sum(detNegPix)
657 self.metadata[
"nBadPixelsDetectedPositive"] = np.sum(detPosPix)
658 self.metadata[
"nBadPixelsDetectedNegative"] = np.sum(detNegPix)
660 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
661 for maskPlane
in metricsMaskPlanes:
663 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = evaluateMaskFraction(mask, maskPlane)
664 except InvalidParameterError:
665 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = -1
666 self.log.info(
"Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
668 def _runStreakMasking(self, difference):
669 """Do streak masking and optionally save the resulting streak
670 fit parameters in a catalog.
672 Only returns non-empty streakInfo if self.config.writeStreakInfo
673 is set. The difference image is binned by self.config.streakBinFactor
674 (and detection is run a second time) so that regions with lower
675 surface brightness streaks are more likely to fall above the
680 difference: `lsst.afw.image.Exposure`
681 The exposure in which to search for streaks. Must have a detection
686 streakInfo: `lsst.pipe.base.Struct`
687 ``rho`` : `np.ndarray`
688 Angle of detected streak.
689 ``theta`` : `np.ndarray`
690 Distance from center of detected streak.
691 ``sigma`` : `np.ndarray`
692 Width of streak profile.
693 ``reducedChi2`` : `np.ndarray`
694 Reduced chi2 of the best-fit streak profile.
695 ``modelMaximum`` : `np.ndarray`
696 Peak value of the fit line profile.
698 maskedImage = difference.maskedImage
701 self.config.streakBinFactor,
702 self.config.streakBinFactor)
703 binnedExposure = afwImage.ExposureF(binnedMaskedImage.getBBox())
704 binnedExposure.setMaskedImage(binnedMaskedImage)
705 binnedExposure.setPsf(difference.psf)
707 _table = afwTable.SourceTable.make(self.schema)
708 self.detection.run(table=_table, exposure=binnedExposure, doSmooth=
True)
709 binnedDetectedMaskPlane = binnedExposure.mask.array & binnedExposure.mask.getPlaneBitMask(
'DETECTED')
710 rescaledDetectedMaskPlane = binnedDetectedMaskPlane.repeat(self.config.streakBinFactor,
711 axis=0).repeat(self.config.streakBinFactor,
714 streakMaskedImage = maskedImage.clone()
715 ysize, xsize = rescaledDetectedMaskPlane.shape
716 streakMaskedImage.mask.array[:ysize, :xsize] |= rescaledDetectedMaskPlane
718 streaks = self.maskStreaks.run(streakMaskedImage)
719 streakMaskPlane = streakMaskedImage.mask.array & streakMaskedImage.mask.getPlaneBitMask(
'STREAK')
721 maskedImage.mask.array |= streakMaskPlane
723 if self.config.writeStreakInfo:
724 rhos = np.array([line.rho
for line
in streaks.lines])
725 thetas = np.array([line.theta
for line
in streaks.lines])
726 sigmas = np.array([line.sigma
for line
in streaks.lines])
727 chi2s = np.array([line.reducedChi2
for line
in streaks.lines])
728 modelMaximums = np.array([line.modelMaximum
for line
in streaks.lines])
729 streakInfo = {
'rho': rhos,
'theta': thetas,
'sigma': sigmas,
'reducedChi2': chi2s,
730 'modelMaximum': modelMaximums}
732 streakInfo = {
'rho': np.array([]),
'theta': np.array([]),
'sigma': np.array([]),
733 'reducedChi2': np.array([]),
'modelMaximum': np.array([])}
734 return pipeBase.Struct(maskedStreaks=streakInfo)
738 scoreExposure = pipeBase.connectionTypes.Input(
739 doc=
"Maximum likelihood image for detection.",
740 dimensions=(
"instrument",
"visit",
"detector"),
741 storageClass=
"ExposureF",
742 name=
"{fakesType}{coaddName}Diff_scoreExp",
746class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
747 pipelineConnections=DetectAndMeasureScoreConnections):
751class DetectAndMeasureScoreTask(DetectAndMeasureTask):
752 """Detect DIA sources using a score image,
753 and measure the detections on the difference image.
755 Source detection is run on the supplied score, or maximum likelihood,
756 image. Note that no additional convolution will be done in this case.
757 Close positive and negative detections will optionally be merged into
759 Sky sources, or forced detections in background regions, will optionally
760 be added, and the configured measurement algorithm will be run on all
763 ConfigClass = DetectAndMeasureScoreConfig
764 _DefaultName =
"detectAndMeasureScore"
767 def run(self, science, matchedTemplate, difference, scoreExposure,
769 """Detect and measure sources on a score image.
773 science : `lsst.afw.image.ExposureF`
774 Science exposure that the template was subtracted from.
775 matchedTemplate : `lsst.afw.image.ExposureF`
776 Warped and PSF-matched template that was used produce the
778 difference : `lsst.afw.image.ExposureF`
779 Result of subtracting template from the science image.
780 scoreExposure : `lsst.afw.image.ExposureF`
781 Score or maximum likelihood difference image
782 idFactory : `lsst.afw.table.IdFactory`, optional
783 Generator object used to assign ids to detected sources in the
784 difference image. Ids from this generator are not set until after
785 deblending and merging positive/negative peaks.
789 measurementResults : `lsst.pipe.base.Struct`
791 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
792 Subtracted exposure with detection mask applied.
793 ``diaSources`` : `lsst.afw.table.SourceCatalog`
794 The catalog of detected sources.
796 if idFactory
is None:
799 self._prepareInputs(scoreExposure)
804 table = afwTable.SourceTable.make(self.schema)
805 results = self.detection.run(
807 exposure=scoreExposure,
811 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
813 sources, positives, negatives = self._deblend(difference,
817 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
818 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)