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

282 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-19 19:30 +0000

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 .defects import Defects 

37 

38 

39def createPsf(fwhm): 

40 """Make a double Gaussian PSF. 

41 

42 Parameters 

43 ---------- 

44 fwhm : scalar 

45 FWHM of double Gaussian smoothing kernel. 

46 

47 Returns 

48 ------- 

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

50 The created smoothing kernel. 

51 """ 

52 ksize = 4*int(fwhm) + 1 

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

54 

55 

56def transposeMaskedImage(maskedImage): 

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

58 

59 Parameters 

60 ---------- 

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

62 Image to process. 

63 

64 Returns 

65 ------- 

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

67 The transposed copy of the input image. 

68 """ 

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

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

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

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

73 return transposed 

74 

75 

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

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

78 

79 Parameters 

80 ---------- 

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

82 Image to process. 

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

84 List of defects to interpolate over. 

85 fwhm : scalar 

86 FWHM of double Gaussian smoothing kernel. 

87 fallbackValue : scalar, optional 

88 Fallback value if an interpolated value cannot be determined. 

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

90 """ 

91 psf = createPsf(fwhm) 

92 if fallbackValue is None: 

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

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

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

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

97 return maskedImage 

98 

99 

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

101 """Mask pixels based on threshold detection. 

102 

103 Parameters 

104 ---------- 

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

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

107 threshold : scalar 

108 Detection threshold. 

109 growFootprints : scalar, optional 

110 Number of pixels to grow footprints of detected regions. 

111 maskName : str, optional 

112 Mask plane name, or list of names to convert 

113 

114 Returns 

115 ------- 

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

117 Defect list constructed from pixels above the threshold. 

118 """ 

119 # find saturated regions 

120 thresh = afwDetection.Threshold(threshold) 

121 fs = afwDetection.FootprintSet(maskedImage, thresh) 

122 

123 if growFootprints > 0: 

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

125 fpList = fs.getFootprints() 

126 

127 # set mask 

128 mask = maskedImage.getMask() 

129 bitmask = mask.getPlaneBitMask(maskName) 

130 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask) 

131 

132 return Defects.fromFootprintList(fpList) 

133 

134 

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

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

137 

138 Parameters 

139 ---------- 

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

141 Mask image to process. 

142 radius : scalar 

143 Amount to grow the mask. 

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

145 Mask names that should be grown. 

146 maskValue : `str` 

147 Mask plane to assign the newly masked pixels to. 

148 """ 

149 if radius > 0: 

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

151 fpSet = afwDetection.FootprintSet(mask, thresh) 

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

153 fpSet.setMask(mask, maskValue) 

154 

155 

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

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

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

159 

160 Parameters 

161 ---------- 

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

163 Image to process. 

164 fwhm : scalar 

165 FWHM of double Gaussian smoothing kernel. 

166 growSaturatedFootprints : scalar, optional 

167 Number of pixels to grow footprints for saturated pixels. 

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

169 Mask plane name. 

170 fallbackValue : scalar, optional 

171 Value of last resort for interpolation. 

172 """ 

173 mask = maskedImage.getMask() 

174 

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

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

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

178 # 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, 

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

397 # some other mechanism. 

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

399 scalingType = afwMath.stringToStatisticsProperty(scalingType) 

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

401 elif scalingType == 'USER': 

402 flatScale = userScale 

403 else: 

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

405 

406 if not invert: 

407 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage) 

408 else: 

409 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage) 

410 

411 

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

413 """Apply illumination correction in place. 

414 

415 Parameters 

416 ---------- 

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

418 Image to process. The image is modified. 

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

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

421 illumScale : scalar 

422 Scale factor for the illumination correction. 

423 trimToFit : `Bool`, optional 

424 If True, raw data is symmetrically trimmed to match 

425 calibration size. 

426 

427 Raises 

428 ------ 

429 RuntimeError 

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

431 the same size. 

432 """ 

433 if trimToFit: 

434 maskedImage = trimToMatchCalibBBox(maskedImage, illumMaskedImage) 

435 

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

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

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

439 

440 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage) 

441 

442 

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

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

445 

446 Parameters 

447 ---------- 

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

449 Exposure to have brighter-fatter correction applied. Modified 

450 by this method. 

451 kernel : `numpy.ndarray` 

452 Brighter-fatter kernel to apply. 

453 maxIter : scalar 

454 Number of correction iterations to run. 

455 threshold : scalar 

456 Convergence threshold in terms of the sum of absolute 

457 deviations between an iteration and the previous one. 

458 applyGain : `Bool` 

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

460 to correction. 

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

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

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

464 

465 Returns 

466 ------- 

467 diff : `float` 

468 Final difference between iterations achieved in correction. 

469 iteration : `int` 

470 Number of iterations used to calculate correction. 

471 

472 Notes 

473 ----- 

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

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

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

477 

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

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

480 

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

482 

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

484 

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

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

487 

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

489 we apply the correction iteratively to reconstruct the original 

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

491 difference between the current corrected image and the one from 

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

493 convergence because the number of iterations is too large a 

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

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

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

497 DocuShare Document-19407. 

498 

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

500 have spurious values due to the convolution. 

501 """ 

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

503 

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

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

506 

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

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

509 kernelImage = afwImage.ImageD(kLx, kLy) 

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

511 tempImage = image.clone() 

512 

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

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

515 

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

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

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

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

520 fixedKernel = afwMath.FixedKernel(kernelImage) 

521 

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

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

524 # because of the second derivative terms. 

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

526 # numpy index ranges. 

527 startX = kLx//2 

528 endX = -kLx//2 

529 startY = kLy//2 

530 endY = -kLy//2 

531 

532 for iteration in range(maxIter): 

533 

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

535 tmpArray = tempImage.getArray() 

536 outArray = outImage.getArray() 

537 

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

539 # First derivative term 

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

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

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

543 

544 # Second derivative term 

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

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

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

548 

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

550 

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

552 tmpArray[nanIndex] = 0. 

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

554 

555 if iteration > 0: 

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

557 

558 if diff < threshold: 

559 break 

560 prev_image[:, :] = tmpArray[:, :] 

561 

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

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

564 

565 return diff, iteration 

566 

567 

568@contextmanager 

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

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

571 

572 Parameters 

573 ---------- 

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

575 Exposure to apply/remove gain. 

576 image : `lsst.afw.image.Image` 

577 Image to apply/remove gain. 

578 apply : `Bool` 

579 If True, apply and remove the amplifier gain. 

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

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

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

583 

584 Yields 

585 ------ 

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

587 Exposure with the gain applied. 

588 """ 

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

590 # be a real mess 

591 if gains and apply is True: 

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

593 for ampName in ampNames: 

594 if ampName not in gains.keys(): 

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

596 

597 if apply: 

598 ccd = exp.getDetector() 

599 for amp in ccd: 

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

601 if gains: 

602 gain = gains[amp.getName()] 

603 else: 

604 gain = amp.getGain() 

605 sim *= gain 

606 

607 try: 

608 yield exp 

609 finally: 

610 if apply: 

611 ccd = exp.getDetector() 

612 for amp in ccd: 

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

614 if gains: 

615 gain = gains[amp.getName()] 

616 else: 

617 gain = amp.getGain() 

618 sim /= gain 

619 

620 

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

622 sensorTransmission=None, atmosphereTransmission=None): 

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

624 different components. 

625 

626 Parameters 

627 ---------- 

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

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

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

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

632 associated with sensorTransmission. 

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

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

635 to be evaluated in focal-plane coordinates. 

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

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

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

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

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

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

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

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

644 atmosphere, assumed to be spatially constant. 

645 

646 Returns 

647 ------- 

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

649 The TransmissionCurve attached to the exposure. 

650 

651 Notes 

652 ----- 

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

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

655 """ 

656 combined = afwImage.TransmissionCurve.makeIdentity() 

657 if atmosphereTransmission is not None: 

658 combined *= atmosphereTransmission 

659 if opticsTransmission is not None: 

660 combined *= opticsTransmission 

661 if filterTransmission is not None: 

662 combined *= filterTransmission 

663 detector = exposure.getDetector() 

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

665 toSys=camGeom.PIXELS) 

666 combined = combined.transformedBy(fpToPix) 

667 if sensorTransmission is not None: 

668 combined *= sensorTransmission 

669 exposure.getInfo().setTransmissionCurve(combined) 

670 return combined 

671 

672 

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

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

675 

676 Parameters 

677 ---------- 

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

679 Exposure to process. The image is modified. 

680 normalizeGains : `Bool`, optional 

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

682 each amplifier to equal the median of those medians. 

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

684 Dictionary keyed by amp name containing the PTC gains. 

685 """ 

686 ccd = exposure.getDetector() 

687 ccdImage = exposure.getMaskedImage() 

688 

689 medians = [] 

690 for amp in ccd: 

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

692 if ptcGains: 

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

694 else: 

695 sim *= amp.getGain() 

696 

697 if normalizeGains: 

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

699 

700 if normalizeGains: 

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

702 for index, amp in enumerate(ccd): 

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

704 if medians[index] != 0.0: 

705 sim *= median/medians[index] 

706 

707 

708def widenSaturationTrails(mask): 

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

710 trail. 

711 

712 Parameters 

713 ---------- 

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

715 Mask which will have the saturated areas grown. 

716 """ 

717 

718 extraGrowDict = {} 

719 for i in range(1, 6): 

720 extraGrowDict[i] = 0 

721 for i in range(6, 8): 

722 extraGrowDict[i] = 1 

723 for i in range(8, 10): 

724 extraGrowDict[i] = 3 

725 extraGrowMax = 4 

726 

727 if extraGrowMax <= 0: 

728 return 

729 

730 saturatedBit = mask.getPlaneBitMask("SAT") 

731 

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

733 width = mask.getWidth() 

734 

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

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

737 

738 for fp in fpList: 

739 for s in fp.getSpans(): 

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

741 

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

743 if extraGrow > 0: 

744 y = s.getY() - ymin 

745 x0 -= xmin + extraGrow 

746 x1 -= xmin - extraGrow 

747 

748 if x0 < 0: 

749 x0 = 0 

750 if x1 >= width - 1: 

751 x1 = width - 1 

752 

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

754 

755 

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

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

758 

759 Parameters 

760 ---------- 

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

762 Exposure to mask. The exposure mask is modified. 

763 badStatistic : `str`, optional 

764 Statistic to use to generate the replacement value from the 

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

766 

767 Returns 

768 ------- 

769 badPixelCount : scalar 

770 Number of bad pixels masked. 

771 badPixelValue : scalar 

772 Value substituted for bad pixels. 

773 

774 Raises 

775 ------ 

776 RuntimeError 

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

778 """ 

779 if badStatistic == "MEDIAN": 

780 statistic = afwMath.MEDIAN 

781 elif badStatistic == "MEANCLIP": 

782 statistic = afwMath.MEANCLIP 

783 else: 

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

785 

786 mi = exposure.getMaskedImage() 

787 mask = mi.getMask() 

788 BAD = mask.getPlaneBitMask("BAD") 

789 INTRP = mask.getPlaneBitMask("INTRP") 

790 

791 sctrl = afwMath.StatisticsControl() 

792 sctrl.setAndMask(BAD) 

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

794 

795 maskArray = mask.getArray() 

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

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

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

799 

800 return badPixels.sum(), value 

801 

802 

803def checkFilter(exposure, filterList, log): 

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

805 

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

807 for all filter dependent stages. 

808 

809 Parameters 

810 ---------- 

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

812 Exposure to examine. 

813 filterList : `list` [`str`] 

814 List of physical_filter names to check. 

815 log : `logging.Logger` 

816 Logger to handle messages. 

817 

818 Returns 

819 ------- 

820 result : `bool` 

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

822 """ 

823 if len(filterList) == 0: 

824 return False 

825 thisFilter = exposure.getFilter() 

826 if thisFilter is None: 

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

828 return False 

829 

830 thisPhysicalFilter = getPhysicalFilter(thisFilter, log) 

831 if thisPhysicalFilter in filterList: 

832 return True 

833 elif thisFilter.bandLabel in filterList: 

834 if log: 

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

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

837 return True 

838 else: 

839 return False 

840 

841 

842def getPhysicalFilter(filterLabel, log): 

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

844 

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

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

847 "Unknown". 

848 

849 Parameters 

850 ---------- 

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

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

853 physical filter label. 

854 log : `logging.Logger` 

855 Logger to handle messages. 

856 

857 Returns 

858 ------- 

859 physicalFilter : `str` 

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

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

862 """ 

863 if filterLabel is None: 

864 physicalFilter = "Unknown" 

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

866 else: 

867 try: 

868 physicalFilter = filterLabel.physicalLabel 

869 except RuntimeError: 

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

871 physicalFilter = "Unknown" 

872 return physicalFilter