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

292 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-14 12:22 +0000

1# This file is part of ip_isr. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

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

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

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

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

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

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

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

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

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

687 image : `lsst.afw.image.MaskedImage` 

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