24 from lsstDebug
import getDebugFrame
37 from lsst.meas.base import SingleFrameMeasurementTask, ApplyApCorrTask, CatalogCalculationTask
39 from .measurePsf
import MeasurePsfTask
40 from .repair
import RepairTask
43 __all__ = [
"CharacterizeImageConfig",
"CharacterizeImageTask"]
47 dimensions=(
"instrument",
"visit",
"detector")):
49 doc=
"Input exposure data",
51 storageClass=
"Exposure",
52 dimensions=[
"instrument",
"exposure",
"detector"],
54 characterized = cT.Output(
55 doc=
"Output characterized data.",
57 storageClass=
"ExposureF",
58 dimensions=[
"instrument",
"visit",
"detector"],
60 sourceCat = cT.Output(
61 doc=
"Output source catalog.",
63 storageClass=
"SourceCatalog",
64 dimensions=[
"instrument",
"visit",
"detector"],
66 backgroundModel = cT.Output(
67 doc=
"Output background model.",
68 name=
"icExpBackground",
69 storageClass=
"Background",
70 dimensions=[
"instrument",
"visit",
"detector"],
72 outputSchema = cT.InitOutput(
73 doc=
"Schema of the catalog produced by CharacterizeImage",
75 storageClass=
"SourceCatalog",
78 def adjustQuantum(self, datasetRefMap: pipeBase.InputQuantizedConnection):
82 except pipeBase.ScalarError
as err:
83 raise pipeBase.ScalarError(
84 f
"CharacterizeImageTask can at present only be run on visits that are associated with "
85 f
"exactly one exposure. Either this is not a valid exposure for this pipeline, or the "
86 f
"snap-combination step you probably want hasn't been configured to run between ISR and "
87 f
"this task (as of this writing, that would be because it hasn't been implemented yet)."
92 pipelineConnections=CharacterizeImageConnections):
94 """!Config for CharacterizeImageTask"""
95 doMeasurePsf = pexConfig.Field(
98 doc=
"Measure PSF? If False then for all subsequent operations use either existing PSF "
99 "model when present, or install simple PSF model when not (see installSimplePsf "
102 doWrite = pexConfig.Field(
105 doc=
"Persist results?",
107 doWriteExposure = pexConfig.Field(
110 doc=
"Write icExp and icExpBackground in addition to icSrc? Ignored if doWrite False.",
112 psfIterations = pexConfig.RangeField(
116 doc=
"Number of iterations of detect sources, measure sources, "
117 "estimate PSF. If useSimplePsf is True then 2 should be plenty; "
118 "otherwise more may be wanted.",
120 background = pexConfig.ConfigurableField(
121 target=SubtractBackgroundTask,
122 doc=
"Configuration for initial background estimation",
124 detection = pexConfig.ConfigurableField(
125 target=SourceDetectionTask,
128 doDeblend = pexConfig.Field(
131 doc=
"Run deblender input exposure"
133 deblend = pexConfig.ConfigurableField(
134 target=SourceDeblendTask,
135 doc=
"Split blended source into their components"
137 measurement = pexConfig.ConfigurableField(
138 target=SingleFrameMeasurementTask,
139 doc=
"Measure sources"
141 doApCorr = pexConfig.Field(
144 doc=
"Run subtasks to measure and apply aperture corrections"
146 measureApCorr = pexConfig.ConfigurableField(
147 target=MeasureApCorrTask,
148 doc=
"Subtask to measure aperture corrections"
150 applyApCorr = pexConfig.ConfigurableField(
151 target=ApplyApCorrTask,
152 doc=
"Subtask to apply aperture corrections"
156 catalogCalculation = pexConfig.ConfigurableField(
157 target=CatalogCalculationTask,
158 doc=
"Subtask to run catalogCalculation plugins on catalog"
160 useSimplePsf = pexConfig.Field(
163 doc=
"Replace the existing PSF model with a simplified version that has the same sigma "
164 "at the start of each PSF determination iteration? Doing so makes PSF determination "
165 "converge more robustly and quickly.",
167 installSimplePsf = pexConfig.ConfigurableField(
168 target=InstallGaussianPsfTask,
169 doc=
"Install a simple PSF model",
171 refObjLoader = pexConfig.ConfigurableField(
172 target=LoadIndexedReferenceObjectsTask,
173 doc=
"reference object loader",
175 ref_match = pexConfig.ConfigurableField(
177 doc=
"Task to load and match reference objects. Only used if measurePsf can use matches. "
178 "Warning: matching will only work well if the initial WCS is accurate enough "
179 "to give good matches (roughly: good to 3 arcsec across the CCD).",
181 measurePsf = pexConfig.ConfigurableField(
182 target=MeasurePsfTask,
185 repair = pexConfig.ConfigurableField(
187 doc=
"Remove cosmic rays",
189 requireCrForPsf = pexConfig.Field(
192 doc=
"Require cosmic ray detection and masking to run successfully before measuring the PSF."
194 checkUnitsParseStrict = pexConfig.Field(
195 doc=
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
205 self.
detection.includeThresholdMultiplier = 10.0
206 self.
detection.doTempLocalBackground =
False
219 "base_CircularApertureFlux",
224 raise RuntimeError(
"Must measure PSF to measure aperture correction, "
225 "because flags determined by PSF measurement are used to identify "
226 "sources used to measure aperture correction")
237 r"""!Measure bright sources and use this to estimate background and PSF of an exposure
239 @anchor CharacterizeImageTask_
241 @section pipe_tasks_characterizeImage_Contents Contents
243 - @ref pipe_tasks_characterizeImage_Purpose
244 - @ref pipe_tasks_characterizeImage_Initialize
245 - @ref pipe_tasks_characterizeImage_IO
246 - @ref pipe_tasks_characterizeImage_Config
247 - @ref pipe_tasks_characterizeImage_Debug
250 @section pipe_tasks_characterizeImage_Purpose Description
252 Given an exposure with defects repaired (masked and interpolated over, e.g. as output by IsrTask):
253 - detect and measure bright sources
255 - measure and subtract background
258 @section pipe_tasks_characterizeImage_Initialize Task initialisation
260 @copydoc \_\_init\_\_
262 @section pipe_tasks_characterizeImage_IO Invoking the Task
264 If you want this task to unpersist inputs or persist outputs, then call
265 the `runDataRef` method (a thin wrapper around the `run` method).
267 If you already have the inputs unpersisted and do not want to persist the output
268 then it is more direct to call the `run` method:
270 @section pipe_tasks_characterizeImage_Config Configuration parameters
272 See @ref CharacterizeImageConfig
274 @section pipe_tasks_characterizeImage_Debug Debug variables
276 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a flag
277 `--debug` to import `debug.py` from your `$PYTHONPATH`; see @ref baseDebug for more about `debug.py`.
279 CharacterizeImageTask has a debug dictionary with the following keys:
282 <dd>int: if specified, the frame of first debug image displayed (defaults to 1)
284 <dd>bool; if True display image after each repair in the measure PSF loop
286 <dd>bool; if True display image after each background subtraction in the measure PSF loop
288 <dd>bool; if True display image and sources at the end of each iteration of the measure PSF loop
289 See @ref lsst.meas.astrom.displayAstrometry for the meaning of the various symbols.
291 <dd>bool; if True display image and sources after PSF is measured;
292 this will be identical to the final image displayed by measure_iter if measure_iter is true
294 <dd>bool; if True display image and sources after final repair
296 <dd>bool; if True display image and sources after final measurement
299 For example, put something like:
303 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
304 if name == "lsst.pipe.tasks.characterizeImage":
311 lsstDebug.Info = DebugInfo
313 into your `debug.py` file and run `calibrateTask.py` with the `--debug` flag.
315 Some subtasks may have their own debug variables; see individual Task documentation.
320 ConfigClass = CharacterizeImageConfig
321 _DefaultName =
"characterizeImage"
322 RunnerClass = pipeBase.ButlerInitializedTaskRunner
325 inputs = butlerQC.get(inputRefs)
326 if 'exposureIdInfo' not in inputs.keys():
328 exposureIdInfo.expId, exposureIdInfo.expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
330 inputs[
'exposureIdInfo'] = exposureIdInfo
331 outputs = self.
run(**inputs)
332 butlerQC.put(outputs, outputRefs)
334 def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs):
335 """!Construct a CharacterizeImageTask
337 @param[in] butler A butler object is passed to the refObjLoader constructor in case
338 it is needed to load catalogs. May be None if a catalog-based star selector is
339 not used, if the reference object loader constructor does not require a butler,
340 or if a reference object loader is passed directly via the refObjLoader argument.
341 @param[in] refObjLoader An instance of LoadReferenceObjectsTasks that supplies an
342 external reference catalog to a catalog-based star selector. May be None if a
343 catalog star selector is not used or the loader can be constructed from the
345 @param[in,out] schema initial schema (an lsst.afw.table.SourceTable), or None
346 @param[in,out] kwargs other keyword arguments for lsst.pipe.base.CmdLineTask
351 schema = SourceTable.makeMinimalSchema()
353 self.makeSubtask(
"background")
354 self.makeSubtask(
"installSimplePsf")
355 self.makeSubtask(
"repair")
356 self.makeSubtask(
"measurePsf", schema=self.
schema)
357 if self.config.doMeasurePsf
and self.measurePsf.usesMatches:
359 self.makeSubtask(
'refObjLoader', butler=butler)
360 refObjLoader = self.refObjLoader
361 self.makeSubtask(
"ref_match", refObjLoader=refObjLoader)
363 self.makeSubtask(
'detection', schema=self.
schema)
364 if self.config.doDeblend:
365 self.makeSubtask(
"deblend", schema=self.
schema)
366 self.makeSubtask(
'measurement', schema=self.
schema, algMetadata=self.
algMetadata)
367 if self.config.doApCorr:
368 self.makeSubtask(
'measureApCorr', schema=self.
schema)
369 self.makeSubtask(
'applyApCorr', schema=self.
schema)
370 self.makeSubtask(
'catalogCalculation', schema=self.
schema)
373 self.
schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
378 outputCatSchema.getTable().setMetadata(self.
algMetadata)
379 return {
'outputSchema': outputCatSchema}
382 def runDataRef(self, dataRef, exposure=None, background=None, doUnpersist=True):
383 """!Characterize a science image and, if wanted, persist the results
385 This simply unpacks the exposure and passes it to the characterize method to do the work.
387 @param[in] dataRef: butler data reference for science exposure
388 @param[in,out] exposure exposure to characterize (an lsst.afw.image.ExposureF or similar).
389 If None then unpersist from "postISRCCD".
390 The following changes are made, depending on the config:
391 - set psf to the measured PSF
392 - set apCorrMap to the measured aperture correction
393 - subtract background
394 - interpolate over cosmic rays
395 - update detection and cosmic ray mask planes
396 @param[in,out] background initial model of background already subtracted from exposure
397 (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted,
398 which is typical for image characterization.
399 A refined background model is output.
400 @param[in] doUnpersist if True the exposure is read from the repository
401 and the exposure and background arguments must be None;
402 if False the exposure must be provided.
403 True is intended for running as a command-line task, False for running as a subtask
405 @return same data as the characterize method
408 self.log.
info(
"Processing %s" % (dataRef.dataId))
411 if exposure
is not None or background
is not None:
412 raise RuntimeError(
"doUnpersist true; exposure and background must be None")
413 exposure = dataRef.get(
"postISRCCD", immediate=
True)
414 elif exposure
is None:
415 raise RuntimeError(
"doUnpersist false; exposure must be provided")
417 exposureIdInfo = dataRef.get(
"expIdInfo")
421 exposureIdInfo=exposureIdInfo,
422 background=background,
425 if self.config.doWrite:
426 dataRef.put(charRes.sourceCat,
"icSrc")
427 if self.config.doWriteExposure:
428 dataRef.put(charRes.exposure,
"icExp")
429 dataRef.put(charRes.background,
"icExpBackground")
434 def run(self, exposure, exposureIdInfo=None, background=None):
435 """!Characterize a science image
437 Peforms the following operations:
438 - Iterate the following config.psfIterations times, or once if config.doMeasurePsf false:
439 - detect and measure sources and estimate PSF (see detectMeasureAndEstimatePsf for details)
440 - interpolate over cosmic rays
441 - perform final measurement
443 @param[in,out] exposure exposure to characterize (an lsst.afw.image.ExposureF or similar).
444 The following changes are made:
447 - update detection and cosmic ray mask planes
448 - subtract background and interpolate over cosmic rays
449 @param[in] exposureIdInfo ID info for exposure (an lsst.obs.base.ExposureIdInfo).
450 If not provided, returned SourceCatalog IDs will not be globally unique.
451 @param[in,out] background initial model of background already subtracted from exposure
452 (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted,
453 which is typical for image characterization.
455 @return pipe_base Struct containing these fields, all from the final iteration
456 of detectMeasureAndEstimatePsf:
457 - exposure: characterized exposure; image is repaired by interpolating over cosmic rays,
458 mask is updated accordingly, and the PSF model is set
459 - sourceCat: detected sources (an lsst.afw.table.SourceCatalog)
460 - background: model of background subtracted from exposure (an lsst.afw.math.BackgroundList)
461 - psfCellSet: spatial cells of PSF candidates (an lsst.afw.math.SpatialCellSet)
465 if not self.config.doMeasurePsf
and not exposure.hasPsf():
466 self.log.
warn(
"Source catalog detected and measured with placeholder or default PSF")
467 self.installSimplePsf.
run(exposure=exposure)
469 if exposureIdInfo
is None:
473 background = self.background.
run(exposure).background
475 psfIterations = self.config.psfIterations
if self.config.doMeasurePsf
else 1
476 for i
in range(psfIterations):
479 exposureIdInfo=exposureIdInfo,
480 background=background,
483 psf = dmeRes.exposure.getPsf()
484 psfSigma = psf.computeShape().getDeterminantRadius()
485 psfDimensions = psf.computeImage().getDimensions()
486 medBackground = np.median(dmeRes.background.getImage().getArray())
487 self.log.
info(
"iter %s; PSF sigma=%0.2f, dimensions=%s; median background=%0.2f" %
488 (i + 1, psfSigma, psfDimensions, medBackground))
490 self.
display(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
493 self.repair.
run(exposure=dmeRes.exposure)
494 self.
display(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
498 self.measurement.
run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
499 exposureId=exposureIdInfo.expId)
500 if self.config.doApCorr:
501 apCorrMap = self.measureApCorr.
run(exposure=dmeRes.exposure, catalog=dmeRes.sourceCat).apCorrMap
502 dmeRes.exposure.getInfo().setApCorrMap(apCorrMap)
503 self.applyApCorr.
run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
504 self.catalogCalculation.
run(dmeRes.sourceCat)
506 self.
display(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
508 return pipeBase.Struct(
509 exposure=dmeRes.exposure,
510 sourceCat=dmeRes.sourceCat,
511 background=dmeRes.background,
512 psfCellSet=dmeRes.psfCellSet,
514 characterized=dmeRes.exposure,
515 backgroundModel=dmeRes.background
520 """!Perform one iteration of detect, measure and estimate PSF
522 Performs the following operations:
523 - if config.doMeasurePsf or not exposure.hasPsf():
524 - install a simple PSF model (replacing the existing one, if need be)
525 - interpolate over cosmic rays with keepCRs=True
526 - estimate background and subtract it from the exposure
527 - detect, deblend and measure sources, and subtract a refined background model;
528 - if config.doMeasurePsf:
531 @param[in,out] exposure exposure to characterize (an lsst.afw.image.ExposureF or similar)
532 The following changes are made:
534 - update detection and cosmic ray mask planes
535 - subtract background
536 @param[in] exposureIdInfo ID info for exposure (an lsst.obs_base.ExposureIdInfo)
537 @param[in,out] background initial model of background already subtracted from exposure
538 (an lsst.afw.math.BackgroundList).
540 @return pipe_base Struct containing these fields, all from the final iteration
541 of detect sources, measure sources and estimate PSF:
542 - exposure characterized exposure; image is repaired by interpolating over cosmic rays,
543 mask is updated accordingly, and the PSF model is set
544 - sourceCat detected sources (an lsst.afw.table.SourceCatalog)
545 - background model of background subtracted from exposure (an lsst.afw.math.BackgroundList)
546 - psfCellSet spatial cells of PSF candidates (an lsst.afw.math.SpatialCellSet)
549 if not exposure.hasPsf()
or (self.config.doMeasurePsf
and self.config.useSimplePsf):
550 self.log.
warn(
"Source catalog detected and measured with placeholder or default PSF")
551 self.installSimplePsf.
run(exposure=exposure)
554 if self.config.requireCrForPsf:
555 self.repair.
run(exposure=exposure, keepCRs=
True)
558 self.repair.
run(exposure=exposure, keepCRs=
True)
560 self.log.
warn(
"Skipping cosmic ray detection: Too many CR pixels (max %0.f)" %
561 self.config.repair.cosmicray.nCrPixelMax)
563 self.
display(
"repair_iter", exposure=exposure)
565 if background
is None:
568 sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId, exposureIdInfo.unusedBits)
569 table = SourceTable.make(self.
schema, sourceIdFactory)
572 detRes = self.detection.
run(table=table, exposure=exposure, doSmooth=
True)
573 sourceCat = detRes.sources
574 if detRes.fpSets.background:
575 for bg
in detRes.fpSets.background:
576 background.append(bg)
578 if self.config.doDeblend:
579 self.deblend.
run(exposure=exposure, sources=sourceCat)
581 self.measurement.
run(measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId)
583 measPsfRes = pipeBase.Struct(cellSet=
None)
584 if self.config.doMeasurePsf:
585 if self.measurePsf.usesMatches:
586 matches = self.ref_match.loadAndMatch(exposure=exposure, sourceCat=sourceCat).matches
589 measPsfRes = self.measurePsf.
run(exposure=exposure, sources=sourceCat, matches=matches,
590 expId=exposureIdInfo.expId)
591 self.
display(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
593 return pipeBase.Struct(
596 background=background,
597 psfCellSet=measPsfRes.cellSet,
601 """Return a dict of empty catalogs for each catalog dataset produced by this task.
605 return {
"icSrc": sourceCat}
607 def display(self, itemName, exposure, sourceCat=None):
608 """Display exposure and sources on next frame, if display of itemName has been requested
610 @param[in] itemName name of item in debugInfo
611 @param[in] exposure exposure to display
612 @param[in] sourceCat source catalog to display