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

291 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-10 10:51 +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 makeThresholdMask(maskIm, threshold=self.config.numSigmaClip, growFootprints=0) 

222 maskPix = countMaskedPixels(maskIm, self.config.maskPlanes) 

223 xSize, ySize = parallelOverscanBBox.getDimensions() 

224 if maskPix > xSize*ySize*self.config.parallelOverscanMaskThreshold: 

225 self.log.warning('Fraction of masked pixels for parallel overscan calculation larger' 

226 ' than %f of total pixels (i.e. %f masked pixels) on amp %s.', 

227 self.config.parallelOverscanMaskThreshold, maskPix, amp.getName()) 

228 self.log.warning('Not doing parallel overscan correction.') 

229 else: 

230 parallelResults = self.correctOverscan(exposure, amp, 

231 imageBBox, parallelOverscanBBox, 

232 isTransposed=not isTransposed) 

233 

234 overscanMean = (overscanMean, parallelResults.overscanMean) 

235 overscanSigma = (overscanSigma, parallelResults.overscanSigma) 

236 residualMean = (residualMean, parallelResults.overscanMeanResidual) 

237 residualSigma = (residualSigma, parallelResults.overscanSigmaResidual) 

238 parallelOverscanFit = parallelResults.overscanOverscanModel if parallelResults else None 

239 parallelOverscanImage = parallelResults.overscanImage if parallelResults else None 

240 

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

242 overscanFit=serialResults.overscanOverscanModel, 

243 overscanImage=serialResults.overscanImage, 

244 

245 parallelOverscanFit=parallelOverscanFit, 

246 parallelOverscanImage=parallelOverscanImage, 

247 overscanMean=overscanMean, 

248 overscanSigma=overscanSigma, 

249 residualMean=residualMean, 

250 residualSigma=residualSigma) 

251 

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

253 """ 

254 """ 

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

256 self.config.leadingColumnsToSkip, 

257 self.config.trailingColumnsToSkip, 

258 transpose=isTransposed) 

259 overscanImage = exposure[overscanBox].getMaskedImage() 

260 overscanArray = overscanImage.image.array 

261 

262 # Mask pixels. 

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

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

265 

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

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

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

269 

270 # Do overscan fit. 

271 # CZW: Handle transposed correctly. 

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

273 

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

275 ampImage = exposure[imageBBox] 

276 ampOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue, 

277 ampImage.image.array, 

278 transpose=isTransposed) 

279 ampImage.image.array -= ampOverscanModel 

280 

281 # Correct overscan region (and possibly doubly-overscaned 

282 # region). 

283 overscanImage = exposure[overscanBBox] 

284 # CZW: Transposed? 

285 overscanOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue, 

286 overscanImage.image.array) 

287 overscanImage.image.array -= overscanOverscanModel 

288 

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

290 

291 # Find residual fit statistics. 

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

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

294 residualMean = stats.getValue(afwMath.MEDIAN) 

295 residualSigma = stats.getValue(afwMath.STDEVCLIP) 

296 

297 return pipeBase.Struct(ampOverscanModel=ampOverscanModel, 

298 overscanOverscanModel=overscanOverscanModel, 

299 overscanImage=overscanImage, 

300 overscanValue=overscanResults.overscanValue, 

301 

302 overscanMean=overscanResults.overscanMean, 

303 overscanSigma=overscanResults.overscanSigma, 

304 overscanMeanResidual=residualMean, 

305 overscanSigmaResidual=residualSigma 

306 ) 

307 

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

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

310 

311 Parameters 

312 ---------- 

313 overscanValue : `numpy.ndarray`, (Nrows, ) or scalar 

314 Overscan fit to broadcast. 

315 imageArray : `numpy.ndarray`, (Nrows, Ncols) 

316 Image array that we want to match. 

317 transpose : `bool`, optional 

318 Switch order to broadcast along the other axis. 

319 

320 Returns 

321 ------- 

322 overscanModel : `numpy.ndarray`, (Nrows, Ncols) or scalar 

323 Expanded overscan fit. 

324 

325 Raises 

326 ------ 

327 RuntimeError 

328 Raised if no axis has the appropriate dimension. 

329 """ 

330 if isinstance(overscanValue, np.ndarray): 

331 overscanModel = np.zeros_like(imageArray) 

332 

333 if transpose is False: 

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

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

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

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

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

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

340 else: 

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

342 f"match {imageArray.shape}") 

343 else: 

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

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

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

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

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

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

350 else: 

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

352 f"match {imageArray.shape}") 

353 else: 

354 overscanModel = overscanValue 

355 

356 return overscanModel 

357 

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

359 """Trim overscan region to remove edges. 

360 

361 Parameters 

362 ---------- 

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

364 Exposure containing data. 

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

366 Amplifier containing geometry information. 

367 bbox : `lsst.geom.Box2I` 

368 Bounding box of the overscan region. 

369 skipLeading : `int` 

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

371 skipTrailing : `int` 

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

373 transpose : `bool`, optional 

374 Operate on the transposed array. 

375 

376 Returns 

377 ------- 

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

379 Data array to fit. 

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

381 Data mask. 

382 """ 

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

384 dataBBox = amp.getRawDataBBox() 

385 if transpose: 

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

387 dy0 += skipLeading 

388 dy1 -= skipTrailing 

389 else: 

390 dy0 += skipTrailing 

391 dy1 -= skipLeading 

392 else: 

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

394 dx0 += skipLeading 

395 dx1 -= skipTrailing 

396 else: 

397 dx0 += skipTrailing 

398 dx1 -= skipLeading 

399 

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

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

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

403 return overscanBBox 

404 

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

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

407 # Transposition has no effect here. 

408 overscanResult = self.measureConstantOverscan(overscanImage) 

409 overscanValue = overscanResult.overscanValue 

410 overscanMean = overscanValue 

411 overscanSigma = 0.0 

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

413 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'): 

414 # Force transposes as needed 

415 overscanResult = self.measureVectorOverscan(overscanImage, isTransposed) 

416 overscanValue = overscanResult.overscanValue 

417 

418 stats = afwMath.makeStatistics(overscanResult.overscanValue, 

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

420 overscanMean = stats.getValue(afwMath.MEDIAN) 

421 overscanSigma = stats.getValue(afwMath.STDEVCLIP) 

422 else: 

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

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

425 

426 return pipeBase.Struct(overscanValue=overscanValue, 

427 overscanMean=overscanMean, 

428 overscanSigma=overscanSigma, 

429 ) 

430 

431 @staticmethod 

432 def integerConvert(image): 

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

434 

435 Parameters 

436 ---------- 

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

438 Image to convert to integers. 

439 

440 Returns 

441 ------- 

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

443 The integer converted image. 

444 

445 Raises 

446 ------ 

447 RuntimeError 

448 Raised if the input image could not be converted. 

449 """ 

450 if hasattr(image, "image"): 

451 # Is a maskedImage: 

452 imageI = image.image.convertI() 

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

454 elif hasattr(image, "convertI"): 

455 # Is an Image: 

456 outI = image.convertI() 

457 elif hasattr(image, "astype"): 

458 # Is a numpy array: 

459 outI = image.astype(int) 

460 else: 

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

462 image, type(image), dir(image)) 

463 return outI 

464 

465 # Constant methods 

466 def measureConstantOverscan(self, image): 

467 """Measure a constant overscan value. 

468 

469 Parameters 

470 ---------- 

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

472 Image data to measure the overscan from. 

473 

474 Returns 

475 ------- 

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

477 Overscan result with entries: 

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

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

480 """ 

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

482 calcImage = self.integerConvert(image) 

483 else: 

484 calcImage = image 

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

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

487 

488 return pipeBase.Struct(overscanValue=overscanValue, 

489 isTransposed=False) 

490 

491 # Vector correction utilities 

492 def getImageArray(self, image): 

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

494 

495 Parameters 

496 ---------- 

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

498 Image data to pull array from. 

499 

500 calcImage : `numpy.ndarray` 

501 Image data array for numpy operating. 

502 """ 

503 if hasattr(image, "getImage"): 

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

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

506 calcImage) 

507 else: 

508 calcImage = image.getArray() 

509 return calcImage 

510 

511 def maskOutliers(self, imageArray): 

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

513 clipping procedure. 

514 

515 Parameters 

516 ---------- 

517 imageArray : `numpy.ndarray` 

518 Image to filter along numpy axis=1. 

519 

520 Returns 

521 ------- 

522 maskedArray : `numpy.ma.masked_array` 

523 Masked image marking outliers. 

524 """ 

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

526 axisMedians = median 

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

528 

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

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

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

532 

533 @staticmethod 

534 def collapseArray(maskedArray): 

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

536 

537 Parameters 

538 ---------- 

539 maskedArray : `numpy.ma.masked_array` 

540 Masked array of input overscan data. 

541 

542 Returns 

543 ------- 

544 collapsed : `numpy.ma.masked_array` 

545 Single dimensional overscan data, combined with the mean. 

546 """ 

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

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

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

550 return collapsed 

551 

552 def collapseArrayMedian(self, maskedArray): 

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

554 correct integer median of row-values. 

555 

556 Parameters 

557 ---------- 

558 maskedArray : `numpy.ma.masked_array` 

559 Masked array of input overscan data. 

560 

561 Returns 

562 ------- 

563 collapsed : `numpy.ma.masked_array` 

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

565 """ 

566 integerMI = self.integerConvert(maskedArray) 

567 

568 collapsed = [] 

569 fitType = afwMath.stringToStatisticsProperty('MEDIAN') 

570 for row in integerMI: 

571 newRow = row.compressed() 

572 if len(newRow) > 0: 

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

574 else: 

575 rowMedian = np.nan 

576 collapsed.append(rowMedian) 

577 

578 return np.array(collapsed) 

579 

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

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

582 

583 Parameters 

584 ---------- 

585 indices : `numpy.ndarray` 

586 Locations to evaluate the spline. 

587 collapsed : `numpy.ndarray` 

588 Collapsed overscan values corresponding to the spline 

589 evaluation points. 

590 numBins : `int` 

591 Number of bins to use in constructing the spline. 

592 

593 Returns 

594 ------- 

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

596 Interpolation object for later evaluation. 

597 """ 

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

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

600 

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

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

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

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

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

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

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

608 

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

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

611 len(binCenters[numPerBin > 0])) 

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

613 # return zero. This amplifier is hopefully already 

614 # masked. 

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

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

617 else: 

618 return 0.0 

619 

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

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

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

623 return interp 

624 

625 @staticmethod 

626 def splineEval(indices, interp): 

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

628 API. 

629 

630 Parameters 

631 ---------- 

632 indices : `numpy.ndarray` 

633 Locations to evaluate the spline. 

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

635 Interpolation object to use. 

636 

637 Returns 

638 ------- 

639 values : `numpy.ndarray` 

640 Evaluated spline values at each index. 

641 """ 

642 

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

644 

645 @staticmethod 

646 def maskExtrapolated(collapsed): 

647 """Create mask if edges are extrapolated. 

648 

649 Parameters 

650 ---------- 

651 collapsed : `numpy.ma.masked_array` 

652 Masked array to check the edges of. 

653 

654 Returns 

655 ------- 

656 maskArray : `numpy.ndarray` 

657 Boolean numpy array of pixels to mask. 

658 """ 

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

660 if np.ma.is_masked(collapsed): 

661 num = len(collapsed) 

662 for low in range(num): 

663 if not collapsed.mask[low]: 

664 break 

665 if low > 0: 

666 maskArray[:low] = True 

667 for high in range(1, num): 

668 if not collapsed.mask[-high]: 

669 break 

670 if high > 1: 

671 maskArray[-high:] = True 

672 return maskArray 

673 

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

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

676 

677 Parameters 

678 ---------- 

679 image : `lsst.afw.image.MaskedImage` 

680 Image containing the overscan data. 

681 isTransposed : `bool` 

682 If true, the image has been transposed. 

683 

684 Returns 

685 ------- 

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

687 Overscan result with entries: 

688 

689 ``overscanValue`` 

690 Overscan value to subtract (`float`) 

691 ``maskArray`` 

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

693 overscan solution is applied. (`list` [ `bool` ]) 

694 ``isTransposed`` 

695 Indicates if the overscan data was transposed during 

696 calcuation, noting along which axis the overscan should be 

697 subtracted. (`bool`) 

698 """ 

699 calcImage = self.getImageArray(image) 

700 

701 # operate on numpy-arrays from here 

702 if isTransposed: 

703 calcImage = np.transpose(calcImage) 

704 masked = self.maskOutliers(calcImage) 

705 

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

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

708 masked = masked.astype(int) 

709 if isTransposed: 

710 masked = masked.transpose() 

711 

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

713 if bool(masked.mask.shape): 

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

715 

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

717 maskArray = self.maskExtrapolated(overscanVector) 

718 else: 

719 collapsed = self.collapseArray(masked) 

720 

721 num = len(collapsed) 

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

723 

724 poly = np.polynomial 

725 fitter, evaler = { 

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

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

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

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

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

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

732 }[self.config.fitType] 

733 

734 # These are the polynomial coefficients, or an 

735 # interpolation object. 

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

737 

738 if isinstance(coeffs, float): 

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

740 coeffs) 

741 overscanVector = np.full_like(indices, coeffs) 

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

743 else: 

744 # Otherwise we can just use things as normal. 

745 overscanVector = evaler(indices, coeffs) 

746 maskArray = self.maskExtrapolated(collapsed) 

747 

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

749 maskArray=maskArray, 

750 isTransposed=isTransposed) 

751 

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

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

754 

755 Parameters 

756 ---------- 

757 image : `lsst.afw.image.Image` 

758 Input image the overscan solution was determined from. 

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

760 Overscan model determined for the image. 

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

762 Amplifier to extract diagnostic information. 

763 """ 

764 import lsstDebug 

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

766 return 

767 if not self.allowDebug: 

768 return 

769 

770 calcImage = self.getImageArray(image) 

771 # CZW: Check that this is ok 

772 calcImage = np.transpose(calcImage) 

773 masked = self.maskOutliers(calcImage) 

774 collapsed = self.collapseArray(masked) 

775 

776 num = len(collapsed) 

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

778 

779 if np.ma.is_masked(collapsed): 

780 collapsedMask = collapsed.mask 

781 else: 

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

783 

784 import matplotlib.pyplot as plot 

785 figure = plot.figure(1) 

786 figure.clear() 

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

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

789 if collapsedMask.sum() > 0: 

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

791 if isinstance(model, np.ndarray): 

792 plotModel = model 

793 else: 

794 plotModel = np.zeros_like(indices) 

795 plotModel += model 

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

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

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

799 if amp: 

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

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

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

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

804 else: 

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

806 figure.show() 

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

808 while True: 

809 ans = input(prompt).lower() 

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

811 break 

812 elif ans in ("p", ): 

813 import pdb 

814 pdb.set_trace() 

815 elif ans in ('x', ): 

816 self.allowDebug = False 

817 break 

818 elif ans in ("h", ): 

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

820 plot.close()