Coverage for python/lsst/ip/isr/overscan.py: 12%

288 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-08 02:21 -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/>. 

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( 115 ↛ exitline 115 didn't jump to the function exit

116 dtype=float, 

117 doc="Maximum deviation from median (in ADU) to mask in overscan correction.", 

118 default=1000.0, check=lambda x: x > 0, 

119 ) 

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. 

155 amp : `lsst.afw.cameraGeom.Amplifier` 

156 Amplifier to use for debugging purposes. 

157 isTransposed : `bool`, optional 

158 Is the image transposed, such that serial and parallel 

159 overscan regions are reversed? Default is False. 

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. 

353 amp : `lsst.afw.cameraGeom.Amplifier` 

354 Amplifier containing geometry information. 

355 bbox : `lsst.geom.Box2I` 

356 Bounding box of the overscan region. 

357 skipLeading : `int` 

358 Number of leading (towards data region) rows/columns to skip. 

359 skipTrailing : `int` 

360 Number of trailing (away from data region) rows/columns to skip. 

361 transpose : `bool`, optional 

362 Operate on the transposed array. 

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 ---------- 

459 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` 

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 ---------- 

485 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` 

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 ------- 

583 interp : `lsst.afw.math.Interpolate` 

584 Interpolation object for later evaluation. 

585 """ 

586 if not np.ma.is_masked(collapsed): 

587 collapsed.mask = np.array(len(collapsed)*[np.ma.nomask]) 

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 ---------- 

667 image : `lsst.afw.image.MaskedImage` 

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()