151 self.
statControl = afwMath.StatisticsControl(self.config.nSigmaClip, self.config.nIter,
152 afwImage.Mask.getPlaneBitMask(self.config.badMask))
153 self.
statType = afwMath.stringToStatisticsProperty(self.config.stat)
155 def run(self, inputExp, ptc=None, overscanResults=None, **kwargs):
156 """Task to run arbitrary statistics.
158 The statistics should be measured by individual methods, and
159 add to the dictionary in the return struct.
163 inputExp : `lsst.afw.image.Exposure`
164 The exposure to measure.
165 ptc : `lsst.ip.isr.PtcDataset`, optional
166 A PTC object containing gains to use.
167 overscanResults : `list` [`lsst.pipe.base.Struct`], optional
168 List of overscan results. Expected fields are:
171 Value or fit subtracted from the amplifier image data
172 (scalar or `lsst.afw.image.Image`).
174 Value or fit subtracted from the overscan image data
175 (scalar or `lsst.afw.image.Image`).
177 Image of the overscan region with the overscan
178 correction applied (`lsst.afw.image.Image`). This
179 quantity is used to estimate the amplifier read noise
184 resultStruct : `lsst.pipe.base.Struct`
185 Contains the measured statistics as a dict stored in a
186 field named ``results``.
191 Raised if the amplifier gains could not be found.
194 detector = inputExp.getDetector()
197 elif detector
is not None:
198 gains = {amp.getName(): amp.getGain()
for amp
in detector.getAmplifiers()}
200 raise RuntimeError(
"No source of gains provided.")
203 if self.config.doCtiStatistics:
204 ctiResults = self.
measureCti(inputExp, overscanResults, gains)
206 bandingResults =
None
207 if self.config.doBandingStatistics:
210 projectionResults =
None
211 if self.config.doProjectionStatistics:
214 calibDistributionResults =
None
215 if self.config.doCopyCalibDistributionStatistics:
218 return pipeBase.Struct(
219 results={
"CTI": ctiResults,
220 "BANDING": bandingResults,
221 "PROJECTION": projectionResults,
222 "CALIBDIST": calibDistributionResults,
227 """Task to measure CTI statistics.
231 inputExp : `lsst.afw.image.Exposure`
233 overscans : `list` [`lsst.pipe.base.Struct`]
234 List of overscan results. Expected fields are:
237 Value or fit subtracted from the amplifier image data
238 (scalar or `lsst.afw.image.Image`).
240 Value or fit subtracted from the overscan image data
241 (scalar or `lsst.afw.image.Image`).
243 Image of the overscan region with the overscan
244 correction applied (`lsst.afw.image.Image`). This
245 quantity is used to estimate the amplifier read noise
247 gains : `dict` [`str` `float`]
248 Dictionary of per-amplifier gains, indexed by amplifier name.
252 outputStats : `dict` [`str`, [`dict` [`str`,`float]]
253 Dictionary of measurements, keyed by amplifier name and
258 detector = inputExp.getDetector()
259 image = inputExp.image
262 assert len(overscans) == len(detector.getAmplifiers())
264 for ampIter, amp
in enumerate(detector.getAmplifiers()):
266 gain = gains[amp.getName()]
267 readoutCorner = amp.getReadoutCorner()
269 dataRegion = image[amp.getBBox()]
270 ampStats[
"IMAGE_MEAN"] = afwMath.makeStatistics(dataRegion, self.
statType,
274 pixelA = afwMath.makeStatistics(dataRegion.array[:, 0],
277 pixelZ = afwMath.makeStatistics(dataRegion.array[:, -1],
283 if readoutCorner
in (ReadoutCorner.LR, ReadoutCorner.UR):
284 ampStats[
"FIRST_MEAN"] = pixelZ
285 ampStats[
"LAST_MEAN"] = pixelA
287 ampStats[
"FIRST_MEAN"] = pixelA
288 ampStats[
"LAST_MEAN"] = pixelZ
291 if overscans[ampIter]
is None:
294 self.log.warning(
"No overscan information available for ISR statistics for amp %s.",
296 nCols = amp.getSerialOverscanBBox().getWidth()
297 ampStats[
"OVERSCAN_COLUMNS"] = np.full((nCols, ), np.nan)
298 ampStats[
"OVERSCAN_VALUES"] = np.full((nCols, ), np.nan)
300 overscanImage = overscans[ampIter].overscanImage
303 for column
in range(0, overscanImage.getWidth()):
304 osMean = afwMath.makeStatistics(overscanImage.image.array[:, column],
306 columns.append(column)
307 if self.config.doApplyGainsForCtiStatistics:
308 values.append(gain * osMean)
310 values.append(osMean)
314 if readoutCorner
in (ReadoutCorner.LR, ReadoutCorner.UR):
315 ampStats[
"OVERSCAN_COLUMNS"] = list(reversed(columns))
316 ampStats[
"OVERSCAN_VALUES"] = list(reversed(values))
318 ampStats[
"OVERSCAN_COLUMNS"] = columns
319 ampStats[
"OVERSCAN_VALUES"] = values
321 outputStats[amp.getName()] = ampStats
346 """Task to measure banding statistics.
350 inputExp : `lsst.afw.image.Exposure`
352 overscans : `list` [`lsst.pipe.base.Struct`]
353 List of overscan results. Expected fields are:
356 Value or fit subtracted from the amplifier image data
357 (scalar or `lsst.afw.image.Image`).
359 Value or fit subtracted from the overscan image data
360 (scalar or `lsst.afw.image.Image`).
362 Image of the overscan region with the overscan
363 correction applied (`lsst.afw.image.Image`). This
364 quantity is used to estimate the amplifier read noise
369 outputStats : `dict` [`str`, [`dict` [`str`,`float]]
370 Dictionary of measurements, keyed by amplifier name and
375 detector = inputExp.getDetector()
376 kernel = self.
makeKernel(self.config.bandingKernelSize)
378 outputStats[
"AMP_BANDING"] = []
379 for amp, overscanData
in zip(detector.getAmplifiers(), overscans):
380 overscanFit = np.array(overscanData.overscanFit)
381 overscanArray = overscanData.overscanImage.image.array
382 rawOverscan = np.mean(overscanArray + overscanFit, axis=1)
384 smoothedOverscan = np.convolve(rawOverscan, kernel, mode=
"valid")
386 low, high = np.quantile(smoothedOverscan, [self.config.bandingFractionLow,
387 self.config.bandingFractionHigh])
388 outputStats[
"AMP_BANDING"].append(float(high - low))
390 if self.config.bandingUseHalfDetector:
391 fullLength = len(outputStats[
"AMP_BANDING"])
392 outputStats[
"DET_BANDING"] = float(np.nanmedian(outputStats[
"AMP_BANDING"][0:fullLength//2]))
394 outputStats[
"DET_BANDING"] = float(np.nanmedian(outputStats[
"AMP_BANDING"]))
399 """Task to measure metrics from image slicing.
403 inputExp : `lsst.afw.image.Exposure`
405 overscans : `list` [`lsst.pipe.base.Struct`]
406 List of overscan results. Expected fields are:
409 Value or fit subtracted from the amplifier image data
410 (scalar or `lsst.afw.image.Image`).
412 Value or fit subtracted from the overscan image data
413 (scalar or `lsst.afw.image.Image`).
415 Image of the overscan region with the overscan
416 correction applied (`lsst.afw.image.Image`). This
417 quantity is used to estimate the amplifier read noise
422 outputStats : `dict` [`str`, [`dict` [`str`,`float]]
423 Dictionary of measurements, keyed by amplifier name and
428 detector = inputExp.getDetector()
429 kernel = self.
makeKernel(self.config.projectionKernelSize)
431 outputStats[
"AMP_VPROJECTION"] = {}
432 outputStats[
"AMP_HPROJECTION"] = {}
433 convolveMode =
"valid"
434 if self.config.doProjectionFft:
435 outputStats[
"AMP_VFFT_REAL"] = {}
436 outputStats[
"AMP_VFFT_IMAG"] = {}
437 outputStats[
"AMP_HFFT_REAL"] = {}
438 outputStats[
"AMP_HFFT_IMAG"] = {}
439 convolveMode =
"same"
441 for amp
in detector.getAmplifiers():
442 ampArray = inputExp.image[amp.getBBox()].array
444 horizontalProjection = np.mean(ampArray, axis=0)
445 verticalProjection = np.mean(ampArray, axis=1)
447 horizontalProjection = np.convolve(horizontalProjection, kernel, mode=convolveMode)
448 verticalProjection = np.convolve(verticalProjection, kernel, mode=convolveMode)
450 outputStats[
"AMP_HPROJECTION"][amp.getName()] = horizontalProjection.tolist()
451 outputStats[
"AMP_VPROJECTION"][amp.getName()] = verticalProjection.tolist()
453 if self.config.doProjectionFft:
454 horizontalWindow = np.ones_like(horizontalProjection)
455 verticalWindow = np.ones_like(verticalProjection)
456 if self.config.projectionFftWindow ==
"NONE":
458 elif self.config.projectionFftWindow ==
"HAMMING":
459 horizontalWindow = hamming(len(horizontalProjection))
460 verticalWindow = hamming(len(verticalProjection))
461 elif self.config.projectionFftWindow ==
"HANN":
462 horizontalWindow = hann(len(horizontalProjection))
463 verticalWindow = hann(len(verticalProjection))
464 elif self.config.projectionFftWindow ==
"GAUSSIAN":
465 horizontalWindow = gaussian(len(horizontalProjection))
466 verticalWindow = gaussian(len(verticalProjection))
468 raise RuntimeError(f
"Invalid window function: {self.config.projectionFftWindow}")
470 horizontalFFT = np.fft.rfft(np.multiply(horizontalProjection, horizontalWindow))
471 verticalFFT = np.fft.rfft(np.multiply(verticalProjection, verticalWindow))
472 outputStats[
"AMP_HFFT_REAL"][amp.getName()] = np.real(horizontalFFT).tolist()
473 outputStats[
"AMP_HFFT_IMAG"][amp.getName()] = np.imag(horizontalFFT).tolist()
474 outputStats[
"AMP_VFFT_REAL"][amp.getName()] = np.real(verticalFFT).tolist()
475 outputStats[
"AMP_VFFT_IMAG"][amp.getName()] = np.imag(verticalFFT).tolist()
480 """Copy calibration statistics for this exposure.
484 inputExp : `lsst.afw.image.Exposure`
485 The exposure being processed.
487 Keyword arguments with calibrations.
491 outputStats : `dict` [`str`, [`dict` [`str`,`float]]
492 Dictionary of measurements, keyed by amplifier name and
497 for amp
in inputExp.getDetector():
500 for calibType
in (
"bias",
"dark",
"flat"):
501 if kwargs.get(calibType,
None)
is not None:
502 metadata = kwargs[calibType].getMetadata()
503 for pct
in self.config.expectedDistributionLevels:
504 key = f
"LSST CALIB {calibType.upper()} {amp.getName()} DISTRIBUTION {pct}-PCT"
505 ampStats[key] = metadata.get(key, np.nan)
506 outputStats[amp.getName()] = ampStats