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
385 interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
386 values.astype(float)[numPerBin > 0],
387 afwMath.stringToInterpStyle(self.config.fitType))
392 """Wrapper function to match spline evaluation API to polynomial fit
397 indices : `numpy.ndarray`
398 Locations to evaluate the spline.
399 interp : `lsst.afw.math.interpolate`
400 Interpolation object to use.
404 values : `numpy.ndarray`
405 Evaluated spline values at each index.
408 return interp.interpolate(indices.astype(float))
412 """Create mask if edges are extrapolated.
416 collapsed : `numpy.ma.masked_array`
417 Masked array to check the edges of.
421 maskArray : `numpy.ndarray`
422 Boolean numpy array of pixels to mask.
424 maskArray = np.full_like(collapsed, False, dtype=bool)
425 if np.ma.is_masked(collapsed):
427 for low
in range(num):
428 if not collapsed.mask[low]:
431 maskArray[:low] =
True
432 for high
in range(1, num):
433 if not collapsed.mask[-high]:
436 maskArray[-high:] =
True
440 """Calculate the 1-d vector overscan from the input overscan image.
445 Image containing the overscan data.
449 results : `lsst.pipe.base.Struct`
450 Overscan result with entries:
451 - ``overscanValue``: Overscan value to subtract (`float`)
452 - ``maskArray`` : `list` [ `bool` ]
453 List of rows that should be masked
as ``SUSPECT`` when the
454 overscan solution
is applied.
455 - ``isTransposed`` : `bool`
456 Indicates
if the overscan data was transposed during
457 calcuation, noting along which axis the overscan should be
463 calcImage, isTransposed = self.
transposetranspose(calcImage)
466 if self.config.fitType ==
'MEDIAN_PER_ROW':
473 indices = 2.0*np.arange(num)/float(num) - 1.0
477 'POLY': (poly.polynomial.polyfit, poly.polynomial.polyval),
478 'CHEB': (poly.chebyshev.chebfit, poly.chebyshev.chebval),
479 'LEG': (poly.legendre.legfit, poly.legendre.legval),
483 }[self.config.fitType]
485 coeffs = fitter(indices, collapsed, self.config.order)
486 overscanVector = evaler(indices, coeffs)
488 return pipeBase.Struct(overscanValue=np.array(overscanVector),
490 isTransposed=isTransposed)
493 """Debug display for the final overscan solution.
498 Input image the overscan solution was determined from.
499 model : `numpy.ndarray`
or `float`
500 Overscan model determined
for the image.
502 Amplifier to extract diagnostic information.
511 calcImage, isTransposed = self.
transposetranspose(calcImage)
516 indices = 2.0 * np.arange(num)/float(num) - 1.0
518 if np.ma.is_masked(collapsed):
519 collapsedMask = collapsed.mask
521 collapsedMask = np.array(num*[np.ma.nomask])
523 import matplotlib.pyplot
as plot
524 figure = plot.figure(1)
526 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
527 axes.plot(indices[~collapsedMask], collapsed[~collapsedMask],
'k+')
528 if collapsedMask.sum() > 0:
529 axes.plot(indices[collapsedMask], collapsed.data[collapsedMask],
'b+')
530 if isinstance(model, np.ndarray):
533 plotModel = np.zeros_like(indices)
535 axes.plot(indices, plotModel,
'r-')
536 plot.xlabel(
"centered/scaled position along overscan region")
537 plot.ylabel(
"pixel value/fit value")
539 plot.title(f
"{amp.getName()} DataX: "
540 f
"[{amp.getRawDataBBox().getBeginX()}:{amp.getRawBBox().getEndX()}]"
541 f
"OscanX: [{amp.getRawHorizontalOverscanBBox().getBeginX()}:"
542 f
"{amp.getRawHorizontalOverscanBBox().getEndX()}] {self.config.fitType}")
544 plot.title(
"No amp supplied.")
546 prompt =
"Press Enter or c to continue [chp]..."
548 ans = input(prompt).lower()
549 if ans
in (
"",
" ",
"c",):
558 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)