25import lsst.pipe.base
as pipeBase
28__all__ = [
"OverscanCorrectionTaskConfig",
"OverscanCorrectionTask"]
32 """Overscan correction options.
34 fitType = pexConfig.ChoiceField(
36 doc="The method for fitting the overscan bias level.",
39 "POLY":
"Fit ordinary polynomial to the longest axis of the overscan region",
40 "CHEB":
"Fit Chebyshev polynomial to the longest axis of the overscan region",
41 "LEG":
"Fit Legendre polynomial to the longest axis of the overscan region",
42 "NATURAL_SPLINE":
"Fit natural spline to the longest axis of the overscan region",
43 "CUBIC_SPLINE":
"Fit cubic spline to the longest axis of the overscan region",
44 "AKIMA_SPLINE":
"Fit Akima spline to the longest axis of the overscan region",
45 "MEAN":
"Correct using the mean of the overscan region",
46 "MEANCLIP":
"Correct using a clipped mean of the overscan region",
47 "MEDIAN":
"Correct using the median of the overscan region",
48 "MEDIAN_PER_ROW":
"Correct using the median per row of the overscan region",
51 order = pexConfig.Field(
53 doc=(
"Order of polynomial to fit if overscan fit type is a polynomial, "
54 "or number of spline knots if overscan fit type is a spline."),
57 numSigmaClip = pexConfig.Field(
59 doc=
"Rejection threshold (sigma) for collapsing overscan before fit",
62 maskPlanes = pexConfig.ListField(
64 doc=
"Mask planes to reject when measuring overscan",
67 overscanIsInt = pexConfig.Field(
69 doc=
"Treat overscan as an integer image for purposes of fitType=MEDIAN"
70 " and fitType=MEDIAN_PER_ROW.",
76 """Correction task for overscan.
78 This class contains a number of utilities that are easier to
79 understand
and use when they are
not embedded
in nested
if/
else
85 Statistics control object.
87 ConfigClass = OverscanCorrectionTaskConfig
88 _DefaultName = "overscan"
90 def __init__(self, statControl=None, **kwargs):
97 self.
statControlstatControl = afwMath.StatisticsControl()
98 self.
statControlstatControl.setNumSigmaClip(self.config.numSigmaClip)
99 self.
statControlstatControl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.maskPlanes))
101 def run(self, ampImage, overscanImage, amp=None):
102 """Measure and remove an overscan from an amplifier image.
107 Image data that will have the overscan removed.
109 Overscan data that the overscan is measured
from.
111 Amplifier to use
for debugging purposes.
115 overscanResults : `lsst.pipe.base.Struct`
116 Result struct
with components:
119 Value
or fit subtracted
from the amplifier image data
122 Value
or fit subtracted
from the overscan image data
125 Image of the overscan region
with the overscan
127 quantity
is used to estimate the amplifier read noise
133 Raised
if an invalid overscan type
is set.
136 if self.config.fitType
in (
'MEAN',
'MEANCLIP',
'MEDIAN'):
138 overscanValue = overscanResult.overscanValue
139 offImage = overscanValue
140 overscanModel = overscanValue
142 elif self.config.fitType
in (
'MEDIAN_PER_ROW',
'POLY',
'CHEB',
'LEG',
143 'NATURAL_SPLINE',
'CUBIC_SPLINE',
'AKIMA_SPLINE'):
145 overscanValue = overscanResult.overscanValue
146 maskArray = overscanResult.maskArray
147 isTransposed = overscanResult.isTransposed
149 offImage = afwImage.ImageF(ampImage.getDimensions())
150 offArray = offImage.getArray()
151 overscanModel = afwImage.ImageF(overscanImage.getDimensions())
152 overscanArray = overscanModel.getArray()
154 if hasattr(ampImage,
'getMask'):
155 maskSuspect = afwImage.Mask(ampImage.getDimensions())
160 offArray[:, :] = overscanValue[np.newaxis, :]
161 overscanArray[:, :] = overscanValue[np.newaxis, :]
163 maskSuspect.getArray()[:, maskArray] |= ampImage.getMask().getPlaneBitMask(
"SUSPECT")
165 offArray[:, :] = overscanValue[:, np.newaxis]
166 overscanArray[:, :] = overscanValue[:, np.newaxis]
168 maskSuspect.getArray()[maskArray, :] |= ampImage.getMask().getPlaneBitMask(
"SUSPECT")
170 raise RuntimeError(
'%s : %s an invalid overscan type' %
171 (
"overscanCorrection", self.config.fitType))
173 self.
debugViewdebugView(overscanImage, overscanValue, amp)
177 ampImage.getMask().getArray()[:, :] |= maskSuspect.getArray()[:, :]
178 overscanImage -= overscanModel
179 return pipeBase.Struct(imageFit=offImage,
180 overscanFit=overscanModel,
181 overscanImage=overscanImage,
182 edgeMask=maskSuspect)
186 """Return an integer version of the input image.
191 Image to convert to integers.
196 The integer converted image.
201 Raised
if the input image could
not be converted.
203 if hasattr(image,
"image"):
205 imageI = image.image.convertI()
206 outI = afwImage.MaskedImageI(imageI, image.mask, image.variance)
207 elif hasattr(image,
"convertI"):
209 outI = image.convertI()
210 elif hasattr(image,
"astype"):
212 outI = image.astype(int)
214 raise RuntimeError(
"Could not convert this to integers: %s %s %s",
215 image, type(image), dir(image))
220 """Measure a constant overscan value.
225 Image data to measure the overscan
from.
229 results : `lsst.pipe.base.Struct`
230 Overscan result
with entries:
231 - ``overscanValue``: Overscan value to subtract (`float`)
232 - ``maskArray``: Placeholder
for a mask array (`list`)
233 - ``isTransposed``: Orientation of the overscan (`bool`)
235 if self.config.fitType ==
'MEDIAN':
240 fitType = afwMath.stringToStatisticsProperty(self.config.fitType)
241 overscanValue = afwMath.makeStatistics(calcImage, fitType, self.
statControlstatControl).getValue()
243 return pipeBase.Struct(overscanValue=overscanValue,
249 """Extract the numpy array from the input image.
254 Image data to pull array
from.
256 calcImage : `numpy.ndarray`
257 Image data array
for numpy operating.
259 if hasattr(image,
"getImage"):
260 calcImage = image.getImage().getArray()
261 calcImage = np.ma.masked_where(image.getMask().getArray() & self.
statControlstatControl.getAndMask(),
264 calcImage = image.getArray()
269 """Transpose input numpy array if necessary.
273 imageArray : `numpy.ndarray`
274 Image data to transpose.
278 imageArray : `numpy.ndarray`
279 Transposed image data.
280 isTransposed : `bool`
281 Indicates whether the input data was transposed.
283 if np.argmin(imageArray.shape) == 0:
284 return np.transpose(imageArray),
True
286 return imageArray,
False
289 """Mask outliers in a row of overscan data
from a robust sigma
294 imageArray : `numpy.ndarray`
295 Image to filter along numpy axis=1.
299 maskedArray : `numpy.ma.masked_array`
300 Masked image marking outliers.
302 lq, median, uq = np.percentile(imageArray, [25.0, 50.0, 75.0], axis=1)
304 axisStdev = 0.74*(uq - lq)
306 diff = np.abs(imageArray - axisMedians[:, np.newaxis])
307 return np.ma.masked_where(diff > self.
statControlstatControl.getNumSigmaClip()
308 * axisStdev[:, np.newaxis], imageArray)
312 """Collapse overscan array (and mask) to a 1-D vector of values.
316 maskedArray : `numpy.ma.masked_array`
317 Masked array of input overscan data.
321 collapsed : `numpy.ma.masked_array`
322 Single dimensional overscan data, combined with the mean.
324 collapsed = np.mean(maskedArray, axis=1)
325 if collapsed.mask.sum() > 0:
326 collapsed.data[collapsed.mask] = np.mean(maskedArray.data[collapsed.mask], axis=1)
330 """Collapse overscan array (and mask) to a 1-D vector of using the
331 correct integer median of row-values.
335 maskedArray : `numpy.ma.masked_array`
336 Masked array of input overscan data.
340 collapsed : `numpy.ma.masked_array`
341 Single dimensional overscan data, combined with the afwMath median.
346 fitType = afwMath.stringToStatisticsProperty('MEDIAN')
347 for row
in integerMI:
348 newRow = row.compressed()
350 rowMedian = afwMath.makeStatistics(newRow, fitType, self.
statControlstatControl).getValue()
353 collapsed.append(rowMedian)
355 return np.array(collapsed)
358 """Wrapper function to match spline fit API to polynomial fit API.
362 indices : `numpy.ndarray`
363 Locations to evaluate the spline.
364 collapsed : `numpy.ndarray`
365 Collapsed overscan values corresponding to the spline
368 Number of bins to use in constructing the spline.
373 Interpolation object
for later evaluation.
375 if not np.ma.is_masked(collapsed):
376 collapsed.mask = np.array(len(collapsed)*[np.ma.nomask])
378 numPerBin, binEdges = np.histogram(indices, bins=numBins,
379 weights=1 - collapsed.mask.astype(int))
380 with np.errstate(invalid=
"ignore"):
381 values = np.histogram(indices, bins=numBins,
382 weights=collapsed.data*~collapsed.mask)[0]/numPerBin
383 binCenters = np.histogram(indices, bins=numBins,
384 weights=indices*~collapsed.mask)[0]/numPerBin
386 if len(binCenters[numPerBin > 0]) < 5:
387 self.log.warn(
"Cannot do spline fitting for overscan: %s valid points.",
388 len(binCenters[numPerBin > 0]))
392 if len(values[numPerBin > 0]) != 0:
393 return float(values[numPerBin > 0][0])
397 interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
398 values.astype(float)[numPerBin > 0],
399 afwMath.stringToInterpStyle(self.config.fitType))
404 """Wrapper function to match spline evaluation API to polynomial fit
409 indices : `numpy.ndarray`
410 Locations to evaluate the spline.
411 interp : `lsst.afw.math.interpolate`
412 Interpolation object to use.
416 values : `numpy.ndarray`
417 Evaluated spline values at each index.
420 return interp.interpolate(indices.astype(float))
424 """Create mask if edges are extrapolated.
428 collapsed : `numpy.ma.masked_array`
429 Masked array to check the edges of.
433 maskArray : `numpy.ndarray`
434 Boolean numpy array of pixels to mask.
436 maskArray = np.full_like(collapsed, False, dtype=bool)
437 if np.ma.is_masked(collapsed):
439 for low
in range(num):
440 if not collapsed.mask[low]:
443 maskArray[:low] =
True
444 for high
in range(1, num):
445 if not collapsed.mask[-high]:
448 maskArray[-high:] =
True
452 """Calculate the 1-d vector overscan from the input overscan image.
457 Image containing the overscan data.
461 results : `lsst.pipe.base.Struct`
462 Overscan result with entries:
463 - ``overscanValue``: Overscan value to subtract (`float`)
464 - ``maskArray`` : `list` [ `bool` ]
465 List of rows that should be masked
as ``SUSPECT`` when the
466 overscan solution
is applied.
467 - ``isTransposed`` : `bool`
468 Indicates
if the overscan data was transposed during
469 calcuation, noting along which axis the overscan should be
475 calcImage, isTransposed = self.
transposetranspose(calcImage)
478 if self.config.fitType ==
'MEDIAN_PER_ROW':
485 indices = 2.0*np.arange(num)/float(num) - 1.0
489 'POLY': (poly.polynomial.polyfit, poly.polynomial.polyval),
490 'CHEB': (poly.chebyshev.chebfit, poly.chebyshev.chebval),
491 'LEG': (poly.legendre.legfit, poly.legendre.legval),
495 }[self.config.fitType]
499 coeffs = fitter(indices, collapsed, self.config.order)
501 if isinstance(coeffs, float):
502 self.log.warn(
"Using fallback value %f due to fitter failure. Amplifier will be masked.",
504 overscanVector = np.full_like(indices, coeffs)
505 maskArray = np.full_like(collapsed,
True, dtype=bool)
508 overscanVector = evaler(indices, coeffs)
510 return pipeBase.Struct(overscanValue=np.array(overscanVector),
512 isTransposed=isTransposed)
515 """Debug display for the final overscan solution.
520 Input image the overscan solution was determined from.
521 model : `numpy.ndarray`
or `float`
522 Overscan model determined
for the image.
524 Amplifier to extract diagnostic information.
533 calcImage, isTransposed = self.
transposetranspose(calcImage)
538 indices = 2.0 * np.arange(num)/float(num) - 1.0
540 if np.ma.is_masked(collapsed):
541 collapsedMask = collapsed.mask
543 collapsedMask = np.array(num*[np.ma.nomask])
545 import matplotlib.pyplot
as plot
546 figure = plot.figure(1)
548 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
549 axes.plot(indices[~collapsedMask], collapsed[~collapsedMask],
'k+')
550 if collapsedMask.sum() > 0:
551 axes.plot(indices[collapsedMask], collapsed.data[collapsedMask],
'b+')
552 if isinstance(model, np.ndarray):
555 plotModel = np.zeros_like(indices)
557 axes.plot(indices, plotModel,
'r-')
558 plot.xlabel(
"centered/scaled position along overscan region")
559 plot.ylabel(
"pixel value/fit value")
561 plot.title(f
"{amp.getName()} DataX: "
562 f
"[{amp.getRawDataBBox().getBeginX()}:{amp.getRawBBox().getEndX()}]"
563 f
"OscanX: [{amp.getRawHorizontalOverscanBBox().getBeginX()}:"
564 f
"{amp.getRawHorizontalOverscanBBox().getEndX()}] {self.config.fitType}")
566 plot.title(
"No amp supplied.")
568 prompt =
"Press Enter or c to continue [chp]..."
570 ans = input(prompt).lower()
571 if ans
in (
"",
" ",
"c",):
580 print(
"[h]elp [c]ontinue [p]db e[x]itDebug")
def collapseArrayMedian(self, maskedArray)
def maskExtrapolated(collapsed)
def measureVectorOverscan(self, image)
def __init__(self, statControl=None, **kwargs)
def collapseArray(maskedArray)
def transpose(imageArray)
def maskOutliers(self, imageArray)
def debugView(self, image, model, amp=None)
def run(self, ampImage, overscanImage, amp=None)
def getImageArray(self, image)
def integerConvert(image)
def splineFit(self, indices, collapsed, numBins)
def splineEval(indices, interp)
def measureConstantOverscan(self, image)