25from lsstDebug
import getDebugFrame
30import lsst.pipe.base.connectionTypes
as cT
37from lsst.obs.base
import ExposureIdInfo
38from lsst.meas.base import SingleFrameMeasurementTask, ApplyApCorrTask, CatalogCalculationTask
40import lsst.meas.extensions.shapeHSM
41from .measurePsf
import MeasurePsfTask
42from .repair
import RepairTask
43from .computeExposureSummaryStats
import ComputeExposureSummaryStatsTask
45from lsst.utils.timer
import timeMethod
47__all__ = [
"CharacterizeImageConfig",
"CharacterizeImageTask"]
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):
98 """!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 an exposure.
255 Compatibility parameter. Should always be `
None`.
256 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
257 Reference object loader
if using a catalog-based star-selector.
259 Initial schema
for icSrc catalog.
262 ConfigClass = CharacterizeImageConfig
263 _DefaultName = "characterizeImage"
265 def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs):
268 if butler
is not None:
269 warnings.warn(
"The 'butler' parameter is no longer used and can be safely removed.",
270 category=FutureWarning, stacklevel=2)
274 schema = SourceTable.makeMinimalSchema()
276 self.makeSubtask(
"background")
277 self.makeSubtask(
"installSimplePsf")
278 self.makeSubtask(
"repair")
279 self.makeSubtask(
"measurePsf", schema=self.
schema)
281 if self.config.doMeasurePsf
and self.measurePsf.usesMatches:
282 self.makeSubtask(
"ref_match", refObjLoader=refObjLoader)
284 self.makeSubtask(
'detection', schema=self.
schema)
285 if self.config.doDeblend:
286 self.makeSubtask(
"deblend", schema=self.
schema)
287 self.makeSubtask(
'measurement', schema=self.
schema, algMetadata=self.
algMetadata)
288 if self.config.doApCorr:
289 self.makeSubtask(
'measureApCorr', schema=self.
schema)
290 self.makeSubtask(
'applyApCorr', schema=self.
schema)
291 self.makeSubtask(
'catalogCalculation', schema=self.
schema)
292 self.
_initialFrame = getDebugFrame(self._display,
"frame")
or 1
294 self.
schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
298 inputs = butlerQC.get(inputRefs)
299 if 'exposureIdInfo' not in inputs.keys():
300 inputs[
'exposureIdInfo'] = ExposureIdInfo.fromDataId(butlerQC.quantum.dataId,
"visit_detector")
301 outputs = self.
run(**inputs)
302 butlerQC.put(outputs, outputRefs)
305 def run(self, exposure, exposureIdInfo=None, background=None):
306 """Characterize a science image.
308 Peforms the following operations:
309 - Iterate the following config.psfIterations times, or once
if config.doMeasurePsf false:
310 - detect
and measure sources
and estimate PSF (see detectMeasureAndEstimatePsf
for details)
311 - interpolate over cosmic rays
312 - perform final measurement
316 exposure : `lsst.afw.image.ExposureF`
317 Exposure to characterize.
318 exposureIdInfo : `lsst.obs.baseExposureIdInfo`, optional
319 Exposure ID info. If
not provided, returned SourceCatalog IDs will
not
321 background : `lsst.afw.math.BackgroundList`, optional
322 Initial model of background already subtracted
from exposure.
326 result : `lsst.pipe.base.Struct`
327 Result structure
with the following attributes:
330 Characterized exposure (`lsst.afw.image.ExposureF`).
334 Model of subtracted background (`lsst.afw.math.BackgroundList`).
338 Another reference to ``exposure``
for compatibility.
340 Another reference to ``background``
for compatibility.
344 if not self.config.doMeasurePsf
and not exposure.hasPsf():
345 self.log.info(
"CharacterizeImageTask initialized with 'simple' PSF.")
346 self.installSimplePsf.run(exposure=exposure)
348 if exposureIdInfo
is None:
349 exposureIdInfo = ExposureIdInfo()
352 background = self.background.run(exposure).background
354 psfIterations = self.config.psfIterations
if self.config.doMeasurePsf
else 1
355 for i
in range(psfIterations):
358 exposureIdInfo=exposureIdInfo,
359 background=background,
362 psf = dmeRes.exposure.getPsf()
364 psfAvgPos = psf.getAveragePosition()
365 psfSigma = psf.computeShape(psfAvgPos).getDeterminantRadius()
366 psfDimensions = psf.computeImage(psfAvgPos).getDimensions()
367 medBackground = np.median(dmeRes.background.getImage().getArray())
368 self.log.info(
"iter %s; PSF sigma=%0.2f, dimensions=%s; median background=%0.2f",
369 i + 1, psfSigma, psfDimensions, medBackground)
370 if np.isnan(psfSigma):
371 raise RuntimeError(
"PSF sigma is NaN, cannot continue PSF determination.")
373 self.
display(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
376 self.repair.run(exposure=dmeRes.exposure)
377 self.
display(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
381 self.measurement.run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
382 exposureId=exposureIdInfo.expId)
383 if self.config.doApCorr:
384 apCorrMap = self.measureApCorr.run(exposure=dmeRes.exposure, catalog=dmeRes.sourceCat).apCorrMap
385 dmeRes.exposure.getInfo().setApCorrMap(apCorrMap)
386 self.applyApCorr.run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
387 self.catalogCalculation.run(dmeRes.sourceCat)
389 self.
display(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
391 return pipeBase.Struct(
392 exposure=dmeRes.exposure,
393 sourceCat=dmeRes.sourceCat,
394 background=dmeRes.background,
395 psfCellSet=dmeRes.psfCellSet,
397 characterized=dmeRes.exposure,
398 backgroundModel=dmeRes.background
403 """Perform one iteration of detect, measure, and estimate PSF.
405 Performs the following operations:
406 - if config.doMeasurePsf
or not exposure.hasPsf():
407 - install a simple PSF model (replacing the existing one,
if need be)
408 - interpolate over cosmic rays
with keepCRs=
True
409 - estimate background
and subtract it
from the exposure
410 - detect, deblend
and measure sources,
and subtract a refined background model;
411 -
if config.doMeasurePsf:
416 exposure : `lsst.afw.image.ExposureF`
417 Exposure to characterize.
418 exposureIdInfo : `lsst.obs.baseExposureIdInfo`
420 background : `lsst.afw.math.BackgroundList`, optional
421 Initial model of background already subtracted
from exposure.
425 result : `lsst.pipe.base.Struct`
426 Result structure
with the following attributes:
429 Characterized exposure (`lsst.afw.image.ExposureF`).
433 Model of subtracted background (`lsst.afw.math.BackgroundList`).
438 if not exposure.hasPsf()
or (self.config.doMeasurePsf
and self.config.useSimplePsf):
439 self.log.info(
"PSF estimation initialized with 'simple' PSF")
440 self.installSimplePsf.run(exposure=exposure)
443 if self.config.requireCrForPsf:
444 self.repair.run(exposure=exposure, keepCRs=
True)
447 self.repair.run(exposure=exposure, keepCRs=
True)
449 self.log.warning(
"Skipping cosmic ray detection: Too many CR pixels (max %0.f)",
450 self.config.repair.cosmicray.nCrPixelMax)
452 self.
display(
"repair_iter", exposure=exposure)
454 if background
is None:
455 background = BackgroundList()
457 sourceIdFactory = exposureIdInfo.makeSourceIdFactory()
458 table = SourceTable.make(self.
schema, sourceIdFactory)
461 detRes = self.detection.run(table=table, exposure=exposure, doSmooth=
True)
462 sourceCat = detRes.sources
463 if detRes.fpSets.background:
464 for bg
in detRes.fpSets.background:
465 background.append(bg)
467 if self.config.doDeblend:
468 self.deblend.run(exposure=exposure, sources=sourceCat)
470 self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId)
472 measPsfRes = pipeBase.Struct(cellSet=
None)
473 if self.config.doMeasurePsf:
475 if self.measurePsf.usesMatches:
476 matches = self.ref_match.loadAndMatch(exposure=exposure, sourceCat=sourceCat).matches
479 measPsfRes = self.measurePsf.run(exposure=exposure, sources=sourceCat, matches=matches,
480 expId=exposureIdInfo.expId)
481 self.
display(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
483 return pipeBase.Struct(
486 background=background,
487 psfCellSet=measPsfRes.cellSet,
491 """Return a dict of empty catalogs for each catalog dataset produced by this task.
493 sourceCat = SourceCatalog(self.schema)
495 return {
"icSrc": sourceCat}
497 def display(self, itemName, exposure, sourceCat=None):
498 """Display exposure and sources on next frame (for debugging).
503 Name of item in ``debugInfo``.
504 exposure : `lsst.afw.image.ExposureF`
509 val = getDebugFrame(self._display, itemName)
513 displayAstrometry(exposure=exposure, sourceCat=sourceCat, frame=self.
_frame, pause=
False)
Config for CharacterizeImageTask.
def adjustQuantum(self, inputs, outputs, label, dataId)
def getSchemaCatalogs(self)
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)