24 from lsstDebug
import getDebugFrame
36 from lsst.obs.base
import ExposureIdInfo
37 from lsst.meas.base import SingleFrameMeasurementTask, ApplyApCorrTask, CatalogCalculationTask
39 from .measurePsf
import MeasurePsfTask
40 from .repair
import RepairTask
41 from .computeExposureSummaryStats
import ComputeExposureSummaryStatsTask
44 __all__ = [
"CharacterizeImageConfig",
"CharacterizeImageTask"]
48 dimensions=(
"instrument",
"visit",
"detector")):
50 doc=
"Input exposure data",
52 storageClass=
"Exposure",
53 dimensions=[
"instrument",
"exposure",
"detector"],
55 characterized = cT.Output(
56 doc=
"Output characterized data.",
58 storageClass=
"ExposureF",
59 dimensions=[
"instrument",
"visit",
"detector"],
61 sourceCat = cT.Output(
62 doc=
"Output source catalog.",
64 storageClass=
"SourceCatalog",
65 dimensions=[
"instrument",
"visit",
"detector"],
67 backgroundModel = cT.Output(
68 doc=
"Output background model.",
69 name=
"icExpBackground",
70 storageClass=
"Background",
71 dimensions=[
"instrument",
"visit",
"detector"],
73 outputSchema = cT.InitOutput(
74 doc=
"Schema of the catalog produced by CharacterizeImage",
76 storageClass=
"SourceCatalog",
79 def adjustQuantum(self, datasetRefMap: pipeBase.InputQuantizedConnection):
83 except pipeBase.ScalarError
as err:
84 raise pipeBase.ScalarError(
85 "CharacterizeImageTask can at present only be run on visits that are associated with "
86 "exactly one exposure. Either this is not a valid exposure for this pipeline, or the "
87 "snap-combination step you probably want hasn't been configured to run between ISR and "
88 "this task (as of this writing, that would be because it hasn't been implemented yet)."
93 pipelineConnections=CharacterizeImageConnections):
95 """!Config for CharacterizeImageTask"""
96 doMeasurePsf = pexConfig.Field(
99 doc=
"Measure PSF? If False then for all subsequent operations use either existing PSF "
100 "model when present, or install simple PSF model when not (see installSimplePsf "
103 doWrite = pexConfig.Field(
106 doc=
"Persist results?",
108 doWriteExposure = pexConfig.Field(
111 doc=
"Write icExp and icExpBackground in addition to icSrc? Ignored if doWrite False.",
113 psfIterations = pexConfig.RangeField(
117 doc=
"Number of iterations of detect sources, measure sources, "
118 "estimate PSF. If useSimplePsf is True then 2 should be plenty; "
119 "otherwise more may be wanted.",
121 background = pexConfig.ConfigurableField(
122 target=SubtractBackgroundTask,
123 doc=
"Configuration for initial background estimation",
125 detection = pexConfig.ConfigurableField(
126 target=SourceDetectionTask,
129 doDeblend = pexConfig.Field(
132 doc=
"Run deblender input exposure"
134 deblend = pexConfig.ConfigurableField(
135 target=SourceDeblendTask,
136 doc=
"Split blended source into their components"
138 measurement = pexConfig.ConfigurableField(
139 target=SingleFrameMeasurementTask,
140 doc=
"Measure sources"
142 doApCorr = pexConfig.Field(
145 doc=
"Run subtasks to measure and apply aperture corrections"
147 measureApCorr = pexConfig.ConfigurableField(
148 target=MeasureApCorrTask,
149 doc=
"Subtask to measure aperture corrections"
151 applyApCorr = pexConfig.ConfigurableField(
152 target=ApplyApCorrTask,
153 doc=
"Subtask to apply aperture corrections"
157 catalogCalculation = pexConfig.ConfigurableField(
158 target=CatalogCalculationTask,
159 doc=
"Subtask to run catalogCalculation plugins on catalog"
161 doComputeSummaryStats = pexConfig.Field(
164 doc=
"Run subtask to measure exposure summary statistics"
166 computeSummaryStats = pexConfig.ConfigurableField(
167 target=ComputeExposureSummaryStatsTask,
168 doc=
"Subtask to run computeSummaryStats on exposure"
170 useSimplePsf = pexConfig.Field(
173 doc=
"Replace the existing PSF model with a simplified version that has the same sigma "
174 "at the start of each PSF determination iteration? Doing so makes PSF determination "
175 "converge more robustly and quickly.",
177 installSimplePsf = pexConfig.ConfigurableField(
178 target=InstallGaussianPsfTask,
179 doc=
"Install a simple PSF model",
181 refObjLoader = pexConfig.ConfigurableField(
182 target=LoadIndexedReferenceObjectsTask,
183 doc=
"reference object loader",
185 ref_match = pexConfig.ConfigurableField(
187 doc=
"Task to load and match reference objects. Only used if measurePsf can use matches. "
188 "Warning: matching will only work well if the initial WCS is accurate enough "
189 "to give good matches (roughly: good to 3 arcsec across the CCD).",
191 measurePsf = pexConfig.ConfigurableField(
192 target=MeasurePsfTask,
195 repair = pexConfig.ConfigurableField(
197 doc=
"Remove cosmic rays",
199 requireCrForPsf = pexConfig.Field(
202 doc=
"Require cosmic ray detection and masking to run successfully before measuring the PSF."
204 checkUnitsParseStrict = pexConfig.Field(
205 doc=
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
214 self.
detectiondetection.thresholdValue = 5.0
215 self.
detectiondetection.includeThresholdMultiplier = 10.0
216 self.
detectiondetection.doTempLocalBackground =
False
229 "base_CircularApertureFlux",
234 raise RuntimeError(
"Must measure PSF to measure aperture correction, "
235 "because flags determined by PSF measurement are used to identify "
236 "sources used to measure aperture correction")
247 r"""!Measure bright sources and use this to estimate background and PSF of an exposure
249 @anchor CharacterizeImageTask_
251 @section pipe_tasks_characterizeImage_Contents Contents
253 - @ref pipe_tasks_characterizeImage_Purpose
254 - @ref pipe_tasks_characterizeImage_Initialize
255 - @ref pipe_tasks_characterizeImage_IO
256 - @ref pipe_tasks_characterizeImage_Config
257 - @ref pipe_tasks_characterizeImage_Debug
260 @section pipe_tasks_characterizeImage_Purpose Description
262 Given an exposure with defects repaired (masked and interpolated over, e.g. as output by IsrTask):
263 - detect and measure bright sources
265 - measure and subtract background
268 @section pipe_tasks_characterizeImage_Initialize Task initialisation
270 @copydoc \_\_init\_\_
272 @section pipe_tasks_characterizeImage_IO Invoking the Task
274 If you want this task to unpersist inputs or persist outputs, then call
275 the `runDataRef` method (a thin wrapper around the `run` method).
277 If you already have the inputs unpersisted and do not want to persist the output
278 then it is more direct to call the `run` method:
280 @section pipe_tasks_characterizeImage_Config Configuration parameters
282 See @ref CharacterizeImageConfig
284 @section pipe_tasks_characterizeImage_Debug Debug variables
286 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a flag
287 `--debug` to import `debug.py` from your `$PYTHONPATH`; see @ref baseDebug for more about `debug.py`.
289 CharacterizeImageTask has a debug dictionary with the following keys:
292 <dd>int: if specified, the frame of first debug image displayed (defaults to 1)
294 <dd>bool; if True display image after each repair in the measure PSF loop
296 <dd>bool; if True display image after each background subtraction in the measure PSF loop
298 <dd>bool; if True display image and sources at the end of each iteration of the measure PSF loop
299 See @ref lsst.meas.astrom.displayAstrometry for the meaning of the various symbols.
301 <dd>bool; if True display image and sources after PSF is measured;
302 this will be identical to the final image displayed by measure_iter if measure_iter is true
304 <dd>bool; if True display image and sources after final repair
306 <dd>bool; if True display image and sources after final measurement
309 For example, put something like:
313 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
314 if name == "lsst.pipe.tasks.characterizeImage":
321 lsstDebug.Info = DebugInfo
323 into your `debug.py` file and run `calibrateTask.py` with the `--debug` flag.
325 Some subtasks may have their own debug variables; see individual Task documentation.
330 ConfigClass = CharacterizeImageConfig
331 _DefaultName =
"characterizeImage"
332 RunnerClass = pipeBase.ButlerInitializedTaskRunner
335 inputs = butlerQC.get(inputRefs)
336 if 'exposureIdInfo' not in inputs.keys():
337 exposureIdInfo = ExposureIdInfo()
338 exposureIdInfo.expId, exposureIdInfo.expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
340 inputs[
'exposureIdInfo'] = exposureIdInfo
341 outputs = self.
runrun(**inputs)
342 butlerQC.put(outputs, outputRefs)
344 def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs):
345 """!Construct a CharacterizeImageTask
347 @param[in] butler A butler object is passed to the refObjLoader constructor in case
348 it is needed to load catalogs. May be None if a catalog-based star selector is
349 not used, if the reference object loader constructor does not require a butler,
350 or if a reference object loader is passed directly via the refObjLoader argument.
351 @param[in] refObjLoader An instance of LoadReferenceObjectsTasks that supplies an
352 external reference catalog to a catalog-based star selector. May be None if a
353 catalog star selector is not used or the loader can be constructed from the
355 @param[in,out] schema initial schema (an lsst.afw.table.SourceTable), or None
356 @param[in,out] kwargs other keyword arguments for lsst.pipe.base.CmdLineTask
361 schema = SourceTable.makeMinimalSchema()
363 self.makeSubtask(
"background")
364 self.makeSubtask(
"installSimplePsf")
365 self.makeSubtask(
"repair")
366 self.makeSubtask(
"measurePsf", schema=self.
schemaschema)
367 if self.config.doMeasurePsf
and self.measurePsf.usesMatches:
369 self.makeSubtask(
'refObjLoader', butler=butler)
370 refObjLoader = self.refObjLoader
371 self.makeSubtask(
"ref_match", refObjLoader=refObjLoader)
373 self.makeSubtask(
'detection', schema=self.
schemaschema)
374 if self.config.doDeblend:
375 self.makeSubtask(
"deblend", schema=self.
schemaschema)
376 self.makeSubtask(
'measurement', schema=self.
schemaschema, algMetadata=self.
algMetadataalgMetadata)
377 if self.config.doApCorr:
378 self.makeSubtask(
'measureApCorr', schema=self.
schemaschema)
379 self.makeSubtask(
'applyApCorr', schema=self.
schemaschema)
380 self.makeSubtask(
'catalogCalculation', schema=self.
schemaschema)
381 if self.config.doComputeSummaryStats:
382 self.makeSubtask(
'computeSummaryStats')
383 self.
_initialFrame_initialFrame = getDebugFrame(self._display,
"frame")
or 1
385 self.
schemaschema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
389 outputCatSchema = afwTable.SourceCatalog(self.
schemaschema)
390 outputCatSchema.getTable().setMetadata(self.
algMetadataalgMetadata)
391 return {
'outputSchema': outputCatSchema}
394 def runDataRef(self, dataRef, exposure=None, background=None, doUnpersist=True):
395 """!Characterize a science image and, if wanted, persist the results
397 This simply unpacks the exposure and passes it to the characterize method to do the work.
399 @param[in] dataRef: butler data reference for science exposure
400 @param[in,out] exposure exposure to characterize (an lsst.afw.image.ExposureF or similar).
401 If None then unpersist from "postISRCCD".
402 The following changes are made, depending on the config:
403 - set psf to the measured PSF
404 - set apCorrMap to the measured aperture correction
405 - subtract background
406 - interpolate over cosmic rays
407 - update detection and cosmic ray mask planes
408 @param[in,out] background initial model of background already subtracted from exposure
409 (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted,
410 which is typical for image characterization.
411 A refined background model is output.
412 @param[in] doUnpersist if True the exposure is read from the repository
413 and the exposure and background arguments must be None;
414 if False the exposure must be provided.
415 True is intended for running as a command-line task, False for running as a subtask
417 @return same data as the characterize method
420 self.log.info(
"Processing %s" % (dataRef.dataId))
423 if exposure
is not None or background
is not None:
424 raise RuntimeError(
"doUnpersist true; exposure and background must be None")
425 exposure = dataRef.get(
"postISRCCD", immediate=
True)
426 elif exposure
is None:
427 raise RuntimeError(
"doUnpersist false; exposure must be provided")
429 exposureIdInfo = dataRef.get(
"expIdInfo")
431 charRes = self.
runrun(
433 exposureIdInfo=exposureIdInfo,
434 background=background,
437 if self.config.doWrite:
438 dataRef.put(charRes.sourceCat,
"icSrc")
439 if self.config.doWriteExposure:
440 dataRef.put(charRes.exposure,
"icExp")
441 dataRef.put(charRes.background,
"icExpBackground")
446 def run(self, exposure, exposureIdInfo=None, background=None):
447 """!Characterize a science image
449 Peforms the following operations:
450 - Iterate the following config.psfIterations times, or once if config.doMeasurePsf false:
451 - detect and measure sources and estimate PSF (see detectMeasureAndEstimatePsf for details)
452 - interpolate over cosmic rays
453 - perform final measurement
455 @param[in,out] exposure exposure to characterize (an lsst.afw.image.ExposureF or similar).
456 The following changes are made:
459 - update detection and cosmic ray mask planes
460 - subtract background and interpolate over cosmic rays
461 @param[in] exposureIdInfo ID info for exposure (an lsst.obs.base.ExposureIdInfo).
462 If not provided, returned SourceCatalog IDs will not be globally unique.
463 @param[in,out] background initial model of background already subtracted from exposure
464 (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted,
465 which is typical for image characterization.
467 @return pipe_base Struct containing these fields, all from the final iteration
468 of detectMeasureAndEstimatePsf:
469 - exposure: characterized exposure; image is repaired by interpolating over cosmic rays,
470 mask is updated accordingly, and the PSF model is set
471 - sourceCat: detected sources (an lsst.afw.table.SourceCatalog)
472 - background: model of background subtracted from exposure (an lsst.afw.math.BackgroundList)
473 - psfCellSet: spatial cells of PSF candidates (an lsst.afw.math.SpatialCellSet)
477 if not self.config.doMeasurePsf
and not exposure.hasPsf():
478 self.log.warn(
"Source catalog detected and measured with placeholder or default PSF")
479 self.installSimplePsf.
run(exposure=exposure)
481 if exposureIdInfo
is None:
482 exposureIdInfo = ExposureIdInfo()
485 background = self.background.
run(exposure).background
487 psfIterations = self.config.psfIterations
if self.config.doMeasurePsf
else 1
488 for i
in range(psfIterations):
491 exposureIdInfo=exposureIdInfo,
492 background=background,
495 psf = dmeRes.exposure.getPsf()
496 psfSigma = psf.computeShape().getDeterminantRadius()
497 psfDimensions = psf.computeImage().getDimensions()
498 medBackground = np.median(dmeRes.background.getImage().getArray())
499 self.log.info(
"iter %s; PSF sigma=%0.2f, dimensions=%s; median background=%0.2f" %
500 (i + 1, psfSigma, psfDimensions, medBackground))
502 self.
displaydisplay(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
505 self.repair.
run(exposure=dmeRes.exposure)
506 self.
displaydisplay(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
510 self.measurement.
run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
511 exposureId=exposureIdInfo.expId)
512 if self.config.doApCorr:
513 apCorrMap = self.measureApCorr.
run(exposure=dmeRes.exposure, catalog=dmeRes.sourceCat).apCorrMap
514 dmeRes.exposure.getInfo().setApCorrMap(apCorrMap)
515 self.applyApCorr.
run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
516 self.catalogCalculation.
run(dmeRes.sourceCat)
517 if self.config.doComputeSummaryStats:
518 summary = self.computeSummaryStats.
run(exposure=dmeRes.exposure,
519 sources=dmeRes.sourceCat,
520 background=dmeRes.background)
521 dmeRes.exposure.getInfo().setSummaryStats(summary)
523 self.
displaydisplay(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
525 return pipeBase.Struct(
526 exposure=dmeRes.exposure,
527 sourceCat=dmeRes.sourceCat,
528 background=dmeRes.background,
529 psfCellSet=dmeRes.psfCellSet,
531 characterized=dmeRes.exposure,
532 backgroundModel=dmeRes.background
537 """!Perform one iteration of detect, measure and estimate PSF
539 Performs the following operations:
540 - if config.doMeasurePsf or not exposure.hasPsf():
541 - install a simple PSF model (replacing the existing one, if need be)
542 - interpolate over cosmic rays with keepCRs=True
543 - estimate background and subtract it from the exposure
544 - detect, deblend and measure sources, and subtract a refined background model;
545 - if config.doMeasurePsf:
548 @param[in,out] exposure exposure to characterize (an lsst.afw.image.ExposureF or similar)
549 The following changes are made:
551 - update detection and cosmic ray mask planes
552 - subtract background
553 @param[in] exposureIdInfo ID info for exposure (an lsst.obs_base.ExposureIdInfo)
554 @param[in,out] background initial model of background already subtracted from exposure
555 (an lsst.afw.math.BackgroundList).
557 @return pipe_base Struct containing these fields, all from the final iteration
558 of detect sources, measure sources and estimate PSF:
559 - exposure characterized exposure; image is repaired by interpolating over cosmic rays,
560 mask is updated accordingly, and the PSF model is set
561 - sourceCat detected sources (an lsst.afw.table.SourceCatalog)
562 - background model of background subtracted from exposure (an lsst.afw.math.BackgroundList)
563 - psfCellSet spatial cells of PSF candidates (an lsst.afw.math.SpatialCellSet)
566 if not exposure.hasPsf()
or (self.config.doMeasurePsf
and self.config.useSimplePsf):
567 self.log.warn(
"Source catalog detected and measured with placeholder or default PSF")
568 self.installSimplePsf.
run(exposure=exposure)
571 if self.config.requireCrForPsf:
572 self.repair.
run(exposure=exposure, keepCRs=
True)
575 self.repair.
run(exposure=exposure, keepCRs=
True)
577 self.log.warn(
"Skipping cosmic ray detection: Too many CR pixels (max %0.f)" %
578 self.config.repair.cosmicray.nCrPixelMax)
580 self.
displaydisplay(
"repair_iter", exposure=exposure)
582 if background
is None:
583 background = BackgroundList()
585 sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId, exposureIdInfo.unusedBits)
586 table = SourceTable.make(self.
schemaschema, sourceIdFactory)
589 detRes = self.detection.
run(table=table, exposure=exposure, doSmooth=
True)
590 sourceCat = detRes.sources
591 if detRes.fpSets.background:
592 for bg
in detRes.fpSets.background:
593 background.append(bg)
595 if self.config.doDeblend:
596 self.deblend.
run(exposure=exposure, sources=sourceCat)
598 self.measurement.
run(measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId)
600 measPsfRes = pipeBase.Struct(cellSet=
None)
601 if self.config.doMeasurePsf:
602 if self.measurePsf.usesMatches:
603 matches = self.ref_match.loadAndMatch(exposure=exposure, sourceCat=sourceCat).matches
606 measPsfRes = self.measurePsf.
run(exposure=exposure, sources=sourceCat, matches=matches,
607 expId=exposureIdInfo.expId)
608 self.
displaydisplay(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
610 return pipeBase.Struct(
613 background=background,
614 psfCellSet=measPsfRes.cellSet,
618 """Return a dict of empty catalogs for each catalog dataset produced by this task.
620 sourceCat = SourceCatalog(self.
schemaschema)
621 sourceCat.getTable().setMetadata(self.
algMetadataalgMetadata)
622 return {
"icSrc": sourceCat}
624 def display(self, itemName, exposure, sourceCat=None):
625 """Display exposure and sources on next frame, if display of itemName has been requested
627 @param[in] itemName name of item in debugInfo
628 @param[in] exposure exposure to display
629 @param[in] sourceCat source catalog to display
631 val = getDebugFrame(self._display, itemName)
635 displayAstrometry(exposure=exposure, sourceCat=sourceCat, frame=self.
_frame_frame, pause=
False)
Config for CharacterizeImageTask.
def adjustQuantum(self, pipeBase.InputQuantizedConnection datasetRefMap)
Measure bright sources and use this to estimate background and PSF of an exposure.
def getSchemaCatalogs(self)
def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs)
Construct a CharacterizeImageTask.
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def getInitOutputDatasets(self)
def runDataRef(self, dataRef, exposure=None, background=None, doUnpersist=True)
Characterize a science image and, if wanted, persist the results.
def run(self, exposure, exposureIdInfo=None, background=None)
Characterize a science image.
def detectMeasureAndEstimatePsf(self, exposure, exposureIdInfo, background)
Perform one iteration of detect, measure and estimate PSF.
def display(self, itemName, exposure, sourceCat=None)