lsst.ip.isr g137835810c+664cb4e857
overscan.py
Go to the documentation of this file.
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/>.
21
22__all__ = ["OverscanCorrectionTaskConfig", "OverscanCorrectionTask"]
23
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
30
31from .isr import fitOverscanImage
32from .isrFunctions import makeThresholdMask, countMaskedPixels
33
34
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 )
77
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 )
93
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 )
114
115 maxDeviation = pexConfig.Field(
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 )
120
121
122class OverscanCorrectionTask(pipeBase.Task):
123 """Correction task for overscan.
124
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.
128
129 Parameters
130 ----------
131 statControl : `lsst.afw.math.StatisticsControl`, optional
132 Statistics control object.
133 """
134 ConfigClass = OverscanCorrectionTaskConfig
135 _DefaultName = "overscan"
136
137 def __init__(self, statControl=None, **kwargs):
138 super().__init__(**kwargs)
139 self.allowDebug = True
140
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))
147
148 def run(self, exposure, amp, isTransposed=False):
149 """Measure and remove an overscan from an amplifier image.
150
151 Parameters
152 ----------
153 exposure : `lsst.afw.image.Exposure`
154 Image data that will have the overscan corrections applied.
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.
160
161 Returns
162 -------
163 overscanResults : `lsst.pipe.base.Struct`
164 Result struct with components:
165
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.
177
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()
186
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)
192
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
203
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()
210
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)
225
226 overscanMean = (overscanMean, parallelResults.overscanMean)
227 overscanSigma = (overscanSigma, parallelResults.overscanSigma)
228 residualMean = (residualMean, parallelResults.overscanMeanResidual)
229 residualSigma = (residualSigma, parallelResults.overscanSigmaResidual)
230
231 return pipeBase.Struct(imageFit=serialResults.ampOverscanModel,
232 overscanFit=serialResults.overscanOverscanModel,
233 overscanImage=serialResults.overscanImage,
234
235 overscanMean=overscanMean,
236 overscanSigma=overscanSigma,
237 residualMean=residualMean,
238 residualSigma=residualSigma)
239
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
249
250 # Mask pixels.
251 maskVal = overscanImage.mask.getPlaneBitMask(self.config.maskPlanes)
252 overscanMask = ~((overscanImage.mask.array & maskVal) == 0)
253
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")
257
258 # Do overscan fit.
259 # CZW: Handle transposed correctly.
260 overscanResults = self.fitOverscan(overscanImage, isTransposed=isTransposed)
261
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
268
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
276
277 self.debugView(overscanImage, overscanResults.overscanValue, amp)
278
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)
284
285 return pipeBase.Struct(ampOverscanModel=ampOverscanModel,
286 overscanOverscanModel=overscanOverscanModel,
287 overscanImage=overscanImage,
288 overscanValue=overscanResults.overscanValue,
289
290 overscanMean=overscanResults.overscanMean,
291 overscanSigma=overscanResults.overscanSigma,
292 overscanMeanResidual=residualMean,
293 overscanSigmaResidual=residualSigma
294 )
295
296 def broadcastFitToImage(self, overscanValue, imageArray, transpose=False):
297 """Broadcast 0 or 1 dimension fit to appropriate shape.
298
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.
307
308 Returns
309 -------
310 overscanModel : `numpy.ndarray`, (Nrows, Ncols) or scalar
311 Expanded overscan fit.
312
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)
320
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
343
344 return overscanModel
345
346 def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False):
347 """Trim overscan region to remove edges.
348
349 Parameters
350 ----------
351 exposure : `lsst.afw.image.Exposure`
352 Exposure containing data.
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.
363
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
387
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
392
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
405
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))
413
414 return pipeBase.Struct(overscanValue=overscanValue,
415 overscanMean=overscanMean,
416 overscanSigma=overscanSigma,
417 )
418
419 @staticmethod
420 def integerConvert(image):
421 """Return an integer version of the input image.
422
423 Parameters
424 ----------
425 image : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
426 Image to convert to integers.
427
428 Returns
429 -------
430 outI : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
431 The integer converted image.
432
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
452
453 # Constant methods
454 def measureConstantOverscan(self, image):
455 """Measure a constant overscan value.
456
457 Parameters
458 ----------
460 Image data to measure the overscan from.
461
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()
475
476 return pipeBase.Struct(overscanValue=overscanValue,
477 isTransposed=False)
478
479 # Vector correction utilities
480 def getImageArray(self, image):
481 """Extract the numpy array from the input image.
482
483 Parameters
484 ----------
486 Image data to pull array from.
487
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
498
499 def maskOutliers(self, imageArray):
500 """Mask outliers in a row of overscan data from a robust sigma
501 clipping procedure.
502
503 Parameters
504 ----------
505 imageArray : `numpy.ndarray`
506 Image to filter along numpy axis=1.
507
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
516
517 diff = np.abs(imageArray - axisMedians[:, np.newaxis])
518 return np.ma.masked_where(diff > self.statControl.getNumSigmaClip()
519 * axisStdev[:, np.newaxis], imageArray)
520
521 @staticmethod
522 def collapseArray(maskedArray):
523 """Collapse overscan array (and mask) to a 1-D vector of values.
524
525 Parameters
526 ----------
527 maskedArray : `numpy.ma.masked_array`
528 Masked array of input overscan data.
529
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
539
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.
543
544 Parameters
545 ----------
546 maskedArray : `numpy.ma.masked_array`
547 Masked array of input overscan data.
548
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)
555
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)
565
566 return np.array(collapsed)
567
568 def splineFit(self, indices, collapsed, numBins):
569 """Wrapper function to match spline fit API to polynomial fit API.
570
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.
580
581 Returns
582 -------
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])
588
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
596
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
607
608 interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
609 values.astype(float)[numPerBin > 0],
610 afwMath.stringToInterpStyle(self.config.fitType))
611 return interp
612
613 @staticmethod
614 def splineEval(indices, interp):
615 """Wrapper function to match spline evaluation API to polynomial fit
616 API.
617
618 Parameters
619 ----------
620 indices : `numpy.ndarray`
621 Locations to evaluate the spline.
622 interp : `lsst.afw.math.interpolate`
623 Interpolation object to use.
624
625 Returns
626 -------
627 values : `numpy.ndarray`
628 Evaluated spline values at each index.
629 """
630
631 return interp.interpolate(indices.astype(float))
632
633 @staticmethod
634 def maskExtrapolated(collapsed):
635 """Create mask if edges are extrapolated.
636
637 Parameters
638 ----------
639 collapsed : `numpy.ma.masked_array`
640 Masked array to check the edges of.
641
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
661
662 def measureVectorOverscan(self, image, isTransposed=False):
663 """Calculate the 1-d vector overscan from the input overscan image.
664
665 Parameters
666 ----------
668 Image containing the overscan data.
669 isTransposed : `bool`
670 If true, the image has been transposed.
671
672 Returns
673 -------
674 results : `lsst.pipe.base.Struct`
675 Overscan result with entries:
676
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)
688
689 # operate on numpy-arrays from here
690 if isTransposed:
691 calcImage = np.transpose(calcImage)
692 masked = self.maskOutliers(calcImage)
693
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()
699
700 mi.image.array[:, :] = masked.data[:, :]
701 if bool(masked.mask.shape):
702 mi.mask.array[:, :] = masked.mask[:, :]
703
704 overscanVector = fitOverscanImage(mi, self.config.maskPlanes, isTransposed)
705 maskArray = self.maskExtrapolated(overscanVector)
706 else:
707 collapsed = self.collapseArray(masked)
708
709 num = len(collapsed)
710 indices = 2.0*np.arange(num)/float(num) - 1.0
711
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]
721
722 # These are the polynomial coefficients, or an
723 # interpolation object.
724 coeffs = fitter(indices, collapsed, self.config.order)
725
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)
735
736 return pipeBase.Struct(overscanValue=np.array(overscanVector),
737 maskArray=maskArray,
738 isTransposed=isTransposed)
739
740 def debugView(self, image, model, amp=None):
741 """Debug display for the final overscan solution.
742
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
757
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)
763
764 num = len(collapsed)
765 indices = 2.0 * np.arange(num)/float(num) - 1.0
766
767 if np.ma.is_masked(collapsed):
768 collapsedMask = collapsed.mask
769 else:
770 collapsedMask = np.array(num*[np.ma.nomask])
771
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()
def collapseArrayMedian(self, maskedArray)
Definition: overscan.py:540
def fitOverscan(self, overscanImage, isTransposed=False)
Definition: overscan.py:393
def measureVectorOverscan(self, image, isTransposed=False)
Definition: overscan.py:662
def __init__(self, statControl=None, **kwargs)
Definition: overscan.py:137
def debugView(self, image, model, amp=None)
Definition: overscan.py:740
def splineFit(self, indices, collapsed, numBins)
Definition: overscan.py:568
def broadcastFitToImage(self, overscanValue, imageArray, transpose=False)
Definition: overscan.py:296
def correctOverscan(self, exposure, amp, imageBBox, overscanBBox, isTransposed=True)
Definition: overscan.py:240
def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False)
Definition: overscan.py:346
def run(self, exposure, amp, isTransposed=False)
Definition: overscan.py:148
def countMaskedPixels(maskedIm, maskPlane)
def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT')
std::vector< double > fitOverscanImage(lsst::afw::image::MaskedImage< ImagePixelT > const &overscan, std::vector< std::string > badPixelMask, bool isTransposed)
Definition: Isr.cc:53