lsst.ip.diffim gee1b14a8c9+0f30a6b5f9
Loading...
Searching...
No Matches
detectAndMeasure.py
Go to the documentation of this file.
1# This file is part of ip_diffim.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22import numpy as np
23
24import lsst.afw.detection as afwDetection
25import lsst.afw.table as afwTable
26import lsst.daf.base as dafBase
27from lsst.meas.algorithms import SkyObjectsTask, SourceDetectionTask, SetPrimaryFlagsTask
28from lsst.meas.base import ForcedMeasurementTask, ApplyApCorrTask, DetectorVisitIdGeneratorConfig
29import lsst.meas.deblender
30import lsst.meas.extensions.trailedSources # noqa: F401
31import lsst.meas.extensions.shapeHSM
32import lsst.pex.config as pexConfig
33import lsst.pipe.base as pipeBase
34import lsst.utils
35from lsst.utils.timer import timeMethod
36
37from . import DipoleFitTask
38
39__all__ = ["DetectAndMeasureConfig", "DetectAndMeasureTask",
40 "DetectAndMeasureScoreConfig", "DetectAndMeasureScoreTask"]
41
42
43class DetectAndMeasureConnections(pipeBase.PipelineTaskConnections,
44 dimensions=("instrument", "visit", "detector"),
45 defaultTemplates={"coaddName": "deep",
46 "warpTypeSuffix": "",
47 "fakesType": ""}):
48 science = pipeBase.connectionTypes.Input(
49 doc="Input science exposure.",
50 dimensions=("instrument", "visit", "detector"),
51 storageClass="ExposureF",
52 name="{fakesType}calexp"
53 )
54 matchedTemplate = pipeBase.connectionTypes.Input(
55 doc="Warped and PSF-matched template used to create the difference image.",
56 dimensions=("instrument", "visit", "detector"),
57 storageClass="ExposureF",
58 name="{fakesType}{coaddName}Diff_matchedExp",
59 )
60 difference = pipeBase.connectionTypes.Input(
61 doc="Result of subtracting template from science.",
62 dimensions=("instrument", "visit", "detector"),
63 storageClass="ExposureF",
64 name="{fakesType}{coaddName}Diff_differenceTempExp",
65 )
66 outputSchema = pipeBase.connectionTypes.InitOutput(
67 doc="Schema (as an example catalog) for output DIASource catalog.",
68 storageClass="SourceCatalog",
69 name="{fakesType}{coaddName}Diff_diaSrc_schema",
70 )
71 diaSources = pipeBase.connectionTypes.Output(
72 doc="Detected diaSources on the difference image.",
73 dimensions=("instrument", "visit", "detector"),
74 storageClass="SourceCatalog",
75 name="{fakesType}{coaddName}Diff_diaSrc",
76 )
77 subtractedMeasuredExposure = pipeBase.connectionTypes.Output(
78 doc="Difference image with detection mask plane filled in.",
79 dimensions=("instrument", "visit", "detector"),
80 storageClass="ExposureF",
81 name="{fakesType}{coaddName}Diff_differenceExp",
82 )
83
84
85class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
86 pipelineConnections=DetectAndMeasureConnections):
87 """Config for DetectAndMeasureTask
88 """
89 doMerge = pexConfig.Field(
90 dtype=bool,
91 default=True,
92 doc="Merge positive and negative diaSources with grow radius "
93 "set by growFootprint"
94 )
95 doForcedMeasurement = pexConfig.Field(
96 dtype=bool,
97 default=True,
98 doc="Force photometer diaSource locations on PVI?")
99 doAddMetrics = pexConfig.Field(
100 dtype=bool,
101 default=False,
102 doc="Add columns to the source table to hold analysis metrics?"
103 )
104 detection = pexConfig.ConfigurableField(
105 target=SourceDetectionTask,
106 doc="Final source detection for diaSource measurement",
107 )
108 deblend = pexConfig.ConfigurableField(
109 target=lsst.meas.deblender.SourceDeblendTask,
110 doc="Task to split blended sources into their components."
111 )
112 measurement = pexConfig.ConfigurableField(
113 target=DipoleFitTask,
114 doc="Task to measure sources on the difference image.",
115 )
116 doApCorr = lsst.pex.config.Field(
117 dtype=bool,
118 default=True,
119 doc="Run subtask to apply aperture corrections"
120 )
121 applyApCorr = lsst.pex.config.ConfigurableField(
122 target=ApplyApCorrTask,
123 doc="Task to apply aperture corrections"
124 )
125 forcedMeasurement = pexConfig.ConfigurableField(
126 target=ForcedMeasurementTask,
127 doc="Task to force photometer science image at diaSource locations.",
128 )
129 growFootprint = pexConfig.Field(
130 dtype=int,
131 default=2,
132 doc="Grow positive and negative footprints by this many pixels before merging"
133 )
134 diaSourceMatchRadius = pexConfig.Field(
135 dtype=float,
136 default=0.5,
137 doc="Match radius (in arcseconds) for DiaSource to Source association"
138 )
139 doSkySources = pexConfig.Field(
140 dtype=bool,
141 default=False,
142 doc="Generate sky sources?",
143 )
144 skySources = pexConfig.ConfigurableField(
145 target=SkyObjectsTask,
146 doc="Generate sky sources",
147 )
148 setPrimaryFlags = pexConfig.ConfigurableField(
149 target=SetPrimaryFlagsTask,
150 doc="Task to add isPrimary and deblending-related flags to the catalog."
151 )
152 badSourceFlags = lsst.pex.config.ListField(
153 dtype=str,
154 doc="Sources with any of these flags set are removed before writing the output catalog.",
155 default=("base_PixelFlags_flag_offimage",
156 "base_PixelFlags_flag_interpolatedCenterAll",
157 "base_PixelFlags_flag_badCenterAll",
158 "base_PixelFlags_flag_edgeCenterAll",
159 ),
160 )
161 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
162
163 def setDefaults(self):
164 # DiaSource Detection
165 self.detection.thresholdPolarity = "both"
166 self.detection.thresholdValue = 5.0
167 self.detection.reEstimateBackground = False
168 self.detection.thresholdType = "pixel_stdev"
169 self.detection.excludeMaskPlanes = ["EDGE"]
170
171 # Add filtered flux measurement, the correct measurement for pre-convolved images.
172 self.measurement.algorithms.names.add("base_PeakLikelihoodFlux")
173 self.measurement.plugins.names |= ["ext_trailedSources_Naive",
174 "base_LocalPhotoCalib",
175 "base_LocalWcs",
176 "ext_shapeHSM_HsmSourceMoments",
177 "ext_shapeHSM_HsmPsfMoments",
178 ]
179 self.measurement.slots.psfShape = "ext_shapeHSM_HsmPsfMoments"
180 self.measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
181 self.measurement.plugins["base_NaiveCentroid"].maxDistToPeak = 5.0
182 self.measurement.plugins["base_SdssCentroid"].maxDistToPeak = 5.0
183 self.forcedMeasurement.plugins = ["base_TransformedCentroid", "base_PsfFlux"]
184 self.forcedMeasurement.copyColumns = {
185 "id": "objectId", "parent": "parentObjectId", "coord_ra": "coord_ra", "coord_dec": "coord_dec"}
186 self.forcedMeasurement.slots.centroid = "base_TransformedCentroid"
187 self.forcedMeasurement.slots.shape = None
188
189 # Keep track of which footprints contain streaks
190 self.measurement.plugins["base_PixelFlags"].masksFpAnywhere = [
191 "STREAK", "INJECTED", "INJECTED_TEMPLATE"]
192 self.measurement.plugins["base_PixelFlags"].masksFpCenter = [
193 "STREAK", "INJECTED", "INJECTED_TEMPLATE"]
194 self.skySources.avoidMask = ["DETECTED", "DETECTED_NEGATIVE", "BAD", "NO_DATA", "EDGE"]
195
196
197class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
198 """Detect and measure sources on a difference image.
199 """
200 ConfigClass = DetectAndMeasureConfig
201 _DefaultName = "detectAndMeasure"
202
203 def __init__(self, **kwargs):
204 super().__init__(**kwargs)
205 self.schema = afwTable.SourceTable.makeMinimalSchema()
206 # Add coordinate error fields:
207 afwTable.CoordKey.addErrorFields(self.schema)
208
209 self.algMetadata = dafBase.PropertyList()
210 self.makeSubtask("detection", schema=self.schema)
211 self.makeSubtask("deblend", schema=self.schema)
212 self.makeSubtask("setPrimaryFlags", schema=self.schema, isSingleFrame=True)
213 self.makeSubtask("measurement", schema=self.schema,
214 algMetadata=self.algMetadata)
215 if self.config.doApCorr:
216 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
217 if self.config.doForcedMeasurement:
218 self.schema.addField(
219 "ip_diffim_forced_PsfFlux_instFlux", "D",
220 "Forced PSF flux measured on the direct image.",
221 units="count")
222 self.schema.addField(
223 "ip_diffim_forced_PsfFlux_instFluxErr", "D",
224 "Forced PSF flux error measured on the direct image.",
225 units="count")
226 self.schema.addField(
227 "ip_diffim_forced_PsfFlux_area", "F",
228 "Forced PSF flux effective area of PSF.",
229 units="pixel")
230 self.schema.addField(
231 "ip_diffim_forced_PsfFlux_flag", "Flag",
232 "Forced PSF flux general failure flag.")
233 self.schema.addField(
234 "ip_diffim_forced_PsfFlux_flag_noGoodPixels", "Flag",
235 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
236 self.schema.addField(
237 "ip_diffim_forced_PsfFlux_flag_edge", "Flag",
238 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
239 self.makeSubtask("forcedMeasurement", refSchema=self.schema)
240
241 self.schema.addField("refMatchId", "L", "unique id of reference catalog match")
242 self.schema.addField("srcMatchId", "L", "unique id of source match")
243 if self.config.doSkySources:
244 self.makeSubtask("skySources", schema=self.schema)
245
246 # Check that the schema and config are consistent
247 for flag in self.config.badSourceFlags:
248 if flag not in self.schema:
249 raise pipeBase.InvalidQuantumError("Field %s not in schema" % flag)
250 # initialize InitOutputs
251 self.outputSchema = afwTable.SourceCatalog(self.schema)
252 self.outputSchema.getTable().setMetadata(self.algMetadata)
253
254 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
255 inputRefs: pipeBase.InputQuantizedConnection,
256 outputRefs: pipeBase.OutputQuantizedConnection):
257 inputs = butlerQC.get(inputRefs)
258 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
259 idFactory = idGenerator.make_table_id_factory()
260 outputs = self.run(**inputs, idFactory=idFactory)
261 butlerQC.put(outputs, outputRefs)
262
263 @timeMethod
264 def run(self, science, matchedTemplate, difference,
265 idFactory=None):
266 """Detect and measure sources on a difference image.
267
268 The difference image will be convolved with a gaussian approximation of
269 the PSF to form a maximum likelihood image for detection.
270 Close positive and negative detections will optionally be merged into
271 dipole diaSources.
272 Sky sources, or forced detections in background regions, will optionally
273 be added, and the configured measurement algorithm will be run on all
274 detections.
275
276 Parameters
277 ----------
278 science : `lsst.afw.image.ExposureF`
279 Science exposure that the template was subtracted from.
280 matchedTemplate : `lsst.afw.image.ExposureF`
281 Warped and PSF-matched template that was used produce the
282 difference image.
283 difference : `lsst.afw.image.ExposureF`
284 Result of subtracting template from the science image.
285 idFactory : `lsst.afw.table.IdFactory`, optional
286 Generator object used to assign ids to detected sources in the
287 difference image. Ids from this generator are not set until after
288 deblending and merging positive/negative peaks.
289
290 Returns
291 -------
292 measurementResults : `lsst.pipe.base.Struct`
293
294 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
295 Subtracted exposure with detection mask applied.
296 ``diaSources`` : `lsst.afw.table.SourceCatalog`
297 The catalog of detected sources.
298 """
299 if idFactory is None:
300 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
301
302 # Ensure that we start with an empty detection mask.
303 mask = difference.mask
304 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
305
306 # Don't use the idFactory until after deblend+merge, so that we aren't
307 # generating ids that just get thrown away (footprint merge doesn't
308 # know about past ids).
309 table = afwTable.SourceTable.make(self.schema)
310 results = self.detection.run(
311 table=table,
312 exposure=difference,
313 doSmooth=True,
314 )
315
316 sources, positives, negatives = self._deblend(difference,
317 results.positive,
318 results.negative)
319
320 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
321 positiveFootprints=positives,
322 negativeFootprints=negatives)
323
324 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
325 positiveFootprints=None, negativeFootprints=None,):
326 """Measure and process the results of source detection.
327
328 Parameters
329 ----------
330 science : `lsst.afw.image.ExposureF`
331 Science exposure that the template was subtracted from.
332 matchedTemplate : `lsst.afw.image.ExposureF`
333 Warped and PSF-matched template that was used produce the
334 difference image.
335 difference : `lsst.afw.image.ExposureF`
336 Result of subtracting template from the science image.
337 sources : `lsst.afw.table.SourceCatalog`
338 Detected sources on the difference exposure.
339 idFactory : `lsst.afw.table.IdFactory`
340 Generator object used to assign ids to detected sources in the
341 difference image.
342 positiveFootprints : `lsst.afw.detection.FootprintSet`, optional
343 Positive polarity footprints.
344 negativeFootprints : `lsst.afw.detection.FootprintSet`, optional
345 Negative polarity footprints.
346
347 Returns
348 -------
349 measurementResults : `lsst.pipe.base.Struct`
350
351 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
352 Subtracted exposure with detection mask applied.
353 ``diaSources`` : `lsst.afw.table.SourceCatalog`
354 The catalog of detected sources.
355 """
356 self.metadata.add("nUnmergedDiaSources", len(sources))
357 if self.config.doMerge:
358 fpSet = positiveFootprints
359 fpSet.merge(negativeFootprints, self.config.growFootprint,
360 self.config.growFootprint, False)
361 initialDiaSources = afwTable.SourceCatalog(self.schema)
362 fpSet.makeSources(initialDiaSources)
363 self.log.info("Merging detections into %d sources", len(initialDiaSources))
364 else:
365 initialDiaSources = sources
366
367 # Assign source ids at the end: deblend/merge mean that we don't keep
368 # track of parents and children, we only care about the final ids.
369 for source in initialDiaSources:
370 source.setId(idFactory())
371 # Ensure sources added after this get correct ids.
372 initialDiaSources.getTable().setIdFactory(idFactory)
373 initialDiaSources.setMetadata(self.algMetadata)
374
375 self.metadata.add("nMergedDiaSources", len(initialDiaSources))
376
377 if self.config.doSkySources:
378 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
379
380 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
381 diaSources = self._removeBadSources(initialDiaSources)
382
383 if self.config.doForcedMeasurement:
384 self.measureForcedSources(diaSources, science, difference.getWcs())
385
386 measurementResults = pipeBase.Struct(
387 subtractedMeasuredExposure=difference,
388 diaSources=diaSources,
389 )
390 self.calculateMetrics(difference)
391
392 return measurementResults
393
394 def _deblend(self, difference, positiveFootprints, negativeFootprints):
395 """Deblend the positive and negative footprints and return a catalog
396 containing just the children, and the deblended footprints.
397
398 Parameters
399 ----------
400 difference : `lsst.afw.image.Exposure`
401 Result of subtracting template from the science image.
402 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
403 Positive and negative polarity footprints measured on
404 ``difference`` to be deblended separately.
405
406 Returns
407 -------
408 sources : `lsst.afw.table.SourceCatalog`
409 Positive and negative deblended children.
410 positives, negatives : `lsst.afw.detection.FootprintSet`
411 Deblended positive and negative polarity footprints measured on
412 ``difference``.
413 """
414 def makeFootprints(sources):
415 footprints = afwDetection.FootprintSet(difference.getBBox())
416 footprints.setFootprints([src.getFootprint() for src in sources])
417 return footprints
418
419 def deblend(footprints):
420 """Deblend a positive or negative footprint set,
421 and return the deblended children.
422 """
423 sources = afwTable.SourceCatalog(self.schema)
424 footprints.makeSources(sources)
425 self.deblend.run(exposure=difference, sources=sources)
426 self.setPrimaryFlags.run(sources)
427 children = sources["detect_isDeblendedSource"] == 1
428 sources = sources[children].copy(deep=True)
429 # Clear parents, so that measurement plugins behave correctly.
430 sources['parent'] = 0
431 return sources.copy(deep=True)
432
433 positives = deblend(positiveFootprints)
434 negatives = deblend(negativeFootprints)
435
436 sources = afwTable.SourceCatalog(self.schema)
437 sources.reserve(len(positives) + len(negatives))
438 sources.extend(positives, deep=True)
439 sources.extend(negatives, deep=True)
440 return sources, makeFootprints(positives), makeFootprints(negatives)
441
442 def _removeBadSources(self, diaSources):
443 """Remove bad diaSources from the catalog.
444
445 Parameters
446 ----------
447 diaSources : `lsst.afw.table.SourceCatalog`
448 The catalog of detected sources.
449
450 Returns
451 -------
452 diaSources : `lsst.afw.table.SourceCatalog`
453 The updated catalog of detected sources, with any source that has a
454 flag in ``config.badSourceFlags`` set removed.
455 """
456 nBadTotal = 0
457 selector = np.ones(len(diaSources), dtype=bool)
458 for flag in self.config.badSourceFlags:
459 flags = diaSources[flag]
460 nBad = np.count_nonzero(flags)
461 if nBad > 0:
462 self.log.info("Found and removed %d unphysical sources with flag %s.", nBad, flag)
463 selector &= ~flags
464 nBadTotal += nBad
465 self.metadata.add("nRemovedBadFlaggedSources", nBadTotal)
466 return diaSources[selector].copy(deep=True)
467
468 def addSkySources(self, diaSources, mask, seed):
469 """Add sources in empty regions of the difference image
470 for measuring the background.
471
472 Parameters
473 ----------
474 diaSources : `lsst.afw.table.SourceCatalog`
475 The catalog of detected sources.
476 mask : `lsst.afw.image.Mask`
477 Mask plane for determining regions where Sky sources can be added.
478 seed : `int`
479 Seed value to initialize the random number generator.
480 """
481 skySourceFootprints = self.skySources.run(mask=mask, seed=seed, catalog=diaSources)
482 self.metadata.add("nSkySources", len(skySourceFootprints))
483
484 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
485 """Use (matched) template and science image to constrain dipole fitting.
486
487 Parameters
488 ----------
489 diaSources : `lsst.afw.table.SourceCatalog`
490 The catalog of detected sources.
491 science : `lsst.afw.image.ExposureF`
492 Science exposure that the template was subtracted from.
493 difference : `lsst.afw.image.ExposureF`
494 Result of subtracting template from the science image.
495 matchedTemplate : `lsst.afw.image.ExposureF`
496 Warped and PSF-matched template that was used produce the
497 difference image.
498 """
499 # Ensure that the required mask planes are present
500 for mp in self.config.measurement.plugins["base_PixelFlags"].masksFpAnywhere:
501 difference.mask.addMaskPlane(mp)
502 # Note that this may not be correct if we convolved the science image.
503 # In the future we may wish to persist the matchedScience image.
504 self.measurement.run(diaSources, difference, science, matchedTemplate)
505 if self.config.doApCorr:
506 apCorrMap = difference.getInfo().getApCorrMap()
507 if apCorrMap is None:
508 self.log.warning("Difference image does not have valid aperture correction; skipping.")
509 else:
510 self.applyApCorr.run(
511 catalog=diaSources,
512 apCorrMap=apCorrMap,
513 )
514
515 def measureForcedSources(self, diaSources, science, wcs):
516 """Perform forced measurement of the diaSources on the science image.
517
518 Parameters
519 ----------
520 diaSources : `lsst.afw.table.SourceCatalog`
521 The catalog of detected sources.
522 science : `lsst.afw.image.ExposureF`
523 Science exposure that the template was subtracted from.
524 wcs : `lsst.afw.geom.SkyWcs`
525 Coordinate system definition (wcs) for the exposure.
526 """
527 # Run forced psf photometry on the PVI at the diaSource locations.
528 # Copy the measured flux and error into the diaSource.
529 forcedSources = self.forcedMeasurement.generateMeasCat(science, diaSources, wcs)
530 self.forcedMeasurement.run(forcedSources, science, diaSources, wcs)
531 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
532 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFlux")[0],
533 "ip_diffim_forced_PsfFlux_instFlux", True)
534 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFluxErr")[0],
535 "ip_diffim_forced_PsfFlux_instFluxErr", True)
536 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_area")[0],
537 "ip_diffim_forced_PsfFlux_area", True)
538 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag")[0],
539 "ip_diffim_forced_PsfFlux_flag", True)
540 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_noGoodPixels")[0],
541 "ip_diffim_forced_PsfFlux_flag_noGoodPixels", True)
542 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_edge")[0],
543 "ip_diffim_forced_PsfFlux_flag_edge", True)
544 for diaSource, forcedSource in zip(diaSources, forcedSources):
545 diaSource.assign(forcedSource, mapper)
546
547 def calculateMetrics(self, difference):
548 """Add image QA metrics to the Task metadata.
549
550 Parameters
551 ----------
552 difference : `lsst.afw.image.Exposure`
553 The target image to calculate metrics for.
554 """
555 mask = difference.mask
556 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
557 self.metadata.add("nGoodPixels", np.sum(~badPix))
558 self.metadata.add("nBadPixels", np.sum(badPix))
559 detPosPix = (mask.array & mask.getPlaneBitMask("DETECTED")) > 0
560 detNegPix = (mask.array & mask.getPlaneBitMask("DETECTED_NEGATIVE")) > 0
561 self.metadata.add("nPixelsDetectedPositive", np.sum(detPosPix))
562 self.metadata.add("nPixelsDetectedNegative", np.sum(detNegPix))
563 detPosPix &= badPix
564 detNegPix &= badPix
565 self.metadata.add("nBadPixelsDetectedPositive", np.sum(detPosPix))
566 self.metadata.add("nBadPixelsDetectedNegative", np.sum(detNegPix))
567
568
570 scoreExposure = pipeBase.connectionTypes.Input(
571 doc="Maximum likelihood image for detection.",
572 dimensions=("instrument", "visit", "detector"),
573 storageClass="ExposureF",
574 name="{fakesType}{coaddName}Diff_scoreExp",
575 )
576
577
579 pipelineConnections=DetectAndMeasureScoreConnections):
580 pass
581
582
584 """Detect DIA sources using a score image,
585 and measure the detections on the difference image.
586
587 Source detection is run on the supplied score, or maximum likelihood,
588 image. Note that no additional convolution will be done in this case.
589 Close positive and negative detections will optionally be merged into
590 dipole diaSources.
591 Sky sources, or forced detections in background regions, will optionally
592 be added, and the configured measurement algorithm will be run on all
593 detections.
594 """
595 ConfigClass = DetectAndMeasureScoreConfig
596 _DefaultName = "detectAndMeasureScore"
597
598 @timeMethod
599 def run(self, science, matchedTemplate, difference, scoreExposure,
600 idFactory=None):
601 """Detect and measure sources on a score image.
602
603 Parameters
604 ----------
605 science : `lsst.afw.image.ExposureF`
606 Science exposure that the template was subtracted from.
607 matchedTemplate : `lsst.afw.image.ExposureF`
608 Warped and PSF-matched template that was used produce the
609 difference image.
610 difference : `lsst.afw.image.ExposureF`
611 Result of subtracting template from the science image.
612 scoreExposure : `lsst.afw.image.ExposureF`
613 Score or maximum likelihood difference image
614 idFactory : `lsst.afw.table.IdFactory`, optional
615 Generator object used to assign ids to detected sources in the
616 difference image. Ids from this generator are not set until after
617 deblending and merging positive/negative peaks.
618
619 Returns
620 -------
621 measurementResults : `lsst.pipe.base.Struct`
622
623 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
624 Subtracted exposure with detection mask applied.
625 ``diaSources`` : `lsst.afw.table.SourceCatalog`
626 The catalog of detected sources.
627 """
628 if idFactory is None:
629 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
630
631 # Ensure that we start with an empty detection mask.
632 mask = scoreExposure.mask
633 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
634
635 # Don't use the idFactory until after deblend+merge, so that we aren't
636 # generating ids that just get thrown away (footprint merge doesn't
637 # know about past ids).
638 table = afwTable.SourceTable.make(self.schema)
639 results = self.detection.run(
640 table=table,
641 exposure=scoreExposure,
642 doSmooth=False,
643 )
644 # Copy the detection mask from the Score image to the difference image
645 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
646
647 sources, positives, negatives = self._deblend(difference,
648 results.positive,
649 results.negative)
650
651 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
652 positiveFootprints=positives, negativeFootprints=negatives)
Asseses the quality of a candidate given a spatial kernel and background model.
run(self, coaddExposures, bbox, wcs, dataIds, physical_filter=None, **kwargs)