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

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

260 statements  

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): 

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 

134 Returns 

135 ------- 

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

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

138 """ 

139 filterLabel = None 

140 psf = None 

141 bbox = None 

142 wcs = None 

143 mask = None 

144 variance = None 

145 photoCalib = None 

146 modelImages = [None]*len(availableCoaddRefs) 

147 

148 for coaddRef in availableCoaddRefs: 

149 subfilter = coaddRef.dataId["subfilter"] 

150 dcrCoadd = coaddRef.get() 

151 if filterLabel is None: 

152 filterLabel = dcrCoadd.getFilterLabel() 

153 if psf is None: 

154 psf = dcrCoadd.getPsf() 

155 if bbox is None: 

156 bbox = dcrCoadd.getBBox() 

157 if wcs is None: 

158 wcs = dcrCoadd.wcs 

159 if mask is None: 

160 mask = dcrCoadd.mask 

161 if variance is None: 

162 variance = dcrCoadd.variance 

163 if photoCalib is None: 

164 photoCalib = dcrCoadd.getPhotoCalib() 

165 modelImages[subfilter] = dcrCoadd.image 

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

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

168 

169 def __len__(self): 

170 """Return the number of subfilters. 

171 

172 Returns 

173 ------- 

174 dcrNumSubfilters : `int` 

175 The number of DCR subfilters in the model. 

176 """ 

177 return self.dcrNumSubfilters 

178 

179 def __getitem__(self, subfilter): 

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

181 

182 Parameters 

183 ---------- 

184 subfilter : `int` 

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

186 Negative indices are allowed, and count in reverse order 

187 from the highest ``subfilter``. 

188 

189 Returns 

190 ------- 

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

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

193 

194 Raises 

195 ------ 

196 IndexError 

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

198 of subfilters in the model. 

199 """ 

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

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

202 return self.modelImages[subfilter] 

203 

204 def __setitem__(self, subfilter, maskedImage): 

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

206 

207 Parameters 

208 ---------- 

209 subfilter : `int` 

210 Index of the current subfilter within the full band. 

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

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

213 

214 Raises 

215 ------ 

216 IndexError 

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

218 of subfilters in the model. 

219 ValueError 

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

221 """ 

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

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

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

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

226 self.modelImages[subfilter] = maskedImage 

227 

228 @property 

229 def effectiveWavelength(self): 

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

231 

232 Returns 

233 ------- 

234 effectiveWavelength : `float` 

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

236 """ 

237 return self._effectiveWavelength 

238 

239 @property 

240 def filter(self): 

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

242 

243 Returns 

244 ------- 

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

246 The filter used for the input observations. 

247 """ 

248 return self._filterLabel 

249 

250 @property 

251 def bandwidth(self): 

252 """Return the bandwidth of the model. 

253 

254 Returns 

255 ------- 

256 bandwidth : `float` 

257 The bandwidth of the current filter, in nanometers. 

258 """ 

259 return self._bandwidth 

260 

261 @property 

262 def psf(self): 

263 """Return the psf of the model. 

264 

265 Returns 

266 ------- 

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

268 Point spread function (PSF) of the model. 

269 """ 

270 return self._psf 

271 

272 @property 

273 def bbox(self): 

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

275 

276 Returns 

277 ------- 

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

279 Bounding box of the DCR model. 

280 """ 

281 return self._bbox 

282 

283 @property 

284 def wcs(self): 

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

286 

287 Returns 

288 ------- 

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

290 Coordinate system definition (wcs) for the exposure. 

291 """ 

292 return self._wcs 

293 

294 @property 

295 def mask(self): 

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

297 

298 Returns 

299 ------- 

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

301 Mask plane of the DCR model. 

302 """ 

303 return self._mask 

304 

305 @property 

306 def variance(self): 

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

308 

309 Returns 

310 ------- 

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

312 Variance plane of the DCR model. 

313 """ 

314 return self._variance 

315 

316 def getReferenceImage(self, bbox=None): 

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

318 images. 

319 

320 Parameters 

321 ---------- 

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

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

324 

325 Returns 

326 ------- 

327 refImage : `numpy.ndarray` 

328 The reference image with no chromatic effects applied. 

329 """ 

330 bbox = bbox or self.bbox 

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

332 

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

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

335 

336 Parameters 

337 ---------- 

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

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

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

341 Sub-region of the coadd. 

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

343 

344 Raises 

345 ------ 

346 ValueError 

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

348 """ 

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

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

351 "between the old and new models.") 

352 bbox = bbox or self.bbox 

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

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

355 

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

357 visitInfo=None, bbox=None, mask=None, 

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

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

360 

361 Parameters 

362 ---------- 

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

364 The input exposure to build a matched template for. 

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

366 order : `int`, optional 

367 Interpolation order of the DCR shift. 

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

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

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

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

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

373 reference mask to use for the template image. 

374 splitSubfilters : `bool`, optional 

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

376 instead of at the midpoint. Default: True 

377 splitThreshold : `float`, optional 

378 Minimum DCR difference within a subfilter required to use 

379 ``splitSubfilters`` 

380 amplifyModel : `float`, optional 

381 Multiplication factor to amplify differences between model planes. 

382 Used to speed convergence of iterative forward modeling. 

383 

384 Returns 

385 ------- 

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

387 The DCR-matched template 

388 

389 Raises 

390 ------ 

391 ValueError 

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

393 """ 

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

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

396 "to calculate DCR.") 

397 if exposure is not None: 

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

399 elif visitInfo is None: 

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

401 if bbox is None: 

402 bbox = self.bbox 

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

404 splitSubfilters=splitSubfilters) 

405 templateImage = afwImage.ImageF(bbox) 

406 refModel = None 

407 for subfilter, dcr in enumerate(dcrShift): 

408 if self[subfilter] is None: 

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

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

411 continue 

412 if amplifyModel > 1: 

413 if refModel is None: 

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

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

416 refModel = self.getReferenceImage(bbox) 

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

418 else: 

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

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

421 splitThreshold=splitThreshold, order=order) 

422 return templateImage 

423 

424 def buildMatchedExposure(self, exposure=None, 

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

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

427 

428 Parameters 

429 ---------- 

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

431 The input exposure to build a matched template for. 

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

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

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

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

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

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

438 reference mask to use for the template image. 

439 

440 Returns 

441 ------- 

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

443 The DCR-matched template 

444 

445 Raises 

446 ------ 

447 RuntimeError 

448 If no `photcCalib` is set. 

449 """ 

450 if bbox is None: 

451 bbox = self.bbox 

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

453 bbox=bbox, mask=mask) 

454 maskedImage = afwImage.MaskedImageF(bbox) 

455 maskedImage.image = templateImage[bbox] 

456 maskedImage.mask = self.mask[bbox] 

457 maskedImage.variance = self.variance[bbox] 

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

459 # times the variance of the individual subfilters. 

460 maskedImage.variance *= self.dcrNumSubfilters 

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

462 templateExposure.setMaskedImage(maskedImage[bbox]) 

463 templateExposure.setPsf(self.psf) 

464 templateExposure.setFilterLabel(self.filter) 

465 if self.photoCalib is None: 

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

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

468 " you must also specify the photoCalib.") 

469 templateExposure.setPhotoCalib(self.photoCalib) 

470 return templateExposure 

471 

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

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

474 

475 Parameters 

476 ---------- 

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

478 The new DCR model images from the current iteration. 

479 The values will be modified in place. 

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

481 Sub-region of the coadd 

482 gain : `float`, optional 

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

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

485 """ 

486 # Calculate weighted averages of the images. 

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

488 newModel *= gain 

489 newModel += model[bbox] 

490 newModel /= 1. + gain 

491 

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

493 regularizationWidth=2): 

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

495 

496 Parameters 

497 ---------- 

498 subfilter : `int` 

499 Index of the current subfilter within the full band. 

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

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

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

503 iteration are modified in place. 

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

505 Sub-region to coadd 

506 regularizationFactor : `float` 

507 Maximum relative change of the model allowed between iterations. 

508 regularizationWidth : int, optional 

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

510 """ 

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

512 highThreshold = np.abs(refImage)*regularizationFactor 

513 lowThreshold = refImage/regularizationFactor 

514 newImage = newModel.array 

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

516 regularizationWidth=regularizationWidth) 

517 

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

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

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

521 

522 Parameters 

523 ---------- 

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

525 The new DCR model images from the current iteration. 

526 The values will be modified in place. 

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

528 Sub-region to coadd 

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

530 Statistics control object for coaddition. 

531 regularizationFactor : `float` 

532 Maximum relative change of the model allowed between subfilters. 

533 regularizationWidth : `int`, optional 

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

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

536 Optional alternate mask 

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

538 Mask planes to use to calculate convergence. 

539 

540 Notes 

541 ----- 

542 This implementation of frequency regularization restricts each 

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

544 image. 

545 """ 

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

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

548 # average will be the square root of that. 

549 maxDiff = np.sqrt(regularizationFactor) 

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

551 referenceImage = self.getReferenceImage(bbox) 

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

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

554 # Skip regularization if there are no valid pixels 

555 return 

556 referenceImage[badPixels] = 0. 

557 filterWidth = regularizationWidth 

558 fwhm = 2.*filterWidth 

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

560 # sqrt(Nsmooth) ~ fwhm pixels 

561 noiseLevel /= fwhm 

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

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

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

565 # variations in color. 

566 smoothRef += 3.*noiseLevel 

567 

568 lowThreshold = smoothRef/maxDiff 

569 highThreshold = smoothRef*maxDiff 

570 for model in modelImages: 

571 self.applyImageThresholds(model.array, 

572 highThreshold=highThreshold, 

573 lowThreshold=lowThreshold, 

574 regularizationWidth=regularizationWidth) 

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

576 smoothModel += 3.*noiseLevel 

577 relativeModel = smoothModel/smoothRef 

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

579 alpha = 3. 

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

581 relativeModel += alpha*(relativeModel - relativeModel2) 

582 model.array = relativeModel*referenceImage 

583 

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

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

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

587 

588 Parameters 

589 ---------- 

590 image : `lsst.afw.image.Image` 

591 The input image to evaluate the background noise properties. 

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

593 Statistics control object for coaddition. 

594 bufferSize : `int` 

595 Number of additional pixels to exclude 

596 from the edges of the bounding box. 

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

598 Mask planes to use to calculate convergence. 

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

600 Optional alternate mask 

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

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

603 

604 Returns 

605 ------- 

606 noiseCutoff : `float` 

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

608 """ 

609 if bbox is None: 

610 bbox = self.bbox 

611 if mask is None: 

612 mask = self.mask[bbox] 

613 bboxShrink = geom.Box2I(bbox) 

614 bboxShrink.grow(-bufferSize) 

615 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes) 

616 

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

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

619 return noiseCutoff 

620 

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

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

623 

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

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

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

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

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

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

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

631 value. 

632 

633 Parameters 

634 ---------- 

635 image : `numpy.ndarray` 

636 The image to apply the thresholds to. 

637 The values will be modified in place. 

638 highThreshold : `numpy.ndarray`, optional 

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

640 lowThreshold : `numpy.ndarray`, optional 

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

642 regularizationWidth : `int`, optional 

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

644 """ 

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

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

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

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

649 regularizationWidth) 

650 if highThreshold is not None: 

651 highPixels = image > highThreshold 

652 if regularizationWidth > 0: 

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

654 highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure) 

655 image[highPixels] = highThreshold[highPixels] 

656 if lowThreshold is not None: 

657 lowPixels = image < lowThreshold 

658 if regularizationWidth > 0: 

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

660 lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure) 

661 image[lowPixels] = lowThreshold[lowPixels] 

662 

663 

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

665 doPrefilter=True, order=3): 

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

667 

668 Parameters 

669 ---------- 

670 image : `numpy.ndarray` 

671 The input image to shift. 

672 dcr : `tuple` 

673 Shift calculated with ``calculateDcr``. 

674 Uses numpy axes ordering (Y, X). 

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

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

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

678 the effective wavelength of the subfilter. 

679 useInverse : `bool`, optional 

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

681 splitSubfilters : `bool`, optional 

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

683 instead of at the midpoint. Default: False 

684 splitThreshold : `float`, optional 

685 Minimum DCR difference within a subfilter required to use 

686 ``splitSubfilters`` 

687 doPrefilter : `bool`, optional 

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

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

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

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

692 precalculate the filter. 

693 order : `int`, optional 

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

695 

696 Returns 

697 ------- 

698 shiftedImage : `numpy.ndarray` 

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

700 """ 

701 if doPrefilter and order > 1: 

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

703 else: 

704 prefilteredImage = image 

705 if splitSubfilters: 

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

707 if shiftAmp >= splitThreshold: 

708 if useInverse: 

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

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

711 else: 

712 shift = dcr[0] 

713 shift1 = dcr[1] 

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

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

716 shiftedImage /= 2. 

717 return shiftedImage 

718 else: 

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

720 # then just use the average shift for efficiency. 

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

722 if useInverse: 

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

724 else: 

725 shift = dcr 

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

727 return shiftedImage 

728 

729 

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

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

732 

733 Parameters 

734 ---------- 

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

736 Metadata for the exposure. 

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

738 Coordinate system definition (wcs) for the exposure. 

739 effectiveWavelength : `float` 

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

741 bandwidth : `float` 

742 The bandwidth of the current filter, in nanometers. 

743 dcrNumSubfilters : `int` 

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

745 splitSubfilters : `bool`, optional 

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

747 instead of at the midpoint. Default: False 

748 

749 Returns 

750 ------- 

751 dcrShift : `tuple` of two `float` 

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

753 Uses numpy axes ordering (Y, X). 

754 """ 

755 rotation = calculateImageParallacticAngle(visitInfo, wcs) 

756 dcrShift = [] 

757 weight = [0.75, 0.25] 

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

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

760 # midpoint of the full band 

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

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

763 observatory=visitInfo.getObservatory(), 

764 weather=visitInfo.getWeather()) 

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

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

767 observatory=visitInfo.getObservatory(), 

768 weather=visitInfo.getWeather()) 

769 if splitSubfilters: 

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

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

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

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

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

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

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

777 else: 

778 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2. 

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

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

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

782 dcrShift.append((shiftY, shiftX)) 

783 return dcrShift 

784 

785 

786def calculateImageParallacticAngle(visitInfo, wcs): 

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

788 

789 Parameters 

790 ---------- 

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

792 Metadata for the exposure. 

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

794 Coordinate system definition (wcs) for the exposure. 

795 

796 Returns 

797 ------- 

798 `lsst.geom.Angle` 

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

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

801 coordinate system. 

802 A rotation angle of 0 degrees is defined with 

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

804 A rotation angle of 90 degrees is defined with 

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

806 """ 

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

808 cd = wcs.getCdMatrix() 

809 if wcs.isFlipped: 

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

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

812 else: 

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

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

815 return rotAngle 

816 

817 

818def wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters): 

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

820 

821 Parameters 

822 ---------- 

823 effectiveWavelength : `float` 

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

825 bandwidth : `float` 

826 The bandwidth of the current filter, in nanometers. 

827 dcrNumSubfilters : `int` 

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

829 

830 Yields 

831 ------ 

832 `tuple` of two `float` 

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

834 """ 

835 lambdaMin = effectiveWavelength - bandwidth/2 

836 lambdaMax = effectiveWavelength + bandwidth/2 

837 wlStep = bandwidth/dcrNumSubfilters 

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

839 yield (wl, wl + wlStep)