22__all__ = [
"CharacterizeImageConfig",
"CharacterizeImageTask"]
27from lsstDebug
import getDebugFrame
32import lsst.pipe.base.connectionTypes
as cT
36 SubtractBackgroundTask,
44from lsst.obs.base
import ExposureIdInfo
45from lsst.meas.base import SingleFrameMeasurementTask, ApplyApCorrTask, CatalogCalculationTask
47import lsst.meas.extensions.shapeHSM
48from .measurePsf
import MeasurePsfTask
49from .repair
import RepairTask
50from .computeExposureSummaryStats
import ComputeExposureSummaryStatsTask
52from lsst.utils.timer
import timeMethod
56 dimensions=(
"instrument",
"visit",
"detector")):
58 doc=
"Input exposure data",
60 storageClass=
"Exposure",
61 dimensions=[
"instrument",
"exposure",
"detector"],
63 characterized = cT.Output(
64 doc=
"Output characterized data.",
66 storageClass=
"ExposureF",
67 dimensions=[
"instrument",
"visit",
"detector"],
69 sourceCat = cT.Output(
70 doc=
"Output source catalog.",
72 storageClass=
"SourceCatalog",
73 dimensions=[
"instrument",
"visit",
"detector"],
75 backgroundModel = cT.Output(
76 doc=
"Output background model.",
77 name=
"icExpBackground",
78 storageClass=
"Background",
79 dimensions=[
"instrument",
"visit",
"detector"],
81 outputSchema = cT.InitOutput(
82 doc=
"Schema of the catalog produced by CharacterizeImage",
84 storageClass=
"SourceCatalog",
91 except pipeBase.ScalarError
as err:
92 raise pipeBase.ScalarError(
93 "CharacterizeImageTask can at present only be run on visits that are associated with "
94 "exactly one exposure. Either this is not a valid exposure for this pipeline, or the "
95 "snap-combination step you probably want hasn't been configured to run between ISR and "
96 "this task (as of this writing, that would be because it hasn't been implemented yet)."
101 pipelineConnections=CharacterizeImageConnections):
102 """Config for CharacterizeImageTask."""
104 doMeasurePsf = pexConfig.Field(
107 doc=
"Measure PSF? If False then for all subsequent operations use either existing PSF "
108 "model when present, or install simple PSF model when not (see installSimplePsf "
111 doWrite = pexConfig.Field(
114 doc=
"Persist results?",
116 doWriteExposure = pexConfig.Field(
119 doc=
"Write icExp and icExpBackground in addition to icSrc? Ignored if doWrite False.",
121 psfIterations = pexConfig.RangeField(
125 doc=
"Number of iterations of detect sources, measure sources, "
126 "estimate PSF. If useSimplePsf is True then 2 should be plenty; "
127 "otherwise more may be wanted.",
129 background = pexConfig.ConfigurableField(
130 target=SubtractBackgroundTask,
131 doc=
"Configuration for initial background estimation",
133 detection = pexConfig.ConfigurableField(
134 target=SourceDetectionTask,
137 doDeblend = pexConfig.Field(
140 doc=
"Run deblender input exposure"
142 deblend = pexConfig.ConfigurableField(
143 target=SourceDeblendTask,
144 doc=
"Split blended source into their components"
146 measurement = pexConfig.ConfigurableField(
147 target=SingleFrameMeasurementTask,
148 doc=
"Measure sources"
150 doApCorr = pexConfig.Field(
153 doc=
"Run subtasks to measure and apply aperture corrections"
155 measureApCorr = pexConfig.ConfigurableField(
156 target=MeasureApCorrTask,
157 doc=
"Subtask to measure aperture corrections"
159 applyApCorr = pexConfig.ConfigurableField(
160 target=ApplyApCorrTask,
161 doc=
"Subtask to apply aperture corrections"
165 catalogCalculation = pexConfig.ConfigurableField(
166 target=CatalogCalculationTask,
167 doc=
"Subtask to run catalogCalculation plugins on catalog"
169 doComputeSummaryStats = pexConfig.Field(
172 doc=
"Run subtask to measure exposure summary statistics",
173 deprecated=(
"This subtask has been moved to CalibrateTask "
176 computeSummaryStats = pexConfig.ConfigurableField(
177 target=ComputeExposureSummaryStatsTask,
178 doc=
"Subtask to run computeSummaryStats on exposure",
179 deprecated=(
"This subtask has been moved to CalibrateTask "
182 useSimplePsf = pexConfig.Field(
185 doc=
"Replace the existing PSF model with a simplified version that has the same sigma "
186 "at the start of each PSF determination iteration? Doing so makes PSF determination "
187 "converge more robustly and quickly.",
189 installSimplePsf = pexConfig.ConfigurableField(
190 target=InstallGaussianPsfTask,
191 doc=
"Install a simple PSF model",
193 refObjLoader = pexConfig.ConfigField(
194 dtype=LoadReferenceObjectsConfig,
195 deprecated=
"This field does nothing. Will be removed after v24 (see DM-34768).",
196 doc=
"reference object loader",
198 ref_match = pexConfig.ConfigurableField(
200 deprecated=
"This field was never usable. Will be removed after v24 (see DM-34768).",
201 doc=
"Task to load and match reference objects. Only used if measurePsf can use matches. "
202 "Warning: matching will only work well if the initial WCS is accurate enough "
203 "to give good matches (roughly: good to 3 arcsec across the CCD).",
205 measurePsf = pexConfig.ConfigurableField(
206 target=MeasurePsfTask,
209 repair = pexConfig.ConfigurableField(
211 doc=
"Remove cosmic rays",
213 requireCrForPsf = pexConfig.Field(
216 doc=
"Require cosmic ray detection and masking to run successfully before measuring the PSF."
218 checkUnitsParseStrict = pexConfig.Field(
219 doc=
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
229 self.
detection.includeThresholdMultiplier = 10.0
230 self.
detection.doTempLocalBackground =
False
240 selector.doUnresolved =
False
241 selector.flags.good = [
"calib_psf_used"]
242 selector.flags.bad = []
248 "ext_shapeHSM_HsmSourceMoments",
251 "base_CircularApertureFlux",
253 self.
measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
257 raise RuntimeError(
"Must measure PSF to measure aperture correction, "
258 "because flags determined by PSF measurement are used to identify "
259 "sources used to measure aperture correction")
263 """Measure bright sources and use this to estimate background and PSF of
266 Given an exposure with defects repaired (masked
and interpolated over,
267 e.g.
as output by `~lsst.ip.isr.IsrTask`):
268 - detect
and measure bright sources
270 - measure
and subtract background
276 Compatibility parameter. Should always be `
None`.
277 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
278 Reference object loader
if using a catalog-based star-selector.
280 Initial schema
for icSrc catalog.
282 Additional keyword arguments.
287 CharacterizeImageTask has a debug dictionary
with the following keys:
290 int:
if specified, the frame of first debug image displayed (defaults to 1)
292 bool;
if True display image after each repair
in the measure PSF loop
294 bool;
if True display image after each background subtraction
in the measure PSF loop
296 bool;
if True display image
and sources at the end of each iteration of the measure PSF loop
297 See `~lsst.meas.astrom.displayAstrometry`
for the meaning of the various symbols.
299 bool;
if True display image
and sources after PSF
is measured;
300 this will be identical to the final image displayed by measure_iter
if measure_iter
is true
302 bool;
if True display image
and sources after final repair
304 bool;
if True display image
and sources after final measurement
307 ConfigClass = CharacterizeImageConfig
308 _DefaultName = "characterizeImage"
310 def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs):
313 if butler
is not None:
314 warnings.warn(
"The 'butler' parameter is no longer used and can be safely removed.",
315 category=FutureWarning, stacklevel=2)
319 schema = SourceTable.makeMinimalSchema()
321 self.makeSubtask(
"background")
322 self.makeSubtask(
"installSimplePsf")
323 self.makeSubtask(
"repair")
324 self.makeSubtask(
"measurePsf", schema=self.
schema)
326 if self.config.doMeasurePsf
and self.measurePsf.usesMatches:
327 self.makeSubtask(
"ref_match", refObjLoader=refObjLoader)
329 self.makeSubtask(
'detection', schema=self.
schema)
330 if self.config.doDeblend:
331 self.makeSubtask(
"deblend", schema=self.
schema)
332 self.makeSubtask(
'measurement', schema=self.
schema, algMetadata=self.
algMetadata)
333 if self.config.doApCorr:
334 self.makeSubtask(
'measureApCorr', schema=self.
schema)
335 self.makeSubtask(
'applyApCorr', schema=self.
schema)
336 self.makeSubtask(
'catalogCalculation', schema=self.
schema)
337 self.
_initialFrame = getDebugFrame(self._display,
"frame")
or 1
339 self.
schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
343 inputs = butlerQC.get(inputRefs)
344 if 'exposureIdInfo' not in inputs.keys():
345 inputs[
'exposureIdInfo'] = ExposureIdInfo.fromDataId(butlerQC.quantum.dataId,
"visit_detector")
346 outputs = self.
run(**inputs)
347 butlerQC.put(outputs, outputRefs)
350 def run(self, exposure, exposureIdInfo=None, background=None):
351 """Characterize a science image.
353 Peforms the following operations:
354 - Iterate the following config.psfIterations times, or once
if config.doMeasurePsf false:
355 - detect
and measure sources
and estimate PSF (see detectMeasureAndEstimatePsf
for details)
356 - interpolate over cosmic rays
357 - perform final measurement
361 exposure : `lsst.afw.image.ExposureF`
362 Exposure to characterize.
363 exposureIdInfo : `lsst.obs.baseExposureIdInfo`, optional
364 Exposure ID info. If
not provided, returned SourceCatalog IDs will
not
366 background : `lsst.afw.math.BackgroundList`, optional
367 Initial model of background already subtracted
from exposure.
371 result : `lsst.pipe.base.Struct`
372 Results
as a struct
with attributes:
375 Characterized exposure (`lsst.afw.image.ExposureF`).
379 Model of subtracted background (`lsst.afw.math.BackgroundList`).
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 exposureIdInfo
is None:
399 exposureIdInfo = ExposureIdInfo()
402 background = self.background.run(exposure).background
404 psfIterations = self.config.psfIterations
if self.config.doMeasurePsf
else 1
405 for i
in range(psfIterations):
408 exposureIdInfo=exposureIdInfo,
409 background=background,
412 psf = dmeRes.exposure.getPsf()
414 psfAvgPos = psf.getAveragePosition()
415 psfSigma = psf.computeShape(psfAvgPos).getDeterminantRadius()
416 psfDimensions = psf.computeImage(psfAvgPos).getDimensions()
417 medBackground = np.median(dmeRes.background.getImage().getArray())
418 self.log.info(
"iter %s; PSF sigma=%0.2f, dimensions=%s; median background=%0.2f",
419 i + 1, psfSigma, psfDimensions, medBackground)
420 if np.isnan(psfSigma):
421 raise RuntimeError(
"PSF sigma is NaN, cannot continue PSF determination.")
423 self.
display(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
426 self.repair.run(exposure=dmeRes.exposure)
427 self.
display(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
431 self.measurement.run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
432 exposureId=exposureIdInfo.expId)
433 if self.config.doApCorr:
435 apCorrMap = self.measureApCorr.run(
436 exposure=dmeRes.exposure,
437 catalog=dmeRes.sourceCat,
439 except MeasureApCorrError:
443 dmeRes.exposure.info.setApCorrMap(
None)
445 dmeRes.exposure.info.setApCorrMap(apCorrMap)
446 self.applyApCorr.run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
448 self.catalogCalculation.run(dmeRes.sourceCat)
450 self.
display(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
452 return pipeBase.Struct(
453 exposure=dmeRes.exposure,
454 sourceCat=dmeRes.sourceCat,
455 background=dmeRes.background,
456 psfCellSet=dmeRes.psfCellSet,
458 characterized=dmeRes.exposure,
459 backgroundModel=dmeRes.background
464 """Perform one iteration of detect, measure, and estimate PSF.
466 Performs the following operations:
468 - if config.doMeasurePsf
or not exposure.hasPsf():
470 - install a simple PSF model (replacing the existing one,
if need be)
472 - interpolate over cosmic rays
with keepCRs=
True
473 - estimate background
and subtract it
from the exposure
474 - detect, deblend
and measure sources,
and subtract a refined background model;
475 -
if config.doMeasurePsf:
480 exposure : `lsst.afw.image.ExposureF`
481 Exposure to characterize.
482 exposureIdInfo : `lsst.obs.baseExposureIdInfo`
484 background : `lsst.afw.math.BackgroundList`, optional
485 Initial model of background already subtracted
from exposure.
489 result : `lsst.pipe.base.Struct`
490 Results
as a struct
with attributes:
493 Characterized exposure (`lsst.afw.image.ExposureF`).
497 Model of subtracted background (`lsst.afw.math.BackgroundList`).
504 Raised
if there are too many CR pixels.
507 if not exposure.hasPsf()
or (self.config.doMeasurePsf
and self.config.useSimplePsf):
508 self.log.info(
"PSF estimation initialized with 'simple' PSF")
509 self.installSimplePsf.run(exposure=exposure)
512 if self.config.requireCrForPsf:
513 self.repair.run(exposure=exposure, keepCRs=
True)
516 self.repair.run(exposure=exposure, keepCRs=
True)
518 self.log.warning(
"Skipping cosmic ray detection: Too many CR pixels (max %0.f)",
519 self.config.repair.cosmicray.nCrPixelMax)
521 self.
display(
"repair_iter", exposure=exposure)
523 if background
is None:
524 background = BackgroundList()
526 sourceIdFactory = exposureIdInfo.makeSourceIdFactory()
527 table = SourceTable.make(self.
schema, sourceIdFactory)
530 detRes = self.detection.run(table=table, exposure=exposure, doSmooth=
True)
531 sourceCat = detRes.sources
532 if detRes.background:
533 for bg
in detRes.background:
534 background.append(bg)
536 if self.config.doDeblend:
537 self.deblend.run(exposure=exposure, sources=sourceCat)
539 self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId)
541 measPsfRes = pipeBase.Struct(cellSet=
None)
542 if self.config.doMeasurePsf:
544 if self.measurePsf.usesMatches:
545 matches = self.ref_match.loadAndMatch(exposure=exposure, sourceCat=sourceCat).matches
548 measPsfRes = self.measurePsf.run(exposure=exposure, sources=sourceCat, matches=matches,
549 expId=exposureIdInfo.expId)
550 self.
display(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
552 return pipeBase.Struct(
555 background=background,
556 psfCellSet=measPsfRes.cellSet,
559 def display(self, itemName, exposure, sourceCat=None):
560 """Display exposure and sources on next frame (for debugging).
565 Name of item in ``debugInfo``.
566 exposure : `lsst.afw.image.ExposureF`
569 Catalog of sources detected on the exposure.
571 val = getDebugFrame(self._display, itemName)
575 displayAstrometry(exposure=exposure, sourceCat=sourceCat, frame=self.
_frame, pause=
False)
def adjustQuantum(self, inputs, outputs, label, dataId)
def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs)
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)