22__all__ = [
"CharacterizeImageConfig",
"CharacterizeImageTask"]
27from lsstDebug
import getDebugFrame
32import lsst.pipe.base.connectionTypes
as cT
39from lsst.obs.base
import ExposureIdInfo
40from lsst.meas.base import SingleFrameMeasurementTask, ApplyApCorrTask, CatalogCalculationTask
42import lsst.meas.extensions.shapeHSM
43from .measurePsf
import MeasurePsfTask
44from .repair
import RepairTask
45from .computeExposureSummaryStats
import ComputeExposureSummaryStatsTask
47from lsst.utils.timer
import timeMethod
51 dimensions=(
"instrument",
"visit",
"detector")):
53 doc=
"Input exposure data",
55 storageClass=
"Exposure",
56 dimensions=[
"instrument",
"exposure",
"detector"],
58 characterized = cT.Output(
59 doc=
"Output characterized data.",
61 storageClass=
"ExposureF",
62 dimensions=[
"instrument",
"visit",
"detector"],
64 sourceCat = cT.Output(
65 doc=
"Output source catalog.",
67 storageClass=
"SourceCatalog",
68 dimensions=[
"instrument",
"visit",
"detector"],
70 backgroundModel = cT.Output(
71 doc=
"Output background model.",
72 name=
"icExpBackground",
73 storageClass=
"Background",
74 dimensions=[
"instrument",
"visit",
"detector"],
76 outputSchema = cT.InitOutput(
77 doc=
"Schema of the catalog produced by CharacterizeImage",
79 storageClass=
"SourceCatalog",
86 except pipeBase.ScalarError
as err:
87 raise pipeBase.ScalarError(
88 "CharacterizeImageTask can at present only be run on visits that are associated with "
89 "exactly one exposure. Either this is not a valid exposure for this pipeline, or the "
90 "snap-combination step you probably want hasn't been configured to run between ISR and "
91 "this task (as of this writing, that would be because it hasn't been implemented yet)."
96 pipelineConnections=CharacterizeImageConnections):
97 """Config for CharacterizeImageTask."""
99 doMeasurePsf = pexConfig.Field(
102 doc=
"Measure PSF? If False then for all subsequent operations use either existing PSF "
103 "model when present, or install simple PSF model when not (see installSimplePsf "
106 doWrite = pexConfig.Field(
109 doc=
"Persist results?",
111 doWriteExposure = pexConfig.Field(
114 doc=
"Write icExp and icExpBackground in addition to icSrc? Ignored if doWrite False.",
116 psfIterations = pexConfig.RangeField(
120 doc=
"Number of iterations of detect sources, measure sources, "
121 "estimate PSF. If useSimplePsf is True then 2 should be plenty; "
122 "otherwise more may be wanted.",
124 background = pexConfig.ConfigurableField(
125 target=SubtractBackgroundTask,
126 doc=
"Configuration for initial background estimation",
128 detection = pexConfig.ConfigurableField(
129 target=SourceDetectionTask,
132 doDeblend = pexConfig.Field(
135 doc=
"Run deblender input exposure"
137 deblend = pexConfig.ConfigurableField(
138 target=SourceDeblendTask,
139 doc=
"Split blended source into their components"
141 measurement = pexConfig.ConfigurableField(
142 target=SingleFrameMeasurementTask,
143 doc=
"Measure sources"
145 doApCorr = pexConfig.Field(
148 doc=
"Run subtasks to measure and apply aperture corrections"
150 measureApCorr = pexConfig.ConfigurableField(
151 target=MeasureApCorrTask,
152 doc=
"Subtask to measure aperture corrections"
154 applyApCorr = pexConfig.ConfigurableField(
155 target=ApplyApCorrTask,
156 doc=
"Subtask to apply aperture corrections"
160 catalogCalculation = pexConfig.ConfigurableField(
161 target=CatalogCalculationTask,
162 doc=
"Subtask to run catalogCalculation plugins on catalog"
164 doComputeSummaryStats = pexConfig.Field(
167 doc=
"Run subtask to measure exposure summary statistics",
168 deprecated=(
"This subtask has been moved to CalibrateTask "
171 computeSummaryStats = pexConfig.ConfigurableField(
172 target=ComputeExposureSummaryStatsTask,
173 doc=
"Subtask to run computeSummaryStats on exposure",
174 deprecated=(
"This subtask has been moved to CalibrateTask "
177 useSimplePsf = pexConfig.Field(
180 doc=
"Replace the existing PSF model with a simplified version that has the same sigma "
181 "at the start of each PSF determination iteration? Doing so makes PSF determination "
182 "converge more robustly and quickly.",
184 installSimplePsf = pexConfig.ConfigurableField(
185 target=InstallGaussianPsfTask,
186 doc=
"Install a simple PSF model",
188 refObjLoader = pexConfig.ConfigField(
189 dtype=LoadReferenceObjectsConfig,
190 deprecated=
"This field does nothing. Will be removed after v24 (see DM-34768).",
191 doc=
"reference object loader",
193 ref_match = pexConfig.ConfigurableField(
195 deprecated=
"This field was never usable. Will be removed after v24 (see DM-34768).",
196 doc=
"Task to load and match reference objects. Only used if measurePsf can use matches. "
197 "Warning: matching will only work well if the initial WCS is accurate enough "
198 "to give good matches (roughly: good to 3 arcsec across the CCD).",
200 measurePsf = pexConfig.ConfigurableField(
201 target=MeasurePsfTask,
204 repair = pexConfig.ConfigurableField(
206 doc=
"Remove cosmic rays",
208 requireCrForPsf = pexConfig.Field(
211 doc=
"Require cosmic ray detection and masking to run successfully before measuring the PSF."
213 checkUnitsParseStrict = pexConfig.Field(
214 doc=
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
224 self.
detection.includeThresholdMultiplier = 10.0
225 self.
detection.doTempLocalBackground =
False
235 "ext_shapeHSM_HsmSourceMoments",
238 "base_CircularApertureFlux",
240 self.
measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
244 raise RuntimeError(
"Must measure PSF to measure aperture correction, "
245 "because flags determined by PSF measurement are used to identify "
246 "sources used to measure aperture correction")
250 """Measure bright sources and use this to estimate background and PSF of
253 Given an exposure with defects repaired (masked
and interpolated over,
255 - detect
and measure bright sources
257 - measure
and subtract background
263 Compatibility parameter. Should always be `
None`.
265 Reference object loader
if using a catalog-based star-selector.
267 Initial schema
for icSrc catalog.
269 Additional keyword arguments.
274 CharacterizeImageTask has a debug dictionary
with the following keys:
277 int:
if specified, the frame of first debug image displayed (defaults to 1)
279 bool;
if True display image after each repair
in the measure PSF loop
281 bool;
if True display image after each background subtraction
in the measure PSF loop
283 bool;
if True display image
and sources at the end of each iteration of the measure PSF loop
284 See `~lsst.meas.astrom.displayAstrometry`
for the meaning of the various symbols.
286 bool;
if True display image
and sources after PSF
is measured;
287 this will be identical to the final image displayed by measure_iter
if measure_iter
is true
289 bool;
if True display image
and sources after final repair
291 bool;
if True display image
and sources after final measurement
294 ConfigClass = CharacterizeImageConfig
295 _DefaultName = "characterizeImage"
297 def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs):
298 super().__init__(**kwargs)
300 if butler
is not None:
301 warnings.warn(
"The 'butler' parameter is no longer used and can be safely removed.",
302 category=FutureWarning, stacklevel=2)
306 schema = SourceTable.makeMinimalSchema()
308 self.makeSubtask(
"background")
309 self.makeSubtask(
"installSimplePsf")
310 self.makeSubtask(
"repair")
311 self.makeSubtask(
"measurePsf", schema=self.
schema)
313 if self.config.doMeasurePsf
and self.measurePsf.usesMatches:
314 self.makeSubtask(
"ref_match", refObjLoader=refObjLoader)
316 self.makeSubtask(
'detection', schema=self.
schema)
317 if self.config.doDeblend:
318 self.makeSubtask(
"deblend", schema=self.
schema)
319 self.makeSubtask(
'measurement', schema=self.
schema, algMetadata=self.
algMetadata)
320 if self.config.doApCorr:
321 self.makeSubtask(
'measureApCorr', schema=self.
schema)
322 self.makeSubtask(
'applyApCorr', schema=self.
schema)
323 self.makeSubtask(
'catalogCalculation', schema=self.
schema)
324 self.
_initialFrame = getDebugFrame(self._display,
"frame")
or 1
326 self.
schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
330 inputs = butlerQC.get(inputRefs)
331 if 'exposureIdInfo' not in inputs.keys():
332 inputs[
'exposureIdInfo'] = ExposureIdInfo.fromDataId(butlerQC.quantum.dataId,
"visit_detector")
333 outputs = self.
run(**inputs)
334 butlerQC.put(outputs, outputRefs)
337 def run(self, exposure, exposureIdInfo=None, background=None):
338 """Characterize a science image.
340 Peforms the following operations:
341 - Iterate the following config.psfIterations times, or once
if config.doMeasurePsf false:
342 - detect
and measure sources
and estimate PSF (see detectMeasureAndEstimatePsf
for details)
343 - interpolate over cosmic rays
344 - perform final measurement
348 exposure : `lsst.afw.image.ExposureF`
349 Exposure to characterize.
350 exposureIdInfo : `lsst.obs.baseExposureIdInfo`, optional
351 Exposure ID info. If
not provided, returned SourceCatalog IDs will
not
354 Initial model of background already subtracted
from exposure.
358 result : `lsst.pipe.base.Struct`
359 Results
as a struct
with attributes:
362 Characterized exposure (`lsst.afw.image.ExposureF`).
370 Another reference to ``exposure``
for compatibility.
372 Another reference to ``background``
for compatibility.
377 Raised
if PSF sigma
is NaN.
381 if not self.config.doMeasurePsf
and not exposure.hasPsf():
382 self.log.info(
"CharacterizeImageTask initialized with 'simple' PSF.")
383 self.installSimplePsf.run(exposure=exposure)
385 if exposureIdInfo
is None:
386 exposureIdInfo = ExposureIdInfo()
389 background = self.background.run(exposure).background
391 psfIterations = self.config.psfIterations
if self.config.doMeasurePsf
else 1
392 for i
in range(psfIterations):
395 exposureIdInfo=exposureIdInfo,
396 background=background,
399 psf = dmeRes.exposure.getPsf()
401 psfAvgPos = psf.getAveragePosition()
402 psfSigma = psf.computeShape(psfAvgPos).getDeterminantRadius()
403 psfDimensions = psf.computeImage(psfAvgPos).getDimensions()
404 medBackground = np.median(dmeRes.background.getImage().getArray())
405 self.log.info(
"iter %s; PSF sigma=%0.2f, dimensions=%s; median background=%0.2f",
406 i + 1, psfSigma, psfDimensions, medBackground)
407 if np.isnan(psfSigma):
408 raise RuntimeError(
"PSF sigma is NaN, cannot continue PSF determination.")
410 self.
display(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
413 self.repair.run(exposure=dmeRes.exposure)
414 self.
display(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
418 self.measurement.run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
419 exposureId=exposureIdInfo.expId)
420 if self.config.doApCorr:
421 apCorrMap = self.measureApCorr.run(exposure=dmeRes.exposure, catalog=dmeRes.sourceCat).apCorrMap
422 dmeRes.exposure.getInfo().setApCorrMap(apCorrMap)
423 self.applyApCorr.run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
424 self.catalogCalculation.run(dmeRes.sourceCat)
426 self.
display(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
428 return pipeBase.Struct(
429 exposure=dmeRes.exposure,
430 sourceCat=dmeRes.sourceCat,
431 background=dmeRes.background,
432 psfCellSet=dmeRes.psfCellSet,
434 characterized=dmeRes.exposure,
435 backgroundModel=dmeRes.background
440 """Perform one iteration of detect, measure, and estimate PSF.
442 Performs the following operations:
444 - if config.doMeasurePsf
or not exposure.hasPsf():
446 - install a simple PSF model (replacing the existing one,
if need be)
448 - interpolate over cosmic rays
with keepCRs=
True
449 - estimate background
and subtract it
from the exposure
450 - detect, deblend
and measure sources,
and subtract a refined background model;
451 -
if config.doMeasurePsf:
456 exposure : `lsst.afw.image.ExposureF`
457 Exposure to characterize.
458 exposureIdInfo : `lsst.obs.baseExposureIdInfo`
461 Initial model of background already subtracted
from exposure.
465 result : `lsst.pipe.base.Struct`
466 Results
as a struct
with attributes:
469 Characterized exposure (`lsst.afw.image.ExposureF`).
480 Raised
if there are too many CR pixels.
483 if not exposure.hasPsf()
or (self.config.doMeasurePsf
and self.config.useSimplePsf):
484 self.log.info(
"PSF estimation initialized with 'simple' PSF")
485 self.installSimplePsf.run(exposure=exposure)
488 if self.config.requireCrForPsf:
489 self.repair.run(exposure=exposure, keepCRs=
True)
492 self.repair.run(exposure=exposure, keepCRs=
True)
494 self.log.warning(
"Skipping cosmic ray detection: Too many CR pixels (max %0.f)",
495 self.config.repair.cosmicray.nCrPixelMax)
497 self.
display(
"repair_iter", exposure=exposure)
499 if background
is None:
502 sourceIdFactory = exposureIdInfo.makeSourceIdFactory()
503 table = SourceTable.make(self.
schema, sourceIdFactory)
506 detRes = self.detection.run(table=table, exposure=exposure, doSmooth=
True)
507 sourceCat = detRes.sources
508 if detRes.fpSets.background:
509 for bg
in detRes.fpSets.background:
510 background.append(bg)
512 if self.config.doDeblend:
513 self.deblend.run(exposure=exposure, sources=sourceCat)
515 self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId)
517 measPsfRes = pipeBase.Struct(cellSet=
None)
518 if self.config.doMeasurePsf:
520 if self.measurePsf.usesMatches:
521 matches = self.ref_match.loadAndMatch(exposure=exposure, sourceCat=sourceCat).matches
524 measPsfRes = self.measurePsf.run(exposure=exposure, sources=sourceCat, matches=matches,
525 expId=exposureIdInfo.expId)
526 self.
display(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
528 return pipeBase.Struct(
531 background=background,
532 psfCellSet=measPsfRes.cellSet,
535 def display(self, itemName, exposure, sourceCat=None):
536 """Display exposure and sources on next frame (for debugging).
541 Name of item in ``debugInfo``.
542 exposure : `lsst.afw.image.ExposureF`
545 Catalog of sources detected on the exposure.
547 val = getDebugFrame(self._display, itemName)
551 displayAstrometry(exposure=exposure, sourceCat=sourceCat, frame=self.
_frame, pause=
False)
A collection of SpatialCells covering an entire image.
Defines the fields and offsets for a table.
Class for storing ordered metadata with comments.
def adjustQuantum(self, inputs, outputs, label, dataId)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def run(self, exposure, exposureIdInfo=None, background=None)
def detectMeasureAndEstimatePsf(self, exposure, exposureIdInfo, background)
def display(self, itemName, exposure, sourceCat=None)