Coverage for python/lsst/ip/isr/isrFunctions.py: 8%

283 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-07 04:27 -0700

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# 

22 

23__all__ = [ 

24 "applyGains", 

25 "attachTransmissionCurve", 

26 "biasCorrection", 

27 "brighterFatterCorrection", 

28 "checkFilter", 

29 "createPsf", 

30 "darkCorrection", 

31 "flatCorrection", 

32 "gainContext", 

33 "getPhysicalFilter", 

34 "growMasks", 

35 "illuminationCorrection", 

36 "interpolateDefectList", 

37 "interpolateFromMask", 

38 "makeThresholdMask", 

39 "saturationCorrection", 

40 "setBadRegions", 

41 "transposeMaskedImage", 

42 "trimToMatchCalibBBox", 

43 "updateVariance", 

44 "widenSaturationTrails", 

45] 

46 

47import math 

48import numpy 

49 

50import lsst.geom 

51import lsst.afw.image as afwImage 

52import lsst.afw.detection as afwDetection 

53import lsst.afw.math as afwMath 

54import lsst.meas.algorithms as measAlg 

55import lsst.afw.cameraGeom as camGeom 

56 

57from lsst.meas.algorithms.detection import SourceDetectionTask 

58 

59from contextlib import contextmanager 

60 

61from .defects import Defects 

62 

63 

64def createPsf(fwhm): 

65 """Make a double Gaussian PSF. 

66 

67 Parameters 

68 ---------- 

69 fwhm : scalar 

70 FWHM of double Gaussian smoothing kernel. 

71 

72 Returns 

73 ------- 

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

75 The created smoothing kernel. 

76 """ 

77 ksize = 4*int(fwhm) + 1 

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

79 

80 

81def transposeMaskedImage(maskedImage): 

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

83 

84 Parameters 

85 ---------- 

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

87 Image to process. 

88 

89 Returns 

90 ------- 

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

92 The transposed copy of the input image. 

93 """ 

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

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

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

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

98 return transposed 

99 

100 

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

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

103 

104 Parameters 

105 ---------- 

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

107 Image to process. 

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

109 List of defects to interpolate over. 

110 fwhm : scalar 

111 FWHM of double Gaussian smoothing kernel. 

112 fallbackValue : scalar, optional 

113 Fallback value if an interpolated value cannot be determined. 

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

115 """ 

116 psf = createPsf(fwhm) 

117 if fallbackValue is None: 

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

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

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

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

122 return maskedImage 

123 

124 

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

126 """Mask pixels based on threshold detection. 

127 

128 Parameters 

129 ---------- 

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

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

132 threshold : scalar 

133 Detection threshold. 

134 growFootprints : scalar, optional 

135 Number of pixels to grow footprints of detected regions. 

136 maskName : str, optional 

137 Mask plane name, or list of names to convert 

138 

139 Returns 

140 ------- 

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

142 Defect list constructed from pixels above the threshold. 

143 """ 

144 # find saturated regions 

145 thresh = afwDetection.Threshold(threshold) 

146 fs = afwDetection.FootprintSet(maskedImage, thresh) 

147 

148 if growFootprints > 0: 

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

150 fpList = fs.getFootprints() 

151 

152 # set mask 

153 mask = maskedImage.getMask() 

154 bitmask = mask.getPlaneBitMask(maskName) 

155 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask) 

156 

157 return Defects.fromFootprintList(fpList) 

158 

159 

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

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

162 

163 Parameters 

164 ---------- 

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

166 Mask image to process. 

167 radius : scalar 

168 Amount to grow the mask. 

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

170 Mask names that should be grown. 

171 maskValue : `str` 

172 Mask plane to assign the newly masked pixels to. 

173 """ 

174 if radius > 0: 

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

176 fpSet = afwDetection.FootprintSet(mask, thresh) 

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

178 fpSet.setMask(mask, maskValue) 

179 

180 

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

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

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

184 

185 Parameters 

186 ---------- 

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

188 Image to process. 

189 fwhm : scalar 

190 FWHM of double Gaussian smoothing kernel. 

191 growSaturatedFootprints : scalar, optional 

192 Number of pixels to grow footprints for saturated pixels. 

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

194 Mask plane name. 

195 fallbackValue : scalar, optional 

196 Value of last resort for interpolation. 

197 """ 

198 mask = maskedImage.getMask() 

199 

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

201 # If we are interpolating over an area larger than the original masked 

202 # region, we need to expand the original mask bit to the full area to 

203 # explain why we interpolated there. 

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

205 

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

207 fpSet = afwDetection.FootprintSet(mask, thresh) 

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

209 

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

211 

212 return maskedImage 

213 

214 

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

216 fallbackValue=None): 

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

218 

219 Parameters 

220 ---------- 

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

222 Image to process. 

223 saturation : scalar 

224 Saturation level used as the detection threshold. 

225 fwhm : scalar 

226 FWHM of double Gaussian smoothing kernel. 

227 growFootprints : scalar, optional 

228 Number of pixels to grow footprints of detected regions. 

229 interpolate : Bool, optional 

230 If True, saturated pixels are interpolated over. 

231 maskName : str, optional 

232 Mask plane name. 

233 fallbackValue : scalar, optional 

234 Value of last resort for interpolation. 

235 """ 

236 defectList = makeThresholdMask( 

237 maskedImage=maskedImage, 

238 threshold=saturation, 

239 growFootprints=growFootprints, 

240 maskName=maskName, 

241 ) 

242 if interpolate: 

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

244 

245 return maskedImage 

246 

247 

248def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage): 

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

250 

251 Use the dimension difference between the raw exposure and the 

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

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

254 each side. 

255 

256 Parameters 

257 ---------- 

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

259 Image to trim. 

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

261 Calibration image to draw new bounding box from. 

262 

263 Returns 

264 ------- 

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

266 ``rawMaskedImage`` trimmed to the appropriate size. 

267 

268 Raises 

269 ------ 

270 RuntimeError 

271 Raised if ``rawMaskedImage`` cannot be symmetrically trimmed to 

272 match ``calibMaskedImage``. 

273 """ 

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

275 if nx != ny: 

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

277 if nx % 2 != 0: 

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

279 if nx < 0: 

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

281 

282 nEdge = nx//2 

283 if nEdge > 0: 

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

285 SourceDetectionTask.setEdgeBits( 

286 rawMaskedImage, 

287 replacementMaskedImage.getBBox(), 

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

289 ) 

290 else: 

291 replacementMaskedImage = rawMaskedImage 

292 

293 return replacementMaskedImage 

294 

295 

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

297 """Apply bias correction in place. 

298 

299 Parameters 

300 ---------- 

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

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

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

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

305 trimToFit : `Bool`, optional 

306 If True, raw data is symmetrically trimmed to match 

307 calibration size. 

308 

309 Raises 

310 ------ 

311 RuntimeError 

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

313 the same size. 

314 

315 """ 

316 if trimToFit: 

317 maskedImage = trimToMatchCalibBBox(maskedImage, biasMaskedImage) 

318 

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

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

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

322 maskedImage -= biasMaskedImage 

323 

324 

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

326 """Apply dark correction in place. 

327 

328 Parameters 

329 ---------- 

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

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

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

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

334 expScale : scalar 

335 Dark exposure time for ``maskedImage``. 

336 darkScale : scalar 

337 Dark exposure time for ``darkMaskedImage``. 

338 invert : `Bool`, optional 

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

340 trimToFit : `Bool`, optional 

341 If True, raw data is symmetrically trimmed to match 

342 calibration size. 

343 

344 Raises 

345 ------ 

346 RuntimeError 

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

348 the same size. 

349 

350 Notes 

351 ----- 

352 The dark correction is applied by calculating: 

353 maskedImage -= dark * expScaling / darkScaling 

354 """ 

355 if trimToFit: 

356 maskedImage = trimToMatchCalibBBox(maskedImage, darkMaskedImage) 

357 

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

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

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

361 

362 scale = expScale / darkScale 

363 if not invert: 

364 maskedImage.scaledMinus(scale, darkMaskedImage) 

365 else: 

366 maskedImage.scaledPlus(scale, darkMaskedImage) 

367 

368 

369def updateVariance(maskedImage, gain, readNoise): 

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

371 

372 Parameters 

373 ---------- 

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

375 Image to process. The variance plane is modified. 

376 gain : scalar 

377 The amplifier gain in electrons/ADU. 

378 readNoise : scalar 

379 The amplifier read nmoise in ADU/pixel. 

380 """ 

381 var = maskedImage.getVariance() 

382 var[:] = maskedImage.getImage() 

383 var /= gain 

384 var += readNoise**2 

385 

386 

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

388 """Apply flat correction in place. 

389 

390 Parameters 

391 ---------- 

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

393 Image to process. The image is modified. 

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

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

396 scalingType : str 

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

398 'MEDIAN', or 'USER'. 

399 userScale : scalar, optional 

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

401 invert : `Bool`, optional 

402 If True, unflatten an already flattened image. 

403 trimToFit : `Bool`, optional 

404 If True, raw data is symmetrically trimmed to match 

405 calibration size. 

406 

407 Raises 

408 ------ 

409 RuntimeError 

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

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

412 """ 

413 if trimToFit: 

414 maskedImage = trimToMatchCalibBBox(maskedImage, flatMaskedImage) 

415 

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

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

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

419 

420 # Figure out scale from the data 

421 # Ideally the flats are normalized by the calibration product pipeline, 

422 # but this allows some flexibility in the case that the flat is created by 

423 # some other mechanism. 

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

425 scalingType = afwMath.stringToStatisticsProperty(scalingType) 

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

427 elif scalingType == 'USER': 

428 flatScale = userScale 

429 else: 

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

431 

432 if not invert: 

433 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage) 

434 else: 

435 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage) 

436 

437 

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

439 """Apply illumination correction in place. 

440 

441 Parameters 

442 ---------- 

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

444 Image to process. The image is modified. 

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

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

447 illumScale : scalar 

448 Scale factor for the illumination correction. 

449 trimToFit : `Bool`, optional 

450 If True, raw data is symmetrically trimmed to match 

451 calibration size. 

452 

453 Raises 

454 ------ 

455 RuntimeError 

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

457 the same size. 

458 """ 

459 if trimToFit: 

460 maskedImage = trimToMatchCalibBBox(maskedImage, illumMaskedImage) 

461 

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

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

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

465 

466 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage) 

467 

468 

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

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

471 

472 Parameters 

473 ---------- 

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

475 Exposure to have brighter-fatter correction applied. Modified 

476 by this method. 

477 kernel : `numpy.ndarray` 

478 Brighter-fatter kernel to apply. 

479 maxIter : scalar 

480 Number of correction iterations to run. 

481 threshold : scalar 

482 Convergence threshold in terms of the sum of absolute 

483 deviations between an iteration and the previous one. 

484 applyGain : `Bool` 

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

486 to correction. 

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

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

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

490 

491 Returns 

492 ------- 

493 diff : `float` 

494 Final difference between iterations achieved in correction. 

495 iteration : `int` 

496 Number of iterations used to calculate correction. 

497 

498 Notes 

499 ----- 

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

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

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

503 

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

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

506 

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

508 

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

510 

511 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y))) 

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

513 

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

515 we apply the correction iteratively to reconstruct the original 

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

517 difference between the current corrected image and the one from 

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

519 convergence because the number of iterations is too large a 

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

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

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

523 DocuShare Document-19407. 

524 

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

526 have spurious values due to the convolution. 

527 """ 

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

529 

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

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

532 

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

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

535 kernelImage = afwImage.ImageD(kLx, kLy) 

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

537 tempImage = image.clone() 

538 

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

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

541 

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

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

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

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

546 fixedKernel = afwMath.FixedKernel(kernelImage) 

547 

548 # Define boundary by convolution region. The region that the 

549 # correction will be calculated for is one fewer in each dimension 

550 # because of the second derivative terms. 

551 # NOTE: these need to use integer math, as we're using start:end as 

552 # numpy index ranges. 

553 startX = kLx//2 

554 endX = -kLx//2 

555 startY = kLy//2 

556 endY = -kLy//2 

557 

558 for iteration in range(maxIter): 

559 

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

561 tmpArray = tempImage.getArray() 

562 outArray = outImage.getArray() 

563 

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

565 # First derivative term 

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

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

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

569 

570 # Second derivative term 

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

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

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

574 

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

576 

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

578 tmpArray[nanIndex] = 0. 

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

580 

581 if iteration > 0: 

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

583 

584 if diff < threshold: 

585 break 

586 prev_image[:, :] = tmpArray[:, :] 

587 

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

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

590 

591 return diff, iteration 

592 

593 

594@contextmanager 

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

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

597 

598 Parameters 

599 ---------- 

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

601 Exposure to apply/remove gain. 

602 image : `lsst.afw.image.Image` 

603 Image to apply/remove gain. 

604 apply : `Bool` 

605 If True, apply and remove the amplifier gain. 

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

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

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

609 

610 Yields 

611 ------ 

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

613 Exposure with the gain applied. 

614 """ 

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

616 # be a real mess 

617 if gains and apply is True: 

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

619 for ampName in ampNames: 

620 if ampName not in gains.keys(): 

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

622 

623 if apply: 

624 ccd = exp.getDetector() 

625 for amp in ccd: 

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

627 if gains: 

628 gain = gains[amp.getName()] 

629 else: 

630 gain = amp.getGain() 

631 sim *= gain 

632 

633 try: 

634 yield exp 

635 finally: 

636 if apply: 

637 ccd = exp.getDetector() 

638 for amp in ccd: 

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

640 if gains: 

641 gain = gains[amp.getName()] 

642 else: 

643 gain = amp.getGain() 

644 sim /= gain 

645 

646 

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

648 sensorTransmission=None, atmosphereTransmission=None): 

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

650 different components. 

651 

652 Parameters 

653 ---------- 

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

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

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

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

658 associated with sensorTransmission. 

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

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

661 to be evaluated in focal-plane coordinates. 

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

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

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

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

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

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

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

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

670 atmosphere, assumed to be spatially constant. 

671 

672 Returns 

673 ------- 

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

675 The TransmissionCurve attached to the exposure. 

676 

677 Notes 

678 ----- 

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

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

681 """ 

682 combined = afwImage.TransmissionCurve.makeIdentity() 

683 if atmosphereTransmission is not None: 

684 combined *= atmosphereTransmission 

685 if opticsTransmission is not None: 

686 combined *= opticsTransmission 

687 if filterTransmission is not None: 

688 combined *= filterTransmission 

689 detector = exposure.getDetector() 

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

691 toSys=camGeom.PIXELS) 

692 combined = combined.transformedBy(fpToPix) 

693 if sensorTransmission is not None: 

694 combined *= sensorTransmission 

695 exposure.getInfo().setTransmissionCurve(combined) 

696 return combined 

697 

698 

699def applyGains(exposure, normalizeGains=False, ptcGains=None): 

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

701 

702 Parameters 

703 ---------- 

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

705 Exposure to process. The image is modified. 

706 normalizeGains : `Bool`, optional 

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

708 each amplifier to equal the median of those medians. 

709 ptcGains : `dict`[`str`], optional 

710 Dictionary keyed by amp name containing the PTC gains. 

711 """ 

712 ccd = exposure.getDetector() 

713 ccdImage = exposure.getMaskedImage() 

714 

715 medians = [] 

716 for amp in ccd: 

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

718 if ptcGains: 

719 sim *= ptcGains[amp.getName()] 

720 else: 

721 sim *= amp.getGain() 

722 

723 if normalizeGains: 

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

725 

726 if normalizeGains: 

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

728 for index, amp in enumerate(ccd): 

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

730 if medians[index] != 0.0: 

731 sim *= median/medians[index] 

732 

733 

734def widenSaturationTrails(mask): 

735 """Grow the saturation trails by an amount dependent on the width of the 

736 trail. 

737 

738 Parameters 

739 ---------- 

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

741 Mask which will have the saturated areas grown. 

742 """ 

743 

744 extraGrowDict = {} 

745 for i in range(1, 6): 

746 extraGrowDict[i] = 0 

747 for i in range(6, 8): 

748 extraGrowDict[i] = 1 

749 for i in range(8, 10): 

750 extraGrowDict[i] = 3 

751 extraGrowMax = 4 

752 

753 if extraGrowMax <= 0: 

754 return 

755 

756 saturatedBit = mask.getPlaneBitMask("SAT") 

757 

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

759 width = mask.getWidth() 

760 

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

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

763 

764 for fp in fpList: 

765 for s in fp.getSpans(): 

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

767 

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

769 if extraGrow > 0: 

770 y = s.getY() - ymin 

771 x0 -= xmin + extraGrow 

772 x1 -= xmin - extraGrow 

773 

774 if x0 < 0: 

775 x0 = 0 

776 if x1 >= width - 1: 

777 x1 = width - 1 

778 

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

780 

781 

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

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

784 

785 Parameters 

786 ---------- 

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

788 Exposure to mask. The exposure mask is modified. 

789 badStatistic : `str`, optional 

790 Statistic to use to generate the replacement value from the 

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

792 

793 Returns 

794 ------- 

795 badPixelCount : scalar 

796 Number of bad pixels masked. 

797 badPixelValue : scalar 

798 Value substituted for bad pixels. 

799 

800 Raises 

801 ------ 

802 RuntimeError 

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

804 """ 

805 if badStatistic == "MEDIAN": 

806 statistic = afwMath.MEDIAN 

807 elif badStatistic == "MEANCLIP": 

808 statistic = afwMath.MEANCLIP 

809 else: 

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

811 

812 mi = exposure.getMaskedImage() 

813 mask = mi.getMask() 

814 BAD = mask.getPlaneBitMask("BAD") 

815 INTRP = mask.getPlaneBitMask("INTRP") 

816 

817 sctrl = afwMath.StatisticsControl() 

818 sctrl.setAndMask(BAD) 

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

820 

821 maskArray = mask.getArray() 

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

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

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

825 

826 return badPixels.sum(), value 

827 

828 

829def checkFilter(exposure, filterList, log): 

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

831 

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

833 for all filter dependent stages. 

834 

835 Parameters 

836 ---------- 

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

838 Exposure to examine. 

839 filterList : `list` [`str`] 

840 List of physical_filter names to check. 

841 log : `logging.Logger` 

842 Logger to handle messages. 

843 

844 Returns 

845 ------- 

846 result : `bool` 

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

848 """ 

849 if len(filterList) == 0: 

850 return False 

851 thisFilter = exposure.getFilter() 

852 if thisFilter is None: 

853 log.warning("No FilterLabel attached to this exposure!") 

854 return False 

855 

856 thisPhysicalFilter = getPhysicalFilter(thisFilter, log) 

857 if thisPhysicalFilter in filterList: 

858 return True 

859 elif thisFilter.bandLabel in filterList: 

860 if log: 

861 log.warning("Physical filter (%s) should be used instead of band %s for filter configurations" 

862 " (%s)", thisPhysicalFilter, thisFilter.bandLabel, filterList) 

863 return True 

864 else: 

865 return False 

866 

867 

868def getPhysicalFilter(filterLabel, log): 

869 """Get the physical filter label associated with the given filterLabel. 

870 

871 If ``filterLabel`` is `None` or there is no physicalLabel attribute 

872 associated with the given ``filterLabel``, the returned label will be 

873 "Unknown". 

874 

875 Parameters 

876 ---------- 

877 filterLabel : `lsst.afw.image.FilterLabel` 

878 The `lsst.afw.image.FilterLabel` object from which to derive the 

879 physical filter label. 

880 log : `logging.Logger` 

881 Logger to handle messages. 

882 

883 Returns 

884 ------- 

885 physicalFilter : `str` 

886 The value returned by the physicalLabel attribute of ``filterLabel`` if 

887 it exists, otherwise set to \"Unknown\". 

888 """ 

889 if filterLabel is None: 

890 physicalFilter = "Unknown" 

891 log.warning("filterLabel is None. Setting physicalFilter to \"Unknown\".") 

892 else: 

893 try: 

894 physicalFilter = filterLabel.physicalLabel 

895 except RuntimeError: 

896 log.warning("filterLabel has no physicalLabel attribute. Setting physicalFilter to \"Unknown\".") 

897 physicalFilter = "Unknown" 

898 return physicalFilter