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

282 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-19 19:30 +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 

22import numpy as np 

23import time 

24import lsst.afw.math as afwMath 

25import lsst.afw.image as afwImage 

26import lsst.geom as geom 

27import lsst.pipe.base as pipeBase 

28import lsst.pex.config as pexConfig 

29 

30from .isr import fitOverscanImage 

31 

32__all__ = ["OverscanCorrectionTaskConfig", "OverscanCorrectionTask"] 

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 

84 leadingColumnsToSkip = pexConfig.Field( 

85 dtype=int, 

86 doc="Number of leading columns to skip in serial overscan correction.", 

87 default=0, 

88 ) 

89 trailingColumnsToSkip = pexConfig.Field( 

90 dtype=int, 

91 doc="Number of trailing columns to skip in serial overscan correction.", 

92 default=0, 

93 ) 

94 leadingRowsToSkip = pexConfig.Field( 

95 dtype=int, 

96 doc="Number of leading rows to skip in parallel overscan correction.", 

97 default=0, 

98 ) 

99 trailingRowsToSkip = pexConfig.Field( 

100 dtype=int, 

101 doc="Number of trailing rows to skip in parallel overscan correction.", 

102 default=0, 

103 ) 

104 

105 maxDeviation = pexConfig.Field( 105 ↛ exitline 105 didn't jump to the function exit

106 dtype=float, 

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

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

109 ) 

110 

111 

112class OverscanCorrectionTask(pipeBase.Task): 

113 """Correction task for overscan. 

114 

115 This class contains a number of utilities that are easier to 

116 understand and use when they are not embedded in nested if/else 

117 loops. 

118 

119 Parameters 

120 ---------- 

121 statControl : `lsst.afw.math.StatisticsControl`, optional 

122 Statistics control object. 

123 """ 

124 ConfigClass = OverscanCorrectionTaskConfig 

125 _DefaultName = "overscan" 

126 

127 def __init__(self, statControl=None, **kwargs): 

128 super().__init__(**kwargs) 

129 self.allowDebug = True 

130 

131 if statControl: 

132 self.statControl = statControl 

133 else: 

134 self.statControl = afwMath.StatisticsControl() 

135 self.statControl.setNumSigmaClip(self.config.numSigmaClip) 

136 self.statControl.setAndMask(afwImage.Mask.getPlaneBitMask(self.config.maskPlanes)) 

137 

138 def run(self, exposure, amp, isTransposed=False): 

139 """Measure and remove an overscan from an amplifier image. 

140 

141 Parameters 

142 ---------- 

143 exposure : `lsst.afw.image.Exposure` 

144 Image data that will have the overscan corrections applied. 

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

146 Amplifier to use for debugging purposes. 

147 isTransposed : `bool`, optional 

148 Is the image transposed, such that serial and parallel 

149 overscan regions are reversed? Default is False. 

150 

151 Returns 

152 ------- 

153 overscanResults : `lsst.pipe.base.Struct` 

154 Result struct with components: 

155 

156 ``imageFit`` 

157 Value or fit subtracted from the amplifier image data 

158 (scalar or `lsst.afw.image.Image`). 

159 ``overscanFit`` 

160 Value or fit subtracted from the overscan image data 

161 (scalar or `lsst.afw.image.Image`). 

162 ``overscanImage`` 

163 Image of the overscan region with the overscan 

164 correction applied (`lsst.afw.image.Image`). This 

165 quantity is used to estimate the amplifier read noise 

166 empirically. 

167 

168 Raises 

169 ------ 

170 RuntimeError 

171 Raised if an invalid overscan type is set. 

172 """ 

173 # Do Serial overscan first. 

174 serialOverscanBBox = amp.getRawSerialOverscanBBox() 

175 imageBBox = amp.getRawDataBBox() 

176 

177 if self.config.doParallelOverscan: 

178 # We need to extend the serial overscan BBox to the full 

179 # size of the detector. 

180 parallelOverscanBBox = amp.getRawParallelOverscanBBox() 

181 imageBBox = imageBBox.expandedTo(parallelOverscanBBox) 

182 

183 serialOverscanBBox = geom.Box2I(geom.Point2I(serialOverscanBBox.getMinX(), 

184 imageBBox.getMinY()), 

185 geom.Extent2I(serialOverscanBBox.getWidth(), 

186 imageBBox.getHeight())) 

187 serialResults = self.correctOverscan(exposure, amp, 

188 imageBBox, serialOverscanBBox, isTransposed=isTransposed) 

189 overscanMean = serialResults.overscanMean 

190 overscanSigma = serialResults.overscanSigma 

191 residualMean = serialResults.overscanMeanResidual 

192 residualSigma = serialResults.overscanSigmaResidual 

193 

194 # Do Parallel Overscan 

195 if self.config.doParallelOverscan: 

196 # This does not need any extensions, as we'll only 

197 # subtract it from the data region. 

198 parallelOverscanBBox = amp.getRawParallelOverscanBBox() 

199 imageBBox = amp.getRawDataBBox() 

200 

201 parallelResults = self.correctOverscan(exposure, amp, 

202 imageBBox, parallelOverscanBBox, 

203 isTransposed=not isTransposed) 

204 

205 overscanMean = (overscanMean, parallelResults.overscanMean) 

206 overscanSigma = (overscanSigma, parallelResults.overscanSigma) 

207 residualMean = (residualMean, parallelResults.overscanMeanResidual) 

208 residualSigma = (residualSigma, parallelResults.overscanSigmaResidual) 

209 

210 return pipeBase.Struct(imageFit=serialResults.ampOverscanModel, 

211 overscanFit=serialResults.overscanOverscanModel, 

212 overscanImage=serialResults.overscanImage, 

213 

214 overscanMean=overscanMean, 

215 overscanSigma=overscanSigma, 

216 residualMean=residualMean, 

217 residualSigma=residualSigma) 

218 

219 def correctOverscan(self, exposure, amp, imageBBox, overscanBBox, isTransposed=True): 

220 """ 

221 """ 

222 overscanBox = self.trimOverscan(exposure, amp, overscanBBox, 

223 self.config.leadingColumnsToSkip, 

224 self.config.trailingColumnsToSkip, 

225 transpose=isTransposed) 

226 overscanImage = exposure[overscanBox].getMaskedImage() 

227 overscanArray = overscanImage.image.array 

228 

229 # Mask pixels. 

230 maskVal = overscanImage.mask.getPlaneBitMask(self.config.maskPlanes) 

231 overscanMask = ~((overscanImage.mask.array & maskVal) == 0) 

232 

233 median = np.ma.median(np.ma.masked_where(overscanMask, overscanArray)) 

234 bad = np.where(np.abs(overscanArray - median) > self.config.maxDeviation) 

235 overscanMask[bad] = overscanImage.mask.getPlaneBitMask("SAT") 

236 

237 # Do overscan fit. 

238 # CZW: Handle transposed correctly. 

239 overscanResults = self.fitOverscan(overscanImage, isTransposed=isTransposed) 

240 

241 # Correct image region (and possibly parallel-overscan region). 

242 ampImage = exposure[imageBBox] 

243 ampOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue, 

244 ampImage.image.array, 

245 transpose=isTransposed) 

246 ampImage.image.array -= ampOverscanModel 

247 

248 # Correct overscan region (and possibly doubly-overscaned 

249 # region). 

250 overscanImage = exposure[overscanBBox] 

251 # CZW: Transposed? 

252 overscanOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue, 

253 overscanImage.image.array) 

254 overscanImage.image.array -= overscanOverscanModel 

255 

256 self.debugView(overscanImage, overscanResults.overscanValue, amp) 

257 

258 # Find residual fit statistics. 

259 stats = afwMath.makeStatistics(overscanImage.getMaskedImage(), 

260 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl) 

261 residualMean = stats.getValue(afwMath.MEDIAN) 

262 residualSigma = stats.getValue(afwMath.STDEVCLIP) 

263 

264 return pipeBase.Struct(ampOverscanModel=ampOverscanModel, 

265 overscanOverscanModel=overscanOverscanModel, 

266 overscanImage=overscanImage, 

267 overscanValue=overscanResults.overscanValue, 

268 

269 overscanMean=overscanResults.overscanMean, 

270 overscanSigma=overscanResults.overscanSigma, 

271 overscanMeanResidual=residualMean, 

272 overscanSigmaResidual=residualSigma 

273 ) 

274 

275 def broadcastFitToImage(self, overscanValue, imageArray, transpose=False): 

276 """Broadcast 0 or 1 dimension fit to appropriate shape. 

277 

278 Parameters 

279 ---------- 

280 overscanValue : `np.ndarray`, (Nrows, ) or scalar 

281 Overscan fit to broadcast. 

282 imageArray : `np.ndarray`, (Nrows, Ncols) 

283 Image array that we want to match. 

284 transpose : `bool`, optional 

285 Switch order to broadcast along the other axis. 

286 

287 Returns 

288 ------- 

289 overscanModel : `np.ndarray`, (Nrows, Ncols) or scalar 

290 Expanded overscan fit. 

291 

292 Raises 

293 ------ 

294 RuntimeError 

295 Raised if no axis has the appropriate dimension. 

296 """ 

297 if isinstance(overscanValue, np.ndarray): 

298 overscanModel = np.zeros_like(imageArray) 

299 

300 if transpose is False: 

301 if imageArray.shape[0] == overscanValue.shape[0]: 

302 overscanModel[:, :] = overscanValue[:, np.newaxis] 

303 elif imageArray.shape[1] == overscanValue.shape[0]: 

304 overscanModel[:, :] = overscanValue[np.newaxis, :] 

305 elif imageArray.shape[0] == overscanValue.shape[1]: 

306 overscanModel[:, :] = overscanValue[np.newaxis, :] 

307 else: 

308 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to " 

309 f"match {imageArray.shape}") 

310 else: 

311 if imageArray.shape[1] == overscanValue.shape[0]: 

312 overscanModel[:, :] = overscanValue[np.newaxis, :] 

313 elif imageArray.shape[0] == overscanValue.shape[0]: 

314 overscanModel[:, :] = overscanValue[:, np.newaxis] 

315 elif imageArray.shape[1] == overscanValue.shape[1]: 

316 overscanModel[:, :] = overscanValue[:, np.newaxis] 

317 else: 

318 raise RuntimeError(f"Could not broadcast {overscanValue.shape} to " 

319 f"match {imageArray.shape}") 

320 else: 

321 overscanModel = overscanValue 

322 

323 return overscanModel 

324 

325 def trimOverscan(self, exposure, amp, bbox, skipLeading, skipTrailing, transpose=False): 

326 """Trim overscan region to remove edges. 

327 

328 Parameters 

329 ---------- 

330 exposure : `lsst.afw.image.Exposure` 

331 Exposure containing data. 

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

333 Amplifier containing geometry information. 

334 bbox : `lsst.geom.Box2I` 

335 Bounding box of the overscan region. 

336 skipLeading : `int` 

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

338 skipTrailing : `int` 

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

340 transpose : `bool`, optional 

341 Operate on the transposed array. 

342 

343 Returns 

344 ------- 

345 overscanArray : `numpy.array`, (N, M) 

346 Data array to fit. 

347 overscanMask : `numpy.array`, (N, M) 

348 Data mask. 

349 """ 

350 dx0, dy0, dx1, dy1 = (0, 0, 0, 0) 

351 dataBBox = amp.getRawDataBBox() 

352 if transpose: 

353 if dataBBox.getBeginY() < bbox.getBeginY(): 

354 dy0 += skipLeading 

355 dy1 -= skipTrailing 

356 else: 

357 dy0 += skipTrailing 

358 dy1 -= skipLeading 

359 else: 

360 if dataBBox.getBeginX() < bbox.getBeginX(): 

361 dx0 += skipLeading 

362 dx1 -= skipTrailing 

363 else: 

364 dx0 += skipTrailing 

365 dx1 -= skipLeading 

366 

367 overscanBBox = geom.Box2I(bbox.getBegin() + geom.Extent2I(dx0, dy0), 

368 geom.Extent2I(bbox.getWidth() - dx0 + dx1, 

369 bbox.getHeight() - dy0 + dy1)) 

370 return overscanBBox 

371 

372 def fitOverscan(self, overscanImage, isTransposed=False): 

373 if self.config.fitType in ('MEAN', 'MEANCLIP', 'MEDIAN'): 

374 # Transposition has no effect here. 

375 overscanResult = self.measureConstantOverscan(overscanImage) 

376 overscanValue = overscanResult.overscanValue 

377 overscanMean = overscanValue 

378 overscanSigma = 0.0 

379 elif self.config.fitType in ('MEDIAN_PER_ROW', 'POLY', 'CHEB', 'LEG', 

380 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'): 

381 # Force transposes as needed 

382 overscanResult = self.measureVectorOverscan(overscanImage, isTransposed) 

383 overscanValue = overscanResult.overscanValue 

384 

385 stats = afwMath.makeStatistics(overscanResult.overscanValue, 

386 afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl) 

387 overscanMean = stats.getValue(afwMath.MEDIAN) 

388 overscanSigma = stats.getValue(afwMath.STDEVCLIP) 

389 else: 

390 raise ValueError('%s : %s an invalid overscan type' % 

391 ("overscanCorrection", self.config.fitType)) 

392 

393 return pipeBase.Struct(overscanValue=overscanValue, 

394 overscanMean=overscanMean, 

395 overscanSigma=overscanSigma, 

396 ) 

397 

398 @staticmethod 

399 def integerConvert(image): 

400 """Return an integer version of the input image. 

401 

402 Parameters 

403 ---------- 

404 image : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage` 

405 Image to convert to integers. 

406 

407 Returns 

408 ------- 

409 outI : `numpy.ndarray`, `lsst.afw.image.Image` or `MaskedImage` 

410 The integer converted image. 

411 

412 Raises 

413 ------ 

414 RuntimeError 

415 Raised if the input image could not be converted. 

416 """ 

417 if hasattr(image, "image"): 

418 # Is a maskedImage: 

419 imageI = image.image.convertI() 

420 outI = afwImage.MaskedImageI(imageI, image.mask, image.variance) 

421 elif hasattr(image, "convertI"): 

422 # Is an Image: 

423 outI = image.convertI() 

424 elif hasattr(image, "astype"): 

425 # Is a numpy array: 

426 outI = image.astype(int) 

427 else: 

428 raise RuntimeError("Could not convert this to integers: %s %s %s", 

429 image, type(image), dir(image)) 

430 return outI 

431 

432 # Constant methods 

433 def measureConstantOverscan(self, image): 

434 """Measure a constant overscan value. 

435 

436 Parameters 

437 ---------- 

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

439 Image data to measure the overscan from. 

440 

441 Returns 

442 ------- 

443 results : `lsst.pipe.base.Struct` 

444 Overscan result with entries: 

445 - ``overscanValue``: Overscan value to subtract (`float`) 

446 - ``isTransposed``: Orientation of the overscan (`bool`) 

447 """ 

448 if self.config.fitType == 'MEDIAN': 

449 calcImage = self.integerConvert(image) 

450 else: 

451 calcImage = image 

452 fitType = afwMath.stringToStatisticsProperty(self.config.fitType) 

453 overscanValue = afwMath.makeStatistics(calcImage, fitType, self.statControl).getValue() 

454 

455 return pipeBase.Struct(overscanValue=overscanValue, 

456 isTransposed=False) 

457 

458 # Vector correction utilities 

459 def getImageArray(self, image): 

460 """Extract the numpy array from the input image. 

461 

462 Parameters 

463 ---------- 

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

465 Image data to pull array from. 

466 

467 calcImage : `numpy.ndarray` 

468 Image data array for numpy operating. 

469 """ 

470 if hasattr(image, "getImage"): 

471 calcImage = image.getImage().getArray() 

472 calcImage = np.ma.masked_where(image.getMask().getArray() & self.statControl.getAndMask(), 

473 calcImage) 

474 else: 

475 calcImage = image.getArray() 

476 return calcImage 

477 

478 def maskOutliers(self, imageArray): 

479 """Mask outliers in a row of overscan data from a robust sigma 

480 clipping procedure. 

481 

482 Parameters 

483 ---------- 

484 imageArray : `numpy.ndarray` 

485 Image to filter along numpy axis=1. 

486 

487 Returns 

488 ------- 

489 maskedArray : `numpy.ma.masked_array` 

490 Masked image marking outliers. 

491 """ 

492 lq, median, uq = np.percentile(imageArray, [25.0, 50.0, 75.0], axis=1) 

493 axisMedians = median 

494 axisStdev = 0.74*(uq - lq) # robust stdev 

495 

496 diff = np.abs(imageArray - axisMedians[:, np.newaxis]) 

497 return np.ma.masked_where(diff > self.statControl.getNumSigmaClip() 

498 * axisStdev[:, np.newaxis], imageArray) 

499 

500 @staticmethod 

501 def collapseArray(maskedArray): 

502 """Collapse overscan array (and mask) to a 1-D vector of values. 

503 

504 Parameters 

505 ---------- 

506 maskedArray : `numpy.ma.masked_array` 

507 Masked array of input overscan data. 

508 

509 Returns 

510 ------- 

511 collapsed : `numpy.ma.masked_array` 

512 Single dimensional overscan data, combined with the mean. 

513 """ 

514 collapsed = np.mean(maskedArray, axis=1) 

515 if collapsed.mask.sum() > 0: 

516 collapsed.data[collapsed.mask] = np.mean(maskedArray.data[collapsed.mask], axis=1) 

517 return collapsed 

518 

519 def collapseArrayMedian(self, maskedArray): 

520 """Collapse overscan array (and mask) to a 1-D vector of using the 

521 correct integer median of row-values. 

522 

523 Parameters 

524 ---------- 

525 maskedArray : `numpy.ma.masked_array` 

526 Masked array of input overscan data. 

527 

528 Returns 

529 ------- 

530 collapsed : `numpy.ma.masked_array` 

531 Single dimensional overscan data, combined with the afwMath median. 

532 """ 

533 integerMI = self.integerConvert(maskedArray) 

534 

535 collapsed = [] 

536 fitType = afwMath.stringToStatisticsProperty('MEDIAN') 

537 for row in integerMI: 

538 newRow = row.compressed() 

539 if len(newRow) > 0: 

540 rowMedian = afwMath.makeStatistics(newRow, fitType, self.statControl).getValue() 

541 else: 

542 rowMedian = np.nan 

543 collapsed.append(rowMedian) 

544 

545 return np.array(collapsed) 

546 

547 def splineFit(self, indices, collapsed, numBins): 

548 """Wrapper function to match spline fit API to polynomial fit API. 

549 

550 Parameters 

551 ---------- 

552 indices : `numpy.ndarray` 

553 Locations to evaluate the spline. 

554 collapsed : `numpy.ndarray` 

555 Collapsed overscan values corresponding to the spline 

556 evaluation points. 

557 numBins : `int` 

558 Number of bins to use in constructing the spline. 

559 

560 Returns 

561 ------- 

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

563 Interpolation object for later evaluation. 

564 """ 

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

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

567 

568 numPerBin, binEdges = np.histogram(indices, bins=numBins, 

569 weights=1 - collapsed.mask.astype(int)) 

570 with np.errstate(invalid="ignore"): 

571 values = np.histogram(indices, bins=numBins, 

572 weights=collapsed.data*~collapsed.mask)[0]/numPerBin 

573 binCenters = np.histogram(indices, bins=numBins, 

574 weights=indices*~collapsed.mask)[0]/numPerBin 

575 

576 if len(binCenters[numPerBin > 0]) < 5: 

577 self.log.warn("Cannot do spline fitting for overscan: %s valid points.", 

578 len(binCenters[numPerBin > 0])) 

579 # Return a scalar value if we have one, otherwise 

580 # return zero. This amplifier is hopefully already 

581 # masked. 

582 if len(values[numPerBin > 0]) != 0: 

583 return float(values[numPerBin > 0][0]) 

584 else: 

585 return 0.0 

586 

587 interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0], 

588 values.astype(float)[numPerBin > 0], 

589 afwMath.stringToInterpStyle(self.config.fitType)) 

590 return interp 

591 

592 @staticmethod 

593 def splineEval(indices, interp): 

594 """Wrapper function to match spline evaluation API to polynomial fit 

595 API. 

596 

597 Parameters 

598 ---------- 

599 indices : `numpy.ndarray` 

600 Locations to evaluate the spline. 

601 interp : `lsst.afw.math.interpolate` 

602 Interpolation object to use. 

603 

604 Returns 

605 ------- 

606 values : `numpy.ndarray` 

607 Evaluated spline values at each index. 

608 """ 

609 

610 return interp.interpolate(indices.astype(float)) 

611 

612 @staticmethod 

613 def maskExtrapolated(collapsed): 

614 """Create mask if edges are extrapolated. 

615 

616 Parameters 

617 ---------- 

618 collapsed : `numpy.ma.masked_array` 

619 Masked array to check the edges of. 

620 

621 Returns 

622 ------- 

623 maskArray : `numpy.ndarray` 

624 Boolean numpy array of pixels to mask. 

625 """ 

626 maskArray = np.full_like(collapsed, False, dtype=bool) 

627 if np.ma.is_masked(collapsed): 

628 num = len(collapsed) 

629 for low in range(num): 

630 if not collapsed.mask[low]: 

631 break 

632 if low > 0: 

633 maskArray[:low] = True 

634 for high in range(1, num): 

635 if not collapsed.mask[-high]: 

636 break 

637 if high > 1: 

638 maskArray[-high:] = True 

639 return maskArray 

640 

641 def measureVectorOverscan(self, image, isTransposed=False): 

642 """Calculate the 1-d vector overscan from the input overscan image. 

643 

644 Parameters 

645 ---------- 

646 image : `lsst.afw.image.MaskedImage` 

647 Image containing the overscan data. 

648 isTransposed : `bool` 

649 If true, the image has been transposed. 

650 

651 Returns 

652 ------- 

653 results : `lsst.pipe.base.Struct` 

654 Overscan result with entries: 

655 - ``overscanValue``: Overscan value to subtract (`float`) 

656 - ``maskArray`` : `list` [ `bool` ] 

657 List of rows that should be masked as ``SUSPECT`` when the 

658 overscan solution is applied. 

659 - ``isTransposed`` : `bool` 

660 Indicates if the overscan data was transposed during 

661 calcuation, noting along which axis the overscan should be 

662 subtracted. 

663 """ 

664 calcImage = self.getImageArray(image) 

665 

666 # operate on numpy-arrays from here 

667 if isTransposed: 

668 calcImage = np.transpose(calcImage) 

669 masked = self.maskOutliers(calcImage) 

670 

671 startTime = time.perf_counter() 

672 

673 if self.config.fitType == 'MEDIAN_PER_ROW': 

674 mi = afwImage.MaskedImageI(image.getBBox()) 

675 masked = masked.astype(int) 

676 if isTransposed: 

677 masked = masked.transpose() 

678 

679 mi.image.array[:, :] = masked.data[:, :] 

680 if bool(masked.mask.shape): 

681 mi.mask.array[:, :] = masked.mask[:, :] 

682 

683 overscanVector = fitOverscanImage(mi, self.config.maskPlanes, isTransposed) 

684 maskArray = self.maskExtrapolated(overscanVector) 

685 else: 

686 collapsed = self.collapseArray(masked) 

687 

688 num = len(collapsed) 

689 indices = 2.0*np.arange(num)/float(num) - 1.0 

690 

691 poly = np.polynomial 

692 fitter, evaler = { 

693 'POLY': (poly.polynomial.polyfit, poly.polynomial.polyval), 

694 'CHEB': (poly.chebyshev.chebfit, poly.chebyshev.chebval), 

695 'LEG': (poly.legendre.legfit, poly.legendre.legval), 

696 'NATURAL_SPLINE': (self.splineFit, self.splineEval), 

697 'CUBIC_SPLINE': (self.splineFit, self.splineEval), 

698 'AKIMA_SPLINE': (self.splineFit, self.splineEval) 

699 }[self.config.fitType] 

700 

701 # These are the polynomial coefficients, or an 

702 # interpolation object. 

703 coeffs = fitter(indices, collapsed, self.config.order) 

704 

705 if isinstance(coeffs, float): 

706 self.log.warn("Using fallback value %f due to fitter failure. Amplifier will be masked.", 

707 coeffs) 

708 overscanVector = np.full_like(indices, coeffs) 

709 maskArray = np.full_like(collapsed, True, dtype=bool) 

710 else: 

711 # Otherwise we can just use things as normal. 

712 overscanVector = evaler(indices, coeffs) 

713 maskArray = self.maskExtrapolated(collapsed) 

714 

715 endTime = time.perf_counter() 

716 self.log.info(f"Overscan measurement took {endTime - startTime}s for {self.config.fitType}") 

717 return pipeBase.Struct(overscanValue=np.array(overscanVector), 

718 maskArray=maskArray, 

719 isTransposed=isTransposed) 

720 

721 def debugView(self, image, model, amp=None): 

722 """Debug display for the final overscan solution. 

723 

724 Parameters 

725 ---------- 

726 image : `lsst.afw.image.Image` 

727 Input image the overscan solution was determined from. 

728 model : `numpy.ndarray` or `float` 

729 Overscan model determined for the image. 

730 amp : `lsst.afw.cameraGeom.Amplifier`, optional 

731 Amplifier to extract diagnostic information. 

732 """ 

733 import lsstDebug 

734 if not lsstDebug.Info(__name__).display: 

735 return 

736 if not self.allowDebug: 

737 return 

738 

739 calcImage = self.getImageArray(image) 

740 # CZW: Check that this is ok 

741 calcImage = np.transpose(calcImage) 

742 masked = self.maskOutliers(calcImage) 

743 collapsed = self.collapseArray(masked) 

744 

745 num = len(collapsed) 

746 indices = 2.0 * np.arange(num)/float(num) - 1.0 

747 

748 if np.ma.is_masked(collapsed): 

749 collapsedMask = collapsed.mask 

750 else: 

751 collapsedMask = np.array(num*[np.ma.nomask]) 

752 

753 import matplotlib.pyplot as plot 

754 figure = plot.figure(1) 

755 figure.clear() 

756 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8)) 

757 axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 'k+') 

758 if collapsedMask.sum() > 0: 

759 axes.plot(indices[collapsedMask], collapsed.data[collapsedMask], 'b+') 

760 if isinstance(model, np.ndarray): 

761 plotModel = model 

762 else: 

763 plotModel = np.zeros_like(indices) 

764 plotModel += model 

765 axes.plot(indices, plotModel, 'r-') 

766 plot.xlabel("centered/scaled position along overscan region") 

767 plot.ylabel("pixel value/fit value") 

768 if amp: 

769 plot.title(f"{amp.getName()} DataX: " 

770 f"[{amp.getRawDataBBox().getBeginX()}:{amp.getRawBBox().getEndX()}]" 

771 f"OscanX: [{amp.getRawHorizontalOverscanBBox().getBeginX()}:" 

772 f"{amp.getRawHorizontalOverscanBBox().getEndX()}] {self.config.fitType}") 

773 else: 

774 plot.title("No amp supplied.") 

775 figure.show() 

776 prompt = "Press Enter or c to continue [chp]..." 

777 while True: 

778 ans = input(prompt).lower() 

779 if ans in ("", " ", "c",): 

780 break 

781 elif ans in ("p", ): 

782 import pdb 

783 pdb.set_trace() 

784 elif ans in ('x', ): 

785 self.allowDebug = False 

786 break 

787 elif ans in ("h", ): 

788 print("[h]elp [c]ontinue [p]db e[x]itDebug") 

789 plot.close()