LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
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:
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 serial overscan image
171 data (scalar or `lsst.afw.image.Image`).
172 ``overscanImage``
173 Image of the serial overscan region with the serial
174 overscan correction applied
175 (`lsst.afw.image.Image`). This quantity is used to
176 estimate the amplifier read noise empirically.
177 ``parallelOverscanFit``
178 Value or fit subtracted from the parallel overscan
179 image data (scalar, `lsst.afw.image.Image`, or None).
180 ``parallelOverscanImage``
181 Image of the parallel overscan region with the
182 parallel overscan correction applied
183 (`lsst.afw.image.Image` or None).
184
185 Raises
186 ------
187 RuntimeError
188 Raised if an invalid overscan type is set.
189 """
190 # Do Serial overscan first.
191 serialOverscanBBox = amp.getRawSerialOverscanBBox()
192 imageBBox = amp.getRawDataBBox()
193
194 if self.config.doParallelOverscan:
195 # We need to extend the serial overscan BBox to the full
196 # size of the detector.
197 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
198 imageBBox = imageBBox.expandedTo(parallelOverscanBBox)
199
200 serialOverscanBBox = geom.Box2I(geom.Point2I(serialOverscanBBox.getMinX(),
201 imageBBox.getMinY()),
202 geom.Extent2I(serialOverscanBBox.getWidth(),
203 imageBBox.getHeight()))
204 serialResults = self.correctOverscan(exposure, amp,
205 imageBBox, serialOverscanBBox, isTransposed=isTransposed)
206 overscanMean = serialResults.overscanMean
207 overscanSigma = serialResults.overscanSigma
208 residualMean = serialResults.overscanMeanResidual
209 residualSigma = serialResults.overscanSigmaResidual
210
211 # Do Parallel Overscan
212 parallelResults = None
213 if self.config.doParallelOverscan:
214 # This does not need any extensions, as we'll only
215 # subtract it from the data region.
216 parallelOverscanBBox = amp.getRawParallelOverscanBBox()
217 imageBBox = amp.getRawDataBBox()
218
219 maskIm = exposure.getMaskedImage()
220 maskIm = maskIm.Factory(maskIm, parallelOverscanBBox)
221
222 # The serial overscan correction has removed the majority
223 # of the signal in the parallel overscan region, so the
224 # mean should be close to zero. The noise in both should
225 # be similar, so we can use the noise from the serial
226 # overscan region to set the threshold for bleed
227 # detection.
228 thresholdLevel = self.config.numSigmaClip * serialResults.overscanSigmaResidual
229 makeThresholdMask(maskIm, threshold=thresholdLevel, growFootprints=0)
230 maskPix = countMaskedPixels(maskIm, self.config.maskPlanes)
231 xSize, ySize = parallelOverscanBBox.getDimensions()
232 if maskPix > xSize*ySize*self.config.parallelOverscanMaskThreshold:
233 self.log.warning('Fraction of masked pixels for parallel overscan calculation larger'
234 ' than %f of total pixels (i.e. %f masked pixels) on amp %s.',
235 self.config.parallelOverscanMaskThreshold, maskPix, amp.getName())
236 self.log.warning('Not doing parallel overscan correction.')
237 else:
238 parallelResults = self.correctOverscan(exposure, amp,
239 imageBBox, parallelOverscanBBox,
240 isTransposed=not isTransposed)
241
242 overscanMean = (overscanMean, parallelResults.overscanMean)
243 overscanSigma = (overscanSigma, parallelResults.overscanSigma)
244 residualMean = (residualMean, parallelResults.overscanMeanResidual)
245 residualSigma = (residualSigma, parallelResults.overscanSigmaResidual)
246 parallelOverscanFit = parallelResults.overscanOverscanModel if parallelResults else None
247 parallelOverscanImage = parallelResults.overscanImage if parallelResults else None
248
249 return pipeBase.Struct(imageFit=serialResults.ampOverscanModel,
250 overscanFit=serialResults.overscanOverscanModel,
251 overscanImage=serialResults.overscanImage,
252
253 parallelOverscanFit=parallelOverscanFit,
254 parallelOverscanImage=parallelOverscanImage,
255 overscanMean=overscanMean,
256 overscanSigma=overscanSigma,
257 residualMean=residualMean,
258 residualSigma=residualSigma)
259
260 def correctOverscan(self, exposure, amp, imageBBox, overscanBBox, isTransposed=True):
261 """
262 """
263 overscanBox = self.trimOverscan(exposure, amp, overscanBBox,
264 self.config.leadingColumnsToSkip,
265 self.config.trailingColumnsToSkip,
266 transpose=isTransposed)
267 overscanImage = exposure[overscanBox].getMaskedImage()
268 overscanArray = overscanImage.image.array
269
270 # Mask pixels.
271 maskVal = overscanImage.mask.getPlaneBitMask(self.config.maskPlanes)
272 overscanMask = ~((overscanImage.mask.array & maskVal) == 0)
273
274 median = np.ma.median(np.ma.masked_where(overscanMask, overscanArray))
275 bad = np.where(np.abs(overscanArray - median) > self.config.maxDeviation)
276 overscanMask[bad] = overscanImage.mask.getPlaneBitMask("SAT")
277
278 # Do overscan fit.
279 # CZW: Handle transposed correctly.
280 overscanResults = self.fitOverscan(overscanImage, isTransposed=isTransposed)
281
282 # Correct image region (and possibly parallel-overscan region).
283 ampImage = exposure[imageBBox]
284 ampOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue,
285 ampImage.image.array,
286 transpose=isTransposed)
287 ampImage.image.array -= ampOverscanModel
288
289 # Correct overscan region (and possibly doubly-overscaned
290 # region).
291 overscanImage = exposure[overscanBBox]
292 # CZW: Transposed?
293 overscanOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue,
294 overscanImage.image.array)
295 overscanImage.image.array -= overscanOverscanModel
296
297 self.debugView(overscanImage, overscanResults.overscanValue, amp)
298
299 # Find residual fit statistics.
300 stats = afwMath.makeStatistics(overscanImage.getMaskedImage(),
301 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl)
302 residualMean = stats.getValue(afwMath.MEDIAN)
303 residualSigma = stats.getValue(afwMath.STDEVCLIP)
304
305 return pipeBase.Struct(ampOverscanModel=ampOverscanModel,
306 overscanOverscanModel=overscanOverscanModel,
307 overscanImage=overscanImage,
308 overscanValue=overscanResults.overscanValue,
309
310 overscanMean=overscanResults.overscanMean,
311 overscanSigma=overscanResults.overscanSigma,
312 overscanMeanResidual=residualMean,
313 overscanSigmaResidual=residualSigma
314 )
315
316 def broadcastFitToImage(self, overscanValue, imageArray, transpose=False):
317 """Broadcast 0 or 1 dimension fit to appropriate shape.
318
319 Parameters
320 ----------
321 overscanValue : `numpy.ndarray`, (Nrows, ) or scalar
322 Overscan fit to broadcast.
323 imageArray : `numpy.ndarray`, (Nrows, Ncols)
324 Image array that we want to match.
325 transpose : `bool`, optional
326 Switch order to broadcast along the other axis.
327
328 Returns
329 -------
330 overscanModel : `numpy.ndarray`, (Nrows, Ncols) or scalar
331 Expanded overscan fit.
332
333 Raises
334 ------
335 RuntimeError
336 Raised if no axis has the appropriate dimension.
337 """
338 if isinstance(overscanValue, np.ndarray):
339 overscanModel = np.zeros_like(imageArray)
340
341 if transpose is False:
342 if imageArray.shape[0] == overscanValue.shape[0]:
343 overscanModel[:, :] = overscanValue[:, np.newaxis]
344 elif imageArray.shape[1] == overscanValue.shape[0]:
345 overscanModel[:, :] = overscanValue[np.newaxis, :]
346 elif imageArray.shape[0] == overscanValue.shape[1]:
347 overscanModel[:, :] = overscanValue[np.newaxis, :]
348 else:
349 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to "
350 f"match {imageArray.shape}")
351 else:
352 if imageArray.shape[1] == overscanValue.shape[0]:
353 overscanModel[:, :] = overscanValue[np.newaxis, :]
354 elif imageArray.shape[0] == overscanValue.shape[0]:
355 overscanModel[:, :] = overscanValue[:, np.newaxis]
356 elif imageArray.shape[1] == overscanValue.shape[1]:
357 overscanModel[:, :] = overscanValue[:, np.newaxis]
358 else:
359 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to "
360 f"match {imageArray.shape}")
361 else:
362 overscanModel = overscanValue
363
364 return overscanModel
365
366 def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False):
367 """Trim overscan region to remove edges.
368
369 Parameters
370 ----------
371 exposure : `lsst.afw.image.Exposure`
372 Exposure containing data.
374 Amplifier containing geometry information.
375 bbox : `lsst.geom.Box2I`
376 Bounding box of the overscan region.
377 skipLeading : `int`
378 Number of leading (towards data region) rows/columns to skip.
379 skipTrailing : `int`
380 Number of trailing (away from data region) rows/columns to skip.
381 transpose : `bool`, optional
382 Operate on the transposed array.
383
384 Returns
385 -------
386 overscanArray : `numpy.array`, (N, M)
387 Data array to fit.
388 overscanMask : `numpy.array`, (N, M)
389 Data mask.
390 """
391 dx0, dy0, dx1, dy1 = (0, 0, 0, 0)
392 dataBBox = amp.getRawDataBBox()
393 if transpose:
394 if dataBBox.getBeginY() < bbox.getBeginY():
395 dy0 += skipLeading
396 dy1 -= skipTrailing
397 else:
398 dy0 += skipTrailing
399 dy1 -= skipLeading
400 else:
401 if dataBBox.getBeginX() < bbox.getBeginX():
402 dx0 += skipLeading
403 dx1 -= skipTrailing
404 else:
405 dx0 += skipTrailing
406 dx1 -= skipLeading
407
408 overscanBBox = geom.Box2I(bbox.getBegin() + geom.Extent2I(dx0, dy0),
409 geom.Extent2I(bbox.getWidth() - dx0 + dx1,
410 bbox.getHeight() - dy0 + dy1))
411 return overscanBBox
412
413 def fitOverscan(self, overscanImage, isTransposed=False):
414 if self.config.fitType in ('MEAN', 'MEANCLIP', 'MEDIAN'):
415 # Transposition has no effect here.
416 overscanResult = self.measureConstantOverscan(overscanImage)
417 overscanValue = overscanResult.overscanValue
418 overscanMean = overscanValue
419 overscanSigma = 0.0
420 elif self.config.fitType in ('MEDIAN_PER_ROW', 'POLY', 'CHEB', 'LEG',
421 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'):
422 # Force transposes as needed
423 overscanResult = self.measureVectorOverscan(overscanImage, isTransposed)
424 overscanValue = overscanResult.overscanValue
425
426 stats = afwMath.makeStatistics(overscanResult.overscanValue,
427 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl)
428 overscanMean = stats.getValue(afwMath.MEDIAN)
429 overscanSigma = stats.getValue(afwMath.STDEVCLIP)
430 else:
431 raise ValueError('%s : %s an invalid overscan type' %
432 ("overscanCorrection", self.config.fitType))
433
434 return pipeBase.Struct(overscanValue=overscanValue,
435 overscanMean=overscanMean,
436 overscanSigma=overscanSigma,
437 )
438
439 @staticmethod
440 def integerConvert(image):
441 """Return an integer version of the input image.
442
443 Parameters
444 ----------
445 image : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
446 Image to convert to integers.
447
448 Returns
449 -------
450 outI : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage`
451 The integer converted image.
452
453 Raises
454 ------
455 RuntimeError
456 Raised if the input image could not be converted.
457 """
458 if hasattr(image, "image"):
459 # Is a maskedImage:
460 imageI = image.image.convertI()
461 outI = afwImage.MaskedImageI(imageI, image.mask, image.variance)
462 elif hasattr(image, "convertI"):
463 # Is an Image:
464 outI = image.convertI()
465 elif hasattr(image, "astype"):
466 # Is a numpy array:
467 outI = image.astype(int)
468 else:
469 raise RuntimeError("Could not convert this to integers: %s %s %s",
470 image, type(image), dir(image))
471 return outI
472
473 # Constant methods
474 def measureConstantOverscan(self, image):
475 """Measure a constant overscan value.
476
477 Parameters
478 ----------
480 Image data to measure the overscan from.
481
482 Returns
483 -------
484 results : `lsst.pipe.base.Struct`
485 Overscan result with entries:
486 - ``overscanValue``: Overscan value to subtract (`float`)
487 - ``isTransposed``: Orientation of the overscan (`bool`)
488 """
489 if self.config.fitType == 'MEDIAN':
490 calcImage = self.integerConvert(image)
491 else:
492 calcImage = image
493 fitType = afwMath.stringToStatisticsProperty(self.config.fitType)
494 overscanValue = afwMath.makeStatistics(calcImage, fitType, self.statControl).getValue()
495
496 return pipeBase.Struct(overscanValue=overscanValue,
497 isTransposed=False)
498
499 # Vector correction utilities
500 def getImageArray(self, image):
501 """Extract the numpy array from the input image.
502
503 Parameters
504 ----------
506 Image data to pull array from.
507
508 calcImage : `numpy.ndarray`
509 Image data array for numpy operating.
510 """
511 if hasattr(image, "getImage"):
512 calcImage = image.getImage().getArray()
513 calcImage = np.ma.masked_where(image.getMask().getArray() & self.statControl.getAndMask(),
514 calcImage)
515 else:
516 calcImage = image.getArray()
517 return calcImage
518
519 def maskOutliers(self, imageArray):
520 """Mask outliers in a row of overscan data from a robust sigma
521 clipping procedure.
522
523 Parameters
524 ----------
525 imageArray : `numpy.ndarray`
526 Image to filter along numpy axis=1.
527
528 Returns
529 -------
530 maskedArray : `numpy.ma.masked_array`
531 Masked image marking outliers.
532 """
533 lq, median, uq = np.percentile(imageArray, [25.0, 50.0, 75.0], axis=1)
534 axisMedians = median
535 axisStdev = 0.74*(uq - lq) # robust stdev
536
537 diff = np.abs(imageArray - axisMedians[:, np.newaxis])
538 return np.ma.masked_where(diff > self.statControl.getNumSigmaClip()
539 * axisStdev[:, np.newaxis], imageArray)
540
541 @staticmethod
542 def collapseArray(maskedArray):
543 """Collapse overscan array (and mask) to a 1-D vector of values.
544
545 Parameters
546 ----------
547 maskedArray : `numpy.ma.masked_array`
548 Masked array of input overscan data.
549
550 Returns
551 -------
552 collapsed : `numpy.ma.masked_array`
553 Single dimensional overscan data, combined with the mean.
554 """
555 collapsed = np.mean(maskedArray, axis=1)
556 if collapsed.mask.sum() > 0:
557 collapsed.data[collapsed.mask] = np.mean(maskedArray.data[collapsed.mask], axis=1)
558 return collapsed
559
560 def collapseArrayMedian(self, maskedArray):
561 """Collapse overscan array (and mask) to a 1-D vector of using the
562 correct integer median of row-values.
563
564 Parameters
565 ----------
566 maskedArray : `numpy.ma.masked_array`
567 Masked array of input overscan data.
568
569 Returns
570 -------
571 collapsed : `numpy.ma.masked_array`
572 Single dimensional overscan data, combined with the afwMath median.
573 """
574 integerMI = self.integerConvert(maskedArray)
575
576 collapsed = []
577 fitType = afwMath.stringToStatisticsProperty('MEDIAN')
578 for row in integerMI:
579 newRow = row.compressed()
580 if len(newRow) > 0:
581 rowMedian = afwMath.makeStatistics(newRow, fitType, self.statControl).getValue()
582 else:
583 rowMedian = np.nan
584 collapsed.append(rowMedian)
585
586 return np.array(collapsed)
587
588 def splineFit(self, indices, collapsed, numBins):
589 """Wrapper function to match spline fit API to polynomial fit API.
590
591 Parameters
592 ----------
593 indices : `numpy.ndarray`
594 Locations to evaluate the spline.
595 collapsed : `numpy.ndarray`
596 Collapsed overscan values corresponding to the spline
597 evaluation points.
598 numBins : `int`
599 Number of bins to use in constructing the spline.
600
601 Returns
602 -------
604 Interpolation object for later evaluation.
605 """
606 if not np.ma.is_masked(collapsed):
607 collapsed.mask = np.array(len(collapsed)*[np.ma.nomask])
608
609 numPerBin, binEdges = np.histogram(indices, bins=numBins,
610 weights=1 - collapsed.mask.astype(int))
611 with np.errstate(invalid="ignore"):
612 values = np.histogram(indices, bins=numBins,
613 weights=collapsed.data*~collapsed.mask)[0]/numPerBin
614 binCenters = np.histogram(indices, bins=numBins,
615 weights=indices*~collapsed.mask)[0]/numPerBin
616
617 if len(binCenters[numPerBin > 0]) < 5:
618 self.log.warn("Cannot do spline fitting for overscan: %s valid points.",
619 len(binCenters[numPerBin > 0]))
620 # Return a scalar value if we have one, otherwise
621 # return zero. This amplifier is hopefully already
622 # masked.
623 if len(values[numPerBin > 0]) != 0:
624 return float(values[numPerBin > 0][0])
625 else:
626 return 0.0
627
628 interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
629 values.astype(float)[numPerBin > 0],
630 afwMath.stringToInterpStyle(self.config.fitType))
631 return interp
632
633 @staticmethod
634 def splineEval(indices, interp):
635 """Wrapper function to match spline evaluation API to polynomial fit
636 API.
637
638 Parameters
639 ----------
640 indices : `numpy.ndarray`
641 Locations to evaluate the spline.
642 interp : `lsst.afw.math.interpolate`
643 Interpolation object to use.
644
645 Returns
646 -------
647 values : `numpy.ndarray`
648 Evaluated spline values at each index.
649 """
650
651 return interp.interpolate(indices.astype(float))
652
653 @staticmethod
654 def maskExtrapolated(collapsed):
655 """Create mask if edges are extrapolated.
656
657 Parameters
658 ----------
659 collapsed : `numpy.ma.masked_array`
660 Masked array to check the edges of.
661
662 Returns
663 -------
664 maskArray : `numpy.ndarray`
665 Boolean numpy array of pixels to mask.
666 """
667 maskArray = np.full_like(collapsed, False, dtype=bool)
668 if np.ma.is_masked(collapsed):
669 num = len(collapsed)
670 for low in range(num):
671 if not collapsed.mask[low]:
672 break
673 if low > 0:
674 maskArray[:low] = True
675 for high in range(1, num):
676 if not collapsed.mask[-high]:
677 break
678 if high > 1:
679 maskArray[-high:] = True
680 return maskArray
681
682 def measureVectorOverscan(self, image, isTransposed=False):
683 """Calculate the 1-d vector overscan from the input overscan image.
684
685 Parameters
686 ----------
688 Image containing the overscan data.
689 isTransposed : `bool`
690 If true, the image has been transposed.
691
692 Returns
693 -------
694 results : `lsst.pipe.base.Struct`
695 Overscan result with entries:
696
697 ``overscanValue``
698 Overscan value to subtract (`float`)
699 ``maskArray``
700 List of rows that should be masked as ``SUSPECT`` when the
701 overscan solution is applied. (`list` [ `bool` ])
702 ``isTransposed``
703 Indicates if the overscan data was transposed during
704 calcuation, noting along which axis the overscan should be
705 subtracted. (`bool`)
706 """
707 calcImage = self.getImageArray(image)
708
709 # operate on numpy-arrays from here
710 if isTransposed:
711 calcImage = np.transpose(calcImage)
712 masked = self.maskOutliers(calcImage)
713
714 if self.config.fitType == 'MEDIAN_PER_ROW':
715 mi = afwImage.MaskedImageI(image.getBBox())
716 masked = masked.astype(int)
717 if isTransposed:
718 masked = masked.transpose()
719
720 mi.image.array[:, :] = masked.data[:, :]
721 if bool(masked.mask.shape):
722 mi.mask.array[:, :] = masked.mask[:, :]
723
724 overscanVector = fitOverscanImage(mi, self.config.maskPlanes, isTransposed)
725 maskArray = self.maskExtrapolated(overscanVector)
726 else:
727 collapsed = self.collapseArray(masked)
728
729 num = len(collapsed)
730 indices = 2.0*np.arange(num)/float(num) - 1.0
731
732 poly = np.polynomial
733 fitter, evaler = {
734 'POLY': (poly.polynomial.polyfit, poly.polynomial.polyval),
735 'CHEB': (poly.chebyshev.chebfit, poly.chebyshev.chebval),
736 'LEG': (poly.legendre.legfit, poly.legendre.legval),
737 'NATURAL_SPLINE': (self.splineFit, self.splineEval),
738 'CUBIC_SPLINE': (self.splineFit, self.splineEval),
739 'AKIMA_SPLINE': (self.splineFit, self.splineEval)
740 }[self.config.fitType]
741
742 # These are the polynomial coefficients, or an
743 # interpolation object.
744 coeffs = fitter(indices, collapsed, self.config.order)
745
746 if isinstance(coeffs, float):
747 self.log.warn("Using fallback value %f due to fitter failure. Amplifier will be masked.",
748 coeffs)
749 overscanVector = np.full_like(indices, coeffs)
750 maskArray = np.full_like(collapsed, True, dtype=bool)
751 else:
752 # Otherwise we can just use things as normal.
753 overscanVector = evaler(indices, coeffs)
754 maskArray = self.maskExtrapolated(collapsed)
755
756 return pipeBase.Struct(overscanValue=np.array(overscanVector),
757 maskArray=maskArray,
758 isTransposed=isTransposed)
759
760 def debugView(self, image, model, amp=None):
761 """Debug display for the final overscan solution.
762
763 Parameters
764 ----------
765 image : `lsst.afw.image.Image`
766 Input image the overscan solution was determined from.
767 model : `numpy.ndarray` or `float`
768 Overscan model determined for the image.
769 amp : `lsst.afw.cameraGeom.Amplifier`, optional
770 Amplifier to extract diagnostic information.
771 """
772 import lsstDebug
773 if not lsstDebug.Info(__name__).display:
774 return
775 if not self.allowDebug:
776 return
777
778 calcImage = self.getImageArray(image)
779 # CZW: Check that this is ok
780 calcImage = np.transpose(calcImage)
781 masked = self.maskOutliers(calcImage)
782 collapsed = self.collapseArray(masked)
783
784 num = len(collapsed)
785 indices = 2.0 * np.arange(num)/float(num) - 1.0
786
787 if np.ma.is_masked(collapsed):
788 collapsedMask = collapsed.mask
789 else:
790 collapsedMask = np.array(num*[np.ma.nomask])
791
792 import matplotlib.pyplot as plot
793 figure = plot.figure(1)
794 figure.clear()
795 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
796 axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 'k+')
797 if collapsedMask.sum() > 0:
798 axes.plot(indices[collapsedMask], collapsed.data[collapsedMask], 'b+')
799 if isinstance(model, np.ndarray):
800 plotModel = model
801 else:
802 plotModel = np.zeros_like(indices)
803 plotModel += model
804 axes.plot(indices, plotModel, 'r-')
805 plot.xlabel("centered/scaled position along overscan region")
806 plot.ylabel("pixel value/fit value")
807 if amp:
808 plot.title(f"{amp.getName()} DataX: "
809 f"[{amp.getRawDataBBox().getBeginX()}:{amp.getRawBBox().getEndX()}]"
810 f"OscanX: [{amp.getRawHorizontalOverscanBBox().getBeginX()}:"
811 f"{amp.getRawHorizontalOverscanBBox().getEndX()}] {self.config.fitType}")
812 else:
813 plot.title("No amp supplied.")
814 figure.show()
815 prompt = "Press Enter or c to continue [chp]..."
816 while True:
817 ans = input(prompt).lower()
818 if ans in ("", " ", "c",):
819 break
820 elif ans in ("p", ):
821 import pdb
822 pdb.set_trace()
823 elif ans in ('x', ):
824 self.allowDebug = False
825 break
826 elif ans in ("h", ):
827 print("[h]elp [c]ontinue [p]db e[x]itDebug")
828 plot.close()
table::Key< int > type
Definition: Detector.cc:163
table::Key< int > to
table::Key< int > a
Geometry and electronic information about raw amplifier images.
Definition: Amplifier.h:86
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
A class to represent a 2-dimensional array of pixels.
Definition: Image.h:51
A class to manipulate images, masks, and variance as a single object.
Definition: MaskedImage.h:74
Pass parameters to a Statistics object.
Definition: Statistics.h:83
An integer coordinate rectangle.
Definition: Box.h:55
def collapseArrayMedian(self, maskedArray)
Definition: overscan.py:560
def fitOverscan(self, overscanImage, isTransposed=False)
Definition: overscan.py:413
def measureVectorOverscan(self, image, isTransposed=False)
Definition: overscan.py:682
def __init__(self, statControl=None, **kwargs)
Definition: overscan.py:137
def debugView(self, image, model, amp=None)
Definition: overscan.py:760
def splineFit(self, indices, collapsed, numBins)
Definition: overscan.py:588
def broadcastFitToImage(self, overscanValue, imageArray, transpose=False)
Definition: overscan.py:316
def correctOverscan(self, exposure, amp, imageBBox, overscanBBox, isTransposed=True)
Definition: overscan.py:260
def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False)
Definition: overscan.py:366
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition: Statistics.h:361
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
Definition: Statistics.cc:762
Interpolate::Style stringToInterpStyle(std::string const &style)
Conversion function to switch a string to an Interpolate::Style.
Definition: Interpolate.cc:261
std::shared_ptr< Interpolate > makeInterpolate(std::vector< double > const &x, std::vector< double > const &y, Interpolate::Style const style=Interpolate::AKIMA_SPLINE)
A factory function to make Interpolate objects.
Definition: Interpolate.cc:347