Coverage for python/lsst/ip/isr/overscan.py: 12%
288 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-20 02:41 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-20 02:41 -0700
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 overscan image data
171 (scalar or `lsst.afw.image.Image`).
172 ``overscanImage``
173 Image of the overscan region with the overscan
174 correction applied (`lsst.afw.image.Image`). This
175 quantity is used to estimate the amplifier read noise
176 empirically.
178 Raises
179 ------
180 RuntimeError
181 Raised if an invalid overscan type is set.
182 """
183 # Do Serial overscan first.
184 serialOverscanBBox = amp.getRawSerialOverscanBBox()
185 imageBBox = amp.getRawDataBBox()
187 if self.config.doParallelOverscan:
188 # We need to extend the serial overscan BBox to the full
189 # size of the detector.
190 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
191 imageBBox = imageBBox.expandedTo(parallelOverscanBBox)
193 serialOverscanBBox = geom.Box2I(geom.Point2I(serialOverscanBBox.getMinX(),
194 imageBBox.getMinY()),
195 geom.Extent2I(serialOverscanBBox.getWidth(),
196 imageBBox.getHeight()))
197 serialResults = self.correctOverscan(exposure, amp,
198 imageBBox, serialOverscanBBox, isTransposed=isTransposed)
199 overscanMean = serialResults.overscanMean
200 overscanSigma = serialResults.overscanSigma
201 residualMean = serialResults.overscanMeanResidual
202 residualSigma = serialResults.overscanSigmaResidual
204 # Do Parallel Overscan
205 if self.config.doParallelOverscan:
206 # This does not need any extensions, as we'll only
207 # subtract it from the data region.
208 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
209 imageBBox = amp.getRawDataBBox()
211 maskIm = exposure.getMaskedImage()
212 maskIm = maskIm.Factory(maskIm, parallelOverscanBBox)
213 makeThresholdMask(maskIm, threshold=self.config.numSigmaClip, growFootprints=0)
214 maskPix = countMaskedPixels(maskIm, self.config.maskPlanes)
215 xSize, ySize = parallelOverscanBBox.getDimensions()
216 if maskPix > xSize*ySize*self.config.parallelOverscanMaskThreshold:
217 self.log.warning('Fraction of masked pixels for parallel overscan calculation larger'
218 ' than %f of total pixels (i.e. %f masked pixels) on amp %s.',
219 self.config.parallelOverscanMaskThreshold, maskPix, amp.getName())
220 self.log.warning('Not doing parallel overscan correction.')
221 else:
222 parallelResults = self.correctOverscan(exposure, amp,
223 imageBBox, parallelOverscanBBox,
224 isTransposed=not isTransposed)
226 overscanMean = (overscanMean, parallelResults.overscanMean)
227 overscanSigma = (overscanSigma, parallelResults.overscanSigma)
228 residualMean = (residualMean, parallelResults.overscanMeanResidual)
229 residualSigma = (residualSigma, parallelResults.overscanSigmaResidual)
231 return pipeBase.Struct(imageFit=serialResults.ampOverscanModel,
232 overscanFit=serialResults.overscanOverscanModel,
233 overscanImage=serialResults.overscanImage,
235 overscanMean=overscanMean,
236 overscanSigma=overscanSigma,
237 residualMean=residualMean,
238 residualSigma=residualSigma)
240 def correctOverscan(self, exposure, amp, imageBBox, overscanBBox, isTransposed=True):
241 """
242 """
243 overscanBox = self.trimOverscan(exposure, amp, overscanBBox,
244 self.config.leadingColumnsToSkip,
245 self.config.trailingColumnsToSkip,
246 transpose=isTransposed)
247 overscanImage = exposure[overscanBox].getMaskedImage()
248 overscanArray = overscanImage.image.array
250 # Mask pixels.
251 maskVal = overscanImage.mask.getPlaneBitMask(self.config.maskPlanes)
252 overscanMask = ~((overscanImage.mask.array & maskVal) == 0)
254 median = np.ma.median(np.ma.masked_where(overscanMask, overscanArray))
255 bad = np.where(np.abs(overscanArray - median) > self.config.maxDeviation)
256 overscanMask[bad] = overscanImage.mask.getPlaneBitMask("SAT")
258 # Do overscan fit.
259 # CZW: Handle transposed correctly.
260 overscanResults = self.fitOverscan(overscanImage, isTransposed=isTransposed)
262 # Correct image region (and possibly parallel-overscan region).
263 ampImage = exposure[imageBBox]
264 ampOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue,
265 ampImage.image.array,
266 transpose=isTransposed)
267 ampImage.image.array -= ampOverscanModel
269 # Correct overscan region (and possibly doubly-overscaned
270 # region).
271 overscanImage = exposure[overscanBBox]
272 # CZW: Transposed?
273 overscanOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue,
274 overscanImage.image.array)
275 overscanImage.image.array -= overscanOverscanModel
277 self.debugView(overscanImage, overscanResults.overscanValue, amp)
279 # Find residual fit statistics.
280 stats = afwMath.makeStatistics(overscanImage.getMaskedImage(),
281 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl)
282 residualMean = stats.getValue(afwMath.MEDIAN)
283 residualSigma = stats.getValue(afwMath.STDEVCLIP)
285 return pipeBase.Struct(ampOverscanModel=ampOverscanModel,
286 overscanOverscanModel=overscanOverscanModel,
287 overscanImage=overscanImage,
288 overscanValue=overscanResults.overscanValue,
290 overscanMean=overscanResults.overscanMean,
291 overscanSigma=overscanResults.overscanSigma,
292 overscanMeanResidual=residualMean,
293 overscanSigmaResidual=residualSigma
294 )
296 def broadcastFitToImage(self, overscanValue, imageArray, transpose=False):
297 """Broadcast 0 or 1 dimension fit to appropriate shape.
299 Parameters
300 ----------
301 overscanValue : `numpy.ndarray`, (Nrows, ) or scalar
302 Overscan fit to broadcast.
303 imageArray : `numpy.ndarray`, (Nrows, Ncols)
304 Image array that we want to match.
305 transpose : `bool`, optional
306 Switch order to broadcast along the other axis.
308 Returns
309 -------
310 overscanModel : `numpy.ndarray`, (Nrows, Ncols) or scalar
311 Expanded overscan fit.
313 Raises
314 ------
315 RuntimeError
316 Raised if no axis has the appropriate dimension.
317 """
318 if isinstance(overscanValue, np.ndarray):
319 overscanModel = np.zeros_like(imageArray)
321 if transpose is False:
322 if imageArray.shape[0] == overscanValue.shape[0]:
323 overscanModel[:, :] = overscanValue[:, np.newaxis]
324 elif imageArray.shape[1] == overscanValue.shape[0]:
325 overscanModel[:, :] = overscanValue[np.newaxis, :]
326 elif imageArray.shape[0] == overscanValue.shape[1]:
327 overscanModel[:, :] = overscanValue[np.newaxis, :]
328 else:
329 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to "
330 f"match {imageArray.shape}")
331 else:
332 if imageArray.shape[1] == overscanValue.shape[0]:
333 overscanModel[:, :] = overscanValue[np.newaxis, :]
334 elif imageArray.shape[0] == overscanValue.shape[0]:
335 overscanModel[:, :] = overscanValue[:, np.newaxis]
336 elif imageArray.shape[1] == overscanValue.shape[1]:
337 overscanModel[:, :] = overscanValue[:, np.newaxis]
338 else:
339 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to "
340 f"match {imageArray.shape}")
341 else:
342 overscanModel = overscanValue
344 return overscanModel
346 def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False):
347 """Trim overscan region to remove edges.
349 Parameters
350 ----------
351 exposure : `lsst.afw.image.Exposure`
352 Exposure containing data.
353 amp : `lsst.afw.cameraGeom.Amplifier`
354 Amplifier containing geometry information.
355 bbox : `lsst.geom.Box2I`
356 Bounding box of the overscan region.
357 skipLeading : `int`
358 Number of leading (towards data region) rows/columns to skip.
359 skipTrailing : `int`
360 Number of trailing (away from data region) rows/columns to skip.
361 transpose : `bool`, optional
362 Operate on the transposed array.
364 Returns
365 -------
366 overscanArray : `numpy.array`, (N, M)
367 Data array to fit.
368 overscanMask : `numpy.array`, (N, M)
369 Data mask.
370 """
371 dx0, dy0, dx1, dy1 = (0, 0, 0, 0)
372 dataBBox = amp.getRawDataBBox()
373 if transpose:
374 if dataBBox.getBeginY() < bbox.getBeginY():
375 dy0 += skipLeading
376 dy1 -= skipTrailing
377 else:
378 dy0 += skipTrailing
379 dy1 -= skipLeading
380 else:
381 if dataBBox.getBeginX() < bbox.getBeginX():
382 dx0 += skipLeading
383 dx1 -= skipTrailing
384 else:
385 dx0 += skipTrailing
386 dx1 -= skipLeading
388 overscanBBox = geom.Box2I(bbox.getBegin() + geom.Extent2I(dx0, dy0),
389 geom.Extent2I(bbox.getWidth() - dx0 + dx1,
390 bbox.getHeight() - dy0 + dy1))
391 return overscanBBox
393 def fitOverscan(self, overscanImage, isTransposed=False):
394 if self.config.fitType in ('MEAN', 'MEANCLIP', 'MEDIAN'):
395 # Transposition has no effect here.
396 overscanResult = self.measureConstantOverscan(overscanImage)
397 overscanValue = overscanResult.overscanValue
398 overscanMean = overscanValue
399 overscanSigma = 0.0
400 elif self.config.fitType in ('MEDIAN_PER_ROW', 'POLY', 'CHEB', 'LEG',
401 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'):
402 # Force transposes as needed
403 overscanResult = self.measureVectorOverscan(overscanImage, isTransposed)
404 overscanValue = overscanResult.overscanValue
406 stats = afwMath.makeStatistics(overscanResult.overscanValue,
407 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl)
408 overscanMean = stats.getValue(afwMath.MEDIAN)
409 overscanSigma = stats.getValue(afwMath.STDEVCLIP)
410 else:
411 raise ValueError('%s : %s an invalid overscan type' %
412 ("overscanCorrection", self.config.fitType))
414 return pipeBase.Struct(overscanValue=overscanValue,
415 overscanMean=overscanMean,
416 overscanSigma=overscanSigma,
417 )
419 @staticmethod
420 def integerConvert(image):
421 """Return an integer version of the input image.
423 Parameters
424 ----------
425 image : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
426 Image to convert to integers.
428 Returns
429 -------
430 outI : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
431 The integer converted image.
433 Raises
434 ------
435 RuntimeError
436 Raised if the input image could not be converted.
437 """
438 if hasattr(image, "image"):
439 # Is a maskedImage:
440 imageI = image.image.convertI()
441 outI = afwImage.MaskedImageI(imageI, image.mask, image.variance)
442 elif hasattr(image, "convertI"):
443 # Is an Image:
444 outI = image.convertI()
445 elif hasattr(image, "astype"):
446 # Is a numpy array:
447 outI = image.astype(int)
448 else:
449 raise RuntimeError("Could not convert this to integers: %s %s %s",
450 image, type(image), dir(image))
451 return outI
453 # Constant methods
454 def measureConstantOverscan(self, image):
455 """Measure a constant overscan value.
457 Parameters
458 ----------
459 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
460 Image data to measure the overscan from.
462 Returns
463 -------
464 results : `lsst.pipe.base.Struct`
465 Overscan result with entries:
466 - ``overscanValue``: Overscan value to subtract (`float`)
467 - ``isTransposed``: Orientation of the overscan (`bool`)
468 """
469 if self.config.fitType == 'MEDIAN':
470 calcImage = self.integerConvert(image)
471 else:
472 calcImage = image
473 fitType = afwMath.stringToStatisticsProperty(self.config.fitType)
474 overscanValue = afwMath.makeStatistics(calcImage, fitType, self.statControl).getValue()
476 return pipeBase.Struct(overscanValue=overscanValue,
477 isTransposed=False)
479 # Vector correction utilities
480 def getImageArray(self, image):
481 """Extract the numpy array from the input image.
483 Parameters
484 ----------
485 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
486 Image data to pull array from.
488 calcImage : `numpy.ndarray`
489 Image data array for numpy operating.
490 """
491 if hasattr(image, "getImage"):
492 calcImage = image.getImage().getArray()
493 calcImage = np.ma.masked_where(image.getMask().getArray() & self.statControl.getAndMask(),
494 calcImage)
495 else:
496 calcImage = image.getArray()
497 return calcImage
499 def maskOutliers(self, imageArray):
500 """Mask outliers in a row of overscan data from a robust sigma
501 clipping procedure.
503 Parameters
504 ----------
505 imageArray : `numpy.ndarray`
506 Image to filter along numpy axis=1.
508 Returns
509 -------
510 maskedArray : `numpy.ma.masked_array`
511 Masked image marking outliers.
512 """
513 lq, median, uq = np.percentile(imageArray, [25.0, 50.0, 75.0], axis=1)
514 axisMedians = median
515 axisStdev = 0.74*(uq - lq) # robust stdev
517 diff = np.abs(imageArray - axisMedians[:, np.newaxis])
518 return np.ma.masked_where(diff > self.statControl.getNumSigmaClip()
519 * axisStdev[:, np.newaxis], imageArray)
521 @staticmethod
522 def collapseArray(maskedArray):
523 """Collapse overscan array (and mask) to a 1-D vector of values.
525 Parameters
526 ----------
527 maskedArray : `numpy.ma.masked_array`
528 Masked array of input overscan data.
530 Returns
531 -------
532 collapsed : `numpy.ma.masked_array`
533 Single dimensional overscan data, combined with the mean.
534 """
535 collapsed = np.mean(maskedArray, axis=1)
536 if collapsed.mask.sum() > 0:
537 collapsed.data[collapsed.mask] = np.mean(maskedArray.data[collapsed.mask], axis=1)
538 return collapsed
540 def collapseArrayMedian(self, maskedArray):
541 """Collapse overscan array (and mask) to a 1-D vector of using the
542 correct integer median of row-values.
544 Parameters
545 ----------
546 maskedArray : `numpy.ma.masked_array`
547 Masked array of input overscan data.
549 Returns
550 -------
551 collapsed : `numpy.ma.masked_array`
552 Single dimensional overscan data, combined with the afwMath median.
553 """
554 integerMI = self.integerConvert(maskedArray)
556 collapsed = []
557 fitType = afwMath.stringToStatisticsProperty('MEDIAN')
558 for row in integerMI:
559 newRow = row.compressed()
560 if len(newRow) > 0:
561 rowMedian = afwMath.makeStatistics(newRow, fitType, self.statControl).getValue()
562 else:
563 rowMedian = np.nan
564 collapsed.append(rowMedian)
566 return np.array(collapsed)
568 def splineFit(self, indices, collapsed, numBins):
569 """Wrapper function to match spline fit API to polynomial fit API.
571 Parameters
572 ----------
573 indices : `numpy.ndarray`
574 Locations to evaluate the spline.
575 collapsed : `numpy.ndarray`
576 Collapsed overscan values corresponding to the spline
577 evaluation points.
578 numBins : `int`
579 Number of bins to use in constructing the spline.
581 Returns
582 -------
583 interp : `lsst.afw.math.Interpolate`
584 Interpolation object for later evaluation.
585 """
586 if not np.ma.is_masked(collapsed):
587 collapsed.mask = np.array(len(collapsed)*[np.ma.nomask])
589 numPerBin, binEdges = np.histogram(indices, bins=numBins,
590 weights=1 - collapsed.mask.astype(int))
591 with np.errstate(invalid="ignore"):
592 values = np.histogram(indices, bins=numBins,
593 weights=collapsed.data*~collapsed.mask)[0]/numPerBin
594 binCenters = np.histogram(indices, bins=numBins,
595 weights=indices*~collapsed.mask)[0]/numPerBin
597 if len(binCenters[numPerBin > 0]) < 5:
598 self.log.warn("Cannot do spline fitting for overscan: %s valid points.",
599 len(binCenters[numPerBin > 0]))
600 # Return a scalar value if we have one, otherwise
601 # return zero. This amplifier is hopefully already
602 # masked.
603 if len(values[numPerBin > 0]) != 0:
604 return float(values[numPerBin > 0][0])
605 else:
606 return 0.0
608 interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
609 values.astype(float)[numPerBin > 0],
610 afwMath.stringToInterpStyle(self.config.fitType))
611 return interp
613 @staticmethod
614 def splineEval(indices, interp):
615 """Wrapper function to match spline evaluation API to polynomial fit
616 API.
618 Parameters
619 ----------
620 indices : `numpy.ndarray`
621 Locations to evaluate the spline.
622 interp : `lsst.afw.math.interpolate`
623 Interpolation object to use.
625 Returns
626 -------
627 values : `numpy.ndarray`
628 Evaluated spline values at each index.
629 """
631 return interp.interpolate(indices.astype(float))
633 @staticmethod
634 def maskExtrapolated(collapsed):
635 """Create mask if edges are extrapolated.
637 Parameters
638 ----------
639 collapsed : `numpy.ma.masked_array`
640 Masked array to check the edges of.
642 Returns
643 -------
644 maskArray : `numpy.ndarray`
645 Boolean numpy array of pixels to mask.
646 """
647 maskArray = np.full_like(collapsed, False, dtype=bool)
648 if np.ma.is_masked(collapsed):
649 num = len(collapsed)
650 for low in range(num):
651 if not collapsed.mask[low]:
652 break
653 if low > 0:
654 maskArray[:low] = True
655 for high in range(1, num):
656 if not collapsed.mask[-high]:
657 break
658 if high > 1:
659 maskArray[-high:] = True
660 return maskArray
662 def measureVectorOverscan(self, image, isTransposed=False):
663 """Calculate the 1-d vector overscan from the input overscan image.
665 Parameters
666 ----------
667 image : `lsst.afw.image.MaskedImage`
668 Image containing the overscan data.
669 isTransposed : `bool`
670 If true, the image has been transposed.
672 Returns
673 -------
674 results : `lsst.pipe.base.Struct`
675 Overscan result with entries:
677 ``overscanValue``
678 Overscan value to subtract (`float`)
679 ``maskArray``
680 List of rows that should be masked as ``SUSPECT`` when the
681 overscan solution is applied. (`list` [ `bool` ])
682 ``isTransposed``
683 Indicates if the overscan data was transposed during
684 calcuation, noting along which axis the overscan should be
685 subtracted. (`bool`)
686 """
687 calcImage = self.getImageArray(image)
689 # operate on numpy-arrays from here
690 if isTransposed:
691 calcImage = np.transpose(calcImage)
692 masked = self.maskOutliers(calcImage)
694 if self.config.fitType == 'MEDIAN_PER_ROW':
695 mi = afwImage.MaskedImageI(image.getBBox())
696 masked = masked.astype(int)
697 if isTransposed:
698 masked = masked.transpose()
700 mi.image.array[:, :] = masked.data[:, :]
701 if bool(masked.mask.shape):
702 mi.mask.array[:, :] = masked.mask[:, :]
704 overscanVector = fitOverscanImage(mi, self.config.maskPlanes, isTransposed)
705 maskArray = self.maskExtrapolated(overscanVector)
706 else:
707 collapsed = self.collapseArray(masked)
709 num = len(collapsed)
710 indices = 2.0*np.arange(num)/float(num) - 1.0
712 poly = np.polynomial
713 fitter, evaler = {
714 'POLY': (poly.polynomial.polyfit, poly.polynomial.polyval),
715 'CHEB': (poly.chebyshev.chebfit, poly.chebyshev.chebval),
716 'LEG': (poly.legendre.legfit, poly.legendre.legval),
717 'NATURAL_SPLINE': (self.splineFit, self.splineEval),
718 'CUBIC_SPLINE': (self.splineFit, self.splineEval),
719 'AKIMA_SPLINE': (self.splineFit, self.splineEval)
720 }[self.config.fitType]
722 # These are the polynomial coefficients, or an
723 # interpolation object.
724 coeffs = fitter(indices, collapsed, self.config.order)
726 if isinstance(coeffs, float):
727 self.log.warn("Using fallback value %f due to fitter failure. Amplifier will be masked.",
728 coeffs)
729 overscanVector = np.full_like(indices, coeffs)
730 maskArray = np.full_like(collapsed, True, dtype=bool)
731 else:
732 # Otherwise we can just use things as normal.
733 overscanVector = evaler(indices, coeffs)
734 maskArray = self.maskExtrapolated(collapsed)
736 return pipeBase.Struct(overscanValue=np.array(overscanVector),
737 maskArray=maskArray,
738 isTransposed=isTransposed)
740 def debugView(self, image, model, amp=None):
741 """Debug display for the final overscan solution.
743 Parameters
744 ----------
745 image : `lsst.afw.image.Image`
746 Input image the overscan solution was determined from.
747 model : `numpy.ndarray` or `float`
748 Overscan model determined for the image.
749 amp : `lsst.afw.cameraGeom.Amplifier`, optional
750 Amplifier to extract diagnostic information.
751 """
752 import lsstDebug
753 if not lsstDebug.Info(__name__).display:
754 return
755 if not self.allowDebug:
756 return
758 calcImage = self.getImageArray(image)
759 # CZW: Check that this is ok
760 calcImage = np.transpose(calcImage)
761 masked = self.maskOutliers(calcImage)
762 collapsed = self.collapseArray(masked)
764 num = len(collapsed)
765 indices = 2.0 * np.arange(num)/float(num) - 1.0
767 if np.ma.is_masked(collapsed):
768 collapsedMask = collapsed.mask
769 else:
770 collapsedMask = np.array(num*[np.ma.nomask])
772 import matplotlib.pyplot as plot
773 figure = plot.figure(1)
774 figure.clear()
775 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
776 axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 'k+')
777 if collapsedMask.sum() > 0:
778 axes.plot(indices[collapsedMask], collapsed.data[collapsedMask], 'b+')
779 if isinstance(model, np.ndarray):
780 plotModel = model
781 else:
782 plotModel = np.zeros_like(indices)
783 plotModel += model
784 axes.plot(indices, plotModel, 'r-')
785 plot.xlabel("centered/scaled position along overscan region")
786 plot.ylabel("pixel value/fit value")
787 if amp:
788 plot.title(f"{amp.getName()} DataX: "
789 f"[{amp.getRawDataBBox().getBeginX()}:{amp.getRawBBox().getEndX()}]"
790 f"OscanX: [{amp.getRawHorizontalOverscanBBox().getBeginX()}:"
791 f"{amp.getRawHorizontalOverscanBBox().getEndX()}] {self.config.fitType}")
792 else:
793 plot.title("No amp supplied.")
794 figure.show()
795 prompt = "Press Enter or c to continue [chp]..."
796 while True:
797 ans = input(prompt).lower()
798 if ans in ("", " ", "c",):
799 break
800 elif ans in ("p", ):
801 import pdb
802 pdb.set_trace()
803 elif ans in ('x', ):
804 self.allowDebug = False
805 break
806 elif ans in ("h", ):
807 print("[h]elp [c]ontinue [p]db e[x]itDebug")
808 plot.close()