Coverage for python/lsst/ip/isr/overscan.py: 13%
278 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-30 02:54 -0700
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-30 02:54 -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
34class OverscanCorrectionTaskConfig(pexConfig.Config):
35 """Overscan correction options.
36 """
37 fitType = pexConfig.ChoiceField(
38 dtype=str,
39 doc="The method for fitting the overscan bias level.",
40 default='MEDIAN',
41 allowed={
42 "POLY": "Fit ordinary polynomial to the longest axis of the overscan region",
43 "CHEB": "Fit Chebyshev polynomial to the longest axis of the overscan region",
44 "LEG": "Fit Legendre polynomial to the longest axis of the overscan region",
45 "NATURAL_SPLINE": "Fit natural spline to the longest axis of the overscan region",
46 "CUBIC_SPLINE": "Fit cubic spline to the longest axis of the overscan region",
47 "AKIMA_SPLINE": "Fit Akima spline to the longest axis of the overscan region",
48 "MEAN": "Correct using the mean of the overscan region",
49 "MEANCLIP": "Correct using a clipped mean of the overscan region",
50 "MEDIAN": "Correct using the median of the overscan region",
51 "MEDIAN_PER_ROW": "Correct using the median per row of the overscan region",
52 },
53 )
54 order = pexConfig.Field(
55 dtype=int,
56 doc=("Order of polynomial to fit if overscan fit type is a polynomial, "
57 "or number of spline knots if overscan fit type is a spline."),
58 default=1,
59 )
60 numSigmaClip = pexConfig.Field(
61 dtype=float,
62 doc="Rejection threshold (sigma) for collapsing overscan before fit",
63 default=3.0,
64 )
65 maskPlanes = pexConfig.ListField(
66 dtype=str,
67 doc="Mask planes to reject when measuring overscan",
68 default=['BAD', 'SAT'],
69 )
70 overscanIsInt = pexConfig.Field(
71 dtype=bool,
72 doc="Treat overscan as an integer image for purposes of fitType=MEDIAN"
73 " and fitType=MEDIAN_PER_ROW.",
74 default=True,
75 )
77 doParallelOverscan = pexConfig.Field(
78 dtype=bool,
79 doc="Correct using parallel overscan after serial overscan correction?",
80 default=False,
81 )
83 leadingColumnsToSkip = pexConfig.Field(
84 dtype=int,
85 doc="Number of leading columns to skip in serial overscan correction.",
86 default=0,
87 )
88 trailingColumnsToSkip = pexConfig.Field(
89 dtype=int,
90 doc="Number of trailing columns to skip in serial overscan correction.",
91 default=0,
92 )
93 leadingRowsToSkip = pexConfig.Field(
94 dtype=int,
95 doc="Number of leading rows to skip in parallel overscan correction.",
96 default=0,
97 )
98 trailingRowsToSkip = pexConfig.Field(
99 dtype=int,
100 doc="Number of trailing rows to skip in parallel overscan correction.",
101 default=0,
102 )
104 maxDeviation = pexConfig.Field( 104 ↛ exitline 104 didn't jump to the function exit
105 dtype=float,
106 doc="Maximum deviation from median (in ADU) to mask in overscan correction.",
107 default=1000.0, check=lambda x: x > 0,
108 )
111class OverscanCorrectionTask(pipeBase.Task):
112 """Correction task for overscan.
114 This class contains a number of utilities that are easier to
115 understand and use when they are not embedded in nested if/else
116 loops.
118 Parameters
119 ----------
120 statControl : `lsst.afw.math.StatisticsControl`, optional
121 Statistics control object.
122 """
123 ConfigClass = OverscanCorrectionTaskConfig
124 _DefaultName = "overscan"
126 def __init__(self, statControl=None, **kwargs):
127 super().__init__(**kwargs)
128 self.allowDebug = True
130 if statControl:
131 self.statControl = statControl
132 else:
133 self.statControl = afwMath.StatisticsControl()
134 self.statControl.setNumSigmaClip(self.config.numSigmaClip)
135 self.statControl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.maskPlanes))
137 def run(self, exposure, amp, isTransposed=False):
138 """Measure and remove an overscan from an amplifier image.
140 Parameters
141 ----------
142 exposure : `lsst.afw.image.Exposure`
143 Image data that will have the overscan corrections applied.
144 amp : `lsst.afw.cameraGeom.Amplifier`
145 Amplifier to use for debugging purposes.
146 isTransposed : `bool`, optional
147 Is the image transposed, such that serial and parallel
148 overscan regions are reversed? Default is False.
150 Returns
151 -------
152 overscanResults : `lsst.pipe.base.Struct`
153 Result struct with components:
155 ``imageFit``
156 Value or fit subtracted from the amplifier image data
157 (scalar or `lsst.afw.image.Image`).
158 ``overscanFit``
159 Value or fit subtracted from the overscan image data
160 (scalar or `lsst.afw.image.Image`).
161 ``overscanImage``
162 Image of the overscan region with the overscan
163 correction applied (`lsst.afw.image.Image`). This
164 quantity is used to estimate the amplifier read noise
165 empirically.
167 Raises
168 ------
169 RuntimeError
170 Raised if an invalid overscan type is set.
171 """
172 # Do Serial overscan first.
173 serialOverscanBBox = amp.getRawSerialOverscanBBox()
174 imageBBox = amp.getRawDataBBox()
176 if self.config.doParallelOverscan:
177 # We need to extend the serial overscan BBox to the full
178 # size of the detector.
179 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
180 imageBBox = imageBBox.expandedTo(parallelOverscanBBox)
182 serialOverscanBBox = geom.Box2I(geom.Point2I(serialOverscanBBox.getMinX(),
183 imageBBox.getMinY()),
184 geom.Extent2I(serialOverscanBBox.getWidth(),
185 imageBBox.getHeight()))
186 serialResults = self.correctOverscan(exposure, amp,
187 imageBBox, serialOverscanBBox, isTransposed=isTransposed)
188 overscanMean = serialResults.overscanMean
189 overscanSigma = serialResults.overscanSigma
190 residualMean = serialResults.overscanMeanResidual
191 residualSigma = serialResults.overscanSigmaResidual
193 # Do Parallel Overscan
194 if self.config.doParallelOverscan:
195 # This does not need any extensions, as we'll only
196 # subtract it from the data region.
197 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
198 imageBBox = amp.getRawDataBBox()
200 parallelResults = self.correctOverscan(exposure, amp,
201 imageBBox, parallelOverscanBBox,
202 isTransposed=not isTransposed)
204 overscanMean = (overscanMean, parallelResults.overscanMean)
205 overscanSigma = (overscanSigma, parallelResults.overscanSigma)
206 residualMean = (residualMean, parallelResults.overscanMeanResidual)
207 residualSigma = (residualSigma, parallelResults.overscanSigmaResidual)
209 return pipeBase.Struct(imageFit=serialResults.ampOverscanModel,
210 overscanFit=serialResults.overscanOverscanModel,
211 overscanImage=serialResults.overscanImage,
213 overscanMean=overscanMean,
214 overscanSigma=overscanSigma,
215 residualMean=residualMean,
216 residualSigma=residualSigma)
218 def correctOverscan(self, exposure, amp, imageBBox, overscanBBox, isTransposed=True):
219 """
220 """
221 overscanBox = self.trimOverscan(exposure, amp, overscanBBox,
222 self.config.leadingColumnsToSkip,
223 self.config.trailingColumnsToSkip,
224 transpose=isTransposed)
225 overscanImage = exposure[overscanBox].getMaskedImage()
226 overscanArray = overscanImage.image.array
228 # Mask pixels.
229 maskVal = overscanImage.mask.getPlaneBitMask(self.config.maskPlanes)
230 overscanMask = ~((overscanImage.mask.array & maskVal) == 0)
232 median = np.ma.median(np.ma.masked_where(overscanMask, overscanArray))
233 bad = np.where(np.abs(overscanArray - median) > self.config.maxDeviation)
234 overscanMask[bad] = overscanImage.mask.getPlaneBitMask("SAT")
236 # Do overscan fit.
237 # CZW: Handle transposed correctly.
238 overscanResults = self.fitOverscan(overscanImage, isTransposed=isTransposed)
240 # Correct image region (and possibly parallel-overscan region).
241 ampImage = exposure[imageBBox]
242 ampOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue,
243 ampImage.image.array,
244 transpose=isTransposed)
245 ampImage.image.array -= ampOverscanModel
247 # Correct overscan region (and possibly doubly-overscaned
248 # region).
249 overscanImage = exposure[overscanBBox]
250 # CZW: Transposed?
251 overscanOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue,
252 overscanImage.image.array)
253 overscanImage.image.array -= overscanOverscanModel
255 self.debugView(overscanImage, overscanResults.overscanValue, amp)
257 # Find residual fit statistics.
258 stats = afwMath.makeStatistics(overscanImage.getMaskedImage(),
259 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl)
260 residualMean = stats.getValue(afwMath.MEDIAN)
261 residualSigma = stats.getValue(afwMath.STDEVCLIP)
263 return pipeBase.Struct(ampOverscanModel=ampOverscanModel,
264 overscanOverscanModel=overscanOverscanModel,
265 overscanImage=overscanImage,
266 overscanValue=overscanResults.overscanValue,
268 overscanMean=overscanResults.overscanMean,
269 overscanSigma=overscanResults.overscanSigma,
270 overscanMeanResidual=residualMean,
271 overscanSigmaResidual=residualSigma
272 )
274 def broadcastFitToImage(self, overscanValue, imageArray, transpose=False):
275 """Broadcast 0 or 1 dimension fit to appropriate shape.
277 Parameters
278 ----------
279 overscanValue : `numpy.ndarray`, (Nrows, ) or scalar
280 Overscan fit to broadcast.
281 imageArray : `numpy.ndarray`, (Nrows, Ncols)
282 Image array that we want to match.
283 transpose : `bool`, optional
284 Switch order to broadcast along the other axis.
286 Returns
287 -------
288 overscanModel : `numpy.ndarray`, (Nrows, Ncols) or scalar
289 Expanded overscan fit.
291 Raises
292 ------
293 RuntimeError
294 Raised if no axis has the appropriate dimension.
295 """
296 if isinstance(overscanValue, np.ndarray):
297 overscanModel = np.zeros_like(imageArray)
299 if transpose is False:
300 if imageArray.shape[0] == overscanValue.shape[0]:
301 overscanModel[:, :] = overscanValue[:, np.newaxis]
302 elif imageArray.shape[1] == overscanValue.shape[0]:
303 overscanModel[:, :] = overscanValue[np.newaxis, :]
304 elif imageArray.shape[0] == overscanValue.shape[1]:
305 overscanModel[:, :] = overscanValue[np.newaxis, :]
306 else:
307 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to "
308 f"match {imageArray.shape}")
309 else:
310 if imageArray.shape[1] == overscanValue.shape[0]:
311 overscanModel[:, :] = overscanValue[np.newaxis, :]
312 elif imageArray.shape[0] == overscanValue.shape[0]:
313 overscanModel[:, :] = overscanValue[:, np.newaxis]
314 elif imageArray.shape[1] == overscanValue.shape[1]:
315 overscanModel[:, :] = overscanValue[:, np.newaxis]
316 else:
317 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to "
318 f"match {imageArray.shape}")
319 else:
320 overscanModel = overscanValue
322 return overscanModel
324 def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False):
325 """Trim overscan region to remove edges.
327 Parameters
328 ----------
329 exposure : `lsst.afw.image.Exposure`
330 Exposure containing data.
331 amp : `lsst.afw.cameraGeom.Amplifier`
332 Amplifier containing geometry information.
333 bbox : `lsst.geom.Box2I`
334 Bounding box of the overscan region.
335 skipLeading : `int`
336 Number of leading (towards data region) rows/columns to skip.
337 skipTrailing : `int`
338 Number of trailing (away from data region) rows/columns to skip.
339 transpose : `bool`, optional
340 Operate on the transposed array.
342 Returns
343 -------
344 overscanArray : `numpy.array`, (N, M)
345 Data array to fit.
346 overscanMask : `numpy.array`, (N, M)
347 Data mask.
348 """
349 dx0, dy0, dx1, dy1 = (0, 0, 0, 0)
350 dataBBox = amp.getRawDataBBox()
351 if transpose:
352 if dataBBox.getBeginY() < bbox.getBeginY():
353 dy0 += skipLeading
354 dy1 -= skipTrailing
355 else:
356 dy0 += skipTrailing
357 dy1 -= skipLeading
358 else:
359 if dataBBox.getBeginX() < bbox.getBeginX():
360 dx0 += skipLeading
361 dx1 -= skipTrailing
362 else:
363 dx0 += skipTrailing
364 dx1 -= skipLeading
366 overscanBBox = geom.Box2I(bbox.getBegin() + geom.Extent2I(dx0, dy0),
367 geom.Extent2I(bbox.getWidth() - dx0 + dx1,
368 bbox.getHeight() - dy0 + dy1))
369 return overscanBBox
371 def fitOverscan(self, overscanImage, isTransposed=False):
372 if self.config.fitType in ('MEAN', 'MEANCLIP', 'MEDIAN'):
373 # Transposition has no effect here.
374 overscanResult = self.measureConstantOverscan(overscanImage)
375 overscanValue = overscanResult.overscanValue
376 overscanMean = overscanValue
377 overscanSigma = 0.0
378 elif self.config.fitType in ('MEDIAN_PER_ROW', 'POLY', 'CHEB', 'LEG',
379 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'):
380 # Force transposes as needed
381 overscanResult = self.measureVectorOverscan(overscanImage, isTransposed)
382 overscanValue = overscanResult.overscanValue
384 stats = afwMath.makeStatistics(overscanResult.overscanValue,
385 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl)
386 overscanMean = stats.getValue(afwMath.MEDIAN)
387 overscanSigma = stats.getValue(afwMath.STDEVCLIP)
388 else:
389 raise ValueError('%s : %s an invalid overscan type' %
390 ("overscanCorrection", self.config.fitType))
392 return pipeBase.Struct(overscanValue=overscanValue,
393 overscanMean=overscanMean,
394 overscanSigma=overscanSigma,
395 )
397 @staticmethod
398 def integerConvert(image):
399 """Return an integer version of the input image.
401 Parameters
402 ----------
403 image : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
404 Image to convert to integers.
406 Returns
407 -------
408 outI : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
409 The integer converted image.
411 Raises
412 ------
413 RuntimeError
414 Raised if the input image could not be converted.
415 """
416 if hasattr(image, "image"):
417 # Is a maskedImage:
418 imageI = image.image.convertI()
419 outI = afwImage.MaskedImageI(imageI, image.mask, image.variance)
420 elif hasattr(image, "convertI"):
421 # Is an Image:
422 outI = image.convertI()
423 elif hasattr(image, "astype"):
424 # Is a numpy array:
425 outI = image.astype(int)
426 else:
427 raise RuntimeError("Could not convert this to integers: %s %s %s",
428 image, type(image), dir(image))
429 return outI
431 # Constant methods
432 def measureConstantOverscan(self, image):
433 """Measure a constant overscan value.
435 Parameters
436 ----------
437 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
438 Image data to measure the overscan from.
440 Returns
441 -------
442 results : `lsst.pipe.base.Struct`
443 Overscan result with entries:
444 - ``overscanValue``: Overscan value to subtract (`float`)
445 - ``isTransposed``: Orientation of the overscan (`bool`)
446 """
447 if self.config.fitType == 'MEDIAN':
448 calcImage = self.integerConvert(image)
449 else:
450 calcImage = image
451 fitType = afwMath.stringToStatisticsProperty(self.config.fitType)
452 overscanValue = afwMath.makeStatistics(calcImage, fitType, self.statControl).getValue()
454 return pipeBase.Struct(overscanValue=overscanValue,
455 isTransposed=False)
457 # Vector correction utilities
458 def getImageArray(self, image):
459 """Extract the numpy array from the input image.
461 Parameters
462 ----------
463 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
464 Image data to pull array from.
466 calcImage : `numpy.ndarray`
467 Image data array for numpy operating.
468 """
469 if hasattr(image, "getImage"):
470 calcImage = image.getImage().getArray()
471 calcImage = np.ma.masked_where(image.getMask().getArray() & self.statControl.getAndMask(),
472 calcImage)
473 else:
474 calcImage = image.getArray()
475 return calcImage
477 def maskOutliers(self, imageArray):
478 """Mask outliers in a row of overscan data from a robust sigma
479 clipping procedure.
481 Parameters
482 ----------
483 imageArray : `numpy.ndarray`
484 Image to filter along numpy axis=1.
486 Returns
487 -------
488 maskedArray : `numpy.ma.masked_array`
489 Masked image marking outliers.
490 """
491 lq, median, uq = np.percentile(imageArray, [25.0, 50.0, 75.0], axis=1)
492 axisMedians = median
493 axisStdev = 0.74*(uq - lq) # robust stdev
495 diff = np.abs(imageArray - axisMedians[:, np.newaxis])
496 return np.ma.masked_where(diff > self.statControl.getNumSigmaClip()
497 * axisStdev[:, np.newaxis], imageArray)
499 @staticmethod
500 def collapseArray(maskedArray):
501 """Collapse overscan array (and mask) to a 1-D vector of values.
503 Parameters
504 ----------
505 maskedArray : `numpy.ma.masked_array`
506 Masked array of input overscan data.
508 Returns
509 -------
510 collapsed : `numpy.ma.masked_array`
511 Single dimensional overscan data, combined with the mean.
512 """
513 collapsed = np.mean(maskedArray, axis=1)
514 if collapsed.mask.sum() > 0:
515 collapsed.data[collapsed.mask] = np.mean(maskedArray.data[collapsed.mask], axis=1)
516 return collapsed
518 def collapseArrayMedian(self, maskedArray):
519 """Collapse overscan array (and mask) to a 1-D vector of using the
520 correct integer median of row-values.
522 Parameters
523 ----------
524 maskedArray : `numpy.ma.masked_array`
525 Masked array of input overscan data.
527 Returns
528 -------
529 collapsed : `numpy.ma.masked_array`
530 Single dimensional overscan data, combined with the afwMath median.
531 """
532 integerMI = self.integerConvert(maskedArray)
534 collapsed = []
535 fitType = afwMath.stringToStatisticsProperty('MEDIAN')
536 for row in integerMI:
537 newRow = row.compressed()
538 if len(newRow) > 0:
539 rowMedian = afwMath.makeStatistics(newRow, fitType, self.statControl).getValue()
540 else:
541 rowMedian = np.nan
542 collapsed.append(rowMedian)
544 return np.array(collapsed)
546 def splineFit(self, indices, collapsed, numBins):
547 """Wrapper function to match spline fit API to polynomial fit API.
549 Parameters
550 ----------
551 indices : `numpy.ndarray`
552 Locations to evaluate the spline.
553 collapsed : `numpy.ndarray`
554 Collapsed overscan values corresponding to the spline
555 evaluation points.
556 numBins : `int`
557 Number of bins to use in constructing the spline.
559 Returns
560 -------
561 interp : `lsst.afw.math.Interpolate`
562 Interpolation object for later evaluation.
563 """
564 if not np.ma.is_masked(collapsed):
565 collapsed.mask = np.array(len(collapsed)*[np.ma.nomask])
567 numPerBin, binEdges = np.histogram(indices, bins=numBins,
568 weights=1 - collapsed.mask.astype(int))
569 with np.errstate(invalid="ignore"):
570 values = np.histogram(indices, bins=numBins,
571 weights=collapsed.data*~collapsed.mask)[0]/numPerBin
572 binCenters = np.histogram(indices, bins=numBins,
573 weights=indices*~collapsed.mask)[0]/numPerBin
575 if len(binCenters[numPerBin > 0]) < 5:
576 self.log.warn("Cannot do spline fitting for overscan: %s valid points.",
577 len(binCenters[numPerBin > 0]))
578 # Return a scalar value if we have one, otherwise
579 # return zero. This amplifier is hopefully already
580 # masked.
581 if len(values[numPerBin > 0]) != 0:
582 return float(values[numPerBin > 0][0])
583 else:
584 return 0.0
586 interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
587 values.astype(float)[numPerBin > 0],
588 afwMath.stringToInterpStyle(self.config.fitType))
589 return interp
591 @staticmethod
592 def splineEval(indices, interp):
593 """Wrapper function to match spline evaluation API to polynomial fit
594 API.
596 Parameters
597 ----------
598 indices : `numpy.ndarray`
599 Locations to evaluate the spline.
600 interp : `lsst.afw.math.interpolate`
601 Interpolation object to use.
603 Returns
604 -------
605 values : `numpy.ndarray`
606 Evaluated spline values at each index.
607 """
609 return interp.interpolate(indices.astype(float))
611 @staticmethod
612 def maskExtrapolated(collapsed):
613 """Create mask if edges are extrapolated.
615 Parameters
616 ----------
617 collapsed : `numpy.ma.masked_array`
618 Masked array to check the edges of.
620 Returns
621 -------
622 maskArray : `numpy.ndarray`
623 Boolean numpy array of pixels to mask.
624 """
625 maskArray = np.full_like(collapsed, False, dtype=bool)
626 if np.ma.is_masked(collapsed):
627 num = len(collapsed)
628 for low in range(num):
629 if not collapsed.mask[low]:
630 break
631 if low > 0:
632 maskArray[:low] = True
633 for high in range(1, num):
634 if not collapsed.mask[-high]:
635 break
636 if high > 1:
637 maskArray[-high:] = True
638 return maskArray
640 def measureVectorOverscan(self, image, isTransposed=False):
641 """Calculate the 1-d vector overscan from the input overscan image.
643 Parameters
644 ----------
645 image : `lsst.afw.image.MaskedImage`
646 Image containing the overscan data.
647 isTransposed : `bool`
648 If true, the image has been transposed.
650 Returns
651 -------
652 results : `lsst.pipe.base.Struct`
653 Overscan result with entries:
655 ``overscanValue``
656 Overscan value to subtract (`float`)
657 ``maskArray``
658 List of rows that should be masked as ``SUSPECT`` when the
659 overscan solution is applied. (`list` [ `bool` ])
660 ``isTransposed``
661 Indicates if the overscan data was transposed during
662 calcuation, noting along which axis the overscan should be
663 subtracted. (`bool`)
664 """
665 calcImage = self.getImageArray(image)
667 # operate on numpy-arrays from here
668 if isTransposed:
669 calcImage = np.transpose(calcImage)
670 masked = self.maskOutliers(calcImage)
672 if self.config.fitType == 'MEDIAN_PER_ROW':
673 mi = afwImage.MaskedImageI(image.getBBox())
674 masked = masked.astype(int)
675 if isTransposed:
676 masked = masked.transpose()
678 mi.image.array[:, :] = masked.data[:, :]
679 if bool(masked.mask.shape):
680 mi.mask.array[:, :] = masked.mask[:, :]
682 overscanVector = fitOverscanImage(mi, self.config.maskPlanes, isTransposed)
683 maskArray = self.maskExtrapolated(overscanVector)
684 else:
685 collapsed = self.collapseArray(masked)
687 num = len(collapsed)
688 indices = 2.0*np.arange(num)/float(num) - 1.0
690 poly = np.polynomial
691 fitter, evaler = {
692 'POLY': (poly.polynomial.polyfit, poly.polynomial.polyval),
693 'CHEB': (poly.chebyshev.chebfit, poly.chebyshev.chebval),
694 'LEG': (poly.legendre.legfit, poly.legendre.legval),
695 'NATURAL_SPLINE': (self.splineFit, self.splineEval),
696 'CUBIC_SPLINE': (self.splineFit, self.splineEval),
697 'AKIMA_SPLINE': (self.splineFit, self.splineEval)
698 }[self.config.fitType]
700 # These are the polynomial coefficients, or an
701 # interpolation object.
702 coeffs = fitter(indices, collapsed, self.config.order)
704 if isinstance(coeffs, float):
705 self.log.warn("Using fallback value %f due to fitter failure. Amplifier will be masked.",
706 coeffs)
707 overscanVector = np.full_like(indices, coeffs)
708 maskArray = np.full_like(collapsed, True, dtype=bool)
709 else:
710 # Otherwise we can just use things as normal.
711 overscanVector = evaler(indices, coeffs)
712 maskArray = self.maskExtrapolated(collapsed)
714 return pipeBase.Struct(overscanValue=np.array(overscanVector),
715 maskArray=maskArray,
716 isTransposed=isTransposed)
718 def debugView(self, image, model, amp=None):
719 """Debug display for the final overscan solution.
721 Parameters
722 ----------
723 image : `lsst.afw.image.Image`
724 Input image the overscan solution was determined from.
725 model : `numpy.ndarray` or `float`
726 Overscan model determined for the image.
727 amp : `lsst.afw.cameraGeom.Amplifier`, optional
728 Amplifier to extract diagnostic information.
729 """
730 import lsstDebug
731 if not lsstDebug.Info(__name__).display:
732 return
733 if not self.allowDebug:
734 return
736 calcImage = self.getImageArray(image)
737 # CZW: Check that this is ok
738 calcImage = np.transpose(calcImage)
739 masked = self.maskOutliers(calcImage)
740 collapsed = self.collapseArray(masked)
742 num = len(collapsed)
743 indices = 2.0 * np.arange(num)/float(num) - 1.0
745 if np.ma.is_masked(collapsed):
746 collapsedMask = collapsed.mask
747 else:
748 collapsedMask = np.array(num*[np.ma.nomask])
750 import matplotlib.pyplot as plot
751 figure = plot.figure(1)
752 figure.clear()
753 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
754 axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 'k+')
755 if collapsedMask.sum() > 0:
756 axes.plot(indices[collapsedMask], collapsed.data[collapsedMask], 'b+')
757 if isinstance(model, np.ndarray):
758 plotModel = model
759 else:
760 plotModel = np.zeros_like(indices)
761 plotModel += model
762 axes.plot(indices, plotModel, 'r-')
763 plot.xlabel("centered/scaled position along overscan region")
764 plot.ylabel("pixel value/fit value")
765 if amp:
766 plot.title(f"{amp.getName()} DataX: "
767 f"[{amp.getRawDataBBox().getBeginX()}:{amp.getRawBBox().getEndX()}]"
768 f"OscanX: [{amp.getRawHorizontalOverscanBBox().getBeginX()}:"
769 f"{amp.getRawHorizontalOverscanBBox().getEndX()}] {self.config.fitType}")
770 else:
771 plot.title("No amp supplied.")
772 figure.show()
773 prompt = "Press Enter or c to continue [chp]..."
774 while True:
775 ans = input(prompt).lower()
776 if ans in ("", " ", "c",):
777 break
778 elif ans in ("p", ):
779 import pdb
780 pdb.set_trace()
781 elif ans in ('x', ):
782 self.allowDebug = False
783 break
784 elif ans in ("h", ):
785 print("[h]elp [c]ontinue [p]db e[x]itDebug")
786 plot.close()