Coverage for python/lsst/ip/isr/overscan.py: 13%
282 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-19 19:30 +0000
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-19 19:30 +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/>.
22import numpy as np
23import time
24import lsst.afw.math as afwMath
25import lsst.afw.image as afwImage
26import lsst.geom as geom
27import lsst.pipe.base as pipeBase
28import lsst.pex.config as pexConfig
30from .isr import fitOverscanImage
32__all__ = ["OverscanCorrectionTaskConfig", "OverscanCorrectionTask"]
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 )
84 leadingColumnsToSkip = pexConfig.Field(
85 dtype=int,
86 doc="Number of leading columns to skip in serial overscan correction.",
87 default=0,
88 )
89 trailingColumnsToSkip = pexConfig.Field(
90 dtype=int,
91 doc="Number of trailing columns to skip in serial overscan correction.",
92 default=0,
93 )
94 leadingRowsToSkip = pexConfig.Field(
95 dtype=int,
96 doc="Number of leading rows to skip in parallel overscan correction.",
97 default=0,
98 )
99 trailingRowsToSkip = pexConfig.Field(
100 dtype=int,
101 doc="Number of trailing rows to skip in parallel overscan correction.",
102 default=0,
103 )
105 maxDeviation = pexConfig.Field( 105 ↛ exitline 105 didn't jump to the function exit
106 dtype=float,
107 doc="Maximum deviation from median (in ADU) to mask in overscan correction.",
108 default=1000.0, check=lambda x: x > 0,
109 )
112class OverscanCorrectionTask(pipeBase.Task):
113 """Correction task for overscan.
115 This class contains a number of utilities that are easier to
116 understand and use when they are not embedded in nested if/else
117 loops.
119 Parameters
120 ----------
121 statControl : `lsst.afw.math.StatisticsControl`, optional
122 Statistics control object.
123 """
124 ConfigClass = OverscanCorrectionTaskConfig
125 _DefaultName = "overscan"
127 def __init__(self, statControl=None, **kwargs):
128 super().__init__(**kwargs)
129 self.allowDebug = True
131 if statControl:
132 self.statControl = statControl
133 else:
134 self.statControl = afwMath.StatisticsControl()
135 self.statControl.setNumSigmaClip(self.config.numSigmaClip)
136 self.statControl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.maskPlanes))
138 def run(self, exposure, amp, isTransposed=False):
139 """Measure and remove an overscan from an amplifier image.
141 Parameters
142 ----------
143 exposure : `lsst.afw.image.Exposure`
144 Image data that will have the overscan corrections applied.
145 amp : `lsst.afw.cameraGeom.Amplifier`
146 Amplifier to use for debugging purposes.
147 isTransposed : `bool`, optional
148 Is the image transposed, such that serial and parallel
149 overscan regions are reversed? Default is False.
151 Returns
152 -------
153 overscanResults : `lsst.pipe.base.Struct`
154 Result struct with components:
156 ``imageFit``
157 Value or fit subtracted from the amplifier image data
158 (scalar or `lsst.afw.image.Image`).
159 ``overscanFit``
160 Value or fit subtracted from the overscan image data
161 (scalar or `lsst.afw.image.Image`).
162 ``overscanImage``
163 Image of the overscan region with the overscan
164 correction applied (`lsst.afw.image.Image`). This
165 quantity is used to estimate the amplifier read noise
166 empirically.
168 Raises
169 ------
170 RuntimeError
171 Raised if an invalid overscan type is set.
172 """
173 # Do Serial overscan first.
174 serialOverscanBBox = amp.getRawSerialOverscanBBox()
175 imageBBox = amp.getRawDataBBox()
177 if self.config.doParallelOverscan:
178 # We need to extend the serial overscan BBox to the full
179 # size of the detector.
180 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
181 imageBBox = imageBBox.expandedTo(parallelOverscanBBox)
183 serialOverscanBBox = geom.Box2I(geom.Point2I(serialOverscanBBox.getMinX(),
184 imageBBox.getMinY()),
185 geom.Extent2I(serialOverscanBBox.getWidth(),
186 imageBBox.getHeight()))
187 serialResults = self.correctOverscan(exposure, amp,
188 imageBBox, serialOverscanBBox, isTransposed=isTransposed)
189 overscanMean = serialResults.overscanMean
190 overscanSigma = serialResults.overscanSigma
191 residualMean = serialResults.overscanMeanResidual
192 residualSigma = serialResults.overscanSigmaResidual
194 # Do Parallel Overscan
195 if self.config.doParallelOverscan:
196 # This does not need any extensions, as we'll only
197 # subtract it from the data region.
198 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
199 imageBBox = amp.getRawDataBBox()
201 parallelResults = self.correctOverscan(exposure, amp,
202 imageBBox, parallelOverscanBBox,
203 isTransposed=not isTransposed)
205 overscanMean = (overscanMean, parallelResults.overscanMean)
206 overscanSigma = (overscanSigma, parallelResults.overscanSigma)
207 residualMean = (residualMean, parallelResults.overscanMeanResidual)
208 residualSigma = (residualSigma, parallelResults.overscanSigmaResidual)
210 return pipeBase.Struct(imageFit=serialResults.ampOverscanModel,
211 overscanFit=serialResults.overscanOverscanModel,
212 overscanImage=serialResults.overscanImage,
214 overscanMean=overscanMean,
215 overscanSigma=overscanSigma,
216 residualMean=residualMean,
217 residualSigma=residualSigma)
219 def correctOverscan(self, exposure, amp, imageBBox, overscanBBox, isTransposed=True):
220 """
221 """
222 overscanBox = self.trimOverscan(exposure, amp, overscanBBox,
223 self.config.leadingColumnsToSkip,
224 self.config.trailingColumnsToSkip,
225 transpose=isTransposed)
226 overscanImage = exposure[overscanBox].getMaskedImage()
227 overscanArray = overscanImage.image.array
229 # Mask pixels.
230 maskVal = overscanImage.mask.getPlaneBitMask(self.config.maskPlanes)
231 overscanMask = ~((overscanImage.mask.array & maskVal) == 0)
233 median = np.ma.median(np.ma.masked_where(overscanMask, overscanArray))
234 bad = np.where(np.abs(overscanArray - median) > self.config.maxDeviation)
235 overscanMask[bad] = overscanImage.mask.getPlaneBitMask("SAT")
237 # Do overscan fit.
238 # CZW: Handle transposed correctly.
239 overscanResults = self.fitOverscan(overscanImage, isTransposed=isTransposed)
241 # Correct image region (and possibly parallel-overscan region).
242 ampImage = exposure[imageBBox]
243 ampOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue,
244 ampImage.image.array,
245 transpose=isTransposed)
246 ampImage.image.array -= ampOverscanModel
248 # Correct overscan region (and possibly doubly-overscaned
249 # region).
250 overscanImage = exposure[overscanBBox]
251 # CZW: Transposed?
252 overscanOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue,
253 overscanImage.image.array)
254 overscanImage.image.array -= overscanOverscanModel
256 self.debugView(overscanImage, overscanResults.overscanValue, amp)
258 # Find residual fit statistics.
259 stats = afwMath.makeStatistics(overscanImage.getMaskedImage(),
260 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl)
261 residualMean = stats.getValue(afwMath.MEDIAN)
262 residualSigma = stats.getValue(afwMath.STDEVCLIP)
264 return pipeBase.Struct(ampOverscanModel=ampOverscanModel,
265 overscanOverscanModel=overscanOverscanModel,
266 overscanImage=overscanImage,
267 overscanValue=overscanResults.overscanValue,
269 overscanMean=overscanResults.overscanMean,
270 overscanSigma=overscanResults.overscanSigma,
271 overscanMeanResidual=residualMean,
272 overscanSigmaResidual=residualSigma
273 )
275 def broadcastFitToImage(self, overscanValue, imageArray, transpose=False):
276 """Broadcast 0 or 1 dimension fit to appropriate shape.
278 Parameters
279 ----------
280 overscanValue : `np.ndarray`, (Nrows, ) or scalar
281 Overscan fit to broadcast.
282 imageArray : `np.ndarray`, (Nrows, Ncols)
283 Image array that we want to match.
284 transpose : `bool`, optional
285 Switch order to broadcast along the other axis.
287 Returns
288 -------
289 overscanModel : `np.ndarray`, (Nrows, Ncols) or scalar
290 Expanded overscan fit.
292 Raises
293 ------
294 RuntimeError
295 Raised if no axis has the appropriate dimension.
296 """
297 if isinstance(overscanValue, np.ndarray):
298 overscanModel = np.zeros_like(imageArray)
300 if transpose is False:
301 if imageArray.shape[0] == overscanValue.shape[0]:
302 overscanModel[:, :] = overscanValue[:, np.newaxis]
303 elif imageArray.shape[1] == overscanValue.shape[0]:
304 overscanModel[:, :] = overscanValue[np.newaxis, :]
305 elif imageArray.shape[0] == overscanValue.shape[1]:
306 overscanModel[:, :] = overscanValue[np.newaxis, :]
307 else:
308 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to "
309 f"match {imageArray.shape}")
310 else:
311 if imageArray.shape[1] == overscanValue.shape[0]:
312 overscanModel[:, :] = overscanValue[np.newaxis, :]
313 elif imageArray.shape[0] == overscanValue.shape[0]:
314 overscanModel[:, :] = overscanValue[:, np.newaxis]
315 elif imageArray.shape[1] == overscanValue.shape[1]:
316 overscanModel[:, :] = overscanValue[:, np.newaxis]
317 else:
318 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to "
319 f"match {imageArray.shape}")
320 else:
321 overscanModel = overscanValue
323 return overscanModel
325 def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False):
326 """Trim overscan region to remove edges.
328 Parameters
329 ----------
330 exposure : `lsst.afw.image.Exposure`
331 Exposure containing data.
332 amp : `lsst.afw.cameraGeom.Amplifier`
333 Amplifier containing geometry information.
334 bbox : `lsst.geom.Box2I`
335 Bounding box of the overscan region.
336 skipLeading : `int`
337 Number of leading (towards data region) rows/columns to skip.
338 skipTrailing : `int`
339 Number of trailing (away from data region) rows/columns to skip.
340 transpose : `bool`, optional
341 Operate on the transposed array.
343 Returns
344 -------
345 overscanArray : `numpy.array`, (N, M)
346 Data array to fit.
347 overscanMask : `numpy.array`, (N, M)
348 Data mask.
349 """
350 dx0, dy0, dx1, dy1 = (0, 0, 0, 0)
351 dataBBox = amp.getRawDataBBox()
352 if transpose:
353 if dataBBox.getBeginY() < bbox.getBeginY():
354 dy0 += skipLeading
355 dy1 -= skipTrailing
356 else:
357 dy0 += skipTrailing
358 dy1 -= skipLeading
359 else:
360 if dataBBox.getBeginX() < bbox.getBeginX():
361 dx0 += skipLeading
362 dx1 -= skipTrailing
363 else:
364 dx0 += skipTrailing
365 dx1 -= skipLeading
367 overscanBBox = geom.Box2I(bbox.getBegin() + geom.Extent2I(dx0, dy0),
368 geom.Extent2I(bbox.getWidth() - dx0 + dx1,
369 bbox.getHeight() - dy0 + dy1))
370 return overscanBBox
372 def fitOverscan(self, overscanImage, isTransposed=False):
373 if self.config.fitType in ('MEAN', 'MEANCLIP', 'MEDIAN'):
374 # Transposition has no effect here.
375 overscanResult = self.measureConstantOverscan(overscanImage)
376 overscanValue = overscanResult.overscanValue
377 overscanMean = overscanValue
378 overscanSigma = 0.0
379 elif self.config.fitType in ('MEDIAN_PER_ROW', 'POLY', 'CHEB', 'LEG',
380 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'):
381 # Force transposes as needed
382 overscanResult = self.measureVectorOverscan(overscanImage, isTransposed)
383 overscanValue = overscanResult.overscanValue
385 stats = afwMath.makeStatistics(overscanResult.overscanValue,
386 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl)
387 overscanMean = stats.getValue(afwMath.MEDIAN)
388 overscanSigma = stats.getValue(afwMath.STDEVCLIP)
389 else:
390 raise ValueError('%s : %s an invalid overscan type' %
391 ("overscanCorrection", self.config.fitType))
393 return pipeBase.Struct(overscanValue=overscanValue,
394 overscanMean=overscanMean,
395 overscanSigma=overscanSigma,
396 )
398 @staticmethod
399 def integerConvert(image):
400 """Return an integer version of the input image.
402 Parameters
403 ----------
404 image : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
405 Image to convert to integers.
407 Returns
408 -------
409 outI : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
410 The integer converted image.
412 Raises
413 ------
414 RuntimeError
415 Raised if the input image could not be converted.
416 """
417 if hasattr(image, "image"):
418 # Is a maskedImage:
419 imageI = image.image.convertI()
420 outI = afwImage.MaskedImageI(imageI, image.mask, image.variance)
421 elif hasattr(image, "convertI"):
422 # Is an Image:
423 outI = image.convertI()
424 elif hasattr(image, "astype"):
425 # Is a numpy array:
426 outI = image.astype(int)
427 else:
428 raise RuntimeError("Could not convert this to integers: %s %s %s",
429 image, type(image), dir(image))
430 return outI
432 # Constant methods
433 def measureConstantOverscan(self, image):
434 """Measure a constant overscan value.
436 Parameters
437 ----------
438 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
439 Image data to measure the overscan from.
441 Returns
442 -------
443 results : `lsst.pipe.base.Struct`
444 Overscan result with entries:
445 - ``overscanValue``: Overscan value to subtract (`float`)
446 - ``isTransposed``: Orientation of the overscan (`bool`)
447 """
448 if self.config.fitType == 'MEDIAN':
449 calcImage = self.integerConvert(image)
450 else:
451 calcImage = image
452 fitType = afwMath.stringToStatisticsProperty(self.config.fitType)
453 overscanValue = afwMath.makeStatistics(calcImage, fitType, self.statControl).getValue()
455 return pipeBase.Struct(overscanValue=overscanValue,
456 isTransposed=False)
458 # Vector correction utilities
459 def getImageArray(self, image):
460 """Extract the numpy array from the input image.
462 Parameters
463 ----------
464 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
465 Image data to pull array from.
467 calcImage : `numpy.ndarray`
468 Image data array for numpy operating.
469 """
470 if hasattr(image, "getImage"):
471 calcImage = image.getImage().getArray()
472 calcImage = np.ma.masked_where(image.getMask().getArray() & self.statControl.getAndMask(),
473 calcImage)
474 else:
475 calcImage = image.getArray()
476 return calcImage
478 def maskOutliers(self, imageArray):
479 """Mask outliers in a row of overscan data from a robust sigma
480 clipping procedure.
482 Parameters
483 ----------
484 imageArray : `numpy.ndarray`
485 Image to filter along numpy axis=1.
487 Returns
488 -------
489 maskedArray : `numpy.ma.masked_array`
490 Masked image marking outliers.
491 """
492 lq, median, uq = np.percentile(imageArray, [25.0, 50.0, 75.0], axis=1)
493 axisMedians = median
494 axisStdev = 0.74*(uq - lq) # robust stdev
496 diff = np.abs(imageArray - axisMedians[:, np.newaxis])
497 return np.ma.masked_where(diff > self.statControl.getNumSigmaClip()
498 * axisStdev[:, np.newaxis], imageArray)
500 @staticmethod
501 def collapseArray(maskedArray):
502 """Collapse overscan array (and mask) to a 1-D vector of values.
504 Parameters
505 ----------
506 maskedArray : `numpy.ma.masked_array`
507 Masked array of input overscan data.
509 Returns
510 -------
511 collapsed : `numpy.ma.masked_array`
512 Single dimensional overscan data, combined with the mean.
513 """
514 collapsed = np.mean(maskedArray, axis=1)
515 if collapsed.mask.sum() > 0:
516 collapsed.data[collapsed.mask] = np.mean(maskedArray.data[collapsed.mask], axis=1)
517 return collapsed
519 def collapseArrayMedian(self, maskedArray):
520 """Collapse overscan array (and mask) to a 1-D vector of using the
521 correct integer median of row-values.
523 Parameters
524 ----------
525 maskedArray : `numpy.ma.masked_array`
526 Masked array of input overscan data.
528 Returns
529 -------
530 collapsed : `numpy.ma.masked_array`
531 Single dimensional overscan data, combined with the afwMath median.
532 """
533 integerMI = self.integerConvert(maskedArray)
535 collapsed = []
536 fitType = afwMath.stringToStatisticsProperty('MEDIAN')
537 for row in integerMI:
538 newRow = row.compressed()
539 if len(newRow) > 0:
540 rowMedian = afwMath.makeStatistics(newRow, fitType, self.statControl).getValue()
541 else:
542 rowMedian = np.nan
543 collapsed.append(rowMedian)
545 return np.array(collapsed)
547 def splineFit(self, indices, collapsed, numBins):
548 """Wrapper function to match spline fit API to polynomial fit API.
550 Parameters
551 ----------
552 indices : `numpy.ndarray`
553 Locations to evaluate the spline.
554 collapsed : `numpy.ndarray`
555 Collapsed overscan values corresponding to the spline
556 evaluation points.
557 numBins : `int`
558 Number of bins to use in constructing the spline.
560 Returns
561 -------
562 interp : `lsst.afw.math.Interpolate`
563 Interpolation object for later evaluation.
564 """
565 if not np.ma.is_masked(collapsed):
566 collapsed.mask = np.array(len(collapsed)*[np.ma.nomask])
568 numPerBin, binEdges = np.histogram(indices, bins=numBins,
569 weights=1 - collapsed.mask.astype(int))
570 with np.errstate(invalid="ignore"):
571 values = np.histogram(indices, bins=numBins,
572 weights=collapsed.data*~collapsed.mask)[0]/numPerBin
573 binCenters = np.histogram(indices, bins=numBins,
574 weights=indices*~collapsed.mask)[0]/numPerBin
576 if len(binCenters[numPerBin > 0]) < 5:
577 self.log.warn("Cannot do spline fitting for overscan: %s valid points.",
578 len(binCenters[numPerBin > 0]))
579 # Return a scalar value if we have one, otherwise
580 # return zero. This amplifier is hopefully already
581 # masked.
582 if len(values[numPerBin > 0]) != 0:
583 return float(values[numPerBin > 0][0])
584 else:
585 return 0.0
587 interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
588 values.astype(float)[numPerBin > 0],
589 afwMath.stringToInterpStyle(self.config.fitType))
590 return interp
592 @staticmethod
593 def splineEval(indices, interp):
594 """Wrapper function to match spline evaluation API to polynomial fit
595 API.
597 Parameters
598 ----------
599 indices : `numpy.ndarray`
600 Locations to evaluate the spline.
601 interp : `lsst.afw.math.interpolate`
602 Interpolation object to use.
604 Returns
605 -------
606 values : `numpy.ndarray`
607 Evaluated spline values at each index.
608 """
610 return interp.interpolate(indices.astype(float))
612 @staticmethod
613 def maskExtrapolated(collapsed):
614 """Create mask if edges are extrapolated.
616 Parameters
617 ----------
618 collapsed : `numpy.ma.masked_array`
619 Masked array to check the edges of.
621 Returns
622 -------
623 maskArray : `numpy.ndarray`
624 Boolean numpy array of pixels to mask.
625 """
626 maskArray = np.full_like(collapsed, False, dtype=bool)
627 if np.ma.is_masked(collapsed):
628 num = len(collapsed)
629 for low in range(num):
630 if not collapsed.mask[low]:
631 break
632 if low > 0:
633 maskArray[:low] = True
634 for high in range(1, num):
635 if not collapsed.mask[-high]:
636 break
637 if high > 1:
638 maskArray[-high:] = True
639 return maskArray
641 def measureVectorOverscan(self, image, isTransposed=False):
642 """Calculate the 1-d vector overscan from the input overscan image.
644 Parameters
645 ----------
646 image : `lsst.afw.image.MaskedImage`
647 Image containing the overscan data.
648 isTransposed : `bool`
649 If true, the image has been transposed.
651 Returns
652 -------
653 results : `lsst.pipe.base.Struct`
654 Overscan result with entries:
655 - ``overscanValue``: Overscan value to subtract (`float`)
656 - ``maskArray`` : `list` [ `bool` ]
657 List of rows that should be masked as ``SUSPECT`` when the
658 overscan solution is applied.
659 - ``isTransposed`` : `bool`
660 Indicates if the overscan data was transposed during
661 calcuation, noting along which axis the overscan should be
662 subtracted.
663 """
664 calcImage = self.getImageArray(image)
666 # operate on numpy-arrays from here
667 if isTransposed:
668 calcImage = np.transpose(calcImage)
669 masked = self.maskOutliers(calcImage)
671 startTime = time.perf_counter()
673 if self.config.fitType == 'MEDIAN_PER_ROW':
674 mi = afwImage.MaskedImageI(image.getBBox())
675 masked = masked.astype(int)
676 if isTransposed:
677 masked = masked.transpose()
679 mi.image.array[:, :] = masked.data[:, :]
680 if bool(masked.mask.shape):
681 mi.mask.array[:, :] = masked.mask[:, :]
683 overscanVector = fitOverscanImage(mi, self.config.maskPlanes, isTransposed)
684 maskArray = self.maskExtrapolated(overscanVector)
685 else:
686 collapsed = self.collapseArray(masked)
688 num = len(collapsed)
689 indices = 2.0*np.arange(num)/float(num) - 1.0
691 poly = np.polynomial
692 fitter, evaler = {
693 'POLY': (poly.polynomial.polyfit, poly.polynomial.polyval),
694 'CHEB': (poly.chebyshev.chebfit, poly.chebyshev.chebval),
695 'LEG': (poly.legendre.legfit, poly.legendre.legval),
696 'NATURAL_SPLINE': (self.splineFit, self.splineEval),
697 'CUBIC_SPLINE': (self.splineFit, self.splineEval),
698 'AKIMA_SPLINE': (self.splineFit, self.splineEval)
699 }[self.config.fitType]
701 # These are the polynomial coefficients, or an
702 # interpolation object.
703 coeffs = fitter(indices, collapsed, self.config.order)
705 if isinstance(coeffs, float):
706 self.log.warn("Using fallback value %f due to fitter failure. Amplifier will be masked.",
707 coeffs)
708 overscanVector = np.full_like(indices, coeffs)
709 maskArray = np.full_like(collapsed, True, dtype=bool)
710 else:
711 # Otherwise we can just use things as normal.
712 overscanVector = evaler(indices, coeffs)
713 maskArray = self.maskExtrapolated(collapsed)
715 endTime = time.perf_counter()
716 self.log.info(f"Overscan measurement took {endTime - startTime}s for {self.config.fitType}")
717 return pipeBase.Struct(overscanValue=np.array(overscanVector),
718 maskArray=maskArray,
719 isTransposed=isTransposed)
721 def debugView(self, image, model, amp=None):
722 """Debug display for the final overscan solution.
724 Parameters
725 ----------
726 image : `lsst.afw.image.Image`
727 Input image the overscan solution was determined from.
728 model : `numpy.ndarray` or `float`
729 Overscan model determined for the image.
730 amp : `lsst.afw.cameraGeom.Amplifier`, optional
731 Amplifier to extract diagnostic information.
732 """
733 import lsstDebug
734 if not lsstDebug.Info(__name__).display:
735 return
736 if not self.allowDebug:
737 return
739 calcImage = self.getImageArray(image)
740 # CZW: Check that this is ok
741 calcImage = np.transpose(calcImage)
742 masked = self.maskOutliers(calcImage)
743 collapsed = self.collapseArray(masked)
745 num = len(collapsed)
746 indices = 2.0 * np.arange(num)/float(num) - 1.0
748 if np.ma.is_masked(collapsed):
749 collapsedMask = collapsed.mask
750 else:
751 collapsedMask = np.array(num*[np.ma.nomask])
753 import matplotlib.pyplot as plot
754 figure = plot.figure(1)
755 figure.clear()
756 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
757 axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 'k+')
758 if collapsedMask.sum() > 0:
759 axes.plot(indices[collapsedMask], collapsed.data[collapsedMask], 'b+')
760 if isinstance(model, np.ndarray):
761 plotModel = model
762 else:
763 plotModel = np.zeros_like(indices)
764 plotModel += model
765 axes.plot(indices, plotModel, 'r-')
766 plot.xlabel("centered/scaled position along overscan region")
767 plot.ylabel("pixel value/fit value")
768 if amp:
769 plot.title(f"{amp.getName()} DataX: "
770 f"[{amp.getRawDataBBox().getBeginX()}:{amp.getRawBBox().getEndX()}]"
771 f"OscanX: [{amp.getRawHorizontalOverscanBBox().getBeginX()}:"
772 f"{amp.getRawHorizontalOverscanBBox().getEndX()}] {self.config.fitType}")
773 else:
774 plot.title("No amp supplied.")
775 figure.show()
776 prompt = "Press Enter or c to continue [chp]..."
777 while True:
778 ans = input(prompt).lower()
779 if ans in ("", " ", "c",):
780 break
781 elif ans in ("p", ):
782 import pdb
783 pdb.set_trace()
784 elif ans in ('x', ):
785 self.allowDebug = False
786 break
787 elif ans in ("h", ):
788 print("[h]elp [c]ontinue [p]db e[x]itDebug")
789 plot.close()