22 from __future__
import absolute_import, division, print_function
23 from builtins
import range
26 from lsstDebug
import getDebugFrame
27 import lsst.pex.config
as pexConfig
28 import lsst.pipe.base
as pipeBase
29 import lsst.daf.base
as dafBase
30 from lsst.afw.math
import BackgroundList
31 from lsst.afw.table
import SourceTable, SourceCatalog, IdFactory
32 from lsst.meas.algorithms
import SubtractBackgroundTask, SourceDetectionTask, MeasureApCorrTask
33 from lsst.meas.algorithms.installGaussianPsf
import InstallGaussianPsfTask
34 from lsst.meas.astrom
import RefMatchTask, displayAstrometry
35 from lsst.meas.extensions.astrometryNet
import LoadAstrometryNetObjectsTask
36 from lsst.obs.base
import ExposureIdInfo
37 from lsst.meas.base
import SingleFrameMeasurementTask, ApplyApCorrTask, CatalogCalculationTask
38 from lsst.meas.deblender
import SourceDeblendTask
39 from .measurePsf
import MeasurePsfTask
40 from .repair
import RepairTask
42 __all__ = [
"CharacterizeImageConfig",
"CharacterizeImageTask"]
46 """!Config for CharacterizeImageTask"""
47 doMeasurePsf = pexConfig.Field(
50 doc =
"Measure PSF? If False then for all subsequent operations use either existing PSF "
51 "model when present, or install simple PSF model when not (see installSimplePsf "
54 doWrite = pexConfig.Field(
57 doc =
"Persist results?",
59 doWriteExposure = pexConfig.Field(
62 doc =
"Write icExp and icExpBackground in addition to icSrc? Ignored if doWrite False.",
64 psfIterations = pexConfig.RangeField(
68 doc =
"Number of iterations of detect sources, measure sources, "
69 "estimate PSF. If useSimplePsf is True then 2 should be plenty; "
70 "otherwise more may be wanted.",
72 background = pexConfig.ConfigurableField(
73 target = SubtractBackgroundTask,
74 doc =
"Configuration for initial background estimation",
76 detection = pexConfig.ConfigurableField(
77 target = SourceDetectionTask,
78 doc =
"Detect sources"
80 doDeblend = pexConfig.Field(
83 doc =
"Run deblender input exposure"
85 deblend = pexConfig.ConfigurableField(
86 target = SourceDeblendTask,
87 doc =
"Split blended source into their components"
89 measurement = pexConfig.ConfigurableField(
90 target = SingleFrameMeasurementTask,
91 doc =
"Measure sources"
93 doApCorr = pexConfig.Field(
96 doc =
"Run subtasks to measure and apply aperture corrections"
98 measureApCorr = pexConfig.ConfigurableField(
99 target = MeasureApCorrTask,
100 doc =
"Subtask to measure aperture corrections"
102 applyApCorr = pexConfig.ConfigurableField(
103 target = ApplyApCorrTask,
104 doc =
"Subtask to apply aperture corrections"
108 catalogCalculation = pexConfig.ConfigurableField(
109 target = CatalogCalculationTask,
110 doc =
"Subtask to run catalogCalculation plugins on catalog"
112 useSimplePsf = pexConfig.Field(
115 doc =
"Replace the existing PSF model with a simplified version that has the same sigma "
116 "at the start of each PSF determination iteration? Doing so makes PSF determination "
117 "converge more robustly and quickly.",
119 installSimplePsf = pexConfig.ConfigurableField(
120 target = InstallGaussianPsfTask,
121 doc =
"Install a simple PSF model",
123 refObjLoader = pexConfig.ConfigurableField(
124 target = LoadAstrometryNetObjectsTask,
125 doc =
"reference object loader",
127 ref_match = pexConfig.ConfigurableField(
128 target = RefMatchTask,
129 doc =
"Task to load and match reference objects. Only used if measurePsf can use matches. "
130 "Warning: matching will only work well if the initial WCS is accurate enough "
131 "to give good matches (roughly: good to 3 arcsec across the CCD).",
133 measurePsf = pexConfig.ConfigurableField(
134 target = MeasurePsfTask,
137 repair = pexConfig.ConfigurableField(
139 doc =
"Remove cosmic rays",
141 checkUnitsParseStrict = pexConfig.Field(
142 doc =
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
148 pexConfig.Config.setDefaults(self)
151 self.detection.thresholdValue = 5.0
152 self.detection.includeThresholdMultiplier = 10.0
159 self.measurement.plugins.names = [
165 "base_CircularApertureFlux",
170 raise RuntimeError(
"Must measure PSF to measure aperture correction, "
171 "because flags determined by PSF measurement are used to identify "
172 "sources used to measure aperture correction")
183 """!Measure bright sources and use this to estimate background and PSF of an exposure
185 @anchor CharacterizeImageTask_
187 @section pipe_tasks_characterizeImage_Contents Contents
189 - @ref pipe_tasks_characterizeImage_Purpose
190 - @ref pipe_tasks_characterizeImage_Initialize
191 - @ref pipe_tasks_characterizeImage_IO
192 - @ref pipe_tasks_characterizeImage_Config
193 - @ref pipe_tasks_characterizeImage_Debug
196 @section pipe_tasks_characterizeImage_Purpose Description
198 Given an exposure with defects repaired (masked and interpolated over, e.g. as output by IsrTask):
199 - detect and measure bright sources
201 - measure and subtract background
204 @section pipe_tasks_characterizeImage_Initialize Task initialisation
206 @copydoc \_\_init\_\_
208 @section pipe_tasks_characterizeImage_IO Invoking the Task
210 If you want this task to unpersist inputs or persist outputs, then call
211 the `run` method (a thin wrapper around the `characterize` method).
213 If you already have the inputs unpersisted and do not want to persist the output
214 then it is more direct to call the `characterize` method:
216 @section pipe_tasks_characterizeImage_Config Configuration parameters
218 See @ref CharacterizeImageConfig
220 @section pipe_tasks_characterizeImage_Debug Debug variables
222 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a flag
223 `--debug` to import `debug.py` from your `$PYTHONPATH`; see @ref baseDebug for more about `debug.py`.
225 CharacterizeImageTask has a debug dictionary with the following keys:
228 <dd>int: if specified, the frame of first debug image displayed (defaults to 1)
230 <dd>bool; if True display image after each repair in the measure PSF loop
232 <dd>bool; if True display image after each background subtraction in the measure PSF loop
234 <dd>bool; if True display image and sources at the end of each iteration of the measure PSF loop
235 See @ref lsst.meas.astrom.displayAstrometry for the meaning of the various symbols.
237 <dd>bool; if True display image and sources after PSF is measured;
238 this will be identical to the final image displayed by measure_iter if measure_iter is true
240 <dd>bool; if True display image and sources after final repair
242 <dd>bool; if True display image and sources after final measurement
245 For example, put something like:
249 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
250 if name == "lsst.pipe.tasks.characterizeImage":
257 lsstDebug.Info = DebugInfo
259 into your `debug.py` file and run `calibrateTask.py` with the `--debug` flag.
261 Some subtasks may have their own debug variables; see individual Task documentation.
266 ConfigClass = CharacterizeImageConfig
267 _DefaultName =
"characterizeImage"
268 RunnerClass = pipeBase.ButlerInitializedTaskRunner
270 def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs):
271 """!Construct a CharacterizeImageTask
273 @param[in] butler A butler object is passed to the refObjLoader constructor in case
274 it is needed to load catalogs. May be None if a catalog-based star selector is
275 not used, if the reference object loader constructor does not require a butler,
276 or if a reference object loader is passed directly via the refObjLoader argument.
277 @param[in] refObjLoader An instance of LoadReferenceObjectsTasks that supplies an
278 external reference catalog to a catalog-based star selector. May be None if a
279 catalog star selector is not used or the loader can be constructed from the
281 @param[in,out] schema initial schema (an lsst.afw.table.SourceTable), or None
282 @param[in,out] kwargs other keyword arguments for lsst.pipe.base.CmdLineTask
284 pipeBase.CmdLineTask.__init__(self, **kwargs)
286 schema = SourceTable.makeMinimalSchema()
288 self.makeSubtask(
"background")
289 self.makeSubtask(
"installSimplePsf")
290 self.makeSubtask(
"repair")
291 self.makeSubtask(
"measurePsf", schema=self.
schema)
292 if self.config.doMeasurePsf
and self.measurePsf.usesMatches:
294 self.makeSubtask(
'refObjLoader', butler=butler)
295 refObjLoader = self.refObjLoader
296 self.makeSubtask(
"ref_match", refObjLoader=refObjLoader)
298 self.makeSubtask(
'detection', schema=self.
schema)
299 if self.config.doDeblend:
300 self.makeSubtask(
"deblend", schema=self.
schema)
301 self.makeSubtask(
'measurement', schema=self.
schema, algMetadata=self.
algMetadata)
302 if self.config.doApCorr:
303 self.makeSubtask(
'measureApCorr', schema=self.
schema)
304 self.makeSubtask(
'applyApCorr', schema=self.
schema)
305 self.makeSubtask(
'catalogCalculation', schema=self.
schema)
306 self.
_initialFrame = getDebugFrame(self._display,
"frame")
or 1
308 self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
311 def run(self, dataRef, exposure=None, background=None, doUnpersist=True):
312 """!Characterize a science image and, if wanted, persist the results
314 This simply unpacks the exposure and passes it to the characterize method to do the work.
316 @param[in] dataRef: butler data reference for science exposure
317 @param[in,out] exposure exposure to characterize (an lsst.afw.image.ExposureF or similar).
318 If None then unpersist from "postISRCCD".
319 The following changes are made, depending on the config:
320 - set psf to the measured PSF
321 - set apCorrMap to the measured aperture correction
322 - subtract background
323 - interpolate over cosmic rays
324 - update detection and cosmic ray mask planes
325 @param[in,out] background initial model of background already subtracted from exposure
326 (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted,
327 which is typical for image characterization.
328 A refined background model is output.
329 @param[in] doUnpersist if True the exposure is read from the repository
330 and the exposure and background arguments must be None;
331 if False the exposure must be provided.
332 True is intended for running as a command-line task, False for running as a subtask
334 @return same data as the characterize method
337 self.log.info(
"Processing %s" % (dataRef.dataId))
340 if exposure
is not None or background
is not None:
341 raise RuntimeError(
"doUnpersist true; exposure and background must be None")
342 exposure = dataRef.get(
"postISRCCD", immediate=
True)
343 elif exposure
is None:
344 raise RuntimeError(
"doUnpersist false; exposure must be provided")
346 exposureIdInfo = dataRef.get(
"expIdInfo")
350 exposureIdInfo = exposureIdInfo,
351 background = background,
354 if self.config.doWrite:
355 dataRef.put(charRes.sourceCat,
"icSrc")
356 if self.config.doWriteExposure:
357 dataRef.put(charRes.exposure,
"icExp")
358 dataRef.put(charRes.background,
"icExpBackground")
363 def characterize(self, exposure, exposureIdInfo=None, background=None):
364 """!Characterize a science image
366 Peforms the following operations:
367 - Iterate the following config.psfIterations times, or once if config.doMeasurePsf false:
368 - detect and measure sources and estimate PSF (see detectMeasureAndEstimatePsf for details)
369 - interpolate over cosmic rays
370 - perform final measurement
372 @param[in,out] exposure exposure to characterize (an lsst.afw.image.ExposureF or similar).
373 The following changes are made:
376 - update detection and cosmic ray mask planes
377 - subtract background and interpolate over cosmic rays
378 @param[in] exposureIdInfo ID info for exposure (an lsst.obs.base.ExposureIdInfo).
379 If not provided, returned SourceCatalog IDs will not be globally unique.
380 @param[in,out] background initial model of background already subtracted from exposure
381 (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted,
382 which is typical for image characterization.
384 @return pipe_base Struct containing these fields, all from the final iteration
385 of detectMeasureAndEstimatePsf:
386 - exposure: characterized exposure; image is repaired by interpolating over cosmic rays,
387 mask is updated accordingly, and the PSF model is set
388 - sourceCat: detected sources (an lsst.afw.table.SourceCatalog)
389 - background: model of background subtracted from exposure (an lsst.afw.math.BackgroundList)
390 - psfCellSet: spatial cells of PSF candidates (an lsst.afw.math.SpatialCellSet)
394 if not self.config.doMeasurePsf
and not exposure.hasPsf():
395 self.log.warn(
"Source catalog detected and measured with placeholder or default 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()
413 psfSigma = psf.computeShape().getDeterminantRadius()
414 psfDimensions = psf.computeImage().getDimensions()
415 medBackground = np.median(dmeRes.background.getImage().getArray())
416 self.log.info(
"iter %s; PSF sigma=%0.2f, dimensions=%s; median background=%0.2f" %
417 (i + 1, psfSigma, psfDimensions, medBackground))
419 self.
display(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
422 self.repair.run(exposure=dmeRes.exposure)
423 self.
display(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
427 self.measurement.run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
428 exposureId = exposureIdInfo.expId)
429 if self.config.doApCorr:
430 apCorrMap = self.measureApCorr.run(exposure=dmeRes.exposure, catalog=dmeRes.sourceCat).apCorrMap
431 dmeRes.exposure.getInfo().setApCorrMap(apCorrMap)
432 self.applyApCorr.run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
433 self.catalogCalculation.run(dmeRes.sourceCat)
435 self.
display(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
441 """!Perform one iteration of detect, measure and estimate PSF
443 Performs the following operations:
444 - if config.doMeasurePsf or not exposure.hasPsf():
445 - install a simple PSF model (replacing the existing one, if need be)
446 - interpolate over cosmic rays with keepCRs=True
447 - estimate background and subtract it from the exposure
448 - detect, deblend and measure sources, and subtract a refined background model;
449 - if config.doMeasurePsf:
452 @param[in,out] exposure exposure to characterize (an lsst.afw.image.ExposureF or similar)
453 The following changes are made:
455 - update detection and cosmic ray mask planes
456 - subtract background
457 @param[in] exposureIdInfo ID info for exposure (an lsst.obs_base.ExposureIdInfo)
458 @param[in,out] background initial model of background already subtracted from exposure
459 (an lsst.afw.math.BackgroundList).
461 @return pipe_base Struct containing these fields, all from the final iteration
462 of detect sources, measure sources and estimate PSF:
463 - exposure characterized exposure; image is repaired by interpolating over cosmic rays,
464 mask is updated accordingly, and the PSF model is set
465 - sourceCat detected sources (an lsst.afw.table.SourceCatalog)
466 - background model of background subtracted from exposure (an lsst.afw.math.BackgroundList)
467 - psfCellSet spatial cells of PSF candidates (an lsst.afw.math.SpatialCellSet)
470 if not exposure.hasPsf()
or (self.config.doMeasurePsf
and self.config.useSimplePsf):
471 self.log.warn(
"Source catalog detected and measured with placeholder or default PSF")
472 self.installSimplePsf.run(exposure=exposure)
475 self.repair.run(exposure=exposure, keepCRs=
True)
476 self.
display(
"repair_iter", exposure=exposure)
478 if background
is None:
479 background = BackgroundList()
481 sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId, exposureIdInfo.unusedBits)
482 table = SourceTable.make(self.
schema, sourceIdFactory)
485 detRes = self.detection.run(table=table, exposure=exposure, doSmooth=
True)
486 sourceCat = detRes.sources
487 if detRes.fpSets.background:
488 background.append(detRes.fpSets.background)
490 if self.config.doDeblend:
491 self.deblend.run(exposure=exposure, sources=sourceCat)
493 self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId)
495 measPsfRes = pipeBase.Struct(cellSet=
None)
496 if self.config.doMeasurePsf:
497 if self.measurePsf.usesMatches:
498 matches = self.ref_match.loadAndMatch(exposure=exposure, sourceCat=sourceCat).matches
501 measPsfRes = self.measurePsf.run(exposure=exposure, sources=sourceCat, matches=matches)
502 self.
display(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
504 return pipeBase.Struct(
506 sourceCat = sourceCat,
507 background = background,
508 psfCellSet = measPsfRes.cellSet,
512 """Return a dict of empty catalogs for each catalog dataset produced by this task.
514 sourceCat = SourceCatalog(self.
schema)
516 return {
"icSrc": sourceCat}
518 def display(self, itemName, exposure, sourceCat=None):
519 """Display exposure and sources on next frame, if display of itemName has been requested
521 @param[in] itemName name of item in debugInfo
522 @param[in] exposure exposure to display
523 @param[in] sourceCat source catalog to display
525 val = getDebugFrame(self._display, itemName)
529 displayAstrometry(exposure=exposure, sourceCat=sourceCat, frame=self.
_frame, pause=
False)
def detectMeasureAndEstimatePsf
Perform one iteration of detect, measure and estimate PSF.
def __init__
Construct a CharacterizeImageTask.
Measure bright sources and use this to estimate background and PSF of an exposure.
def characterize
Characterize a science image.
def run
Characterize a science image and, if wanted, persist the results.
Config for CharacterizeImageTask.