Coverage for python/lsst/ip/isr/overscan.py: 12%
291 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-17 10:27 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-17 10:27 +0000
1# This file is part of ip_isr.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22__all__ = ["OverscanCorrectionTaskConfig", "OverscanCorrectionTask"]
24import numpy as np
25import lsst.afw.math as afwMath
26import lsst.afw.image as afwImage
27import lsst.geom as geom
28import lsst.pipe.base as pipeBase
29import lsst.pex.config as pexConfig
31from .isr import fitOverscanImage
32from .isrFunctions import makeThresholdMask, countMaskedPixels
35class OverscanCorrectionTaskConfig(pexConfig.Config):
36 """Overscan correction options.
37 """
38 fitType = pexConfig.ChoiceField(
39 dtype=str,
40 doc="The method for fitting the overscan bias level.",
41 default='MEDIAN',
42 allowed={
43 "POLY": "Fit ordinary polynomial to the longest axis of the overscan region",
44 "CHEB": "Fit Chebyshev polynomial to the longest axis of the overscan region",
45 "LEG": "Fit Legendre polynomial to the longest axis of the overscan region",
46 "NATURAL_SPLINE": "Fit natural spline to the longest axis of the overscan region",
47 "CUBIC_SPLINE": "Fit cubic spline to the longest axis of the overscan region",
48 "AKIMA_SPLINE": "Fit Akima spline to the longest axis of the overscan region",
49 "MEAN": "Correct using the mean of the overscan region",
50 "MEANCLIP": "Correct using a clipped mean of the overscan region",
51 "MEDIAN": "Correct using the median of the overscan region",
52 "MEDIAN_PER_ROW": "Correct using the median per row of the overscan region",
53 },
54 )
55 order = pexConfig.Field(
56 dtype=int,
57 doc=("Order of polynomial to fit if overscan fit type is a polynomial, "
58 "or number of spline knots if overscan fit type is a spline."),
59 default=1,
60 )
61 numSigmaClip = pexConfig.Field(
62 dtype=float,
63 doc="Rejection threshold (sigma) for collapsing overscan before fit",
64 default=3.0,
65 )
66 maskPlanes = pexConfig.ListField(
67 dtype=str,
68 doc="Mask planes to reject when measuring overscan",
69 default=['BAD', 'SAT'],
70 )
71 overscanIsInt = pexConfig.Field(
72 dtype=bool,
73 doc="Treat overscan as an integer image for purposes of fitType=MEDIAN"
74 " and fitType=MEDIAN_PER_ROW.",
75 default=True,
76 )
78 doParallelOverscan = pexConfig.Field(
79 dtype=bool,
80 doc="Correct using parallel overscan after serial overscan correction?",
81 default=False,
82 )
83 parallelOverscanMaskThreshold = pexConfig.RangeField(
84 dtype=float,
85 doc="Minimum fraction of pixels in parallel overscan region necessary "
86 "for parallel overcan correction.",
87 default=0.1,
88 min=0.0,
89 max=1.0,
90 inclusiveMin=True,
91 inclusiveMax=True,
92 )
94 leadingColumnsToSkip = pexConfig.Field(
95 dtype=int,
96 doc="Number of leading columns to skip in serial overscan correction.",
97 default=0,
98 )
99 trailingColumnsToSkip = pexConfig.Field(
100 dtype=int,
101 doc="Number of trailing columns to skip in serial overscan correction.",
102 default=0,
103 )
104 leadingRowsToSkip = pexConfig.Field(
105 dtype=int,
106 doc="Number of leading rows to skip in parallel overscan correction.",
107 default=0,
108 )
109 trailingRowsToSkip = pexConfig.Field(
110 dtype=int,
111 doc="Number of trailing rows to skip in parallel overscan correction.",
112 default=0,
113 )
115 maxDeviation = pexConfig.Field( 115 ↛ exitline 115 didn't jump to the function exit
116 dtype=float,
117 doc="Maximum deviation from median (in ADU) to mask in overscan correction.",
118 default=1000.0, check=lambda x: x > 0,
119 )
122class OverscanCorrectionTask(pipeBase.Task):
123 """Correction task for overscan.
125 This class contains a number of utilities that are easier to
126 understand and use when they are not embedded in nested if/else
127 loops.
129 Parameters
130 ----------
131 statControl : `lsst.afw.math.StatisticsControl`, optional
132 Statistics control object.
133 """
134 ConfigClass = OverscanCorrectionTaskConfig
135 _DefaultName = "overscan"
137 def __init__(self, statControl=None, **kwargs):
138 super().__init__(**kwargs)
139 self.allowDebug = True
141 if statControl:
142 self.statControl = statControl
143 else:
144 self.statControl = afwMath.StatisticsControl()
145 self.statControl.setNumSigmaClip(self.config.numSigmaClip)
146 self.statControl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.maskPlanes))
148 def run(self, exposure, amp, isTransposed=False):
149 """Measure and remove an overscan from an amplifier image.
151 Parameters
152 ----------
153 exposure : `lsst.afw.image.Exposure`
154 Image data that will have the overscan corrections applied.
155 amp : `lsst.afw.cameraGeom.Amplifier`
156 Amplifier to use for debugging purposes.
157 isTransposed : `bool`, optional
158 Is the image transposed, such that serial and parallel
159 overscan regions are reversed? Default is False.
161 Returns
162 -------
163 overscanResults : `lsst.pipe.base.Struct`
164 Result struct with components:
166 ``imageFit``
167 Value or fit subtracted from the amplifier image data
168 (scalar or `lsst.afw.image.Image`).
169 ``overscanFit``
170 Value or fit subtracted from the serial overscan image
171 data (scalar or `lsst.afw.image.Image`).
172 ``overscanImage``
173 Image of the serial overscan region with the serial
174 overscan correction applied
175 (`lsst.afw.image.Image`). This quantity is used to
176 estimate the amplifier read noise empirically.
177 ``parallelOverscanFit``
178 Value or fit subtracted from the parallel overscan
179 image data (scalar, `lsst.afw.image.Image`, or None).
180 ``parallelOverscanImage``
181 Image of the parallel overscan region with the
182 parallel overscan correction applied
183 (`lsst.afw.image.Image` or None).
185 Raises
186 ------
187 RuntimeError
188 Raised if an invalid overscan type is set.
189 """
190 # Do Serial overscan first.
191 serialOverscanBBox = amp.getRawSerialOverscanBBox()
192 imageBBox = amp.getRawDataBBox()
194 if self.config.doParallelOverscan:
195 # We need to extend the serial overscan BBox to the full
196 # size of the detector.
197 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
198 imageBBox = imageBBox.expandedTo(parallelOverscanBBox)
200 serialOverscanBBox = geom.Box2I(geom.Point2I(serialOverscanBBox.getMinX(),
201 imageBBox.getMinY()),
202 geom.Extent2I(serialOverscanBBox.getWidth(),
203 imageBBox.getHeight()))
204 serialResults = self.correctOverscan(exposure, amp,
205 imageBBox, serialOverscanBBox, isTransposed=isTransposed)
206 overscanMean = serialResults.overscanMean
207 overscanSigma = serialResults.overscanSigma
208 residualMean = serialResults.overscanMeanResidual
209 residualSigma = serialResults.overscanSigmaResidual
211 # Do Parallel Overscan
212 parallelResults = None
213 if self.config.doParallelOverscan:
214 # This does not need any extensions, as we'll only
215 # subtract it from the data region.
216 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
217 imageBBox = amp.getRawDataBBox()
219 maskIm = exposure.getMaskedImage()
220 maskIm = maskIm.Factory(maskIm, parallelOverscanBBox)
221 makeThresholdMask(maskIm, threshold=self.config.numSigmaClip, growFootprints=0)
222 maskPix = countMaskedPixels(maskIm, self.config.maskPlanes)
223 xSize, ySize = parallelOverscanBBox.getDimensions()
224 if maskPix > xSize*ySize*self.config.parallelOverscanMaskThreshold:
225 self.log.warning('Fraction of masked pixels for parallel overscan calculation larger'
226 ' than %f of total pixels (i.e. %f masked pixels) on amp %s.',
227 self.config.parallelOverscanMaskThreshold, maskPix, amp.getName())
228 self.log.warning('Not doing parallel overscan correction.')
229 else:
230 parallelResults = self.correctOverscan(exposure, amp,
231 imageBBox, parallelOverscanBBox,
232 isTransposed=not isTransposed)
234 overscanMean = (overscanMean, parallelResults.overscanMean)
235 overscanSigma = (overscanSigma, parallelResults.overscanSigma)
236 residualMean = (residualMean, parallelResults.overscanMeanResidual)
237 residualSigma = (residualSigma, parallelResults.overscanSigmaResidual)
238 parallelOverscanFit = parallelResults.overscanOverscanModel if parallelResults else None
239 parallelOverscanImage = parallelResults.overscanImage if parallelResults else None
241 return pipeBase.Struct(imageFit=serialResults.ampOverscanModel,
242 overscanFit=serialResults.overscanOverscanModel,
243 overscanImage=serialResults.overscanImage,
245 parallelOverscanFit=parallelOverscanFit,
246 parallelOverscanImage=parallelOverscanImage,
247 overscanMean=overscanMean,
248 overscanSigma=overscanSigma,
249 residualMean=residualMean,
250 residualSigma=residualSigma)
252 def correctOverscan(self, exposure, amp, imageBBox, overscanBBox, isTransposed=True):
253 """
254 """
255 overscanBox = self.trimOverscan(exposure, amp, overscanBBox,
256 self.config.leadingColumnsToSkip,
257 self.config.trailingColumnsToSkip,
258 transpose=isTransposed)
259 overscanImage = exposure[overscanBox].getMaskedImage()
260 overscanArray = overscanImage.image.array
262 # Mask pixels.
263 maskVal = overscanImage.mask.getPlaneBitMask(self.config.maskPlanes)
264 overscanMask = ~((overscanImage.mask.array & maskVal) == 0)
266 median = np.ma.median(np.ma.masked_where(overscanMask, overscanArray))
267 bad = np.where(np.abs(overscanArray - median) > self.config.maxDeviation)
268 overscanMask[bad] = overscanImage.mask.getPlaneBitMask("SAT")
270 # Do overscan fit.
271 # CZW: Handle transposed correctly.
272 overscanResults = self.fitOverscan(overscanImage, isTransposed=isTransposed)
274 # Correct image region (and possibly parallel-overscan region).
275 ampImage = exposure[imageBBox]
276 ampOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue,
277 ampImage.image.array,
278 transpose=isTransposed)
279 ampImage.image.array -= ampOverscanModel
281 # Correct overscan region (and possibly doubly-overscaned
282 # region).
283 overscanImage = exposure[overscanBBox]
284 # CZW: Transposed?
285 overscanOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue,
286 overscanImage.image.array)
287 overscanImage.image.array -= overscanOverscanModel
289 self.debugView(overscanImage, overscanResults.overscanValue, amp)
291 # Find residual fit statistics.
292 stats = afwMath.makeStatistics(overscanImage.getMaskedImage(),
293 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl)
294 residualMean = stats.getValue(afwMath.MEDIAN)
295 residualSigma = stats.getValue(afwMath.STDEVCLIP)
297 return pipeBase.Struct(ampOverscanModel=ampOverscanModel,
298 overscanOverscanModel=overscanOverscanModel,
299 overscanImage=overscanImage,
300 overscanValue=overscanResults.overscanValue,
302 overscanMean=overscanResults.overscanMean,
303 overscanSigma=overscanResults.overscanSigma,
304 overscanMeanResidual=residualMean,
305 overscanSigmaResidual=residualSigma
306 )
308 def broadcastFitToImage(self, overscanValue, imageArray, transpose=False):
309 """Broadcast 0 or 1 dimension fit to appropriate shape.
311 Parameters
312 ----------
313 overscanValue : `numpy.ndarray`, (Nrows, ) or scalar
314 Overscan fit to broadcast.
315 imageArray : `numpy.ndarray`, (Nrows, Ncols)
316 Image array that we want to match.
317 transpose : `bool`, optional
318 Switch order to broadcast along the other axis.
320 Returns
321 -------
322 overscanModel : `numpy.ndarray`, (Nrows, Ncols) or scalar
323 Expanded overscan fit.
325 Raises
326 ------
327 RuntimeError
328 Raised if no axis has the appropriate dimension.
329 """
330 if isinstance(overscanValue, np.ndarray):
331 overscanModel = np.zeros_like(imageArray)
333 if transpose is False:
334 if imageArray.shape[0] == overscanValue.shape[0]:
335 overscanModel[:, :] = overscanValue[:, np.newaxis]
336 elif imageArray.shape[1] == overscanValue.shape[0]:
337 overscanModel[:, :] = overscanValue[np.newaxis, :]
338 elif imageArray.shape[0] == overscanValue.shape[1]:
339 overscanModel[:, :] = overscanValue[np.newaxis, :]
340 else:
341 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to "
342 f"match {imageArray.shape}")
343 else:
344 if imageArray.shape[1] == overscanValue.shape[0]:
345 overscanModel[:, :] = overscanValue[np.newaxis, :]
346 elif imageArray.shape[0] == overscanValue.shape[0]:
347 overscanModel[:, :] = overscanValue[:, np.newaxis]
348 elif imageArray.shape[1] == overscanValue.shape[1]:
349 overscanModel[:, :] = overscanValue[:, np.newaxis]
350 else:
351 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to "
352 f"match {imageArray.shape}")
353 else:
354 overscanModel = overscanValue
356 return overscanModel
358 def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False):
359 """Trim overscan region to remove edges.
361 Parameters
362 ----------
363 exposure : `lsst.afw.image.Exposure`
364 Exposure containing data.
365 amp : `lsst.afw.cameraGeom.Amplifier`
366 Amplifier containing geometry information.
367 bbox : `lsst.geom.Box2I`
368 Bounding box of the overscan region.
369 skipLeading : `int`
370 Number of leading (towards data region) rows/columns to skip.
371 skipTrailing : `int`
372 Number of trailing (away from data region) rows/columns to skip.
373 transpose : `bool`, optional
374 Operate on the transposed array.
376 Returns
377 -------
378 overscanArray : `numpy.array`, (N, M)
379 Data array to fit.
380 overscanMask : `numpy.array`, (N, M)
381 Data mask.
382 """
383 dx0, dy0, dx1, dy1 = (0, 0, 0, 0)
384 dataBBox = amp.getRawDataBBox()
385 if transpose:
386 if dataBBox.getBeginY() < bbox.getBeginY():
387 dy0 += skipLeading
388 dy1 -= skipTrailing
389 else:
390 dy0 += skipTrailing
391 dy1 -= skipLeading
392 else:
393 if dataBBox.getBeginX() < bbox.getBeginX():
394 dx0 += skipLeading
395 dx1 -= skipTrailing
396 else:
397 dx0 += skipTrailing
398 dx1 -= skipLeading
400 overscanBBox = geom.Box2I(bbox.getBegin() + geom.Extent2I(dx0, dy0),
401 geom.Extent2I(bbox.getWidth() - dx0 + dx1,
402 bbox.getHeight() - dy0 + dy1))
403 return overscanBBox
405 def fitOverscan(self, overscanImage, isTransposed=False):
406 if self.config.fitType in ('MEAN', 'MEANCLIP', 'MEDIAN'):
407 # Transposition has no effect here.
408 overscanResult = self.measureConstantOverscan(overscanImage)
409 overscanValue = overscanResult.overscanValue
410 overscanMean = overscanValue
411 overscanSigma = 0.0
412 elif self.config.fitType in ('MEDIAN_PER_ROW', 'POLY', 'CHEB', 'LEG',
413 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'):
414 # Force transposes as needed
415 overscanResult = self.measureVectorOverscan(overscanImage, isTransposed)
416 overscanValue = overscanResult.overscanValue
418 stats = afwMath.makeStatistics(overscanResult.overscanValue,
419 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl)
420 overscanMean = stats.getValue(afwMath.MEDIAN)
421 overscanSigma = stats.getValue(afwMath.STDEVCLIP)
422 else:
423 raise ValueError('%s : %s an invalid overscan type' %
424 ("overscanCorrection", self.config.fitType))
426 return pipeBase.Struct(overscanValue=overscanValue,
427 overscanMean=overscanMean,
428 overscanSigma=overscanSigma,
429 )
431 @staticmethod
432 def integerConvert(image):
433 """Return an integer version of the input image.
435 Parameters
436 ----------
437 image : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
438 Image to convert to integers.
440 Returns
441 -------
442 outI : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
443 The integer converted image.
445 Raises
446 ------
447 RuntimeError
448 Raised if the input image could not be converted.
449 """
450 if hasattr(image, "image"):
451 # Is a maskedImage:
452 imageI = image.image.convertI()
453 outI = afwImage.MaskedImageI(imageI, image.mask, image.variance)
454 elif hasattr(image, "convertI"):
455 # Is an Image:
456 outI = image.convertI()
457 elif hasattr(image, "astype"):
458 # Is a numpy array:
459 outI = image.astype(int)
460 else:
461 raise RuntimeError("Could not convert this to integers: %s %s %s",
462 image, type(image), dir(image))
463 return outI
465 # Constant methods
466 def measureConstantOverscan(self, image):
467 """Measure a constant overscan value.
469 Parameters
470 ----------
471 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
472 Image data to measure the overscan from.
474 Returns
475 -------
476 results : `lsst.pipe.base.Struct`
477 Overscan result with entries:
478 - ``overscanValue``: Overscan value to subtract (`float`)
479 - ``isTransposed``: Orientation of the overscan (`bool`)
480 """
481 if self.config.fitType == 'MEDIAN':
482 calcImage = self.integerConvert(image)
483 else:
484 calcImage = image
485 fitType = afwMath.stringToStatisticsProperty(self.config.fitType)
486 overscanValue = afwMath.makeStatistics(calcImage, fitType, self.statControl).getValue()
488 return pipeBase.Struct(overscanValue=overscanValue,
489 isTransposed=False)
491 # Vector correction utilities
492 def getImageArray(self, image):
493 """Extract the numpy array from the input image.
495 Parameters
496 ----------
497 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
498 Image data to pull array from.
500 calcImage : `numpy.ndarray`
501 Image data array for numpy operating.
502 """
503 if hasattr(image, "getImage"):
504 calcImage = image.getImage().getArray()
505 calcImage = np.ma.masked_where(image.getMask().getArray() & self.statControl.getAndMask(),
506 calcImage)
507 else:
508 calcImage = image.getArray()
509 return calcImage
511 def maskOutliers(self, imageArray):
512 """Mask outliers in a row of overscan data from a robust sigma
513 clipping procedure.
515 Parameters
516 ----------
517 imageArray : `numpy.ndarray`
518 Image to filter along numpy axis=1.
520 Returns
521 -------
522 maskedArray : `numpy.ma.masked_array`
523 Masked image marking outliers.
524 """
525 lq, median, uq = np.percentile(imageArray, [25.0, 50.0, 75.0], axis=1)
526 axisMedians = median
527 axisStdev = 0.74*(uq - lq) # robust stdev
529 diff = np.abs(imageArray - axisMedians[:, np.newaxis])
530 return np.ma.masked_where(diff > self.statControl.getNumSigmaClip()
531 * axisStdev[:, np.newaxis], imageArray)
533 @staticmethod
534 def collapseArray(maskedArray):
535 """Collapse overscan array (and mask) to a 1-D vector of values.
537 Parameters
538 ----------
539 maskedArray : `numpy.ma.masked_array`
540 Masked array of input overscan data.
542 Returns
543 -------
544 collapsed : `numpy.ma.masked_array`
545 Single dimensional overscan data, combined with the mean.
546 """
547 collapsed = np.mean(maskedArray, axis=1)
548 if collapsed.mask.sum() > 0:
549 collapsed.data[collapsed.mask] = np.mean(maskedArray.data[collapsed.mask], axis=1)
550 return collapsed
552 def collapseArrayMedian(self, maskedArray):
553 """Collapse overscan array (and mask) to a 1-D vector of using the
554 correct integer median of row-values.
556 Parameters
557 ----------
558 maskedArray : `numpy.ma.masked_array`
559 Masked array of input overscan data.
561 Returns
562 -------
563 collapsed : `numpy.ma.masked_array`
564 Single dimensional overscan data, combined with the afwMath median.
565 """
566 integerMI = self.integerConvert(maskedArray)
568 collapsed = []
569 fitType = afwMath.stringToStatisticsProperty('MEDIAN')
570 for row in integerMI:
571 newRow = row.compressed()
572 if len(newRow) > 0:
573 rowMedian = afwMath.makeStatistics(newRow, fitType, self.statControl).getValue()
574 else:
575 rowMedian = np.nan
576 collapsed.append(rowMedian)
578 return np.array(collapsed)
580 def splineFit(self, indices, collapsed, numBins):
581 """Wrapper function to match spline fit API to polynomial fit API.
583 Parameters
584 ----------
585 indices : `numpy.ndarray`
586 Locations to evaluate the spline.
587 collapsed : `numpy.ndarray`
588 Collapsed overscan values corresponding to the spline
589 evaluation points.
590 numBins : `int`
591 Number of bins to use in constructing the spline.
593 Returns
594 -------
595 interp : `lsst.afw.math.Interpolate`
596 Interpolation object for later evaluation.
597 """
598 if not np.ma.is_masked(collapsed):
599 collapsed.mask = np.array(len(collapsed)*[np.ma.nomask])
601 numPerBin, binEdges = np.histogram(indices, bins=numBins,
602 weights=1 - collapsed.mask.astype(int))
603 with np.errstate(invalid="ignore"):
604 values = np.histogram(indices, bins=numBins,
605 weights=collapsed.data*~collapsed.mask)[0]/numPerBin
606 binCenters = np.histogram(indices, bins=numBins,
607 weights=indices*~collapsed.mask)[0]/numPerBin
609 if len(binCenters[numPerBin > 0]) < 5:
610 self.log.warn("Cannot do spline fitting for overscan: %s valid points.",
611 len(binCenters[numPerBin > 0]))
612 # Return a scalar value if we have one, otherwise
613 # return zero. This amplifier is hopefully already
614 # masked.
615 if len(values[numPerBin > 0]) != 0:
616 return float(values[numPerBin > 0][0])
617 else:
618 return 0.0
620 interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
621 values.astype(float)[numPerBin > 0],
622 afwMath.stringToInterpStyle(self.config.fitType))
623 return interp
625 @staticmethod
626 def splineEval(indices, interp):
627 """Wrapper function to match spline evaluation API to polynomial fit
628 API.
630 Parameters
631 ----------
632 indices : `numpy.ndarray`
633 Locations to evaluate the spline.
634 interp : `lsst.afw.math.interpolate`
635 Interpolation object to use.
637 Returns
638 -------
639 values : `numpy.ndarray`
640 Evaluated spline values at each index.
641 """
643 return interp.interpolate(indices.astype(float))
645 @staticmethod
646 def maskExtrapolated(collapsed):
647 """Create mask if edges are extrapolated.
649 Parameters
650 ----------
651 collapsed : `numpy.ma.masked_array`
652 Masked array to check the edges of.
654 Returns
655 -------
656 maskArray : `numpy.ndarray`
657 Boolean numpy array of pixels to mask.
658 """
659 maskArray = np.full_like(collapsed, False, dtype=bool)
660 if np.ma.is_masked(collapsed):
661 num = len(collapsed)
662 for low in range(num):
663 if not collapsed.mask[low]:
664 break
665 if low > 0:
666 maskArray[:low] = True
667 for high in range(1, num):
668 if not collapsed.mask[-high]:
669 break
670 if high > 1:
671 maskArray[-high:] = True
672 return maskArray
674 def measureVectorOverscan(self, image, isTransposed=False):
675 """Calculate the 1-d vector overscan from the input overscan image.
677 Parameters
678 ----------
679 image : `lsst.afw.image.MaskedImage`
680 Image containing the overscan data.
681 isTransposed : `bool`
682 If true, the image has been transposed.
684 Returns
685 -------
686 results : `lsst.pipe.base.Struct`
687 Overscan result with entries:
689 ``overscanValue``
690 Overscan value to subtract (`float`)
691 ``maskArray``
692 List of rows that should be masked as ``SUSPECT`` when the
693 overscan solution is applied. (`list` [ `bool` ])
694 ``isTransposed``
695 Indicates if the overscan data was transposed during
696 calcuation, noting along which axis the overscan should be
697 subtracted. (`bool`)
698 """
699 calcImage = self.getImageArray(image)
701 # operate on numpy-arrays from here
702 if isTransposed:
703 calcImage = np.transpose(calcImage)
704 masked = self.maskOutliers(calcImage)
706 if self.config.fitType == 'MEDIAN_PER_ROW':
707 mi = afwImage.MaskedImageI(image.getBBox())
708 masked = masked.astype(int)
709 if isTransposed:
710 masked = masked.transpose()
712 mi.image.array[:, :] = masked.data[:, :]
713 if bool(masked.mask.shape):
714 mi.mask.array[:, :] = masked.mask[:, :]
716 overscanVector = fitOverscanImage(mi, self.config.maskPlanes, isTransposed)
717 maskArray = self.maskExtrapolated(overscanVector)
718 else:
719 collapsed = self.collapseArray(masked)
721 num = len(collapsed)
722 indices = 2.0*np.arange(num)/float(num) - 1.0
724 poly = np.polynomial
725 fitter, evaler = {
726 'POLY': (poly.polynomial.polyfit, poly.polynomial.polyval),
727 'CHEB': (poly.chebyshev.chebfit, poly.chebyshev.chebval),
728 'LEG': (poly.legendre.legfit, poly.legendre.legval),
729 'NATURAL_SPLINE': (self.splineFit, self.splineEval),
730 'CUBIC_SPLINE': (self.splineFit, self.splineEval),
731 'AKIMA_SPLINE': (self.splineFit, self.splineEval)
732 }[self.config.fitType]
734 # These are the polynomial coefficients, or an
735 # interpolation object.
736 coeffs = fitter(indices, collapsed, self.config.order)
738 if isinstance(coeffs, float):
739 self.log.warn("Using fallback value %f due to fitter failure. Amplifier will be masked.",
740 coeffs)
741 overscanVector = np.full_like(indices, coeffs)
742 maskArray = np.full_like(collapsed, True, dtype=bool)
743 else:
744 # Otherwise we can just use things as normal.
745 overscanVector = evaler(indices, coeffs)
746 maskArray = self.maskExtrapolated(collapsed)
748 return pipeBase.Struct(overscanValue=np.array(overscanVector),
749 maskArray=maskArray,
750 isTransposed=isTransposed)
752 def debugView(self, image, model, amp=None):
753 """Debug display for the final overscan solution.
755 Parameters
756 ----------
757 image : `lsst.afw.image.Image`
758 Input image the overscan solution was determined from.
759 model : `numpy.ndarray` or `float`
760 Overscan model determined for the image.
761 amp : `lsst.afw.cameraGeom.Amplifier`, optional
762 Amplifier to extract diagnostic information.
763 """
764 import lsstDebug
765 if not lsstDebug.Info(__name__).display:
766 return
767 if not self.allowDebug:
768 return
770 calcImage = self.getImageArray(image)
771 # CZW: Check that this is ok
772 calcImage = np.transpose(calcImage)
773 masked = self.maskOutliers(calcImage)
774 collapsed = self.collapseArray(masked)
776 num = len(collapsed)
777 indices = 2.0 * np.arange(num)/float(num) - 1.0
779 if np.ma.is_masked(collapsed):
780 collapsedMask = collapsed.mask
781 else:
782 collapsedMask = np.array(num*[np.ma.nomask])
784 import matplotlib.pyplot as plot
785 figure = plot.figure(1)
786 figure.clear()
787 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
788 axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 'k+')
789 if collapsedMask.sum() > 0:
790 axes.plot(indices[collapsedMask], collapsed.data[collapsedMask], 'b+')
791 if isinstance(model, np.ndarray):
792 plotModel = model
793 else:
794 plotModel = np.zeros_like(indices)
795 plotModel += model
796 axes.plot(indices, plotModel, 'r-')
797 plot.xlabel("centered/scaled position along overscan region")
798 plot.ylabel("pixel value/fit value")
799 if amp:
800 plot.title(f"{amp.getName()} DataX: "
801 f"[{amp.getRawDataBBox().getBeginX()}:{amp.getRawBBox().getEndX()}]"
802 f"OscanX: [{amp.getRawHorizontalOverscanBBox().getBeginX()}:"
803 f"{amp.getRawHorizontalOverscanBBox().getEndX()}] {self.config.fitType}")
804 else:
805 plot.title("No amp supplied.")
806 figure.show()
807 prompt = "Press Enter or c to continue [chp]..."
808 while True:
809 ans = input(prompt).lower()
810 if ans in ("", " ", "c",):
811 break
812 elif ans in ("p", ):
813 import pdb
814 pdb.set_trace()
815 elif ans in ('x', ):
816 self.allowDebug = False
817 break
818 elif ans in ("h", ):
819 print("[h]elp [c]ontinue [p]db e[x]itDebug")
820 plot.close()