Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# 

2# LSST Data Management System 

3# Copyright 2008, 2009, 2010 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

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

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

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

11# (at your option) any later version. 

12# 

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

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

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

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22import math 

23import numpy 

24 

25import lsst.geom 

26import lsst.afw.image as afwImage 

27import lsst.afw.detection as afwDetection 

28import lsst.afw.math as afwMath 

29import lsst.meas.algorithms as measAlg 

30import lsst.afw.cameraGeom as camGeom 

31 

32from lsst.meas.algorithms.detection import SourceDetectionTask 

33 

34from contextlib import contextmanager 

35 

36from .overscan import OverscanCorrectionTask, OverscanCorrectionTaskConfig 

37from .defects import Defects 

38 

39 

40def createPsf(fwhm): 

41 """Make a double Gaussian PSF. 

42 

43 Parameters 

44 ---------- 

45 fwhm : scalar 

46 FWHM of double Gaussian smoothing kernel. 

47 

48 Returns 

49 ------- 

50 psf : `lsst.meas.algorithms.DoubleGaussianPsf` 

51 The created smoothing kernel. 

52 """ 

53 ksize = 4*int(fwhm) + 1 

54 return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2)))) 

55 

56 

57def transposeMaskedImage(maskedImage): 

58 """Make a transposed copy of a masked image. 

59 

60 Parameters 

61 ---------- 

62 maskedImage : `lsst.afw.image.MaskedImage` 

63 Image to process. 

64 

65 Returns 

66 ------- 

67 transposed : `lsst.afw.image.MaskedImage` 

68 The transposed copy of the input image. 

69 """ 

70 transposed = maskedImage.Factory(lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth())) 

71 transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T 

72 transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T 

73 transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T 

74 return transposed 

75 

76 

77def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None): 

78 """Interpolate over defects specified in a defect list. 

79 

80 Parameters 

81 ---------- 

82 maskedImage : `lsst.afw.image.MaskedImage` 

83 Image to process. 

84 defectList : `lsst.meas.algorithms.Defects` 

85 List of defects to interpolate over. 

86 fwhm : scalar 

87 FWHM of double Gaussian smoothing kernel. 

88 fallbackValue : scalar, optional 

89 Fallback value if an interpolated value cannot be determined. 

90 If None, then the clipped mean of the image is used. 

91 """ 

92 psf = createPsf(fwhm) 

93 if fallbackValue is None: 

94 fallbackValue = afwMath.makeStatistics(maskedImage.getImage(), afwMath.MEANCLIP).getValue() 

95 if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict(): 

96 maskedImage.getMask().addMaskPlane('INTRP') 

97 measAlg.interpolateOverDefects(maskedImage, psf, defectList, fallbackValue, True) 

98 return maskedImage 

99 

100 

101def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT'): 

102 """Mask pixels based on threshold detection. 

103 

104 Parameters 

105 ---------- 

106 maskedImage : `lsst.afw.image.MaskedImage` 

107 Image to process. Only the mask plane is updated. 

108 threshold : scalar 

109 Detection threshold. 

110 growFootprints : scalar, optional 

111 Number of pixels to grow footprints of detected regions. 

112 maskName : str, optional 

113 Mask plane name, or list of names to convert 

114 

115 Returns 

116 ------- 

117 defectList : `lsst.meas.algorithms.Defects` 

118 Defect list constructed from pixels above the threshold. 

119 """ 

120 # find saturated regions 

121 thresh = afwDetection.Threshold(threshold) 

122 fs = afwDetection.FootprintSet(maskedImage, thresh) 

123 

124 if growFootprints > 0: 

125 fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=False) 

126 fpList = fs.getFootprints() 

127 

128 # set mask 

129 mask = maskedImage.getMask() 

130 bitmask = mask.getPlaneBitMask(maskName) 

131 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask) 

132 

133 return Defects.fromFootprintList(fpList) 

134 

135 

136def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"): 

137 """Grow a mask by an amount and add to the requested plane. 

138 

139 Parameters 

140 ---------- 

141 mask : `lsst.afw.image.Mask` 

142 Mask image to process. 

143 radius : scalar 

144 Amount to grow the mask. 

145 maskNameList : `str` or `list` [`str`] 

146 Mask names that should be grown. 

147 maskValue : `str` 

148 Mask plane to assign the newly masked pixels to. 

149 """ 

150 if radius > 0: 

151 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK) 

152 fpSet = afwDetection.FootprintSet(mask, thresh) 

153 fpSet = afwDetection.FootprintSet(fpSet, rGrow=radius, isotropic=False) 

154 fpSet.setMask(mask, maskValue) 

155 

156 

157def interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1, 

158 maskNameList=['SAT'], fallbackValue=None): 

159 """Interpolate over defects identified by a particular set of mask planes. 

160 

161 Parameters 

162 ---------- 

163 maskedImage : `lsst.afw.image.MaskedImage` 

164 Image to process. 

165 fwhm : scalar 

166 FWHM of double Gaussian smoothing kernel. 

167 growSaturatedFootprints : scalar, optional 

168 Number of pixels to grow footprints for saturated pixels. 

169 maskNameList : `List` of `str`, optional 

170 Mask plane name. 

171 fallbackValue : scalar, optional 

172 Value of last resort for interpolation. 

173 """ 

174 mask = maskedImage.getMask() 

175 

176 if growSaturatedFootprints > 0 and "SAT" in maskNameList: 

177 # If we are interpolating over an area larger than the original masked region, we need 

178 # to expand the original mask bit to the full area to explain why we interpolated there. 

179 growMasks(mask, radius=growSaturatedFootprints, maskNameList=['SAT'], maskValue="SAT") 

180 

181 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK) 

182 fpSet = afwDetection.FootprintSet(mask, thresh) 

183 defectList = Defects.fromFootprintList(fpSet.getFootprints()) 

184 

185 interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue) 

186 

187 return maskedImage 

188 

189 

190def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT', 

191 fallbackValue=None): 

192 """Mark saturated pixels and optionally interpolate over them 

193 

194 Parameters 

195 ---------- 

196 maskedImage : `lsst.afw.image.MaskedImage` 

197 Image to process. 

198 saturation : scalar 

199 Saturation level used as the detection threshold. 

200 fwhm : scalar 

201 FWHM of double Gaussian smoothing kernel. 

202 growFootprints : scalar, optional 

203 Number of pixels to grow footprints of detected regions. 

204 interpolate : Bool, optional 

205 If True, saturated pixels are interpolated over. 

206 maskName : str, optional 

207 Mask plane name. 

208 fallbackValue : scalar, optional 

209 Value of last resort for interpolation. 

210 """ 

211 defectList = makeThresholdMask( 

212 maskedImage=maskedImage, 

213 threshold=saturation, 

214 growFootprints=growFootprints, 

215 maskName=maskName, 

216 ) 

217 if interpolate: 

218 interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=fallbackValue) 

219 

220 return maskedImage 

221 

222 

223def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage): 

224 """Compute number of edge trim pixels to match the calibration data. 

225 

226 Use the dimension difference between the raw exposure and the 

227 calibration exposure to compute the edge trim pixels. This trim 

228 is applied symmetrically, with the same number of pixels masked on 

229 each side. 

230 

231 Parameters 

232 ---------- 

233 rawMaskedImage : `lsst.afw.image.MaskedImage` 

234 Image to trim. 

235 calibMaskedImage : `lsst.afw.image.MaskedImage` 

236 Calibration image to draw new bounding box from. 

237 

238 Returns 

239 ------- 

240 replacementMaskedImage : `lsst.afw.image.MaskedImage` 

241 ``rawMaskedImage`` trimmed to the appropriate size 

242 Raises 

243 ------ 

244 RuntimeError 

245 Rasied if ``rawMaskedImage`` cannot be symmetrically trimmed to 

246 match ``calibMaskedImage``. 

247 """ 

248 nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions() 

249 if nx != ny: 

250 raise RuntimeError("Raw and calib maskedImages are trimmed differently in X and Y.") 

251 if nx % 2 != 0: 

252 raise RuntimeError("Calibration maskedImage is trimmed unevenly in X.") 

253 if nx < 0: 

254 raise RuntimeError("Calibration maskedImage is larger than raw data.") 

255 

256 nEdge = nx//2 

257 if nEdge > 0: 

258 replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL] 

259 SourceDetectionTask.setEdgeBits( 

260 rawMaskedImage, 

261 replacementMaskedImage.getBBox(), 

262 rawMaskedImage.getMask().getPlaneBitMask("EDGE") 

263 ) 

264 else: 

265 replacementMaskedImage = rawMaskedImage 

266 

267 return replacementMaskedImage 

268 

269 

270def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False): 

271 """Apply bias correction in place. 

272 

273 Parameters 

274 ---------- 

275 maskedImage : `lsst.afw.image.MaskedImage` 

276 Image to process. The image is modified by this method. 

277 biasMaskedImage : `lsst.afw.image.MaskedImage` 

278 Bias image of the same size as ``maskedImage`` 

279 trimToFit : `Bool`, optional 

280 If True, raw data is symmetrically trimmed to match 

281 calibration size. 

282 

283 Raises 

284 ------ 

285 RuntimeError 

286 Raised if ``maskedImage`` and ``biasMaskedImage`` do not have 

287 the same size. 

288 

289 """ 

290 if trimToFit: 

291 maskedImage = trimToMatchCalibBBox(maskedImage, biasMaskedImage) 

292 

293 if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL): 

294 raise RuntimeError("maskedImage bbox %s != biasMaskedImage bbox %s" % 

295 (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL))) 

296 maskedImage -= biasMaskedImage 

297 

298 

299def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False): 

300 """Apply dark correction in place. 

301 

302 Parameters 

303 ---------- 

304 maskedImage : `lsst.afw.image.MaskedImage` 

305 Image to process. The image is modified by this method. 

306 darkMaskedImage : `lsst.afw.image.MaskedImage` 

307 Dark image of the same size as ``maskedImage``. 

308 expScale : scalar 

309 Dark exposure time for ``maskedImage``. 

310 darkScale : scalar 

311 Dark exposure time for ``darkMaskedImage``. 

312 invert : `Bool`, optional 

313 If True, re-add the dark to an already corrected image. 

314 trimToFit : `Bool`, optional 

315 If True, raw data is symmetrically trimmed to match 

316 calibration size. 

317 

318 Raises 

319 ------ 

320 RuntimeError 

321 Raised if ``maskedImage`` and ``darkMaskedImage`` do not have 

322 the same size. 

323 

324 Notes 

325 ----- 

326 The dark correction is applied by calculating: 

327 maskedImage -= dark * expScaling / darkScaling 

328 """ 

329 if trimToFit: 

330 maskedImage = trimToMatchCalibBBox(maskedImage, darkMaskedImage) 

331 

332 if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL): 

333 raise RuntimeError("maskedImage bbox %s != darkMaskedImage bbox %s" % 

334 (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL))) 

335 

336 scale = expScale / darkScale 

337 if not invert: 

338 maskedImage.scaledMinus(scale, darkMaskedImage) 

339 else: 

340 maskedImage.scaledPlus(scale, darkMaskedImage) 

341 

342 

343def updateVariance(maskedImage, gain, readNoise): 

344 """Set the variance plane based on the image plane. 

345 

346 Parameters 

347 ---------- 

348 maskedImage : `lsst.afw.image.MaskedImage` 

349 Image to process. The variance plane is modified. 

350 gain : scalar 

351 The amplifier gain in electrons/ADU. 

352 readNoise : scalar 

353 The amplifier read nmoise in ADU/pixel. 

354 """ 

355 var = maskedImage.getVariance() 

356 var[:] = maskedImage.getImage() 

357 var /= gain 

358 var += readNoise**2 

359 

360 

361def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False): 

362 """Apply flat correction in place. 

363 

364 Parameters 

365 ---------- 

366 maskedImage : `lsst.afw.image.MaskedImage` 

367 Image to process. The image is modified. 

368 flatMaskedImage : `lsst.afw.image.MaskedImage` 

369 Flat image of the same size as ``maskedImage`` 

370 scalingType : str 

371 Flat scale computation method. Allowed values are 'MEAN', 

372 'MEDIAN', or 'USER'. 

373 userScale : scalar, optional 

374 Scale to use if ``scalingType``='USER'. 

375 invert : `Bool`, optional 

376 If True, unflatten an already flattened image. 

377 trimToFit : `Bool`, optional 

378 If True, raw data is symmetrically trimmed to match 

379 calibration size. 

380 

381 Raises 

382 ------ 

383 RuntimeError 

384 Raised if ``maskedImage`` and ``flatMaskedImage`` do not have 

385 the same size or if ``scalingType`` is not an allowed value. 

386 """ 

387 if trimToFit: 

388 maskedImage = trimToMatchCalibBBox(maskedImage, flatMaskedImage) 

389 

390 if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL): 

391 raise RuntimeError("maskedImage bbox %s != flatMaskedImage bbox %s" % 

392 (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL))) 

393 

394 # Figure out scale from the data 

395 # Ideally the flats are normalized by the calibration product pipeline, but this allows some flexibility 

396 # in the case that the flat is created by some other mechanism. 

397 if scalingType in ('MEAN', 'MEDIAN'): 

398 scalingType = afwMath.stringToStatisticsProperty(scalingType) 

399 flatScale = afwMath.makeStatistics(flatMaskedImage.image, scalingType).getValue() 

400 elif scalingType == 'USER': 

401 flatScale = userScale 

402 else: 

403 raise RuntimeError('%s : %s not implemented' % ("flatCorrection", scalingType)) 

404 

405 if not invert: 

406 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage) 

407 else: 

408 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage) 

409 

410 

411def illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True): 

412 """Apply illumination correction in place. 

413 

414 Parameters 

415 ---------- 

416 maskedImage : `lsst.afw.image.MaskedImage` 

417 Image to process. The image is modified. 

418 illumMaskedImage : `lsst.afw.image.MaskedImage` 

419 Illumination correction image of the same size as ``maskedImage``. 

420 illumScale : scalar 

421 Scale factor for the illumination correction. 

422 trimToFit : `Bool`, optional 

423 If True, raw data is symmetrically trimmed to match 

424 calibration size. 

425 

426 Raises 

427 ------ 

428 RuntimeError 

429 Raised if ``maskedImage`` and ``illumMaskedImage`` do not have 

430 the same size. 

431 """ 

432 if trimToFit: 

433 maskedImage = trimToMatchCalibBBox(maskedImage, illumMaskedImage) 

434 

435 if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL): 

436 raise RuntimeError("maskedImage bbox %s != illumMaskedImage bbox %s" % 

437 (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL))) 

438 

439 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage) 

440 

441 

442def overscanCorrection(ampMaskedImage, overscanImage, fitType='MEDIAN', order=1, collapseRej=3.0, 

443 statControl=None, overscanIsInt=True): 

444 """Apply overscan correction in place. 

445 

446 Parameters 

447 ---------- 

448 ampMaskedImage : `lsst.afw.image.MaskedImage` 

449 Image of amplifier to correct; modified. 

450 overscanImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage` 

451 Image of overscan; modified. 

452 fitType : `str` 

453 Type of fit for overscan correction. May be one of: 

454 

455 - ``MEAN``: use mean of overscan. 

456 - ``MEANCLIP``: use clipped mean of overscan. 

457 - ``MEDIAN``: use median of overscan. 

458 - ``MEDIAN_PER_ROW``: use median per row of overscan. 

459 - ``POLY``: fit with ordinary polynomial. 

460 - ``CHEB``: fit with Chebyshev polynomial. 

461 - ``LEG``: fit with Legendre polynomial. 

462 - ``NATURAL_SPLINE``: fit with natural spline. 

463 - ``CUBIC_SPLINE``: fit with cubic spline. 

464 - ``AKIMA_SPLINE``: fit with Akima spline. 

465 

466 order : `int` 

467 Polynomial order or number of spline knots; ignored unless 

468 ``fitType`` indicates a polynomial or spline. 

469 statControl : `lsst.afw.math.StatisticsControl` 

470 Statistics control object. In particular, we pay attention to numSigmaClip 

471 overscanIsInt : `bool` 

472 Treat the overscan region as consisting of integers, even if it's been 

473 converted to float. E.g. handle ties properly. 

474 

475 Returns 

476 ------- 

477 result : `lsst.pipe.base.Struct` 

478 Result struct with components: 

479 

480 - ``imageFit``: Value(s) removed from image (scalar or 

481 `lsst.afw.image.Image`) 

482 - ``overscanFit``: Value(s) removed from overscan (scalar or 

483 `lsst.afw.image.Image`) 

484 - ``overscanImage``: Overscan corrected overscan region 

485 (`lsst.afw.image.Image`) 

486 Raises 

487 ------ 

488 RuntimeError 

489 Raised if ``fitType`` is not an allowed value. 

490 

491 Notes 

492 ----- 

493 The ``ampMaskedImage`` and ``overscanImage`` are modified, with the fit 

494 subtracted. Note that the ``overscanImage`` should not be a subimage of 

495 the ``ampMaskedImage``, to avoid being subtracted twice. 

496 

497 Debug plots are available for the SPLINE fitTypes by setting the 

498 `debug.display` for `name` == "lsst.ip.isr.isrFunctions". These 

499 plots show the scatter plot of the overscan data (collapsed along 

500 the perpendicular dimension) as a function of position on the CCD 

501 (normalized between +/-1). 

502 """ 

503 ampImage = ampMaskedImage.getImage() 

504 

505 config = OverscanCorrectionTaskConfig() 

506 if fitType: 

507 config.fitType = fitType 

508 if order: 

509 config.order = order 

510 if collapseRej: 

511 config.numSigmaClip = collapseRej 

512 if overscanIsInt: 

513 config.overscanIsInt = True 

514 

515 overscanTask = OverscanCorrectionTask(config=config) 

516 return overscanTask.run(ampImage, overscanImage) 

517 

518 

519def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None): 

520 """Apply brighter fatter correction in place for the image. 

521 

522 Parameters 

523 ---------- 

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

525 Exposure to have brighter-fatter correction applied. Modified 

526 by this method. 

527 kernel : `numpy.ndarray` 

528 Brighter-fatter kernel to apply. 

529 maxIter : scalar 

530 Number of correction iterations to run. 

531 threshold : scalar 

532 Convergence threshold in terms of the sum of absolute 

533 deviations between an iteration and the previous one. 

534 applyGain : `Bool` 

535 If True, then the exposure values are scaled by the gain prior 

536 to correction. 

537 gains : `dict` [`str`, `float`] 

538 A dictionary, keyed by amplifier name, of the gains to use. 

539 If gains is None, the nominal gains in the amplifier object are used. 

540 

541 Returns 

542 ------- 

543 diff : `float` 

544 Final difference between iterations achieved in correction. 

545 iteration : `int` 

546 Number of iterations used to calculate correction. 

547 

548 Notes 

549 ----- 

550 This correction takes a kernel that has been derived from flat 

551 field images to redistribute the charge. The gradient of the 

552 kernel is the deflection field due to the accumulated charge. 

553 

554 Given the original image I(x) and the kernel K(x) we can compute 

555 the corrected image Ic(x) using the following equation: 

556 

557 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y)))) 

558 

559 To evaluate the derivative term we expand it as follows: 

560 

561 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y))) + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) ) 

562 

563 Because we use the measured counts instead of the incident counts 

564 we apply the correction iteratively to reconstruct the original 

565 counts and the correction. We stop iterating when the summed 

566 difference between the current corrected image and the one from 

567 the previous iteration is below the threshold. We do not require 

568 convergence because the number of iterations is too large a 

569 computational cost. How we define the threshold still needs to be 

570 evaluated, the current default was shown to work reasonably well 

571 on a small set of images. For more information on the method see 

572 DocuShare Document-19407. 

573 

574 The edges as defined by the kernel are not corrected because they 

575 have spurious values due to the convolution. 

576 """ 

577 image = exposure.getMaskedImage().getImage() 

578 

579 # The image needs to be units of electrons/holes 

580 with gainContext(exposure, image, applyGain, gains): 

581 

582 kLx = numpy.shape(kernel)[0] 

583 kLy = numpy.shape(kernel)[1] 

584 kernelImage = afwImage.ImageD(kLx, kLy) 

585 kernelImage.getArray()[:, :] = kernel 

586 tempImage = image.clone() 

587 

588 nanIndex = numpy.isnan(tempImage.getArray()) 

589 tempImage.getArray()[nanIndex] = 0. 

590 

591 outImage = afwImage.ImageF(image.getDimensions()) 

592 corr = numpy.zeros_like(image.getArray()) 

593 prev_image = numpy.zeros_like(image.getArray()) 

594 convCntrl = afwMath.ConvolutionControl(False, True, 1) 

595 fixedKernel = afwMath.FixedKernel(kernelImage) 

596 

597 # Define boundary by convolution region. The region that the correction will be 

598 # calculated for is one fewer in each dimension because of the second derivative terms. 

599 # NOTE: these need to use integer math, as we're using start:end as numpy index ranges. 

600 startX = kLx//2 

601 endX = -kLx//2 

602 startY = kLy//2 

603 endY = -kLy//2 

604 

605 for iteration in range(maxIter): 

606 

607 afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl) 

608 tmpArray = tempImage.getArray() 

609 outArray = outImage.getArray() 

610 

611 with numpy.errstate(invalid="ignore", over="ignore"): 

612 # First derivative term 

613 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX]) 

614 gradOut = numpy.gradient(outArray[startY:endY, startX:endX]) 

615 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1] 

616 

617 # Second derivative term 

618 diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1] 

619 diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX] 

620 second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21) 

621 

622 corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second) 

623 

624 tmpArray[:, :] = image.getArray()[:, :] 

625 tmpArray[nanIndex] = 0. 

626 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX] 

627 

628 if iteration > 0: 

629 diff = numpy.sum(numpy.abs(prev_image - tmpArray)) 

630 

631 if diff < threshold: 

632 break 

633 prev_image[:, :] = tmpArray[:, :] 

634 

635 image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \ 

636 corr[startY + 1:endY - 1, startX + 1:endX - 1] 

637 

638 return diff, iteration 

639 

640 

641@contextmanager 

642def gainContext(exp, image, apply, gains=None): 

643 """Context manager that applies and removes gain. 

644 

645 Parameters 

646 ---------- 

647 exp : `lsst.afw.image.Exposure` 

648 Exposure to apply/remove gain. 

649 image : `lsst.afw.image.Image` 

650 Image to apply/remove gain. 

651 apply : `Bool` 

652 If True, apply and remove the amplifier gain. 

653 gains : `dict` [`str`, `float`] 

654 A dictionary, keyed by amplifier name, of the gains to use. 

655 If gains is None, the nominal gains in the amplifier object are used. 

656 

657 Yields 

658 ------ 

659 exp : `lsst.afw.image.Exposure` 

660 Exposure with the gain applied. 

661 """ 

662 # check we have all of them if provided because mixing and matching would 

663 # be a real mess 

664 if gains and apply is True: 

665 ampNames = [amp.getName() for amp in exp.getDetector()] 

666 for ampName in ampNames: 

667 if ampName not in gains.keys(): 

668 raise RuntimeError(f"Gains provided to gain context, but no entry found for amp {ampName}") 

669 

670 if apply: 

671 ccd = exp.getDetector() 

672 for amp in ccd: 

673 sim = image.Factory(image, amp.getBBox()) 

674 if gains: 

675 gain = gains[amp.getName()] 

676 else: 

677 gain = amp.getGain() 

678 sim *= gain 

679 

680 try: 

681 yield exp 

682 finally: 

683 if apply: 

684 ccd = exp.getDetector() 

685 for amp in ccd: 

686 sim = image.Factory(image, amp.getBBox()) 

687 if gains: 

688 gain = gains[amp.getName()] 

689 else: 

690 gain = amp.getGain() 

691 sim /= gain 

692 

693 

694def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None, 

695 sensorTransmission=None, atmosphereTransmission=None): 

696 """Attach a TransmissionCurve to an Exposure, given separate curves for 

697 different components. 

698 

699 Parameters 

700 ---------- 

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

702 Exposure object to modify by attaching the product of all given 

703 ``TransmissionCurves`` in post-assembly trimmed detector coordinates. 

704 Must have a valid ``Detector`` attached that matches the detector 

705 associated with sensorTransmission. 

706 opticsTransmission : `lsst.afw.image.TransmissionCurve` 

707 A ``TransmissionCurve`` that represents the throughput of the optics, 

708 to be evaluated in focal-plane coordinates. 

709 filterTransmission : `lsst.afw.image.TransmissionCurve` 

710 A ``TransmissionCurve`` that represents the throughput of the filter 

711 itself, to be evaluated in focal-plane coordinates. 

712 sensorTransmission : `lsst.afw.image.TransmissionCurve` 

713 A ``TransmissionCurve`` that represents the throughput of the sensor 

714 itself, to be evaluated in post-assembly trimmed detector coordinates. 

715 atmosphereTransmission : `lsst.afw.image.TransmissionCurve` 

716 A ``TransmissionCurve`` that represents the throughput of the 

717 atmosphere, assumed to be spatially constant. 

718 

719 Returns 

720 ------- 

721 combined : `lsst.afw.image.TransmissionCurve` 

722 The TransmissionCurve attached to the exposure. 

723 

724 Notes 

725 ----- 

726 All ``TransmissionCurve`` arguments are optional; if none are provided, the 

727 attached ``TransmissionCurve`` will have unit transmission everywhere. 

728 """ 

729 combined = afwImage.TransmissionCurve.makeIdentity() 

730 if atmosphereTransmission is not None: 

731 combined *= atmosphereTransmission 

732 if opticsTransmission is not None: 

733 combined *= opticsTransmission 

734 if filterTransmission is not None: 

735 combined *= filterTransmission 

736 detector = exposure.getDetector() 

737 fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE, 

738 toSys=camGeom.PIXELS) 

739 combined = combined.transformedBy(fpToPix) 

740 if sensorTransmission is not None: 

741 combined *= sensorTransmission 

742 exposure.getInfo().setTransmissionCurve(combined) 

743 return combined 

744 

745 

746def applyGains(exposure, normalizeGains=False): 

747 """Scale an exposure by the amplifier gains. 

748 

749 Parameters 

750 ---------- 

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

752 Exposure to process. The image is modified. 

753 normalizeGains : `Bool`, optional 

754 If True, then amplifiers are scaled to force the median of 

755 each amplifier to equal the median of those medians. 

756 """ 

757 ccd = exposure.getDetector() 

758 ccdImage = exposure.getMaskedImage() 

759 

760 medians = [] 

761 for amp in ccd: 

762 sim = ccdImage.Factory(ccdImage, amp.getBBox()) 

763 sim *= amp.getGain() 

764 

765 if normalizeGains: 

766 medians.append(numpy.median(sim.getImage().getArray())) 

767 

768 if normalizeGains: 

769 median = numpy.median(numpy.array(medians)) 

770 for index, amp in enumerate(ccd): 

771 sim = ccdImage.Factory(ccdImage, amp.getBBox()) 

772 if medians[index] != 0.0: 

773 sim *= median/medians[index] 

774 

775 

776def widenSaturationTrails(mask): 

777 """Grow the saturation trails by an amount dependent on the width of the trail. 

778 

779 Parameters 

780 ---------- 

781 mask : `lsst.afw.image.Mask` 

782 Mask which will have the saturated areas grown. 

783 """ 

784 

785 extraGrowDict = {} 

786 for i in range(1, 6): 

787 extraGrowDict[i] = 0 

788 for i in range(6, 8): 

789 extraGrowDict[i] = 1 

790 for i in range(8, 10): 

791 extraGrowDict[i] = 3 

792 extraGrowMax = 4 

793 

794 if extraGrowMax <= 0: 

795 return 

796 

797 saturatedBit = mask.getPlaneBitMask("SAT") 

798 

799 xmin, ymin = mask.getBBox().getMin() 

800 width = mask.getWidth() 

801 

802 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK) 

803 fpList = afwDetection.FootprintSet(mask, thresh).getFootprints() 

804 

805 for fp in fpList: 

806 for s in fp.getSpans(): 

807 x0, x1 = s.getX0(), s.getX1() 

808 

809 extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax) 

810 if extraGrow > 0: 

811 y = s.getY() - ymin 

812 x0 -= xmin + extraGrow 

813 x1 -= xmin - extraGrow 

814 

815 if x0 < 0: 

816 x0 = 0 

817 if x1 >= width - 1: 

818 x1 = width - 1 

819 

820 mask.array[y, x0:x1+1] |= saturatedBit 

821 

822 

823def setBadRegions(exposure, badStatistic="MEDIAN"): 

824 """Set all BAD areas of the chip to the average of the rest of the exposure 

825 

826 Parameters 

827 ---------- 

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

829 Exposure to mask. The exposure mask is modified. 

830 badStatistic : `str`, optional 

831 Statistic to use to generate the replacement value from the 

832 image data. Allowed values are 'MEDIAN' or 'MEANCLIP'. 

833 

834 Returns 

835 ------- 

836 badPixelCount : scalar 

837 Number of bad pixels masked. 

838 badPixelValue : scalar 

839 Value substituted for bad pixels. 

840 

841 Raises 

842 ------ 

843 RuntimeError 

844 Raised if `badStatistic` is not an allowed value. 

845 """ 

846 if badStatistic == "MEDIAN": 

847 statistic = afwMath.MEDIAN 

848 elif badStatistic == "MEANCLIP": 

849 statistic = afwMath.MEANCLIP 

850 else: 

851 raise RuntimeError("Impossible method %s of bad region correction" % badStatistic) 

852 

853 mi = exposure.getMaskedImage() 

854 mask = mi.getMask() 

855 BAD = mask.getPlaneBitMask("BAD") 

856 INTRP = mask.getPlaneBitMask("INTRP") 

857 

858 sctrl = afwMath.StatisticsControl() 

859 sctrl.setAndMask(BAD) 

860 value = afwMath.makeStatistics(mi, statistic, sctrl).getValue() 

861 

862 maskArray = mask.getArray() 

863 imageArray = mi.getImage().getArray() 

864 badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0) 

865 imageArray[:] = numpy.where(badPixels, value, imageArray) 

866 

867 return badPixels.sum(), value 

868 

869 

870def checkFilter(exposure, filterList, log): 

871 """Check to see if an exposure is in a filter specified by a list. 

872 

873 The goal of this is to provide a unified filter checking interface 

874 for all filter dependent stages. 

875 

876 Parameters 

877 ---------- 

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

879 Exposure to examine. 

880 filterList : `list` [`str`] 

881 List of physical_filter names to check. 

882 log : `lsst.log.Log` 

883 Logger to handle messages. 

884 

885 Returns 

886 ------- 

887 result : `bool` 

888 True if the exposure's filter is contained in the list. 

889 """ 

890 thisFilter = exposure.getFilterLabel() 

891 if thisFilter is None: 

892 log.warn("No FilterLabel attached to this exposure!") 

893 return False 

894 

895 if thisFilter.physicalLabel in filterList: 

896 return True 

897 elif thisFilter.bandLabel in filterList: 

898 if log: 

899 log.warn("Physical filter (%s) should be used instead of band %s for filter configurations (%s)", 

900 thisFilter.physicalLabel, thisFilter.bandLabel, filterList) 

901 return True 

902 else: 

903 return False