22__all__ = [
"CharacterizeImageConfig",
"CharacterizeImageTask"]
27from lsstDebug
import getDebugFrame
30import lsst.pipe.base
as pipeBase
32import lsst.pipe.base.connectionTypes
as cT
36 SubtractBackgroundTask,
45 SingleFrameMeasurementTask,
47 CatalogCalculationTask,
49 DetectorVisitIdGeneratorConfig,
52import lsst.meas.extensions.shapeHSM
53from .measurePsf
import MeasurePsfTask
54from .repair
import RepairTask
55from .computeExposureSummaryStats
import ComputeExposureSummaryStatsTask
57from lsst.utils.timer
import timeMethod
61 dimensions=(
"instrument",
"visit",
"detector")):
63 doc=
"Input exposure data",
65 storageClass=
"Exposure",
66 dimensions=[
"instrument",
"exposure",
"detector"],
68 characterized = cT.Output(
69 doc=
"Output characterized data.",
71 storageClass=
"ExposureF",
72 dimensions=[
"instrument",
"visit",
"detector"],
74 sourceCat = cT.Output(
75 doc=
"Output source catalog.",
77 storageClass=
"SourceCatalog",
78 dimensions=[
"instrument",
"visit",
"detector"],
80 backgroundModel = cT.Output(
81 doc=
"Output background model.",
82 name=
"icExpBackground",
83 storageClass=
"Background",
84 dimensions=[
"instrument",
"visit",
"detector"],
86 outputSchema = cT.InitOutput(
87 doc=
"Schema of the catalog produced by CharacterizeImage",
89 storageClass=
"SourceCatalog",
96 except pipeBase.ScalarError
as err:
97 raise pipeBase.ScalarError(
98 "CharacterizeImageTask can at present only be run on visits that are associated with "
99 "exactly one exposure. Either this is not a valid exposure for this pipeline, or the "
100 "snap-combination step you probably want hasn't been configured to run between ISR and "
101 "this task (as of this writing, that would be because it hasn't been implemented yet)."
106 pipelineConnections=CharacterizeImageConnections):
107 """Config for CharacterizeImageTask."""
109 doMeasurePsf = pexConfig.Field(
112 doc=
"Measure PSF? If False then for all subsequent operations use either existing PSF "
113 "model when present, or install simple PSF model when not (see installSimplePsf "
116 doWrite = pexConfig.Field(
119 doc=
"Persist results?",
121 doWriteExposure = pexConfig.Field(
124 doc=
"Write icExp and icExpBackground in addition to icSrc? Ignored if doWrite False.",
126 psfIterations = pexConfig.RangeField(
130 doc=
"Number of iterations of detect sources, measure sources, "
131 "estimate PSF. If useSimplePsf is True then 2 should be plenty; "
132 "otherwise more may be wanted.",
134 background = pexConfig.ConfigurableField(
135 target=SubtractBackgroundTask,
136 doc=
"Configuration for initial background estimation",
138 detection = pexConfig.ConfigurableField(
139 target=SourceDetectionTask,
142 doDeblend = pexConfig.Field(
145 doc=
"Run deblender input exposure"
147 deblend = pexConfig.ConfigurableField(
148 target=SourceDeblendTask,
149 doc=
"Split blended source into their components"
151 measurement = pexConfig.ConfigurableField(
152 target=SingleFrameMeasurementTask,
153 doc=
"Measure sources"
155 doApCorr = pexConfig.Field(
158 doc=
"Run subtasks to measure and apply aperture corrections"
160 measureApCorr = pexConfig.ConfigurableField(
161 target=MeasureApCorrTask,
162 doc=
"Subtask to measure aperture corrections"
164 applyApCorr = pexConfig.ConfigurableField(
165 target=ApplyApCorrTask,
166 doc=
"Subtask to apply aperture corrections"
170 catalogCalculation = pexConfig.ConfigurableField(
171 target=CatalogCalculationTask,
172 doc=
"Subtask to run catalogCalculation plugins on catalog"
174 doComputeSummaryStats = pexConfig.Field(
177 doc=
"Run subtask to measure exposure summary statistics",
178 deprecated=(
"This subtask has been moved to CalibrateTask "
181 computeSummaryStats = pexConfig.ConfigurableField(
182 target=ComputeExposureSummaryStatsTask,
183 doc=
"Subtask to run computeSummaryStats on exposure",
184 deprecated=(
"This subtask has been moved to CalibrateTask "
187 useSimplePsf = pexConfig.Field(
190 doc=
"Replace the existing PSF model with a simplified version that has the same sigma "
191 "at the start of each PSF determination iteration? Doing so makes PSF determination "
192 "converge more robustly and quickly.",
194 installSimplePsf = pexConfig.ConfigurableField(
195 target=InstallGaussianPsfTask,
196 doc=
"Install a simple PSF model",
198 refObjLoader = pexConfig.ConfigField(
199 dtype=LoadReferenceObjectsConfig,
200 deprecated=
"This field does nothing. Will be removed after v24 (see DM-34768).",
201 doc=
"reference object loader",
203 ref_match = pexConfig.ConfigurableField(
205 deprecated=
"This field was never usable. Will be removed after v24 (see DM-34768).",
206 doc=
"Task to load and match reference objects. Only used if measurePsf can use matches. "
207 "Warning: matching will only work well if the initial WCS is accurate enough "
208 "to give good matches (roughly: good to 3 arcsec across the CCD).",
210 measurePsf = pexConfig.ConfigurableField(
211 target=MeasurePsfTask,
214 repair = pexConfig.ConfigurableField(
216 doc=
"Remove cosmic rays",
218 requireCrForPsf = pexConfig.Field(
221 doc=
"Require cosmic ray detection and masking to run successfully before measuring the PSF."
223 checkUnitsParseStrict = pexConfig.Field(
224 doc=
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
228 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
235 self.
detection.includeThresholdMultiplier = 10.0
236 self.
detection.doTempLocalBackground =
False
246 selector.doUnresolved =
False
247 selector.flags.good = [
"calib_psf_used"]
248 selector.flags.bad = []
254 "ext_shapeHSM_HsmSourceMoments",
257 "base_CircularApertureFlux",
259 self.
measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
263 raise RuntimeError(
"Must measure PSF to measure aperture correction, "
264 "because flags determined by PSF measurement are used to identify "
265 "sources used to measure aperture correction")
269 """Measure bright sources and use this to estimate background and PSF of
272 Given an exposure with defects repaired (masked
and interpolated over,
273 e.g.
as output by `~lsst.ip.isr.IsrTask`):
274 - detect
and measure bright sources
276 - measure
and subtract background
282 Compatibility parameter. Should always be `
None`.
283 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
284 Reference object loader
if using a catalog-based star-selector.
286 Initial schema
for icSrc catalog.
288 Additional keyword arguments.
293 CharacterizeImageTask has a debug dictionary
with the following keys:
296 int:
if specified, the frame of first debug image displayed (defaults to 1)
298 bool;
if True display image after each repair
in the measure PSF loop
300 bool;
if True display image after each background subtraction
in the measure PSF loop
302 bool;
if True display image
and sources at the end of each iteration of the measure PSF loop
303 See `~lsst.meas.astrom.displayAstrometry`
for the meaning of the various symbols.
305 bool;
if True display image
and sources after PSF
is measured;
306 this will be identical to the final image displayed by measure_iter
if measure_iter
is true
308 bool;
if True display image
and sources after final repair
310 bool;
if True display image
and sources after final measurement
313 ConfigClass = CharacterizeImageConfig
314 _DefaultName = "characterizeImage"
316 def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs):
319 if butler
is not None:
320 warnings.warn(
"The 'butler' parameter is no longer used and can be safely removed.",
321 category=FutureWarning, stacklevel=2)
325 schema = SourceTable.makeMinimalSchema()
327 self.makeSubtask(
"background")
328 self.makeSubtask(
"installSimplePsf")
329 self.makeSubtask(
"repair")
330 self.makeSubtask(
"measurePsf", schema=self.
schema)
332 if self.config.doMeasurePsf
and self.measurePsf.usesMatches:
333 self.makeSubtask(
"ref_match", refObjLoader=refObjLoader)
335 self.makeSubtask(
'detection', schema=self.
schema)
336 if self.config.doDeblend:
337 self.makeSubtask(
"deblend", schema=self.
schema)
338 self.makeSubtask(
'measurement', schema=self.
schema, algMetadata=self.
algMetadata)
339 if self.config.doApCorr:
340 self.makeSubtask(
'measureApCorr', schema=self.
schema)
341 self.makeSubtask(
'applyApCorr', schema=self.
schema)
342 self.makeSubtask(
'catalogCalculation', schema=self.
schema)
345 self.
schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
349 inputs = butlerQC.get(inputRefs)
350 if 'idGenerator' not in inputs.keys():
351 inputs[
'idGenerator'] = self.config.idGenerator.apply(butlerQC.quantum.dataId)
352 outputs = self.
run(**inputs)
353 butlerQC.put(outputs, outputRefs)
356 def run(self, exposure, exposureIdInfo=None, background=None, idGenerator=None):
357 """Characterize a science image.
359 Peforms the following operations:
360 - Iterate the following config.psfIterations times, or once
if config.doMeasurePsf false:
361 - detect
and measure sources
and estimate PSF (see detectMeasureAndEstimatePsf
for details)
362 - interpolate over cosmic rays
363 - perform final measurement
367 exposure : `lsst.afw.image.ExposureF`
368 Exposure to characterize.
369 exposureIdInfo : `lsst.obs.base.ExposureIdInfo`, optional
370 Exposure ID info. Deprecated
in favor of ``idGenerator``,
and
371 ignored
if that
is provided.
372 background : `lsst.afw.math.BackgroundList`, optional
373 Initial model of background already subtracted
from exposure.
374 idGenerator : `lsst.meas.base.IdGenerator`, optional
375 Object that generates source IDs
and provides RNG seeds.
379 result : `lsst.pipe.base.Struct`
380 Results
as a struct
with attributes:
383 Characterized exposure (`lsst.afw.image.ExposureF`).
387 Model of subtracted background (`lsst.afw.math.BackgroundList`).
391 Another reference to ``exposure``
for compatibility.
393 Another reference to ``background``
for compatibility.
398 Raised
if PSF sigma
is NaN.
402 if not self.config.doMeasurePsf
and not exposure.hasPsf():
403 self.log.info(
"CharacterizeImageTask initialized with 'simple' PSF.")
404 self.installSimplePsf.run(exposure=exposure)
406 if idGenerator
is None:
407 if exposureIdInfo
is not None:
408 idGenerator = IdGenerator._from_exposure_id_info(exposureIdInfo)
410 idGenerator = IdGenerator()
415 background = self.background.run(exposure).background
417 psfIterations = self.config.psfIterations
if self.config.doMeasurePsf
else 1
418 for i
in range(psfIterations):
421 idGenerator=idGenerator,
422 background=background,
425 psf = dmeRes.exposure.getPsf()
427 psfAvgPos = psf.getAveragePosition()
428 psfSigma = psf.computeShape(psfAvgPos).getDeterminantRadius()
429 psfDimensions = psf.computeImage(psfAvgPos).getDimensions()
430 medBackground = np.median(dmeRes.background.getImage().getArray())
431 self.log.info(
"iter %s; PSF sigma=%0.2f, dimensions=%s; median background=%0.2f",
432 i + 1, psfSigma, psfDimensions, medBackground)
433 if np.isnan(psfSigma):
434 raise RuntimeError(
"PSF sigma is NaN, cannot continue PSF determination.")
436 self.
display(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
439 self.repair.run(exposure=dmeRes.exposure)
440 self.
display(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
444 self.measurement.run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
445 exposureId=idGenerator.catalog_id)
446 if self.config.doApCorr:
448 apCorrMap = self.measureApCorr.run(
449 exposure=dmeRes.exposure,
450 catalog=dmeRes.sourceCat,
452 except MeasureApCorrError:
456 dmeRes.exposure.info.setApCorrMap(
None)
458 dmeRes.exposure.info.setApCorrMap(apCorrMap)
459 self.applyApCorr.run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
461 self.catalogCalculation.run(dmeRes.sourceCat)
463 self.
display(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
465 return pipeBase.Struct(
466 exposure=dmeRes.exposure,
467 sourceCat=dmeRes.sourceCat,
468 background=dmeRes.background,
469 psfCellSet=dmeRes.psfCellSet,
471 characterized=dmeRes.exposure,
472 backgroundModel=dmeRes.background
477 """Perform one iteration of detect, measure, and estimate PSF.
479 Performs the following operations:
481 - if config.doMeasurePsf
or not exposure.hasPsf():
483 - install a simple PSF model (replacing the existing one,
if need be)
485 - interpolate over cosmic rays
with keepCRs=
True
486 - estimate background
and subtract it
from the exposure
487 - detect, deblend
and measure sources,
and subtract a refined background model;
488 -
if config.doMeasurePsf:
493 exposure : `lsst.afw.image.ExposureF`
494 Exposure to characterize.
495 idGenerator : `lsst.meas.base.IdGenerator`
496 Object that generates source IDs
and provides RNG seeds.
497 background : `lsst.afw.math.BackgroundList`, optional
498 Initial model of background already subtracted
from exposure.
502 result : `lsst.pipe.base.Struct`
503 Results
as a struct
with attributes:
506 Characterized exposure (`lsst.afw.image.ExposureF`).
510 Model of subtracted background (`lsst.afw.math.BackgroundList`).
517 Raised
if there are too many CR pixels.
520 if not exposure.hasPsf()
or (self.config.doMeasurePsf
and self.config.useSimplePsf):
521 self.log.info(
"PSF estimation initialized with 'simple' PSF")
522 self.installSimplePsf.run(exposure=exposure)
525 if self.config.requireCrForPsf:
526 self.repair.run(exposure=exposure, keepCRs=
True)
529 self.repair.run(exposure=exposure, keepCRs=
True)
531 self.log.warning(
"Skipping cosmic ray detection: Too many CR pixels (max %0.f)",
532 self.config.repair.cosmicray.nCrPixelMax)
534 self.
display(
"repair_iter", exposure=exposure)
536 if background
is None:
537 background = BackgroundList()
539 sourceIdFactory = idGenerator.make_table_id_factory()
540 table = SourceTable.make(self.
schema, sourceIdFactory)
543 detRes = self.detection.run(table=table, exposure=exposure, doSmooth=
True)
544 sourceCat = detRes.sources
545 if detRes.background:
546 for bg
in detRes.background:
547 background.append(bg)
549 if self.config.doDeblend:
550 self.deblend.run(exposure=exposure, sources=sourceCat)
552 if not sourceCat.isContiguous():
553 sourceCat = sourceCat.copy(deep=
True)
555 self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=idGenerator.catalog_id)
557 measPsfRes = pipeBase.Struct(cellSet=
None)
558 if self.config.doMeasurePsf:
560 if self.measurePsf.usesMatches:
561 matches = self.ref_match.loadAndMatch(exposure=exposure, sourceCat=sourceCat).matches
564 measPsfRes = self.measurePsf.run(exposure=exposure, sources=sourceCat, matches=matches,
565 expId=idGenerator.catalog_id)
566 self.
display(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
568 return pipeBase.Struct(
571 background=background,
572 psfCellSet=measPsfRes.cellSet,
575 def display(self, itemName, exposure, sourceCat=None):
576 """Display exposure and sources on next frame (for debugging).
581 Name of item in ``debugInfo``.
582 exposure : `lsst.afw.image.ExposureF`
585 Catalog of sources detected on the exposure.
587 val = getDebugFrame(self._display, itemName)
591 displayAstrometry(exposure=exposure, sourceCat=sourceCat, frame=self.
_frame, pause=
False)
adjustQuantum(self, inputs, outputs, label, dataId)
detectMeasureAndEstimatePsf(self, exposure, idGenerator, background)
__init__(self, butler=None, refObjLoader=None, schema=None, **kwargs)
display(self, itemName, exposure, sourceCat=None)
runQuantum(self, butlerQC, inputRefs, outputRefs)
run(self, exposure, exposureIdInfo=None, background=None, idGenerator=None)