32 from astro_metadata_translator
import merge_headers, ObservationGroup
33 from astro_metadata_translator.serialize
import dates_to_fits
38 """Parameters controlling the measurement of background statistics.
40 stat = pexConfig.Field(
43 doc=
"Statistic name to use to estimate background (from lsst.afw.math)",
45 clip = pexConfig.Field(
48 doc=
"Clipping threshold for background",
50 nIter = pexConfig.Field(
53 doc=
"Clipping iterations for background",
55 mask = pexConfig.ListField(
57 default=[
"DETECTED",
"BAD",
"NO_DATA"],
58 doc=
"Mask planes to reject",
63 """Measure statistics on the background
65 This can be useful for scaling the background, e.g., for flats and fringe frames.
67 ConfigClass = CalibStatsConfig
69 def run(self, exposureOrImage):
70 """Measure a particular statistic on an image (of some sort).
74 exposureOrImage : `lsst.afw.image.Exposure`, `lsst.afw.image.MaskedImage`, or `lsst.afw.image.Image`
75 Exposure or image to calculate statistics on.
80 Resulting statistic value.
83 afwImage.Mask.getPlaneBitMask(self.config.mask))
85 image = exposureOrImage.getMaskedImage()
88 image = exposureOrImage.getImage()
90 image = exposureOrImage
96 dimensions=(
"instrument",
"detector")):
99 doc=
"Input pre-processed exposures to combine.",
100 storageClass=
"Exposure",
101 dimensions=(
"instrument",
"detector",
"exposure"),
104 inputScales = cT.Input(
106 doc=
"Input scale factors to use.",
107 storageClass=
"StructuredDataDict",
108 dimensions=(
"instrument", ),
112 outputData = cT.Output(
114 doc=
"Output combined proposed calibration.",
115 storageClass=
"ExposureF",
116 dimensions=(
"instrument",
"detector"),
123 if config
and config.exposureScaling !=
'InputList':
124 self.inputs.discard(
"inputScales")
126 if config
and len(config.calibrationDimensions) != 0:
127 newDimensions = tuple(config.calibrationDimensions)
128 newOutputData = cT.Output(
131 storageClass=self.
outputDataoutputData.storageClass,
132 dimensions=self.allConnections[
'outputData'].dimensions + newDimensions,
135 self.dimensions.update(config.calibrationDimensions)
138 if config.exposureScaling ==
'InputList':
139 newInputScales = cT.PrerequisiteInput(
142 storageClass=self.
inputScalesinputScales.storageClass,
143 dimensions=self.allConnections[
'inputScales'].dimensions + newDimensions
145 self.dimensions.update(config.calibrationDimensions)
151 pipelineConnections=CalibCombineConnections):
152 """Configuration for combining calib exposures.
154 calibrationType = pexConfig.Field(
156 default=
"calibration",
157 doc=
"Name of calibration to be generated.",
159 calibrationDimensions = pexConfig.ListField(
162 doc=
"List of updated dimensions to append to output."
165 exposureScaling = pexConfig.ChoiceField(
168 "Unity":
"Do not scale inputs. Scale factor is 1.0.",
169 "ExposureTime":
"Scale inputs by their exposure time.",
170 "DarkTime":
"Scale inputs by their dark time.",
171 "MeanStats":
"Scale inputs based on their mean values.",
172 "InputList":
"Scale inputs based on a list of values.",
175 doc=
"Scaling to be applied to each input exposure.",
177 scalingLevel = pexConfig.ChoiceField(
180 "DETECTOR":
"Scale by detector.",
181 "AMP":
"Scale by amplifier.",
184 doc=
"Region to scale.",
186 maxVisitsToCalcErrorFromInputVariance = pexConfig.Field(
189 doc=
"Maximum number of visits to estimate variance from input variance, not per-pixel spread",
192 doVignette = pexConfig.Field(
195 doc=
"Copy vignette polygon to output and censor vignetted pixels?"
198 mask = pexConfig.ListField(
200 default=[
"SAT",
"DETECTED",
"INTRP"],
201 doc=
"Mask planes to respect",
203 combine = pexConfig.Field(
206 doc=
"Statistic name to use for combination (from lsst.afw.math)",
208 clip = pexConfig.Field(
211 doc=
"Clipping threshold for combination",
213 nIter = pexConfig.Field(
216 doc=
"Clipping iterations for combination",
218 stats = pexConfig.ConfigurableField(
219 target=CalibStatsTask,
220 doc=
"Background statistics configuration",
225 pipeBase.CmdLineTask):
226 """Task to combine calib exposures."""
227 ConfigClass = CalibCombineConfig
228 _DefaultName =
'cpCombine'
232 self.makeSubtask(
"stats")
235 inputs = butlerQC.get(inputRefs)
237 dimensions = [exp.dataId.byName()
for exp
in inputRefs.inputExps]
238 inputs[
'inputDims'] = dimensions
240 outputs = self.
runrun(**inputs)
241 butlerQC.put(outputs, outputRefs)
243 def run(self, inputExps, inputScales=None, inputDims=None):
244 """Combine calib exposures for a single detector.
248 inputExps : `list` [`lsst.afw.image.Exposure`]
249 Input list of exposures to combine.
250 inputScales : `dict` [`dict` [`dict` [`float`]]], optional
251 Dictionary of scales, indexed by detector (`int`),
252 amplifier (`int`), and exposure (`int`). Used for
254 inputDims : `list` [`dict`]
255 List of dictionaries of input data dimensions/values.
256 Each list entry should contain:
259 exposure id value (`int`)
261 detector id value (`int`)
265 combinedExp : `lsst.afw.image.Exposure`
266 Final combined exposure generated from the inputs.
271 Raised if no input data is found. Also raised if
272 config.exposureScaling == InputList, and a necessary scale
277 afwImage.Mask.getPlaneBitMask(self.config.mask))
278 numExps = len(inputExps)
280 raise RuntimeError(
"No valid input data")
281 if numExps < self.config.maxVisitsToCalcErrorFromInputVariance:
282 stats.setCalcErrorFromInputVariance(
True)
285 combined = afwImage.MaskedImageF(width, height)
290 if inputDims
is None:
291 inputDims = [dict()
for i
in inputExps]
293 for index, (exp, dims)
in enumerate(zip(inputExps, inputDims)):
296 self.log.
warn(
"Input %d is None (%s); unable to scale exp.", index, dims)
299 if self.config.exposureScaling ==
"ExposureTime":
300 scale = exp.getInfo().getVisitInfo().getExposureTime()
301 elif self.config.exposureScaling ==
"DarkTime":
302 scale = exp.getInfo().getVisitInfo().getDarkTime()
303 elif self.config.exposureScaling ==
"MeanStats":
304 scale = self.stats.
run(exp)
305 elif self.config.exposureScaling ==
"InputList":
306 visitId = dims.get(
'exposure',
None)
307 detectorId = dims.get(
'detector',
None)
308 if visitId
is None or detectorId
is None:
309 raise RuntimeError(f
"Could not identify scaling for input {index} ({dims})")
310 if detectorId
not in inputScales[
'expScale']:
311 raise RuntimeError(f
"Could not identify a scaling for input {index}"
312 f
" detector {detectorId}")
314 if self.config.scalingLevel ==
"DETECTOR":
315 if visitId
not in inputScales[
'expScale'][detectorId]:
316 raise RuntimeError(f
"Could not identify a scaling for input {index}"
317 f
"detector {detectorId} visit {visitId}")
318 scale = inputScales[
'expScale'][detectorId][visitId]
319 elif self.config.scalingLevel ==
'AMP':
320 scale = [inputScales[
'expScale'][detectorId][amp.getName()][visitId]
321 for amp
in exp.getDetector()]
323 raise RuntimeError(f
"Unknown scaling level: {self.config.scalingLevel}")
324 elif self.config.exposureScaling ==
'Unity':
327 raise RuntimeError(f
"Unknown scaling type: {self.config.exposureScaling}.")
329 expScales.append(scale)
330 self.log.
info(
"Scaling input %d by %s", index, scale)
333 self.
combinecombine(combined, inputExps, stats)
337 if self.config.doVignette:
338 polygon = inputExps[0].
getInfo().getValidPolygon()
340 doSetValue=
True, vignetteValue=0.0)
344 calibType=self.config.calibrationType, scales=expScales)
347 return pipeBase.Struct(
348 outputData=combinedExp,
352 """Get dimensions of the inputs.
356 expList : `list` [`lsst.afw.image.Exposure`]
357 Exps to check the sizes of.
361 width, height : `int`
362 Unique set of input dimensions.
364 dimList = [exp.getDimensions()
for exp
in expList
if exp
is not None]
365 return self.
getSizegetSize(dimList)
368 """Determine a consistent size, given a list of image sizes.
372 dimList : iterable of `tuple` (`int`, `int`)
378 If input dimensions are inconsistent.
382 width, height : `int`
385 dim =
set((w, h)
for w, h
in dimList)
387 raise RuntimeError(
"Inconsistent dimensions: %s" % dim)
391 """Apply scale to input exposure.
393 This implementation applies a flux scaling: the input exposure is
394 divided by the provided scale.
398 exposure : `lsst.afw.image.Exposure`
400 scale : `float` or `list` [`float`], optional
401 Constant scale to divide the exposure by.
403 if scale
is not None:
404 mi = exposure.getMaskedImage()
405 if isinstance(scale, list):
406 for amp, ampScale
in zip(exposure.getDetector(), scale):
407 ampIm = mi[amp.getBBox()]
413 """Combine multiple images.
417 target : `lsst.afw.image.Exposure`
418 Output exposure to construct.
419 expList : `list` [`lsst.afw.image.Exposure`]
420 Input exposures to combine.
421 stats : `lsst.afw.math.StatisticsControl`
422 Control explaining how to combine the input images.
424 images = [img.getMaskedImage()
for img
in expList
if img
is not None]
429 """Combine input headers to determine the set of common headers,
430 supplemented by calibration inputs.
434 expList : `list` of `lsst.afw.image.Exposure`
435 Input list of exposures to combine.
436 calib : `lsst.afw.image.Exposure`
437 Output calibration to construct headers for.
438 calibType: `str`, optional
439 OBSTYPE the output should claim.
440 scales: `list` of `float`, optional
441 Scale values applied to each input to record.
445 header : `lsst.daf.base.PropertyList`
449 header = calib.getMetadata()
450 header.set(
"OBSTYPE", calibType)
453 comments = {
"TIMESYS":
"Time scale for all dates",
454 "DATE-OBS":
"Start date of earliest input observation",
455 "MJD-OBS":
"[d] Start MJD of earliest input observation",
456 "DATE-END":
"End date of oldest input observation",
457 "MJD-END":
"[d] End MJD of oldest input observation",
458 "MJD-AVG":
"[d] MJD midpoint of all input observations",
459 "DATE-AVG":
"Midpoint date of all input observations"}
462 now = time.localtime()
463 calibDate = time.strftime(
"%Y-%m-%d", now)
464 calibTime = time.strftime(
"%X %Z", now)
465 header.set(
"CALIB_CREATE_DATE", calibDate)
466 header.set(
"CALIB_CREATE_TIME", calibTime)
469 inputHeaders = [exp.getMetadata()
for exp
in expList
if exp
is not None]
470 merged = merge_headers(inputHeaders, mode=
'drop')
471 for k, v
in merged.items():
473 md = expList[0].getMetadata()
474 comment = md.getComment(k)
if k
in md
else None
475 header.set(k, v, comment=comment)
478 visitInfoList = [exp.getInfo().getVisitInfo()
for exp
in expList
if exp
is not None]
479 for i, visit
in enumerate(visitInfoList):
482 header.set(
"CPP_INPUT_%d" % (i,), visit.getExposureId())
483 header.set(
"CPP_INPUT_DATE_%d" % (i,), str(visit.getDate()))
484 header.set(
"CPP_INPUT_EXPT_%d" % (i,), visit.getExposureTime())
485 if scales
is not None:
486 header.set(
"CPP_INPUT_SCALE_%d" % (i,), scales[i])
493 group = ObservationGroup(visitInfoList, pedantic=
False)
495 self.log.
warn(
"Exception making an obs group for headers. Continuing.")
497 dateCards = {
"DATE-OBS":
"{}T00:00:00.00".
format(calibDate)}
498 comments[
"DATE-OBS"] =
"Date of start of day of calibration midpoint"
500 oldest, newest = group.extremes()
501 dateCards = dates_to_fits(oldest.datetime_begin, newest.datetime_end)
503 for k, v
in dateCards.items():
504 header.set(k, v, comment=comments.get(k,
None))
509 """Interpolate over NANs in the combined image.
511 NANs can result from masked areas on the CCD. We don't want them getting
512 into our science images, so we replace them with the median of the image.
516 exp : `lsst.afw.image.Exposure`
517 Exp to check for NaNs.
519 array = exp.getImage().getArray()
520 bad = np.isnan(array)
522 median = np.median(array[np.logical_not(bad)])
523 count = np.sum(np.logical_not(bad))
526 self.log.
warn(
"Found %s NAN pixels", count)
530 doUpdateMask=True, maskPlane='BAD',
531 doSetValue=False, vignetteValue=0.0,
533 """Apply vignetted polygon to image pixels.
537 exposure : `lsst.afw.image.Exposure`
539 doUpdateMask : `bool`, optional
540 Update the exposure mask for vignetted area?
541 maskPlane : `str`, optional,
542 Mask plane to assign.
543 doSetValue : `bool`, optional
544 Set image value for vignetted area?
545 vignetteValue : `float`, optional
547 log : `lsst.log.Log`, optional
553 Raised if no valid polygon exists.
555 polygon = polygon
if polygon
else exposure.getInfo().getValidPolygon()
557 raise RuntimeError(
"Could not find valid polygon!")
558 log = log
if log
else Log.getLogger(__name__.partition(
".")[2])
560 fullyIlluminated =
True
561 for corner
in exposure.getBBox().getCorners():
562 if not polygon.contains(
Point2D(corner)):
563 fullyIlluminated =
False
565 log.info(
"Exposure is fully illuminated? %s", fullyIlluminated)
567 if not fullyIlluminated:
569 mask = exposure.getMask()
570 numPixels = mask.getBBox().getArea()
572 xx, yy = np.meshgrid(np.arange(0, mask.getWidth(), dtype=int),
573 np.arange(0, mask.getHeight(), dtype=int))
575 vignMask = np.array([
not polygon.contains(
Point2D(x, y))
for x, y
in
576 zip(xx.reshape(numPixels), yy.reshape(numPixels))])
577 vignMask = vignMask.reshape(mask.getHeight(), mask.getWidth())
580 bitMask = mask.getPlaneBitMask(maskPlane)
581 maskArray = mask.getArray()
582 maskArray[vignMask] |= bitMask
584 imageArray = exposure.getImage().getArray()
585 imageArray[vignMask] = vignetteValue
586 log.info(
"Exposure contains %d vignetted pixels.",
587 np.count_nonzero(vignMask))
Pass parameters to a Statistics object.
def __init__(self, *config=None)
def applyScale(self, exposure, scale=None)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def __init__(self, **kwargs)
def run(self, inputExps, inputScales=None, inputDims=None)
def combine(self, target, expList, stats)
def getDimensions(self, expList)
def getSize(self, dimList)
def interpolateNans(self, exp)
def combineHeaders(self, expList, calib, calibType="CALIB", scales=None)
def run(self, exposureOrImage)
daf::base::PropertySet * set
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
std::shared_ptr< lsst::afw::image::Image< PixelT > > statisticsStack(std::vector< std::shared_ptr< lsst::afw::image::Image< PixelT >>> &images, Property flags, StatisticsControl const &sctrl=StatisticsControl(), std::vector< lsst::afw::image::VariancePixel > const &wvector=std::vector< lsst::afw::image::VariancePixel >(0))
A function to compute some statistics of a stack of Images.
def VignetteExposure(exposure, polygon=None, doUpdateMask=True, maskPlane='BAD', doSetValue=False, vignetteValue=0.0, log=None)
Point< double, 2 > Point2D
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)