22__all__ = [
"CharacterizeImageConfig",
"CharacterizeImageTask"]
26from lsstDebug
import getDebugFrame
31import lsst.pipe.base.connectionTypes
as cT
35 SubtractBackgroundTask,
44 SingleFrameMeasurementTask,
46 CatalogCalculationTask,
48 DetectorVisitIdGeneratorConfig,
51import lsst.meas.extensions.shapeHSM
52from .measurePsf
import MeasurePsfTask
53from .repair
import RepairTask
54from .computeExposureSummaryStats
import ComputeExposureSummaryStatsTask
56from lsst.utils.timer
import timeMethod
60 dimensions=(
"instrument",
"visit",
"detector")):
62 doc=
"Input exposure data",
64 storageClass=
"Exposure",
65 dimensions=[
"instrument",
"exposure",
"detector"],
67 characterized = cT.Output(
68 doc=
"Output characterized data.",
70 storageClass=
"ExposureF",
71 dimensions=[
"instrument",
"visit",
"detector"],
73 sourceCat = cT.Output(
74 doc=
"Output source catalog.",
76 storageClass=
"SourceCatalog",
77 dimensions=[
"instrument",
"visit",
"detector"],
79 backgroundModel = cT.Output(
80 doc=
"Output background model.",
81 name=
"icExpBackground",
82 storageClass=
"Background",
83 dimensions=[
"instrument",
"visit",
"detector"],
85 outputSchema = cT.InitOutput(
86 doc=
"Schema of the catalog produced by CharacterizeImage",
88 storageClass=
"SourceCatalog",
95 except pipeBase.ScalarError
as err:
96 raise pipeBase.ScalarError(
97 "CharacterizeImageTask can at present only be run on visits that are associated with "
98 "exactly one exposure. Either this is not a valid exposure for this pipeline, or the "
99 "snap-combination step you probably want hasn't been configured to run between ISR and "
100 "this task (as of this writing, that would be because it hasn't been implemented yet)."
105 pipelineConnections=CharacterizeImageConnections):
106 """Config for CharacterizeImageTask."""
108 doMeasurePsf = pexConfig.Field(
111 doc=
"Measure PSF? If False then for all subsequent operations use either existing PSF "
112 "model when present, or install simple PSF model when not (see installSimplePsf "
115 doWrite = pexConfig.Field(
118 doc=
"Persist results?",
120 doWriteExposure = pexConfig.Field(
123 doc=
"Write icExp and icExpBackground in addition to icSrc? Ignored if doWrite False.",
125 psfIterations = pexConfig.RangeField(
129 doc=
"Number of iterations of detect sources, measure sources, "
130 "estimate PSF. If useSimplePsf is True then 2 should be plenty; "
131 "otherwise more may be wanted.",
133 background = pexConfig.ConfigurableField(
134 target=SubtractBackgroundTask,
135 doc=
"Configuration for initial background estimation",
137 detection = pexConfig.ConfigurableField(
138 target=SourceDetectionTask,
141 doDeblend = pexConfig.Field(
144 doc=
"Run deblender input exposure"
146 deblend = pexConfig.ConfigurableField(
147 target=SourceDeblendTask,
148 doc=
"Split blended source into their components"
150 measurement = pexConfig.ConfigurableField(
151 target=SingleFrameMeasurementTask,
152 doc=
"Measure sources"
154 doApCorr = pexConfig.Field(
157 doc=
"Run subtasks to measure and apply aperture corrections"
159 measureApCorr = pexConfig.ConfigurableField(
160 target=MeasureApCorrTask,
161 doc=
"Subtask to measure aperture corrections"
163 applyApCorr = pexConfig.ConfigurableField(
164 target=ApplyApCorrTask,
165 doc=
"Subtask to apply aperture corrections"
169 catalogCalculation = pexConfig.ConfigurableField(
170 target=CatalogCalculationTask,
171 doc=
"Subtask to run catalogCalculation plugins on catalog"
173 doComputeSummaryStats = pexConfig.Field(
176 doc=
"Run subtask to measure exposure summary statistics",
177 deprecated=(
"This subtask has been moved to CalibrateTask "
180 computeSummaryStats = pexConfig.ConfigurableField(
181 target=ComputeExposureSummaryStatsTask,
182 doc=
"Subtask to run computeSummaryStats on exposure",
183 deprecated=(
"This subtask has been moved to CalibrateTask "
186 useSimplePsf = pexConfig.Field(
189 doc=
"Replace the existing PSF model with a simplified version that has the same sigma "
190 "at the start of each PSF determination iteration? Doing so makes PSF determination "
191 "converge more robustly and quickly.",
193 installSimplePsf = pexConfig.ConfigurableField(
194 target=InstallGaussianPsfTask,
195 doc=
"Install a simple PSF model",
197 refObjLoader = pexConfig.ConfigField(
198 dtype=LoadReferenceObjectsConfig,
199 deprecated=
"This field does nothing. Will be removed after v24 (see DM-34768).",
200 doc=
"reference object loader",
202 ref_match = pexConfig.ConfigurableField(
204 deprecated=
"This field was never usable. Will be removed after v24 (see DM-34768).",
205 doc=
"Task to load and match reference objects. Only used if measurePsf can use matches. "
206 "Warning: matching will only work well if the initial WCS is accurate enough "
207 "to give good matches (roughly: good to 3 arcsec across the CCD).",
209 measurePsf = pexConfig.ConfigurableField(
210 target=MeasurePsfTask,
213 repair = pexConfig.ConfigurableField(
215 doc=
"Remove cosmic rays",
217 requireCrForPsf = pexConfig.Field(
220 doc=
"Require cosmic ray detection and masking to run successfully before measuring the PSF."
222 checkUnitsParseStrict = pexConfig.Field(
223 doc=
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
227 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
234 self.
detection.includeThresholdMultiplier = 10.0
235 self.
detection.doTempLocalBackground =
False
245 selector.doUnresolved =
False
246 selector.flags.good = [
"calib_psf_used"]
247 selector.flags.bad = []
253 "ext_shapeHSM_HsmSourceMoments",
256 "base_CircularApertureFlux",
258 self.
measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
262 raise RuntimeError(
"Must measure PSF to measure aperture correction, "
263 "because flags determined by PSF measurement are used to identify "
264 "sources used to measure aperture correction")
268 """Measure bright sources and use this to estimate background and PSF of
271 Given an exposure with defects repaired (masked
and interpolated over,
273 - detect
and measure bright sources
275 - measure
and subtract background
281 Reference object loader
if using a catalog-based star-selector.
283 Initial schema
for icSrc catalog.
285 Additional keyword arguments.
290 CharacterizeImageTask has a debug dictionary
with the following keys:
293 int:
if specified, the frame of first debug image displayed (defaults to 1)
295 bool;
if True display image after each repair
in the measure PSF loop
297 bool;
if True display image after each background subtraction
in the measure PSF loop
299 bool;
if True display image
and sources at the end of each iteration of the measure PSF loop
300 See `~lsst.meas.astrom.displayAstrometry`
for the meaning of the various symbols.
302 bool;
if True display image
and sources after PSF
is measured;
303 this will be identical to the final image displayed by measure_iter
if measure_iter
is true
305 bool;
if True display image
and sources after final repair
307 bool;
if True display image
and sources after final measurement
310 ConfigClass = CharacterizeImageConfig
311 _DefaultName = "characterizeImage"
313 def __init__(self, refObjLoader=None, schema=None, **kwargs):
314 super().__init__(**kwargs)
317 schema = SourceTable.makeMinimalSchema()
319 self.makeSubtask(
"background")
320 self.makeSubtask(
"installSimplePsf")
321 self.makeSubtask(
"repair")
322 self.makeSubtask(
"measurePsf", schema=self.
schema)
324 if self.config.doMeasurePsf
and self.measurePsf.usesMatches:
325 self.makeSubtask(
"ref_match", refObjLoader=refObjLoader)
327 self.makeSubtask(
'detection', schema=self.
schema)
328 if self.config.doDeblend:
329 self.makeSubtask(
"deblend", schema=self.
schema)
330 self.makeSubtask(
'measurement', schema=self.
schema, algMetadata=self.
algMetadata)
331 if self.config.doApCorr:
332 self.makeSubtask(
'measureApCorr', schema=self.
schema)
333 self.makeSubtask(
'applyApCorr', schema=self.
schema)
334 self.makeSubtask(
'catalogCalculation', schema=self.
schema)
337 self.
schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
341 inputs = butlerQC.get(inputRefs)
342 if 'idGenerator' not in inputs.keys():
343 inputs[
'idGenerator'] = self.config.idGenerator.apply(butlerQC.quantum.dataId)
344 outputs = self.
run(**inputs)
345 butlerQC.put(outputs, outputRefs)
348 def run(self, exposure, exposureIdInfo=None, background=None, idGenerator=None):
349 """Characterize a science image.
351 Peforms the following operations:
352 - Iterate the following config.psfIterations times, or once
if config.doMeasurePsf false:
353 - detect
and measure sources
and estimate PSF (see detectMeasureAndEstimatePsf
for details)
354 - interpolate over cosmic rays
355 - perform final measurement
359 exposure : `lsst.afw.image.ExposureF`
360 Exposure to characterize.
361 exposureIdInfo : `lsst.obs.base.ExposureIdInfo`, optional
362 Exposure ID info. Deprecated
in favor of ``idGenerator``,
and
363 ignored
if that
is provided.
365 Initial model of background already subtracted
from exposure.
367 Object that generates source IDs
and provides RNG seeds.
371 result : `lsst.pipe.base.Struct`
372 Results
as a struct
with attributes:
375 Characterized exposure (`lsst.afw.image.ExposureF`).
383 Another reference to ``exposure``
for compatibility.
385 Another reference to ``background``
for compatibility.
390 Raised
if PSF sigma
is NaN.
394 if not self.config.doMeasurePsf
and not exposure.hasPsf():
395 self.log.info(
"CharacterizeImageTask initialized with 'simple' PSF.")
396 self.installSimplePsf.run(exposure=exposure)
398 if idGenerator
is None:
399 if exposureIdInfo
is not None:
400 idGenerator = IdGenerator._from_exposure_id_info(exposureIdInfo)
407 background = self.background.run(exposure).background
409 psfIterations = self.config.psfIterations
if self.config.doMeasurePsf
else 1
410 for i
in range(psfIterations):
413 idGenerator=idGenerator,
414 background=background,
417 psf = dmeRes.exposure.getPsf()
419 psfAvgPos = psf.getAveragePosition()
420 psfSigma = psf.computeShape(psfAvgPos).getDeterminantRadius()
421 psfDimensions = psf.computeImage(psfAvgPos).getDimensions()
422 medBackground = np.median(dmeRes.background.getImage().getArray())
423 self.log.info(
"iter %s; PSF sigma=%0.2f, dimensions=%s; median background=%0.2f",
424 i + 1, psfSigma, psfDimensions, medBackground)
425 if np.isnan(psfSigma):
426 raise RuntimeError(
"PSF sigma is NaN, cannot continue PSF determination.")
428 self.
display(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
431 self.repair.run(exposure=dmeRes.exposure)
432 self.
display(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
436 self.measurement.run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
437 exposureId=idGenerator.catalog_id)
438 if self.config.doApCorr:
440 apCorrMap = self.measureApCorr.run(
441 exposure=dmeRes.exposure,
442 catalog=dmeRes.sourceCat,
444 except MeasureApCorrError:
448 dmeRes.exposure.info.setApCorrMap(
None)
450 dmeRes.exposure.info.setApCorrMap(apCorrMap)
451 self.applyApCorr.run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
453 self.catalogCalculation.run(dmeRes.sourceCat)
455 self.
display(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
457 return pipeBase.Struct(
458 exposure=dmeRes.exposure,
459 sourceCat=dmeRes.sourceCat,
460 background=dmeRes.background,
461 psfCellSet=dmeRes.psfCellSet,
463 characterized=dmeRes.exposure,
464 backgroundModel=dmeRes.background
469 """Perform one iteration of detect, measure, and estimate PSF.
471 Performs the following operations:
473 - if config.doMeasurePsf
or not exposure.hasPsf():
475 - install a simple PSF model (replacing the existing one,
if need be)
477 - interpolate over cosmic rays
with keepCRs=
True
478 - estimate background
and subtract it
from the exposure
479 - detect, deblend
and measure sources,
and subtract a refined background model;
480 -
if config.doMeasurePsf:
485 exposure : `lsst.afw.image.ExposureF`
486 Exposure to characterize.
488 Object that generates source IDs
and provides RNG seeds.
490 Initial model of background already subtracted
from exposure.
494 result : `lsst.pipe.base.Struct`
495 Results
as a struct
with attributes:
498 Characterized exposure (`lsst.afw.image.ExposureF`).
509 Raised
if there are too many CR pixels.
512 if not exposure.hasPsf()
or (self.config.doMeasurePsf
and self.config.useSimplePsf):
513 self.log.info(
"PSF estimation initialized with 'simple' PSF")
514 self.installSimplePsf.run(exposure=exposure)
517 if self.config.requireCrForPsf:
518 self.repair.run(exposure=exposure, keepCRs=
True)
521 self.repair.run(exposure=exposure, keepCRs=
True)
523 self.log.warning(
"Skipping cosmic ray detection: Too many CR pixels (max %0.f)",
524 self.config.repair.cosmicray.nCrPixelMax)
526 self.
display(
"repair_iter", exposure=exposure)
528 if background
is None:
531 sourceIdFactory = idGenerator.make_table_id_factory()
532 table = SourceTable.make(self.
schema, sourceIdFactory)
535 detRes = self.detection.run(table=table, exposure=exposure, doSmooth=
True)
536 sourceCat = detRes.sources
537 if detRes.background:
538 for bg
in detRes.background:
539 background.append(bg)
541 if self.config.doDeblend:
542 self.deblend.run(exposure=exposure, sources=sourceCat)
544 if not sourceCat.isContiguous():
545 sourceCat = sourceCat.copy(deep=
True)
547 self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=idGenerator.catalog_id)
549 measPsfRes = pipeBase.Struct(cellSet=
None)
550 if self.config.doMeasurePsf:
552 if self.measurePsf.usesMatches:
553 matches = self.ref_match.loadAndMatch(exposure=exposure, sourceCat=sourceCat).matches
556 measPsfRes = self.measurePsf.run(exposure=exposure, sources=sourceCat, matches=matches,
557 expId=idGenerator.catalog_id)
558 self.
display(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
560 return pipeBase.Struct(
563 background=background,
564 psfCellSet=measPsfRes.cellSet,
567 def display(self, itemName, exposure, sourceCat=None):
568 """Display exposure and sources on next frame (for debugging).
573 Name of item in ``debugInfo``.
574 exposure : `lsst.afw.image.ExposureF`
577 Catalog of sources detected on the exposure.
579 val = getDebugFrame(self._display, itemName)
583 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.
adjustQuantum(self, inputs, outputs, label, dataId)
detectMeasureAndEstimatePsf(self, exposure, idGenerator, background)
display(self, itemName, exposure, sourceCat=None)
runQuantum(self, butlerQC, inputRefs, outputRefs)
run(self, exposure, exposureIdInfo=None, background=None, idGenerator=None)