Coverage for python/lsst/ip/diffim/dcrModel.py: 17%

260 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-28 03:47 -0700

1# This file is part of ip_diffim. 

2# 

3# LSST Data Management System 

4# This product includes software developed by the 

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

6# See COPYRIGHT file at the top of the source tree. 

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 <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23import numpy as np 

24from scipy import ndimage 

25from lsst.afw.coord import differentialRefraction 

26import lsst.afw.image as afwImage 

27import lsst.geom as geom 

28 

29__all__ = ["DcrModel", "applyDcr", "calculateDcr", "calculateImageParallacticAngle"] 

30 

31 

32class DcrModel: 

33 """A model of the true sky after correcting chromatic effects. 

34 

35 Attributes 

36 ---------- 

37 dcrNumSubfilters : `int` 

38 Number of sub-filters used to model chromatic effects within a band. 

39 modelImages : `list` of `lsst.afw.image.Image` 

40 A list of masked images, each containing the model for one subfilter 

41 

42 Notes 

43 ----- 

44 The ``DcrModel`` contains an estimate of the true sky, at a higher 

45 wavelength resolution than the input observations. It can be forward- 

46 modeled to produce Differential Chromatic Refraction (DCR) matched 

47 templates for a given ``Exposure``, and provides utilities for conditioning 

48 the model in ``dcrAssembleCoadd`` to avoid oscillating solutions between 

49 iterations of forward modeling or between the subfilters of the model. 

50 """ 

51 

52 def __init__(self, modelImages, effectiveWavelength, bandwidth, filterLabel=None, psf=None, 

53 bbox=None, wcs=None, mask=None, variance=None, photoCalib=None): 

54 self.dcrNumSubfilters = len(modelImages) 

55 self.modelImages = modelImages 

56 self._filterLabel = filterLabel 

57 self._effectiveWavelength = effectiveWavelength 

58 self._bandwidth = bandwidth 

59 self._psf = psf 

60 self._bbox = bbox 

61 self._wcs = wcs 

62 self._mask = mask 

63 self._variance = variance 

64 self.photoCalib = photoCalib 

65 

66 @classmethod 

67 def fromImage(cls, maskedImage, dcrNumSubfilters, effectiveWavelength, bandwidth, 

68 wcs=None, filterLabel=None, psf=None, photoCalib=None): 

69 """Initialize a DcrModel by dividing a coadd between the subfilters. 

70 

71 Parameters 

72 ---------- 

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

74 Input coadded image to divide equally between the subfilters. 

75 dcrNumSubfilters : `int` 

76 Number of sub-filters used to model chromatic effects within a 

77 band. 

78 effectiveWavelength : `float` 

79 The effective wavelengths of the current filter, in nanometers. 

80 bandwidth : `float` 

81 The bandwidth of the current filter, in nanometers. 

82 wcs : `lsst.afw.geom.SkyWcs` 

83 Coordinate system definition (wcs) for the exposure. 

84 filterLabel : `lsst.afw.image.FilterLabel`, optional 

85 The filter label, set in the current instruments' obs package. 

86 Required for any calculation of DCR, including making matched 

87 templates. 

88 psf : `lsst.afw.detection.Psf`, optional 

89 Point spread function (PSF) of the model. 

90 Required if the ``DcrModel`` will be persisted. 

91 photoCalib : `lsst.afw.image.PhotoCalib`, optional 

92 Calibration to convert instrumental flux and 

93 flux error to nanoJansky. 

94 

95 Returns 

96 ------- 

97 dcrModel : `lsst.pipe.tasks.DcrModel` 

98 Best fit model of the true sky after correcting chromatic effects. 

99 """ 

100 # NANs will potentially contaminate the entire image, 

101 # depending on the shift or convolution type used. 

102 model = maskedImage.image.clone() 

103 mask = maskedImage.mask.clone() 

104 bbox = maskedImage.getBBox() 

105 # We divide the variance by N and not N**2 because we will assume each 

106 # subfilter is independent. That means that the significance of 

107 # detected sources will be lower by a factor of sqrt(N) in the 

108 # subfilter images, but we will recover it when we combine the 

109 # subfilter images to construct matched templates. 

110 variance = maskedImage.variance.clone() 

111 variance /= dcrNumSubfilters 

112 model /= dcrNumSubfilters 

113 modelImages = [model, ] 

114 for subfilter in range(1, dcrNumSubfilters): 

115 modelImages.append(model.clone()) 

116 return cls(modelImages, effectiveWavelength, bandwidth, 

117 filterLabel=filterLabel, psf=psf, bbox=bbox, wcs=wcs, 

118 mask=mask, variance=variance, photoCalib=photoCalib) 

119 

120 @classmethod 

121 def fromQuantum(cls, availableCoaddRefs, effectiveWavelength, bandwidth, numSubfilters): 

122 """Load an existing DcrModel from a Gen 3 repository. 

123 

124 Parameters 

125 ---------- 

126 availableCoaddRefs : `dict` [`int`, `lsst.daf.butler.DeferredDatasetHandle`] 

127 Dictionary of spatially relevant retrieved coadd patches, 

128 indexed by their sequential patch number. 

129 effectiveWavelength : `float` 

130 The effective wavelengths of the current filter, in nanometers. 

131 bandwidth : `float` 

132 The bandwidth of the current filter, in nanometers. 

133 numSubfilters : `int` 

134 Number of subfilters in the DcrCoadd. 

135 

136 Returns 

137 ------- 

138 dcrModel : `lsst.pipe.tasks.DcrModel` 

139 Best fit model of the true sky after correcting chromatic effects. 

140 """ 

141 filterLabel = None 

142 psf = None 

143 bbox = None 

144 wcs = None 

145 mask = None 

146 variance = None 

147 photoCalib = None 

148 modelImages = [None]*numSubfilters 

149 

150 for coaddRef in availableCoaddRefs: 

151 subfilter = coaddRef.dataId["subfilter"] 

152 dcrCoadd = coaddRef.get() 

153 if filterLabel is None: 

154 filterLabel = dcrCoadd.getFilter() 

155 if psf is None: 

156 psf = dcrCoadd.getPsf() 

157 if bbox is None: 

158 bbox = dcrCoadd.getBBox() 

159 if wcs is None: 

160 wcs = dcrCoadd.wcs 

161 if mask is None: 

162 mask = dcrCoadd.mask 

163 if variance is None: 

164 variance = dcrCoadd.variance 

165 if photoCalib is None: 

166 photoCalib = dcrCoadd.getPhotoCalib() 

167 modelImages[subfilter] = dcrCoadd.image 

168 return cls(modelImages, effectiveWavelength, bandwidth, filterLabel, 

169 psf, bbox, wcs, mask, variance, photoCalib) 

170 

171 def __len__(self): 

172 """Return the number of subfilters. 

173 

174 Returns 

175 ------- 

176 dcrNumSubfilters : `int` 

177 The number of DCR subfilters in the model. 

178 """ 

179 return self.dcrNumSubfilters 

180 

181 def __getitem__(self, subfilter): 

182 """Iterate over the subfilters of the DCR model. 

183 

184 Parameters 

185 ---------- 

186 subfilter : `int` 

187 Index of the current ``subfilter`` within the full band. 

188 Negative indices are allowed, and count in reverse order 

189 from the highest ``subfilter``. 

190 

191 Returns 

192 ------- 

193 modelImage : `lsst.afw.image.Image` 

194 The DCR model for the given ``subfilter``. 

195 

196 Raises 

197 ------ 

198 IndexError 

199 If the requested ``subfilter`` is greater or equal to the number 

200 of subfilters in the model. 

201 """ 

202 if np.abs(subfilter) >= len(self): 

203 raise IndexError("subfilter out of bounds.") 

204 return self.modelImages[subfilter] 

205 

206 def __setitem__(self, subfilter, maskedImage): 

207 """Update the model image for one subfilter. 

208 

209 Parameters 

210 ---------- 

211 subfilter : `int` 

212 Index of the current subfilter within the full band. 

213 maskedImage : `lsst.afw.image.Image` 

214 The DCR model to set for the given ``subfilter``. 

215 

216 Raises 

217 ------ 

218 IndexError 

219 If the requested ``subfilter`` is greater or equal to the number 

220 of subfilters in the model. 

221 ValueError 

222 If the bounding box of the new image does not match. 

223 """ 

224 if np.abs(subfilter) >= len(self): 

225 raise IndexError("subfilter out of bounds.") 

226 if maskedImage.getBBox() != self.bbox: 

227 raise ValueError("The bounding box of a subfilter must not change.") 

228 self.modelImages[subfilter] = maskedImage 

229 

230 @property 

231 def effectiveWavelength(self): 

232 """Return the effective wavelength of the model. 

233 

234 Returns 

235 ------- 

236 effectiveWavelength : `float` 

237 The effective wavelength of the current filter, in nanometers. 

238 """ 

239 return self._effectiveWavelength 

240 

241 @property 

242 def filter(self): 

243 """Return the filter label for the model. 

244 

245 Returns 

246 ------- 

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

248 The filter used for the input observations. 

249 """ 

250 return self._filterLabel 

251 

252 @property 

253 def bandwidth(self): 

254 """Return the bandwidth of the model. 

255 

256 Returns 

257 ------- 

258 bandwidth : `float` 

259 The bandwidth of the current filter, in nanometers. 

260 """ 

261 return self._bandwidth 

262 

263 @property 

264 def psf(self): 

265 """Return the psf of the model. 

266 

267 Returns 

268 ------- 

269 psf : `lsst.afw.detection.Psf` 

270 Point spread function (PSF) of the model. 

271 """ 

272 return self._psf 

273 

274 @property 

275 def bbox(self): 

276 """Return the common bounding box of each subfilter image. 

277 

278 Returns 

279 ------- 

280 bbox : `lsst.afw.geom.Box2I` 

281 Bounding box of the DCR model. 

282 """ 

283 return self._bbox 

284 

285 @property 

286 def wcs(self): 

287 """Return the WCS of each subfilter image. 

288 

289 Returns 

290 ------- 

291 bbox : `lsst.afw.geom.SkyWcs` 

292 Coordinate system definition (wcs) for the exposure. 

293 """ 

294 return self._wcs 

295 

296 @property 

297 def mask(self): 

298 """Return the common mask of each subfilter image. 

299 

300 Returns 

301 ------- 

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

303 Mask plane of the DCR model. 

304 """ 

305 return self._mask 

306 

307 @property 

308 def variance(self): 

309 """Return the common variance of each subfilter image. 

310 

311 Returns 

312 ------- 

313 variance : `lsst.afw.image.Image` 

314 Variance plane of the DCR model. 

315 """ 

316 return self._variance 

317 

318 def getReferenceImage(self, bbox=None): 

319 """Calculate a reference image from the average of the subfilter 

320 images. 

321 

322 Parameters 

323 ---------- 

324 bbox : `lsst.afw.geom.Box2I`, optional 

325 Sub-region of the coadd. Returns the entire image if `None`. 

326 

327 Returns 

328 ------- 

329 refImage : `numpy.ndarray` 

330 The reference image with no chromatic effects applied. 

331 """ 

332 bbox = bbox or self.bbox 

333 return np.mean([model[bbox].array for model in self], axis=0) 

334 

335 def assign(self, dcrSubModel, bbox=None): 

336 """Update a sub-region of the ``DcrModel`` with new values. 

337 

338 Parameters 

339 ---------- 

340 dcrSubModel : `lsst.pipe.tasks.DcrModel` 

341 New model of the true scene after correcting chromatic effects. 

342 bbox : `lsst.afw.geom.Box2I`, optional 

343 Sub-region of the coadd. 

344 Defaults to the bounding box of ``dcrSubModel``. 

345 

346 Raises 

347 ------ 

348 ValueError 

349 If the new model has a different number of subfilters. 

350 """ 

351 if len(dcrSubModel) != len(self): 

352 raise ValueError("The number of DCR subfilters must be the same " 

353 "between the old and new models.") 

354 bbox = bbox or self.bbox 

355 for model, subModel in zip(self, dcrSubModel): 

356 model.assign(subModel[bbox], bbox) 

357 

358 def buildMatchedTemplate(self, exposure=None, order=3, 

359 visitInfo=None, bbox=None, mask=None, 

360 splitSubfilters=True, splitThreshold=0., amplifyModel=1.): 

361 """Create a DCR-matched template image for an exposure. 

362 

363 Parameters 

364 ---------- 

365 exposure : `lsst.afw.image.Exposure`, optional 

366 The input exposure to build a matched template for. 

367 May be omitted if all of the metadata is supplied separately 

368 order : `int`, optional 

369 Interpolation order of the DCR shift. 

370 visitInfo : `lsst.afw.image.VisitInfo`, optional 

371 Metadata for the exposure. Ignored if ``exposure`` is set. 

372 bbox : `lsst.afw.geom.Box2I`, optional 

373 Sub-region of the coadd, or use the entire coadd if not supplied. 

374 mask : `lsst.afw.image.Mask`, optional 

375 reference mask to use for the template image. 

376 splitSubfilters : `bool`, optional 

377 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 

378 instead of at the midpoint. Default: True 

379 splitThreshold : `float`, optional 

380 Minimum DCR difference within a subfilter required to use 

381 ``splitSubfilters`` 

382 amplifyModel : `float`, optional 

383 Multiplication factor to amplify differences between model planes. 

384 Used to speed convergence of iterative forward modeling. 

385 

386 Returns 

387 ------- 

388 templateImage : `lsst.afw.image.ImageF` 

389 The DCR-matched template 

390 

391 Raises 

392 ------ 

393 ValueError 

394 If neither ``exposure`` or ``visitInfo`` are set. 

395 """ 

396 if self.effectiveWavelength is None or self.bandwidth is None: 

397 raise ValueError("'effectiveWavelength' and 'bandwidth' must be set for the DcrModel in order " 

398 "to calculate DCR.") 

399 if exposure is not None: 

400 visitInfo = exposure.getInfo().getVisitInfo() 

401 elif visitInfo is None: 

402 raise ValueError("Either exposure or visitInfo must be set.") 

403 if bbox is None: 

404 bbox = self.bbox 

405 dcrShift = calculateDcr(visitInfo, self.wcs, self.effectiveWavelength, self.bandwidth, len(self), 

406 splitSubfilters=splitSubfilters) 

407 templateImage = afwImage.ImageF(bbox) 

408 refModel = None 

409 for subfilter, dcr in enumerate(dcrShift): 

410 if self[subfilter] is None: 

411 # It is possible to load only a single DCR subfilter at a time. 

412 self.log.debug("Skipping missing DCR model subfilter %d", subfilter) 

413 continue 

414 if amplifyModel > 1: 

415 if refModel is None: 

416 # amplifyModel is only an option while constructing the DcrModel, 

417 # and we don't want to calculate a reference image during image differencing. 

418 refModel = self.getReferenceImage(bbox) 

419 model = (self[subfilter][bbox].array - refModel)*amplifyModel + refModel 

420 else: 

421 model = self[subfilter][bbox].array 

422 templateImage.array += applyDcr(model, dcr, splitSubfilters=splitSubfilters, 

423 splitThreshold=splitThreshold, order=order) 

424 return templateImage 

425 

426 def buildMatchedExposure(self, exposure=None, 

427 visitInfo=None, bbox=None, mask=None): 

428 """Wrapper to create an exposure from a template image. 

429 

430 Parameters 

431 ---------- 

432 exposure : `lsst.afw.image.Exposure`, optional 

433 The input exposure to build a matched template for. 

434 May be omitted if all of the metadata is supplied separately 

435 visitInfo : `lsst.afw.image.VisitInfo`, optional 

436 Metadata for the exposure. Ignored if ``exposure`` is set. 

437 bbox : `lsst.afw.geom.Box2I`, optional 

438 Sub-region of the coadd, or use the entire coadd if not supplied. 

439 mask : `lsst.afw.image.Mask`, optional 

440 reference mask to use for the template image. 

441 

442 Returns 

443 ------- 

444 templateExposure : `lsst.afw.image.exposureF` 

445 The DCR-matched template 

446 

447 Raises 

448 ------ 

449 RuntimeError 

450 If no `photcCalib` is set. 

451 """ 

452 if bbox is None: 

453 bbox = self.bbox 

454 templateImage = self.buildMatchedTemplate(exposure=exposure, visitInfo=visitInfo, 

455 bbox=bbox, mask=mask) 

456 maskedImage = afwImage.MaskedImageF(bbox) 

457 maskedImage.image = templateImage[bbox] 

458 maskedImage.mask = self.mask[bbox] 

459 maskedImage.variance = self.variance[bbox] 

460 # The variance of the stacked image will be `dcrNumSubfilters` 

461 # times the variance of the individual subfilters. 

462 maskedImage.variance *= self.dcrNumSubfilters 

463 templateExposure = afwImage.ExposureF(bbox, self.wcs) 

464 templateExposure.setMaskedImage(maskedImage[bbox]) 

465 templateExposure.setPsf(self.psf) 

466 templateExposure.setFilter(self.filter) 

467 if self.photoCalib is None: 

468 raise RuntimeError("No PhotoCalib set for the DcrModel. " 

469 "If the DcrModel was created from a masked image" 

470 " you must also specify the photoCalib.") 

471 templateExposure.setPhotoCalib(self.photoCalib) 

472 return templateExposure 

473 

474 def conditionDcrModel(self, modelImages, bbox, gain=1.): 

475 """Average two iterations' solutions to reduce oscillations. 

476 

477 Parameters 

478 ---------- 

479 modelImages : `list` of `lsst.afw.image.Image` 

480 The new DCR model images from the current iteration. 

481 The values will be modified in place. 

482 bbox : `lsst.afw.geom.Box2I` 

483 Sub-region of the coadd 

484 gain : `float`, optional 

485 Relative weight to give the new solution when updating the model. 

486 Defaults to 1.0, which gives equal weight to both solutions. 

487 """ 

488 # Calculate weighted averages of the images. 

489 for model, newModel in zip(self, modelImages): 

490 newModel *= gain 

491 newModel += model[bbox] 

492 newModel /= 1. + gain 

493 

494 def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, 

495 regularizationWidth=2): 

496 """Restrict large variations in the model between iterations. 

497 

498 Parameters 

499 ---------- 

500 subfilter : `int` 

501 Index of the current subfilter within the full band. 

502 newModel : `lsst.afw.image.Image` 

503 The new DCR model for one subfilter from the current iteration. 

504 Values in ``newModel`` that are extreme compared with the last 

505 iteration are modified in place. 

506 bbox : `lsst.afw.geom.Box2I` 

507 Sub-region to coadd 

508 regularizationFactor : `float` 

509 Maximum relative change of the model allowed between iterations. 

510 regularizationWidth : int, optional 

511 Minimum radius of a region to include in regularization, in pixels. 

512 """ 

513 refImage = self[subfilter][bbox].array 

514 highThreshold = np.abs(refImage)*regularizationFactor 

515 lowThreshold = refImage/regularizationFactor 

516 newImage = newModel.array 

517 self.applyImageThresholds(newImage, highThreshold=highThreshold, lowThreshold=lowThreshold, 

518 regularizationWidth=regularizationWidth) 

519 

520 def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, 

521 regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"): 

522 """Restrict large variations in the model between subfilters. 

523 

524 Parameters 

525 ---------- 

526 modelImages : `list` of `lsst.afw.image.Image` 

527 The new DCR model images from the current iteration. 

528 The values will be modified in place. 

529 bbox : `lsst.afw.geom.Box2I` 

530 Sub-region to coadd 

531 statsCtrl : `lsst.afw.math.StatisticsControl` 

532 Statistics control object for coaddition. 

533 regularizationFactor : `float` 

534 Maximum relative change of the model allowed between subfilters. 

535 regularizationWidth : `int`, optional 

536 Minimum radius of a region to include in regularization, in pixels. 

537 mask : `lsst.afw.image.Mask`, optional 

538 Optional alternate mask 

539 convergenceMaskPlanes : `list` of `str`, or `str`, optional 

540 Mask planes to use to calculate convergence. 

541 

542 Notes 

543 ----- 

544 This implementation of frequency regularization restricts each 

545 subfilter image to be a smoothly-varying function times a reference 

546 image. 

547 """ 

548 # ``regularizationFactor`` is the maximum change between subfilter 

549 # images, so the maximum difference between one subfilter image and the 

550 # average will be the square root of that. 

551 maxDiff = np.sqrt(regularizationFactor) 

552 noiseLevel = self.calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox) 

553 referenceImage = self.getReferenceImage(bbox) 

554 badPixels = np.isnan(referenceImage) | (referenceImage <= 0.) 

555 if np.sum(~badPixels) == 0: 

556 # Skip regularization if there are no valid pixels 

557 return 

558 referenceImage[badPixels] = 0. 

559 filterWidth = regularizationWidth 

560 fwhm = 2.*filterWidth 

561 # The noise should be lower in the smoothed image by 

562 # sqrt(Nsmooth) ~ fwhm pixels 

563 noiseLevel /= fwhm 

564 smoothRef = ndimage.gaussian_filter(referenceImage, filterWidth, mode='constant') 

565 # Add a three sigma offset to both the reference and model to prevent 

566 # dividing by zero. Note that this will also slightly suppress faint 

567 # variations in color. 

568 smoothRef += 3.*noiseLevel 

569 

570 lowThreshold = smoothRef/maxDiff 

571 highThreshold = smoothRef*maxDiff 

572 for model in modelImages: 

573 self.applyImageThresholds(model.array, 

574 highThreshold=highThreshold, 

575 lowThreshold=lowThreshold, 

576 regularizationWidth=regularizationWidth) 

577 smoothModel = ndimage.gaussian_filter(model.array, filterWidth, mode='constant') 

578 smoothModel += 3.*noiseLevel 

579 relativeModel = smoothModel/smoothRef 

580 # Now sharpen the smoothed relativeModel using an alpha of 3. 

581 alpha = 3. 

582 relativeModel2 = ndimage.gaussian_filter(relativeModel, filterWidth/alpha) 

583 relativeModel += alpha*(relativeModel - relativeModel2) 

584 model.array = relativeModel*referenceImage 

585 

586 def calculateNoiseCutoff(self, image, statsCtrl, bufferSize, 

587 convergenceMaskPlanes="DETECTED", mask=None, bbox=None): 

588 """Helper function to calculate the background noise level of an image. 

589 

590 Parameters 

591 ---------- 

592 image : `lsst.afw.image.Image` 

593 The input image to evaluate the background noise properties. 

594 statsCtrl : `lsst.afw.math.StatisticsControl` 

595 Statistics control object for coaddition. 

596 bufferSize : `int` 

597 Number of additional pixels to exclude 

598 from the edges of the bounding box. 

599 convergenceMaskPlanes : `list` of `str`, or `str` 

600 Mask planes to use to calculate convergence. 

601 mask : `lsst.afw.image.Mask`, Optional 

602 Optional alternate mask 

603 bbox : `lsst.afw.geom.Box2I`, optional 

604 Sub-region of the masked image to calculate the noise level over. 

605 

606 Returns 

607 ------- 

608 noiseCutoff : `float` 

609 The threshold value to treat pixels as noise in an image.. 

610 """ 

611 if bbox is None: 

612 bbox = self.bbox 

613 if mask is None: 

614 mask = self.mask[bbox] 

615 bboxShrink = geom.Box2I(bbox) 

616 bboxShrink.grow(-bufferSize) 

617 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes) 

618 

619 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0 

620 noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels]) 

621 return noiseCutoff 

622 

623 def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2): 

624 """Restrict image values to be between upper and lower limits. 

625 

626 This method flags all pixels in an image that are outside of the given 

627 threshold values. The threshold values are taken from a reference 

628 image, so noisy pixels are likely to get flagged. In order to exclude 

629 those noisy pixels, the array of flags is eroded and dilated, which 

630 removes isolated pixels outside of the thresholds from the list of 

631 pixels to be modified. Pixels that remain flagged after this operation 

632 have their values set to the appropriate upper or lower threshold 

633 value. 

634 

635 Parameters 

636 ---------- 

637 image : `numpy.ndarray` 

638 The image to apply the thresholds to. 

639 The values will be modified in place. 

640 highThreshold : `numpy.ndarray`, optional 

641 Array of upper limit values for each pixel of ``image``. 

642 lowThreshold : `numpy.ndarray`, optional 

643 Array of lower limit values for each pixel of ``image``. 

644 regularizationWidth : `int`, optional 

645 Minimum radius of a region to include in regularization, in pixels. 

646 """ 

647 # Generate the structure for binary erosion and dilation, which is used 

648 # to remove noise-like pixels. Groups of pixels with a radius smaller 

649 # than ``regularizationWidth`` will be excluded from regularization. 

650 filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1), 

651 regularizationWidth) 

652 if highThreshold is not None: 

653 highPixels = image > highThreshold 

654 if regularizationWidth > 0: 

655 # Erode and dilate ``highPixels`` to exclude noisy pixels. 

656 highPixels = ndimage.binary_opening(highPixels, structure=filterStructure) 

657 image[highPixels] = highThreshold[highPixels] 

658 if lowThreshold is not None: 

659 lowPixels = image < lowThreshold 

660 if regularizationWidth > 0: 

661 # Erode and dilate ``lowPixels`` to exclude noisy pixels. 

662 lowPixels = ndimage.binary_opening(lowPixels, structure=filterStructure) 

663 image[lowPixels] = lowThreshold[lowPixels] 

664 

665 

666def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0., 

667 doPrefilter=True, order=3): 

668 """Shift an image along the X and Y directions. 

669 

670 Parameters 

671 ---------- 

672 image : `numpy.ndarray` 

673 The input image to shift. 

674 dcr : `tuple` 

675 Shift calculated with ``calculateDcr``. 

676 Uses numpy axes ordering (Y, X). 

677 If ``splitSubfilters`` is set, each element is itself a `tuple` 

678 of two `float`, corresponding to the DCR shift at the two wavelengths. 

679 Otherwise, each element is a `float` corresponding to the DCR shift at 

680 the effective wavelength of the subfilter. 

681 useInverse : `bool`, optional 

682 Apply the shift in the opposite direction. Default: False 

683 splitSubfilters : `bool`, optional 

684 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 

685 instead of at the midpoint. Default: False 

686 splitThreshold : `float`, optional 

687 Minimum DCR difference within a subfilter required to use 

688 ``splitSubfilters`` 

689 doPrefilter : `bool`, optional 

690 Spline filter the image before shifting, if set. Filtering is required, 

691 so only set to False if the image is already filtered. 

692 Filtering takes ~20% of the time of shifting, so if `applyDcr` will be 

693 called repeatedly on the same image it is more efficient to 

694 precalculate the filter. 

695 order : `int`, optional 

696 The order of the spline interpolation, default is 3. 

697 

698 Returns 

699 ------- 

700 shiftedImage : `numpy.ndarray` 

701 A copy of the input image with the specified shift applied. 

702 """ 

703 if doPrefilter and order > 1: 

704 prefilteredImage = ndimage.spline_filter(image, order=order) 

705 else: 

706 prefilteredImage = image 

707 if splitSubfilters: 

708 shiftAmp = np.max(np.abs([_dcr0 - _dcr1 for _dcr0, _dcr1 in zip(dcr[0], dcr[1])])) 

709 if shiftAmp >= splitThreshold: 

710 if useInverse: 

711 shift = [-1.*s for s in dcr[0]] 

712 shift1 = [-1.*s for s in dcr[1]] 

713 else: 

714 shift = dcr[0] 

715 shift1 = dcr[1] 

716 shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=False, order=order) 

717 shiftedImage += ndimage.shift(prefilteredImage, shift1, prefilter=False, order=order) 

718 shiftedImage /= 2. 

719 return shiftedImage 

720 else: 

721 # If the difference in the DCR shifts is less than the threshold, 

722 # then just use the average shift for efficiency. 

723 dcr = (np.mean(dcr[0]), np.mean(dcr[1])) 

724 if useInverse: 

725 shift = [-1.*s for s in dcr] 

726 else: 

727 shift = dcr 

728 shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=False, order=order) 

729 return shiftedImage 

730 

731 

732def calculateDcr(visitInfo, wcs, effectiveWavelength, bandwidth, dcrNumSubfilters, splitSubfilters=False): 

733 """Calculate the shift in pixels of an exposure due to DCR. 

734 

735 Parameters 

736 ---------- 

737 visitInfo : `lsst.afw.image.VisitInfo` 

738 Metadata for the exposure. 

739 wcs : `lsst.afw.geom.SkyWcs` 

740 Coordinate system definition (wcs) for the exposure. 

741 effectiveWavelength : `float` 

742 The effective wavelengths of the current filter, in nanometers. 

743 bandwidth : `float` 

744 The bandwidth of the current filter, in nanometers. 

745 dcrNumSubfilters : `int` 

746 Number of sub-filters used to model chromatic effects within a band. 

747 splitSubfilters : `bool`, optional 

748 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 

749 instead of at the midpoint. Default: False 

750 

751 Returns 

752 ------- 

753 dcrShift : `tuple` of two `float` 

754 The 2D shift due to DCR, in pixels. 

755 Uses numpy axes ordering (Y, X). 

756 """ 

757 rotation = calculateImageParallacticAngle(visitInfo, wcs) 

758 dcrShift = [] 

759 weight = [0.75, 0.25] 

760 for wl0, wl1 in wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters): 

761 # Note that diffRefractAmp can be negative, since it's relative to the 

762 # midpoint of the full band 

763 diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=effectiveWavelength, 

764 elevation=visitInfo.getBoresightAzAlt().getLatitude(), 

765 observatory=visitInfo.getObservatory(), 

766 weather=visitInfo.getWeather()) 

767 diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=effectiveWavelength, 

768 elevation=visitInfo.getBoresightAzAlt().getLatitude(), 

769 observatory=visitInfo.getObservatory(), 

770 weather=visitInfo.getWeather()) 

771 if splitSubfilters: 

772 diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds() 

773 diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds() 

774 diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1], 

775 diffRefractPix0*weight[1] + diffRefractPix1*weight[0]] 

776 shiftX = [diffRefractPix*np.sin(rotation.asRadians()) for diffRefractPix in diffRefractArr] 

777 shiftY = [diffRefractPix*np.cos(rotation.asRadians()) for diffRefractPix in diffRefractArr] 

778 dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1]))) 

779 else: 

780 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2. 

781 diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds() 

782 shiftX = diffRefractPix*np.sin(rotation.asRadians()) 

783 shiftY = diffRefractPix*np.cos(rotation.asRadians()) 

784 dcrShift.append((shiftY, shiftX)) 

785 return dcrShift 

786 

787 

788def calculateImageParallacticAngle(visitInfo, wcs): 

789 """Calculate the total sky rotation angle of an exposure. 

790 

791 Parameters 

792 ---------- 

793 visitInfo : `lsst.afw.image.VisitInfo` 

794 Metadata for the exposure. 

795 wcs : `lsst.afw.geom.SkyWcs` 

796 Coordinate system definition (wcs) for the exposure. 

797 

798 Returns 

799 ------- 

800 `lsst.geom.Angle` 

801 The rotation of the image axis, East from North. 

802 Equal to the parallactic angle plus any additional rotation of the 

803 coordinate system. 

804 A rotation angle of 0 degrees is defined with 

805 North along the +y axis and East along the +x axis. 

806 A rotation angle of 90 degrees is defined with 

807 North along the +x axis and East along the -y axis. 

808 """ 

809 parAngle = visitInfo.getBoresightParAngle().asRadians() 

810 cd = wcs.getCdMatrix() 

811 if wcs.isFlipped: 

812 cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2. 

813 rotAngle = (cdAngle + parAngle)*geom.radians 

814 else: 

815 cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2. 

816 rotAngle = (cdAngle - parAngle)*geom.radians 

817 return rotAngle 

818 

819 

820def wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters): 

821 """Iterate over the wavelength endpoints of subfilters. 

822 

823 Parameters 

824 ---------- 

825 effectiveWavelength : `float` 

826 The effective wavelength of the current filter, in nanometers. 

827 bandwidth : `float` 

828 The bandwidth of the current filter, in nanometers. 

829 dcrNumSubfilters : `int` 

830 Number of sub-filters used to model chromatic effects within a band. 

831 

832 Yields 

833 ------ 

834 `tuple` of two `float` 

835 The next set of wavelength endpoints for a subfilter, in nanometers. 

836 """ 

837 lambdaMin = effectiveWavelength - bandwidth/2 

838 lambdaMax = effectiveWavelength + bandwidth/2 

839 wlStep = bandwidth/dcrNumSubfilters 

840 for wl in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=False): 

841 yield (wl, wl + wlStep)