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

378 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-29 03:26 -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 "SerialOverscanCorrectionTaskConfig", "SerialOverscanCorrectionTask", 

24 "ParallelOverscanCorrectionTaskConfig", "ParallelOverscanCorrectionTask", 

25 ] 

26 

27import numpy as np 

28import lsst.afw.math as afwMath 

29import lsst.afw.image as afwImage 

30import lsst.geom as geom 

31import lsst.pipe.base as pipeBase 

32import lsst.pex.config as pexConfig 

33 

34from .isr import fitOverscanImage 

35from .isrFunctions import makeThresholdMask 

36 

37 

38class OverscanCorrectionTaskConfigBase(pexConfig.Config): 

39 """Overscan correction options. 

40 """ 

41 fitType = pexConfig.ChoiceField( 

42 dtype=str, 

43 doc="The method for fitting the overscan bias level.", 

44 default='MEDIAN', 

45 allowed={ 

46 "POLY": "Fit ordinary polynomial to the longest axis of the overscan region", 

47 "CHEB": "Fit Chebyshev polynomial to the longest axis of the overscan region", 

48 "LEG": "Fit Legendre polynomial to the longest axis of the overscan region", 

49 "NATURAL_SPLINE": "Fit natural spline to the longest axis of the overscan region", 

50 "CUBIC_SPLINE": "Fit cubic spline to the longest axis of the overscan region", 

51 "AKIMA_SPLINE": "Fit Akima spline to the longest axis of the overscan region", 

52 "MEAN": "Correct using the mean of the overscan region", 

53 "MEANCLIP": "Correct using a clipped mean of the overscan region", 

54 "MEDIAN": "Correct using the median of the overscan region", 

55 "MEDIAN_PER_ROW": "Correct using the median per row of the overscan region", 

56 }, 

57 ) 

58 order = pexConfig.Field( 

59 dtype=int, 

60 doc=("Order of polynomial to fit if overscan fit type is a polynomial, " 

61 "or number of spline knots if overscan fit type is a spline."), 

62 default=1, 

63 ) 

64 numSigmaClip = pexConfig.Field( 

65 dtype=float, 

66 doc="Rejection threshold (sigma) for collapsing overscan before fit", 

67 default=3.0, 

68 ) 

69 maskPlanes = pexConfig.ListField( 

70 dtype=str, 

71 doc="Mask planes to reject when measuring overscan", 

72 default=['BAD', 'SAT'], 

73 ) 

74 overscanIsInt = pexConfig.Field( 

75 dtype=bool, 

76 doc="Treat overscan as an integer image for purposes of fitType=MEDIAN" 

77 " and fitType=MEDIAN_PER_ROW.", 

78 default=True, 

79 ) 

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

81 dtype=float, 

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

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

84 ) 

85 

86 

87class OverscanCorrectionTaskConfig(OverscanCorrectionTaskConfigBase): 

88 doParallelOverscan = pexConfig.Field( 

89 dtype=bool, 

90 doc="Correct using parallel overscan after serial overscan correction?", 

91 default=False, 

92 ) 

93 parallelOverscanMaskThreshold = pexConfig.Field( 

94 dtype=int, 

95 doc="Threshold above which pixels in the parallel overscan are masked as bleeds.", 

96 default=100000, 

97 ) 

98 parallelOverscanMaskGrowSize = pexConfig.Field( 

99 dtype=int, 

100 doc="Masks created from saturated bleeds should be grown by this many " 

101 "pixels during construction of the parallel overscan mask. " 

102 "This value determined from the ITL chip in the LATISS camera", 

103 default=7, 

104 ) 

105 leadingColumnsToSkip = pexConfig.Field( 

106 dtype=int, 

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

108 default=0, 

109 ) 

110 trailingColumnsToSkip = pexConfig.Field( 

111 dtype=int, 

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

113 default=0, 

114 ) 

115 leadingRowsToSkip = pexConfig.Field( 

116 dtype=int, 

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

118 default=0, 

119 ) 

120 trailingRowsToSkip = pexConfig.Field( 

121 dtype=int, 

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

123 default=0, 

124 ) 

125 

126 

127class OverscanCorrectionTaskBase(pipeBase.Task): 

128 """Base Correction task for overscan. 

129 

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

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

132 loops. 

133 

134 Parameters 

135 ---------- 

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

137 Statistics control object. 

138 """ 

139 ConfigClass = OverscanCorrectionTaskConfigBase 

140 _DefaultName = "overscanBase" 

141 

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

143 super().__init__(**kwargs) 

144 self.allowDebug = True 

145 

146 if statControl: 

147 self.statControl = statControl 

148 else: 

149 self.statControl = afwMath.StatisticsControl() 

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

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

152 

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

154 raise NotImplementedError("run method is not defined for OverscanCorrectionTaskBase") 

155 

156 def correctOverscan(self, exposure, amp, imageBBox, overscanBBox, 

157 isTransposed=True, leadingToSkip=0, trailingToSkip=0): 

158 """Trim the exposure, fit the overscan, subtract the fit, and 

159 calculate statistics. 

160 

161 Parameters 

162 ---------- 

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

164 Exposure containing the data. 

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

166 The amplifier that is to be corrected. 

167 imageBBox: `lsst.geom.Box2I` 

168 Bounding box of the image data that will have the overscan 

169 subtracted. If parallel overscan will be performed, that 

170 area is added to the image bounding box during serial 

171 overscan correction. 

172 overscanBBox: `lsst.geom.Box2I` 

173 Bounding box for the overscan data. 

174 isTransposed: `bool` 

175 If true, then the data will be transposed before fitting 

176 the overscan. 

177 leadingToSkip : `int`, optional 

178 Leading rows/columns to skip. 

179 trailingToSkip : `int`, optional 

180 Leading rows/columns to skip. 

181 

182 Returns 

183 ------- 

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

185 ``ampOverscanModel`` 

186 Overscan model broadcast to the full image size. 

187 (`lsst.afw.image.Exposure`) 

188 ``overscanOverscanModel`` 

189 Overscan model broadcast to the full overscan image 

190 size. (`lsst.afw.image.Exposure`) 

191 ``overscanImage`` 

192 Overscan image with the overscan fit subtracted. 

193 (`lsst.afw.image.Exposure`) 

194 ``overscanValue`` 

195 Overscan model. (`float` or `np.array`) 

196 ``overscanMean`` 

197 Mean value of the overscan fit. (`float`) 

198 ``overscanMedian`` 

199 Median value of the overscan fit. (`float`) 

200 ``overscanSigma`` 

201 Standard deviation of the overscan fit. (`float`) 

202 ``overscanMeanResidual`` 

203 Mean value of the overscan region after overscan 

204 subtraction. (`float`) 

205 ``overscanMedianResidual`` 

206 Median value of the overscan region after overscan 

207 subtraction. (`float`) 

208 ``overscanSigmaResidual`` 

209 Standard deviation of the overscan region after 

210 overscan subtraction. (`float`) 

211 """ 

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

213 leadingToSkip, 

214 trailingToSkip, 

215 transpose=isTransposed) 

216 overscanImage = exposure[overscanBox].getMaskedImage() 

217 overscanArray = overscanImage.image.array 

218 

219 # Mask pixels. 

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

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

222 

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

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

225 overscanImage.mask.array[bad] = overscanImage.mask.getPlaneBitMask("SAT") 

226 

227 # Do overscan fit. 

228 # CZW: Handle transposed correctly. 

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

230 

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

232 ampImage = exposure[imageBBox] 

233 ampOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue, 

234 ampImage.image.array, 

235 transpose=isTransposed) 

236 ampImage.image.array -= ampOverscanModel 

237 

238 # Correct overscan region (and possibly doubly-overscaned 

239 # region). 

240 overscanImage = exposure[overscanBBox] 

241 # CZW: Transposed? 

242 overscanOverscanModel = self.broadcastFitToImage(overscanResults.overscanValue, 

243 overscanImage.image.array) 

244 self.debugView(overscanImage, overscanResults.overscanValue, amp, isTransposed=isTransposed) 

245 overscanImage.image.array -= overscanOverscanModel 

246 

247 # Find residual fit statistics. 

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

249 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP, self.statControl) 

250 residualMean = stats.getValue(afwMath.MEAN) 

251 residualMedian = stats.getValue(afwMath.MEDIAN) 

252 residualSigma = stats.getValue(afwMath.STDEVCLIP) 

253 

254 return pipeBase.Struct(ampOverscanModel=ampOverscanModel, 

255 overscanOverscanModel=overscanOverscanModel, 

256 overscanImage=overscanImage, 

257 overscanValue=overscanResults.overscanValue, 

258 

259 overscanMean=overscanResults.overscanMean, 

260 overscanMedian=overscanResults.overscanMedian, 

261 overscanSigma=overscanResults.overscanSigma, 

262 overscanMeanResidual=residualMean, 

263 overscanMedianResidual=residualMedian, 

264 overscanSigmaResidual=residualSigma 

265 ) 

266 

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

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

269 

270 Parameters 

271 ---------- 

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

273 Overscan fit to broadcast. 

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

275 Image array that we want to match. 

276 transpose : `bool`, optional 

277 Switch order to broadcast along the other axis. 

278 

279 Returns 

280 ------- 

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

282 Expanded overscan fit. 

283 

284 Raises 

285 ------ 

286 RuntimeError 

287 Raised if no axis has the appropriate dimension. 

288 """ 

289 if isinstance(overscanValue, np.ndarray): 

290 overscanModel = np.zeros_like(imageArray) 

291 

292 if transpose is False: 

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

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

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

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

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

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

299 else: 

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

301 f"match {imageArray.shape}") 

302 else: 

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

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

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

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

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

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

309 else: 

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

311 f"match {imageArray.shape}") 

312 else: 

313 overscanModel = overscanValue 

314 

315 return overscanModel 

316 

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

318 """Trim overscan region to remove edges. 

319 

320 Parameters 

321 ---------- 

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

323 Exposure containing data. 

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

325 Amplifier containing geometry information. 

326 bbox : `lsst.geom.Box2I` 

327 Bounding box of the overscan region. 

328 skipLeading : `int` 

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

330 skipTrailing : `int` 

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

332 transpose : `bool`, optional 

333 Operate on the transposed array. 

334 

335 Returns 

336 ------- 

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

338 Data array to fit. 

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

340 Data mask. 

341 """ 

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

343 dataBBox = amp.getRawDataBBox() 

344 if transpose: 

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

346 dy0 += skipLeading 

347 dy1 -= skipTrailing 

348 else: 

349 dy0 += skipTrailing 

350 dy1 -= skipLeading 

351 else: 

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

353 dx0 += skipLeading 

354 dx1 -= skipTrailing 

355 else: 

356 dx0 += skipTrailing 

357 dx1 -= skipLeading 

358 

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

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

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

362 return overscanBBox 

363 

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

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

366 # Transposition has no effect here. 

367 overscanResult = self.measureConstantOverscan(overscanImage) 

368 overscanValue = overscanResult.overscanValue 

369 overscanMean = overscanValue 

370 overscanMedian = overscanValue 

371 overscanSigma = 0.0 

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

373 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'): 

374 # Force transposes as needed 

375 overscanResult = self.measureVectorOverscan(overscanImage, isTransposed) 

376 overscanValue = overscanResult.overscanValue 

377 

378 stats = afwMath.makeStatistics(overscanResult.overscanValue, 

379 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP, 

380 self.statControl) 

381 overscanMean = stats.getValue(afwMath.MEAN) 

382 overscanMedian = stats.getValue(afwMath.MEDIAN) 

383 overscanSigma = stats.getValue(afwMath.STDEVCLIP) 

384 else: 

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

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

387 

388 return pipeBase.Struct(overscanValue=overscanValue, 

389 overscanMean=overscanMean, 

390 overscanMedian=overscanMedian, 

391 overscanSigma=overscanSigma, 

392 ) 

393 

394 @staticmethod 

395 def integerConvert(image): 

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

397 

398 Parameters 

399 ---------- 

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

401 Image to convert to integers. 

402 

403 Returns 

404 ------- 

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

406 The integer converted image. 

407 

408 Raises 

409 ------ 

410 RuntimeError 

411 Raised if the input image could not be converted. 

412 """ 

413 if hasattr(image, "image"): 

414 # Is a maskedImage: 

415 imageI = image.image.convertI() 

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

417 elif hasattr(image, "convertI"): 

418 # Is an Image: 

419 outI = image.convertI() 

420 elif hasattr(image, "astype"): 

421 # Is a numpy array: 

422 outI = image.astype(int) 

423 else: 

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

425 image, type(image), dir(image)) 

426 return outI 

427 

428 def maskParallelOverscan(self, exposure, detector): 

429 """Mask the union of high values on all amplifiers in the parallel 

430 overscan. 

431 

432 This operates on the image in-place. 

433 

434 Parameters 

435 ---------- 

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

437 An untrimmed raw exposure. 

438 detector : `lsst.afw.cameraGeom.Detector` 

439 The detetor to use for amplifier geometry. 

440 """ 

441 parallelMask = None 

442 

443 for amp in detector: 

444 dataView = afwImage.MaskedImageF(exposure.getMaskedImage(), 

445 amp.getRawParallelOverscanBBox(), 

446 afwImage.PARENT) 

447 makeThresholdMask( 

448 maskedImage=dataView, 

449 threshold=self.config.parallelOverscanMaskThreshold, 

450 growFootprints=self.config.parallelOverscanMaskGrowSize, 

451 maskName="BAD" 

452 ) 

453 if parallelMask is None: 

454 parallelMask = dataView.mask.array 

455 else: 

456 parallelMask |= dataView.mask.array 

457 for amp in detector: 

458 dataView = afwImage.MaskedImageF(exposure.getMaskedImage(), 

459 amp.getRawParallelOverscanBBox(), 

460 afwImage.PARENT) 

461 dataView.mask.array |= parallelMask 

462 

463 # Constant methods 

464 def measureConstantOverscan(self, image): 

465 """Measure a constant overscan value. 

466 

467 Parameters 

468 ---------- 

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

470 Image data to measure the overscan from. 

471 

472 Returns 

473 ------- 

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

475 Overscan result with entries: 

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

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

478 """ 

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

480 overscanValue = afwMath.makeStatistics(image, fitType, self.statControl).getValue() 

481 

482 return pipeBase.Struct(overscanValue=overscanValue, 

483 isTransposed=False) 

484 

485 # Vector correction utilities 

486 def getImageArray(self, image): 

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

488 

489 Parameters 

490 ---------- 

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

492 Image data to pull array from. 

493 

494 calcImage : `numpy.ndarray` 

495 Image data array for numpy operating. 

496 """ 

497 if hasattr(image, "getImage"): 

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

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

500 calcImage) 

501 else: 

502 calcImage = image.getArray() 

503 return calcImage 

504 

505 def maskOutliers(self, imageArray): 

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

507 clipping procedure. 

508 

509 Parameters 

510 ---------- 

511 imageArray : `numpy.ndarray` 

512 Image to filter along numpy axis=1. 

513 

514 Returns 

515 ------- 

516 maskedArray : `numpy.ma.masked_array` 

517 Masked image marking outliers. 

518 """ 

519 lq, median, uq = np.percentile(np.ma.getdata(imageArray), 

520 [25.0, 50.0, 75.0], axis=1) 

521 axisMedians = median 

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

523 

524 # Replace pixels that have excessively large stdev values 

525 # with the median of stdev values. A large stdev likely 

526 # indicates a bleed is spilling into the overscan. 

527 axisStdev = np.where(axisStdev > 2.0 * np.median(axisStdev), 

528 np.median(axisStdev), axisStdev) 

529 

530 # Mask pixels that are N-sigma away from their array medians. 

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

532 masked = np.ma.masked_where(diff > self.statControl.getNumSigmaClip() 

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

534 

535 return masked 

536 

537 def fillMaskedPixels(self, overscanVector): 

538 """Fill masked/NaN pixels in the overscan. 

539 

540 Parameters 

541 ---------- 

542 overscanVector : `np.array` or `np.ma.masked_array` 

543 Overscan vector to fill. 

544 

545 Returns 

546 ------- 

547 overscanVector : `np.ma.masked_array` 

548 Filled vector. 

549 

550 Notes 

551 ----- 

552 Each maskSlice is a section of overscan with contiguous masks. 

553 Ideally this adds 5 pixels from the left and right of that 

554 mask slice, and takes the median of those values to fill the 

555 slice. If this isn't possible, the median of all non-masked 

556 values is used. The mask is removed for the pixels filled. 

557 """ 

558 workingCopy = overscanVector 

559 if not isinstance(overscanVector, np.ma.MaskedArray): 

560 workingCopy = np.ma.masked_array(overscanVector, 

561 mask=~np.isfinite(overscanVector)) 

562 

563 defaultValue = np.median(workingCopy.data[~workingCopy.mask]) 

564 for maskSlice in np.ma.clump_masked(workingCopy): 

565 neighborhood = [] 

566 if maskSlice.start > 5: 

567 neighborhood.extend(workingCopy[maskSlice.start - 5:maskSlice.start].data) 

568 if maskSlice.stop < workingCopy.size - 5: 

569 neighborhood.extend(workingCopy[maskSlice.stop:maskSlice.stop+5].data) 

570 if len(neighborhood) > 0: 

571 workingCopy.data[maskSlice] = np.nanmedian(neighborhood) 

572 workingCopy.mask[maskSlice] = False 

573 else: 

574 workingCopy.data[maskSlice] = defaultValue 

575 workingCopy.mask[maskSlice] = False 

576 return workingCopy 

577 

578 def collapseArray(self, maskedArray, fillMasked=True): 

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

580 

581 Parameters 

582 ---------- 

583 maskedArray : `numpy.ma.masked_array` 

584 Masked array of input overscan data. 

585 fillMasked : `bool`, optional 

586 If true, fill any pixels that are masked with a median of 

587 neighbors. 

588 

589 Returns 

590 ------- 

591 collapsed : `numpy.ma.masked_array` 

592 Single dimensional overscan data, combined with the mean. 

593 

594 """ 

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

596 if collapsed.mask.sum() > 0 and fillMasked: 

597 collapsed = self.fillMaskedPixels(collapsed) 

598 

599 return collapsed 

600 

601 def collapseArrayMedian(self, maskedArray): 

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

603 correct integer median of row-values. 

604 

605 Parameters 

606 ---------- 

607 maskedArray : `numpy.ma.masked_array` 

608 Masked array of input overscan data. 

609 

610 Returns 

611 ------- 

612 collapsed : `numpy.ma.masked_array` 

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

614 """ 

615 integerMI = self.integerConvert(maskedArray) 

616 

617 collapsed = [] 

618 fitType = afwMath.stringToStatisticsProperty('MEDIAN') 

619 for row in integerMI: 

620 newRow = row.compressed() 

621 if len(newRow) > 0: 

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

623 else: 

624 rowMedian = np.nan 

625 collapsed.append(rowMedian) 

626 

627 return np.array(collapsed) 

628 

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

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

631 

632 Parameters 

633 ---------- 

634 indices : `numpy.ndarray` 

635 Locations to evaluate the spline. 

636 collapsed : `numpy.ndarray` 

637 Collapsed overscan values corresponding to the spline 

638 evaluation points. 

639 numBins : `int` 

640 Number of bins to use in constructing the spline. 

641 

642 Returns 

643 ------- 

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

645 Interpolation object for later evaluation. 

646 """ 

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

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

649 

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

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

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

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

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

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

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

657 

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

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

660 len(binCenters[numPerBin > 0])) 

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

662 # return zero. This amplifier is hopefully already 

663 # masked. 

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

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

666 else: 

667 return 0.0 

668 

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

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

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

672 return interp 

673 

674 @staticmethod 

675 def splineEval(indices, interp): 

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

677 API. 

678 

679 Parameters 

680 ---------- 

681 indices : `numpy.ndarray` 

682 Locations to evaluate the spline. 

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

684 Interpolation object to use. 

685 

686 Returns 

687 ------- 

688 values : `numpy.ndarray` 

689 Evaluated spline values at each index. 

690 """ 

691 

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

693 

694 @staticmethod 

695 def maskExtrapolated(collapsed): 

696 """Create mask if edges are extrapolated. 

697 

698 Parameters 

699 ---------- 

700 collapsed : `numpy.ma.masked_array` 

701 Masked array to check the edges of. 

702 

703 Returns 

704 ------- 

705 maskArray : `numpy.ndarray` 

706 Boolean numpy array of pixels to mask. 

707 """ 

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

709 if np.ma.is_masked(collapsed): 

710 num = len(collapsed) 

711 for low in range(num): 

712 if not collapsed.mask[low]: 

713 break 

714 if low > 0: 

715 maskArray[:low] = True 

716 for high in range(1, num): 

717 if not collapsed.mask[-high]: 

718 break 

719 if high > 1: 

720 maskArray[-high:] = True 

721 return maskArray 

722 

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

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

725 

726 Parameters 

727 ---------- 

728 image : `lsst.afw.image.MaskedImage` 

729 Image containing the overscan data. 

730 isTransposed : `bool` 

731 If true, the image has been transposed. 

732 

733 Returns 

734 ------- 

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

736 Overscan result with entries: 

737 

738 ``overscanValue`` 

739 Overscan value to subtract (`float`) 

740 ``maskArray`` 

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

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

743 ``isTransposed`` 

744 Indicates if the overscan data was transposed during 

745 calcuation, noting along which axis the overscan should be 

746 subtracted. (`bool`) 

747 """ 

748 calcImage = self.getImageArray(image) 

749 

750 # operate on numpy-arrays from here 

751 if isTransposed: 

752 calcImage = np.transpose(calcImage) 

753 masked = self.maskOutliers(calcImage) 

754 

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

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

757 masked = masked.astype(int) 

758 if isTransposed: 

759 masked = masked.transpose() 

760 

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

762 if bool(masked.mask.shape): 

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

764 

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

766 overscanVector = self.fillMaskedPixels(overscanVector) 

767 maskArray = self.maskExtrapolated(overscanVector) 

768 else: 

769 collapsed = self.collapseArray(masked) 

770 

771 num = len(collapsed) 

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

773 

774 poly = np.polynomial 

775 fitter, evaler = { 

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

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

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

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

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

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

782 }[self.config.fitType] 

783 

784 # These are the polynomial coefficients, or an 

785 # interpolation object. 

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

787 

788 if isinstance(coeffs, float): 

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

790 coeffs) 

791 overscanVector = np.full_like(indices, coeffs) 

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

793 else: 

794 # Otherwise we can just use things as normal. 

795 overscanVector = evaler(indices, coeffs) 

796 maskArray = self.maskExtrapolated(collapsed) 

797 

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

799 maskArray=maskArray, 

800 isTransposed=isTransposed) 

801 

802 def debugView(self, image, model, amp=None, isTransposed=True): 

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

804 

805 Parameters 

806 ---------- 

807 image : `lsst.afw.image.Image` 

808 Input image the overscan solution was determined from. 

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

810 Overscan model determined for the image. 

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

812 Amplifier to extract diagnostic information. 

813 isTransposed : `bool`, optional 

814 Does the data need to be transposed before display? 

815 """ 

816 import lsstDebug 

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

818 return 

819 if not self.allowDebug: 

820 return 

821 

822 calcImage = self.getImageArray(image) 

823 # CZW: Check that this is ok 

824 if isTransposed: 

825 calcImage = np.transpose(calcImage) 

826 masked = self.maskOutliers(calcImage) 

827 collapsed = self.collapseArray(masked, fillMasked=False) 

828 

829 num = len(collapsed) 

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

831 indices = np.arange(num) 

832 

833 if np.ma.is_masked(collapsed): 

834 collapsedMask = collapsed.mask 

835 else: 

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

837 

838 import matplotlib.pyplot as plot 

839 figure = plot.figure(1) 

840 figure.clear() 

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

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

843 if collapsedMask.sum() > 0: 

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

845 if isinstance(model, np.ndarray): 

846 plotModel = model 

847 else: 

848 plotModel = np.zeros_like(indices) 

849 plotModel += model 

850 

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

852 plot.xlabel("position along overscan region") 

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

854 if amp: 

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

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

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

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

859 else: 

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

861 figure.show() 

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

863 while True: 

864 ans = input(prompt).lower() 

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

866 break 

867 elif ans in ("p", ): 

868 import pdb 

869 pdb.set_trace() 

870 elif ans in ('x', ): 

871 self.allowDebug = False 

872 break 

873 elif ans in ("h", ): 

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

875 plot.close() 

876 

877 

878class OverscanCorrectionTask(OverscanCorrectionTaskBase): 

879 """Correction task for serial/parallel overscan. 

880 

881 (Will be deprecated) 

882 

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

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

885 loops. 

886 

887 Parameters 

888 ---------- 

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

890 Statistics control object. 

891 """ 

892 ConfigClass = OverscanCorrectionTaskConfig 

893 _DefaultName = "overscan" 

894 

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

896 """Measure and remove serial/parallel overscan from an amplifier image. 

897 

898 This will be deprecated. 

899 

900 Parameters 

901 ---------- 

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

903 Image data that will have the overscan corrections applied. 

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

905 Amplifier to use for debugging purposes. 

906 isTransposed : `bool`, optional 

907 Is the image transposed, such that serial and parallel 

908 overscan regions are reversed? Default is False. 

909 

910 Returns 

911 ------- 

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

913 Result struct with components: 

914 

915 ``imageFit`` 

916 Value or fit subtracted from the amplifier image data 

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

918 ``overscanFit`` 

919 Value or fit subtracted from the serial overscan image 

920 data (scalar or `lsst.afw.image.Image`). 

921 ``overscanImage`` 

922 Image of the serial overscan region with the serial 

923 overscan correction applied 

924 (`lsst.afw.image.Image`). This quantity is used to 

925 estimate the amplifier read noise empirically. 

926 ``parallelOverscanFit`` 

927 Value or fit subtracted from the parallel overscan 

928 image data (scalar, `lsst.afw.image.Image`, or None). 

929 ``parallelOverscanImage`` 

930 Image of the parallel overscan region with the 

931 parallel overscan correction applied 

932 (`lsst.afw.image.Image` or None). 

933 ``overscanMean`` 

934 Mean of the fit serial overscan region. 

935 This and the following values will be tuples of 

936 (serial, parallel) if doParallelOverscan=True. 

937 ``overscanMedian`` 

938 Median of the fit serial overscan region. 

939 ``overscanSigma`` 

940 Sigma of the fit serial overscan region. 

941 ``residualMean`` 

942 Mean of the residual of the serial overscan region after 

943 correction. 

944 ``residualMedian`` 

945 Median of the residual of the serial overscan region after 

946 correction. 

947 ``residualSigma`` 

948 Mean of the residual of the serial overscan region after 

949 correction. 

950 

951 

952 Raises 

953 ------ 

954 RuntimeError 

955 Raised if an invalid overscan type is set. 

956 """ 

957 # Do Serial overscan first. 

958 serialOverscanBBox = amp.getRawSerialOverscanBBox() 

959 imageBBox = amp.getRawDataBBox() 

960 

961 if self.config.doParallelOverscan: 

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

963 # size of the detector. 

964 parallelOverscanBBox = amp.getRawParallelOverscanBBox() 

965 imageBBox = imageBBox.expandedTo(parallelOverscanBBox) 

966 

967 if isTransposed: 

968 serialOverscanBBox = geom.Box2I( 

969 geom.Point2I(serialOverscanBBox.getMinX(), imageBBox.getEndY()), 

970 geom.Extent2I(imageBBox.getWidth(), serialOverscanBBox.getHeight()), 

971 ) 

972 else: 

973 serialOverscanBBox = geom.Box2I( 

974 geom.Point2I(serialOverscanBBox.getMinX(), 

975 imageBBox.getMinY()), 

976 geom.Extent2I(serialOverscanBBox.getWidth(), 

977 imageBBox.getHeight()), 

978 ) 

979 

980 serialResults = self.correctOverscan( 

981 exposure, 

982 amp, 

983 imageBBox, 

984 serialOverscanBBox, 

985 isTransposed=isTransposed, 

986 leadingToSkip=self.config.leadingColumnsToSkip, 

987 trailingToSkip=self.config.trailingColumnsToSkip, 

988 ) 

989 overscanMean = serialResults.overscanMean 

990 overscanMedian = serialResults.overscanMedian 

991 overscanSigma = serialResults.overscanSigma 

992 residualMean = serialResults.overscanMeanResidual 

993 residualMedian = serialResults.overscanMedianResidual 

994 residualSigma = serialResults.overscanSigmaResidual 

995 

996 # Do Parallel Overscan 

997 parallelResults = None 

998 if self.config.doParallelOverscan: 

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

1000 # subtract it from the data region. 

1001 parallelOverscanBBox = amp.getRawParallelOverscanBBox() 

1002 imageBBox = amp.getRawDataBBox() 

1003 

1004 # The serial overscan correction has removed some signal 

1005 # from the parallel overscan region, but that is largely a 

1006 # constant offset. The collapseArray method now attempts 

1007 # to fill fully masked columns with the median of 

1008 # neighboring values, with a fallback to the median of the 

1009 # correction in all other columns. Filling with neighbor 

1010 # values ensures that large variations in the parallel 

1011 # overscan do not create new outlier points. The 

1012 # MEDIAN_PER_ROW method does this filling as a separate 

1013 # operation, using the same method. 

1014 parallelResults = self.correctOverscan( 

1015 exposure, 

1016 amp, 

1017 imageBBox, 

1018 parallelOverscanBBox, 

1019 isTransposed=not isTransposed, 

1020 leadingToSkip=self.config.leadingRowsToSkip, 

1021 trailingToSkip=self.config.trailingRowsToSkip, 

1022 ) 

1023 overscanMean = (overscanMean, parallelResults.overscanMean) 

1024 overscanMedian = (overscanMedian, parallelResults.overscanMedian) 

1025 overscanSigma = (overscanSigma, parallelResults.overscanSigma) 

1026 residualMean = (residualMean, parallelResults.overscanMeanResidual) 

1027 residualMedian = (residualMedian, parallelResults.overscanMedianResidual) 

1028 residualSigma = (residualSigma, parallelResults.overscanSigmaResidual) 

1029 

1030 parallelOverscanFit = parallelResults.overscanOverscanModel if parallelResults else None 

1031 parallelOverscanImage = parallelResults.overscanImage if parallelResults else None 

1032 

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

1034 overscanFit=serialResults.overscanOverscanModel, 

1035 overscanImage=serialResults.overscanImage, 

1036 

1037 parallelOverscanFit=parallelOverscanFit, 

1038 parallelOverscanImage=parallelOverscanImage, 

1039 overscanMean=overscanMean, 

1040 overscanMedian=overscanMedian, 

1041 overscanSigma=overscanSigma, 

1042 residualMean=residualMean, 

1043 residualMedian=residualMedian, 

1044 residualSigma=residualSigma) 

1045 

1046 

1047class SerialOverscanCorrectionTaskConfig(OverscanCorrectionTaskConfigBase): 

1048 leadingToSkip = pexConfig.Field( 

1049 dtype=int, 

1050 doc="Number of leading values to skip in serial overscan correction.", 

1051 default=0, 

1052 ) 

1053 trailingToSkip = pexConfig.Field( 

1054 dtype=int, 

1055 doc="Number of trailing values to skip in serial overscan correction.", 

1056 default=0, 

1057 ) 

1058 

1059 

1060class SerialOverscanCorrectionTask(OverscanCorrectionTaskBase): 

1061 """Correction task for serial overscan. 

1062 

1063 Parameters 

1064 ---------- 

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

1066 Statistics control object. 

1067 """ 

1068 ConfigClass = SerialOverscanCorrectionTaskConfig 

1069 _DefaultName = "serialOverscan" 

1070 

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

1072 """Measure and remove serial overscan from an amplifier image. 

1073 

1074 Parameters 

1075 ---------- 

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

1077 Image data that will have the overscan corrections applied. 

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

1079 Amplifier to use for debugging purposes. 

1080 isTransposed : `bool`, optional 

1081 Is the image transposed, such that serial and parallel 

1082 overscan regions are reversed? Default is False. 

1083 

1084 Returns 

1085 ------- 

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

1087 Result struct with components: 

1088 

1089 ``imageFit`` 

1090 Value or fit subtracted from the amplifier image data 

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

1092 ``overscanFit`` 

1093 Value or fit subtracted from the serial overscan image 

1094 data (scalar or `lsst.afw.image.Image`). 

1095 ``overscanImage`` 

1096 Image of the serial overscan region with the serial 

1097 overscan correction applied 

1098 (`lsst.afw.image.Image`). This quantity is used to 

1099 estimate the amplifier read noise empirically. 

1100 ``overscanMean`` 

1101 Mean of the fit serial overscan region. 

1102 ``overscanMedian`` 

1103 Median of the fit serial overscan region. 

1104 ``overscanSigma`` 

1105 Sigma of the fit serial overscan region. 

1106 ``residualMean`` 

1107 Mean of the residual of the serial overscan region after 

1108 correction. 

1109 ``residualMedian`` 

1110 Median of the residual of the serial overscan region after 

1111 correction. 

1112 ``residualSigma`` 

1113 Mean of the residual of the serial overscan region after 

1114 correction. 

1115 

1116 Raises 

1117 ------ 

1118 RuntimeError 

1119 Raised if an invalid overscan type is set. 

1120 """ 

1121 serialOverscanBBox = amp.getRawSerialOverscanBBox() 

1122 imageBBox = amp.getRawDataBBox() 

1123 

1124 # We always want to extend the serial overscan bounding box to 

1125 # the full size of the detector. 

1126 parallelOverscanBBox = amp.getRawParallelOverscanBBox() 

1127 imageBBox = imageBBox.expandedTo(parallelOverscanBBox) 

1128 

1129 if isTransposed: 

1130 serialOverscanBBox = geom.Box2I( 

1131 geom.Point2I(serialOverscanBBox.getMinX(), imageBBox.getEndY()), 

1132 geom.Extent2I(imageBBox.getWidth(), serialOverscanBBox.getHeight()), 

1133 ) 

1134 else: 

1135 serialOverscanBBox = geom.Box2I( 

1136 geom.Point2I(serialOverscanBBox.getMinX(), 

1137 imageBBox.getMinY()), 

1138 geom.Extent2I(serialOverscanBBox.getWidth(), 

1139 imageBBox.getHeight()), 

1140 ) 

1141 

1142 results = self.correctOverscan( 

1143 exposure, 

1144 amp, 

1145 imageBBox, 

1146 serialOverscanBBox, 

1147 isTransposed=isTransposed, 

1148 leadingToSkip=self.config.leadingToSkip, 

1149 trailingToSkip=self.config.trailingToSkip, 

1150 ) 

1151 overscanMean = results.overscanMean 

1152 overscanMedian = results.overscanMedian 

1153 overscanSigma = results.overscanSigma 

1154 residualMean = results.overscanMeanResidual 

1155 residualMedian = results.overscanMedianResidual 

1156 residualSigma = results.overscanSigmaResidual 

1157 

1158 return pipeBase.Struct( 

1159 imageFit=results.ampOverscanModel, 

1160 overscanFit=results.overscanOverscanModel, 

1161 overscanImage=results.overscanImage, 

1162 overscanMean=overscanMean, 

1163 overscanMedian=overscanMedian, 

1164 overscanSigma=overscanSigma, 

1165 residualMean=residualMean, 

1166 residualMedian=residualMedian, 

1167 residualSigma=residualSigma, 

1168 ) 

1169 

1170 

1171class ParallelOverscanCorrectionTaskConfig(OverscanCorrectionTaskConfigBase): 

1172 doParallelOverscanSaturation = pexConfig.Field( 

1173 dtype=bool, 

1174 doc="Mask saturated pixels in parallel overscan region?", 

1175 default=True, 

1176 ) 

1177 parallelOverscanSaturationLevel = pexConfig.Field( 

1178 dtype=float, 

1179 doc="The saturation level to use if not specified in call to " 

1180 "maskParallelOverscan.", 

1181 default=100000., 

1182 ) 

1183 parallelOverscanMaskGrowSize = pexConfig.Field( 

1184 dtype=int, 

1185 doc="Masks created from saturated bleeds should be grown by this many " 

1186 "pixels during construction of the parallel overscan mask. " 

1187 "This value determined from the ITL chip in the LATISS camera", 

1188 default=7, 

1189 ) 

1190 leadingToSkip = pexConfig.Field( 

1191 dtype=int, 

1192 doc="Number of leading values to skip in parallel overscan correction.", 

1193 default=0, 

1194 ) 

1195 trailingToSkip = pexConfig.Field( 

1196 dtype=int, 

1197 doc="Number of trailing values to skip in parallel overscan correction.", 

1198 default=0, 

1199 ) 

1200 

1201 

1202class ParallelOverscanCorrectionTask(OverscanCorrectionTaskBase): 

1203 """Correction task for parallel overscan. 

1204 

1205 Parameters 

1206 ---------- 

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

1208 Statistics control object. 

1209 """ 

1210 ConfigClass = ParallelOverscanCorrectionTaskConfig 

1211 _DefaultName = "parallelOverscan" 

1212 

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

1214 """Measure and remove parallel overscan from an amplifier image. 

1215 

1216 This method assumes that serial overscan has already been 

1217 removed from the amplifier. 

1218 

1219 Parameters 

1220 ---------- 

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

1222 Image data that will have the overscan corrections applied. 

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

1224 Amplifier to use for debugging purposes. 

1225 isTransposed : `bool`, optional 

1226 Is the image transposed, such that serial and parallel 

1227 overscan regions are reversed? Default is False. 

1228 

1229 Returns 

1230 ------- 

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

1232 Result struct with components: 

1233 

1234 ``imageFit`` 

1235 Value or fit subtracted from the amplifier image data 

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

1237 ``overscanFit`` 

1238 Value or fit subtracted from the parallel overscan image 

1239 data (scalar or `lsst.afw.image.Image`). 

1240 ``overscanImage`` 

1241 Image of the parallel overscan region with the parallel 

1242 overscan correction applied 

1243 (`lsst.afw.image.Image`). This quantity is used to 

1244 estimate the amplifier read noise empirically. 

1245 ``overscanMean`` 

1246 Mean of the fit parallel overscan region. 

1247 ``overscanMedian`` 

1248 Median of the fit parallel overscan region. 

1249 ``overscanSigma`` 

1250 Sigma of the fit parallel overscan region. 

1251 ``residualMean`` 

1252 Mean of the residual of the parallel overscan region after 

1253 correction. 

1254 ``residualMedian`` 

1255 Median of the residual of the parallel overscan region after 

1256 correction. 

1257 ``residualSigma`` 

1258 Mean of the residual of the parallel overscan region after 

1259 correction. 

1260 

1261 Raises 

1262 ------ 

1263 RuntimeError 

1264 Raised if an invalid overscan type is set. 

1265 """ 

1266 # This does not need any extending, as we only subtract 

1267 # from the data region. 

1268 parallelOverscanBBox = amp.getRawParallelOverscanBBox() 

1269 imageBBox = amp.getRawDataBBox() 

1270 

1271 # The serial overscan correction has removed some signal 

1272 # from the parallel overscan region, but that is largely a 

1273 # constant offset. The collapseArray method now attempts 

1274 # to fill fully masked columns with the median of 

1275 # neighboring values, with a fallback to the median of the 

1276 # correction in all other columns. Filling with neighbor 

1277 # values ensures that large variations in the parallel 

1278 # overscan do not create new outlier points. The 

1279 # MEDIAN_PER_ROW method does this filling as a separate 

1280 # operation, using the same method. 

1281 results = self.correctOverscan( 

1282 exposure, 

1283 amp, 

1284 imageBBox, 

1285 parallelOverscanBBox, 

1286 isTransposed=not isTransposed, 

1287 leadingToSkip=self.config.leadingToSkip, 

1288 trailingToSkip=self.config.trailingToSkip, 

1289 ) 

1290 overscanMean = results.overscanMean 

1291 overscanMedian = results.overscanMedian 

1292 overscanSigma = results.overscanSigma 

1293 residualMean = results.overscanMeanResidual 

1294 residualMedian = results.overscanMedianResidual 

1295 residualSigma = results.overscanSigmaResidual 

1296 

1297 return pipeBase.Struct( 

1298 imageFit=results.ampOverscanModel, 

1299 overscanFit=results.overscanOverscanModel, 

1300 overscanImage=results.overscanImage, 

1301 overscanMean=overscanMean, 

1302 overscanMedian=overscanMedian, 

1303 overscanSigma=overscanSigma, 

1304 residualMean=residualMean, 

1305 residualMedian=residualMedian, 

1306 residualSigma=residualSigma, 

1307 ) 

1308 

1309 def maskParallelOverscan(self, exposure, detector, saturationLevel=None): 

1310 """Mask parallel overscan, growing saturated pixels. 

1311 

1312 This operates on the image in-place. 

1313 

1314 Parameters 

1315 ---------- 

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

1317 An untrimmed raw exposure. 

1318 detector : `lsst.afw.cameraGeom.Detector` 

1319 The detetor to use for amplifier geometry. 

1320 saturationLevel : `float`, optional 

1321 Saturation level to use for masking. 

1322 """ 

1323 if not self.config.doParallelOverscanSaturation: 

1324 # This is a no-op. 

1325 return 

1326 

1327 if saturationLevel is None: 

1328 saturationLevel = self.config.parallelOverscanSaturationLevel 

1329 

1330 for amp in detector: 

1331 dataView = afwImage.MaskedImageF(exposure.getMaskedImage(), 

1332 amp.getRawParallelOverscanBBox(), 

1333 afwImage.PARENT) 

1334 makeThresholdMask( 

1335 maskedImage=dataView, 

1336 threshold=saturationLevel, 

1337 growFootprints=self.config.parallelOverscanMaskGrowSize, 

1338 maskName="BAD" 

1339 )