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 selector.doUnresolved =
False
236 selector.flags.good = [
"calib_psf_used"]
237 selector.flags.bad = []
243 "ext_shapeHSM_HsmSourceMoments",
246 "base_CircularApertureFlux",
248 self.
measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
252 raise RuntimeError(
"Must measure PSF to measure aperture correction, "
253 "because flags determined by PSF measurement are used to identify "
254 "sources used to measure aperture correction")
258 """Measure bright sources and use this to estimate background and PSF of
261 Given an exposure with defects repaired (masked
and interpolated over,
262 e.g.
as output by `~lsst.ip.isr.IsrTask`):
263 - detect
and measure bright sources
265 - measure
and subtract background
271 Compatibility parameter. Should always be `
None`.
272 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
273 Reference object loader
if using a catalog-based star-selector.
275 Initial schema
for icSrc catalog.
277 Additional keyword arguments.
282 CharacterizeImageTask has a debug dictionary
with the following keys:
285 int:
if specified, the frame of first debug image displayed (defaults to 1)
287 bool;
if True display image after each repair
in the measure PSF loop
289 bool;
if True display image after each background subtraction
in the measure PSF loop
291 bool;
if True display image
and sources at the end of each iteration of the measure PSF loop
292 See `~lsst.meas.astrom.displayAstrometry`
for the meaning of the various symbols.
294 bool;
if True display image
and sources after PSF
is measured;
295 this will be identical to the final image displayed by measure_iter
if measure_iter
is true
297 bool;
if True display image
and sources after final repair
299 bool;
if True display image
and sources after final measurement
302 ConfigClass = CharacterizeImageConfig
303 _DefaultName = "characterizeImage"
305 def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs):
308 if butler
is not None:
309 warnings.warn(
"The 'butler' parameter is no longer used and can be safely removed.",
310 category=FutureWarning, stacklevel=2)
314 schema = SourceTable.makeMinimalSchema()
316 self.makeSubtask(
"background")
317 self.makeSubtask(
"installSimplePsf")
318 self.makeSubtask(
"repair")
319 self.makeSubtask(
"measurePsf", schema=self.
schema)
321 if self.config.doMeasurePsf
and self.measurePsf.usesMatches:
322 self.makeSubtask(
"ref_match", refObjLoader=refObjLoader)
324 self.makeSubtask(
'detection', schema=self.
schema)
325 if self.config.doDeblend:
326 self.makeSubtask(
"deblend", schema=self.
schema)
327 self.makeSubtask(
'measurement', schema=self.
schema, algMetadata=self.
algMetadata)
328 if self.config.doApCorr:
329 self.makeSubtask(
'measureApCorr', schema=self.
schema)
330 self.makeSubtask(
'applyApCorr', schema=self.
schema)
331 self.makeSubtask(
'catalogCalculation', schema=self.
schema)
332 self.
_initialFrame = getDebugFrame(self._display,
"frame")
or 1
334 self.
schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
338 inputs = butlerQC.get(inputRefs)
339 if 'exposureIdInfo' not in inputs.keys():
340 inputs[
'exposureIdInfo'] = ExposureIdInfo.fromDataId(butlerQC.quantum.dataId,
"visit_detector")
341 outputs = self.
run(**inputs)
342 butlerQC.put(outputs, outputRefs)
345 def run(self, exposure, exposureIdInfo=None, background=None):
346 """Characterize a science image.
348 Peforms the following operations:
349 - Iterate the following config.psfIterations times, or once
if config.doMeasurePsf false:
350 - detect
and measure sources
and estimate PSF (see detectMeasureAndEstimatePsf
for details)
351 - interpolate over cosmic rays
352 - perform final measurement
356 exposure : `lsst.afw.image.ExposureF`
357 Exposure to characterize.
358 exposureIdInfo : `lsst.obs.baseExposureIdInfo`, optional
359 Exposure ID info. If
not provided, returned SourceCatalog IDs will
not
361 background : `lsst.afw.math.BackgroundList`, optional
362 Initial model of background already subtracted
from exposure.
366 result : `lsst.pipe.base.Struct`
367 Results
as a struct
with attributes:
370 Characterized exposure (`lsst.afw.image.ExposureF`).
374 Model of subtracted background (`lsst.afw.math.BackgroundList`).
378 Another reference to ``exposure``
for compatibility.
380 Another reference to ``background``
for compatibility.
385 Raised
if PSF sigma
is NaN.
389 if not self.config.doMeasurePsf
and not exposure.hasPsf():
390 self.log.info(
"CharacterizeImageTask initialized with 'simple' PSF.")
391 self.installSimplePsf.run(exposure=exposure)
393 if exposureIdInfo
is None:
394 exposureIdInfo = ExposureIdInfo()
397 background = self.background.run(exposure).background
399 psfIterations = self.config.psfIterations
if self.config.doMeasurePsf
else 1
400 for i
in range(psfIterations):
403 exposureIdInfo=exposureIdInfo,
404 background=background,
407 psf = dmeRes.exposure.getPsf()
409 psfAvgPos = psf.getAveragePosition()
410 psfSigma = psf.computeShape(psfAvgPos).getDeterminantRadius()
411 psfDimensions = psf.computeImage(psfAvgPos).getDimensions()
412 medBackground = np.median(dmeRes.background.getImage().getArray())
413 self.log.info(
"iter %s; PSF sigma=%0.2f, dimensions=%s; median background=%0.2f",
414 i + 1, psfSigma, psfDimensions, medBackground)
415 if np.isnan(psfSigma):
416 raise RuntimeError(
"PSF sigma is NaN, cannot continue PSF determination.")
418 self.
display(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
421 self.repair.run(exposure=dmeRes.exposure)
422 self.
display(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
426 self.measurement.run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
427 exposureId=exposureIdInfo.expId)
428 if self.config.doApCorr:
429 apCorrMap = self.measureApCorr.run(exposure=dmeRes.exposure, catalog=dmeRes.sourceCat).apCorrMap
430 dmeRes.exposure.getInfo().setApCorrMap(apCorrMap)
431 self.applyApCorr.run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
432 self.catalogCalculation.run(dmeRes.sourceCat)
434 self.
display(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
436 return pipeBase.Struct(
437 exposure=dmeRes.exposure,
438 sourceCat=dmeRes.sourceCat,
439 background=dmeRes.background,
440 psfCellSet=dmeRes.psfCellSet,
442 characterized=dmeRes.exposure,
443 backgroundModel=dmeRes.background
448 """Perform one iteration of detect, measure, and estimate PSF.
450 Performs the following operations:
452 - if config.doMeasurePsf
or not exposure.hasPsf():
454 - install a simple PSF model (replacing the existing one,
if need be)
456 - interpolate over cosmic rays
with keepCRs=
True
457 - estimate background
and subtract it
from the exposure
458 - detect, deblend
and measure sources,
and subtract a refined background model;
459 -
if config.doMeasurePsf:
464 exposure : `lsst.afw.image.ExposureF`
465 Exposure to characterize.
466 exposureIdInfo : `lsst.obs.baseExposureIdInfo`
468 background : `lsst.afw.math.BackgroundList`, optional
469 Initial model of background already subtracted
from exposure.
473 result : `lsst.pipe.base.Struct`
474 Results
as a struct
with attributes:
477 Characterized exposure (`lsst.afw.image.ExposureF`).
481 Model of subtracted background (`lsst.afw.math.BackgroundList`).
488 Raised
if there are too many CR pixels.
491 if not exposure.hasPsf()
or (self.config.doMeasurePsf
and self.config.useSimplePsf):
492 self.log.info(
"PSF estimation initialized with 'simple' PSF")
493 self.installSimplePsf.run(exposure=exposure)
496 if self.config.requireCrForPsf:
497 self.repair.run(exposure=exposure, keepCRs=
True)
500 self.repair.run(exposure=exposure, keepCRs=
True)
502 self.log.warning(
"Skipping cosmic ray detection: Too many CR pixels (max %0.f)",
503 self.config.repair.cosmicray.nCrPixelMax)
505 self.
display(
"repair_iter", exposure=exposure)
507 if background
is None:
508 background = BackgroundList()
510 sourceIdFactory = exposureIdInfo.makeSourceIdFactory()
511 table = SourceTable.make(self.
schema, sourceIdFactory)
514 detRes = self.detection.run(table=table, exposure=exposure, doSmooth=
True)
515 sourceCat = detRes.sources
516 if detRes.background:
517 for bg
in detRes.background:
518 background.append(bg)
520 if self.config.doDeblend:
521 self.deblend.run(exposure=exposure, sources=sourceCat)
523 self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId)
525 measPsfRes = pipeBase.Struct(cellSet=
None)
526 if self.config.doMeasurePsf:
528 if self.measurePsf.usesMatches:
529 matches = self.ref_match.loadAndMatch(exposure=exposure, sourceCat=sourceCat).matches
532 measPsfRes = self.measurePsf.run(exposure=exposure, sources=sourceCat, matches=matches,
533 expId=exposureIdInfo.expId)
534 self.
display(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
536 return pipeBase.Struct(
539 background=background,
540 psfCellSet=measPsfRes.cellSet,
543 def display(self, itemName, exposure, sourceCat=None):
544 """Display exposure and sources on next frame (for debugging).
549 Name of item in ``debugInfo``.
550 exposure : `lsst.afw.image.ExposureF`
553 Catalog of sources detected on the exposure.
555 val = getDebugFrame(self._display, itemName)
559 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)