Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# 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 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._mask = mask 

61 self._variance = variance 

62 self.photoCalib = photoCalib 

63 

64 @classmethod 

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

66 filterLabel=None, psf=None, photoCalib=None): 

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

68 

69 Parameters 

70 ---------- 

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

72 Input coadded image to divide equally between the subfilters. 

73 dcrNumSubfilters : `int` 

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

75 band. 

76 effectiveWavelength : `float` 

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

78 bandwidth : `float` 

79 The bandwidth of the current filter, in nanometers. 

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

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

82 Required for any calculation of DCR, including making matched 

83 templates. 

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

85 Point spread function (PSF) of the model. 

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

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

88 Calibration to convert instrumental flux and 

89 flux error to nanoJansky. 

90 

91 Returns 

92 ------- 

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

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

95 """ 

96 # NANs will potentially contaminate the entire image, 

97 # depending on the shift or convolution type used. 

98 model = maskedImage.image.clone() 

99 mask = maskedImage.mask.clone() 

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

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

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

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

104 # subfilter images to construct matched templates. 

105 variance = maskedImage.variance.clone() 

106 variance /= dcrNumSubfilters 

107 model /= dcrNumSubfilters 

108 modelImages = [model, ] 

109 for subfilter in range(1, dcrNumSubfilters): 

110 modelImages.append(model.clone()) 

111 return cls(modelImages, effectiveWavelength, bandwidth, 

112 filterLabel=filterLabel, psf=psf, mask=mask, variance=variance, photoCalib=photoCalib) 

113 

114 @classmethod 

115 def fromDataRef(cls, dataRef, effectiveWavelength, bandwidth, datasetType="dcrCoadd", numSubfilters=None, 

116 **kwargs): 

117 """Load an existing DcrModel from a Gen 2 repository. 

118 

119 Parameters 

120 ---------- 

121 dataRef : `lsst.daf.persistence.ButlerDataRef` 

122 Data reference defining the patch for coaddition and the 

123 reference Warp 

124 effectiveWavelength : `float` 

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

126 bandwidth : `float` 

127 The bandwidth of the current filter, in nanometers. 

128 datasetType : `str`, optional 

129 Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"} 

130 numSubfilters : `int` 

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

132 band. 

133 **kwargs 

134 Additional keyword arguments to pass to look up the model in the 

135 data registry. 

136 Common keywords and their types include: ``tract``:`str`, 

137 ``patch``:`str`, ``bbox``:`lsst.afw.geom.Box2I` 

138 

139 Returns 

140 ------- 

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

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

143 """ 

144 modelImages = [] 

145 filterLabel = None 

146 psf = None 

147 mask = None 

148 variance = None 

149 photoCalib = None 

150 if "subfilter" in kwargs: 

151 kwargs.pop("subfilter") 

152 for subfilter in range(numSubfilters): 

153 dcrCoadd = dataRef.get(datasetType, subfilter=subfilter, 

154 numSubfilters=numSubfilters, **kwargs) 

155 if filterLabel is None: 

156 filterLabel = dcrCoadd.getFilterLabel() 

157 if psf is None: 

158 psf = dcrCoadd.getPsf() 

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.append(dcrCoadd.image) 

166 return cls(modelImages, effectiveWavelength, bandwidth, filterLabel, psf, mask, variance, photoCalib) 

167 

168 @classmethod 

169 def fromQuantum(cls, availableCoaddRefs, effectiveWavelength, bandwidth): 

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

171 

172 Parameters 

173 ---------- 

174 availableCoaddRefs : `dict` of 

175 `int` : `lsst.daf.butler.DeferredDatasetHandle` 

176 Dictionary of spatially relevant retrieved coadd patches, 

177 indexed by their sequential patch number. 

178 effectiveWavelength : `float` 

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

180 bandwidth : `float` 

181 The bandwidth of the current filter, in nanometers. 

182 

183 Returns 

184 ------- 

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

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

187 """ 

188 filterLabel = None 

189 psf = None 

190 mask = None 

191 variance = None 

192 photoCalib = None 

193 modelImages = [None]*len(availableCoaddRefs) 

194 

195 for coaddRef in availableCoaddRefs: 

196 subfilter = coaddRef.dataId["subfilter"] 

197 dcrCoadd = coaddRef.get() 

198 if filterLabel is None: 

199 filterLabel = dcrCoadd.getFilterLabel() 

200 if psf is None: 

201 psf = dcrCoadd.getPsf() 

202 if mask is None: 

203 mask = dcrCoadd.mask 

204 if variance is None: 

205 variance = dcrCoadd.variance 

206 if photoCalib is None: 

207 photoCalib = dcrCoadd.getPhotoCalib() 

208 modelImages[subfilter] = dcrCoadd.image 

209 return cls(modelImages, effectiveWavelength, bandwidth, filterLabel, psf, mask, variance, photoCalib) 

210 

211 def __len__(self): 

212 """Return the number of subfilters. 

213 

214 Returns 

215 ------- 

216 dcrNumSubfilters : `int` 

217 The number of DCR subfilters in the model. 

218 """ 

219 return self.dcrNumSubfilters 

220 

221 def __getitem__(self, subfilter): 

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

223 

224 Parameters 

225 ---------- 

226 subfilter : `int` 

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

228 Negative indices are allowed, and count in reverse order 

229 from the highest ``subfilter``. 

230 

231 Returns 

232 ------- 

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

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

235 

236 Raises 

237 ------ 

238 IndexError 

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

240 of subfilters in the model. 

241 """ 

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

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

244 return self.modelImages[subfilter] 

245 

246 def __setitem__(self, subfilter, maskedImage): 

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

248 

249 Parameters 

250 ---------- 

251 subfilter : `int` 

252 Index of the current subfilter within the full band. 

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

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

255 

256 Raises 

257 ------ 

258 IndexError 

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

260 of subfilters in the model. 

261 ValueError 

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

263 """ 

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

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

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

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

268 self.modelImages[subfilter] = maskedImage 

269 

270 @property 

271 def effectiveWavelength(self): 

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

273 

274 Returns 

275 ------- 

276 effectiveWavelength : `float` 

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

278 """ 

279 return self._effectiveWavelength 

280 

281 @property 

282 def filter(self): 

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

284 

285 Returns 

286 ------- 

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

288 The filter used for the input observations. 

289 """ 

290 return self._filterLabel 

291 

292 @property 

293 def bandwidth(self): 

294 """Return the bandwidth of the model. 

295 

296 Returns 

297 ------- 

298 bandwidth : `float` 

299 The bandwidth of the current filter, in nanometers. 

300 """ 

301 return self._bandwidth 

302 

303 @property 

304 def psf(self): 

305 """Return the psf of the model. 

306 

307 Returns 

308 ------- 

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

310 Point spread function (PSF) of the model. 

311 """ 

312 return self._psf 

313 

314 @property 

315 def bbox(self): 

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

317 

318 Returns 

319 ------- 

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

321 Bounding box of the DCR model. 

322 """ 

323 return self[0].getBBox() 

324 

325 @property 

326 def mask(self): 

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

328 

329 Returns 

330 ------- 

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

332 Mask plane of the DCR model. 

333 """ 

334 return self._mask 

335 

336 @property 

337 def variance(self): 

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

339 

340 Returns 

341 ------- 

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

343 Variance plane of the DCR model. 

344 """ 

345 return self._variance 

346 

347 def getReferenceImage(self, bbox=None): 

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

349 images. 

350 

351 Parameters 

352 ---------- 

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

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

355 

356 Returns 

357 ------- 

358 refImage : `numpy.ndarray` 

359 The reference image with no chromatic effects applied. 

360 """ 

361 bbox = bbox or self.bbox 

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

363 

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

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

366 

367 Parameters 

368 ---------- 

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

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

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

372 Sub-region of the coadd. 

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

374 

375 Raises 

376 ------ 

377 ValueError 

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

379 """ 

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

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

382 "between the old and new models.") 

383 bbox = bbox or self.bbox 

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

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

386 

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

388 visitInfo=None, bbox=None, wcs=None, mask=None, 

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

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

391 

392 Parameters 

393 ---------- 

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

395 The input exposure to build a matched template for. 

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

397 order : `int`, optional 

398 Interpolation order of the DCR shift. 

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

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

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

402 Sub-region of the coadd. Ignored if ``exposure`` is set. 

403 wcs : `lsst.afw.geom.SkyWcs`, optional 

404 Coordinate system definition (wcs) for the exposure. 

405 Ignored if ``exposure`` is set. 

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

407 reference mask to use for the template image. 

408 splitSubfilters : `bool`, optional 

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

410 instead of at the midpoint. Default: True 

411 splitThreshold : `float`, optional 

412 Minimum DCR difference within a subfilter required to use 

413 ``splitSubfilters`` 

414 amplifyModel : `float`, optional 

415 Multiplication factor to amplify differences between model planes. 

416 Used to speed convergence of iterative forward modeling. 

417 

418 Returns 

419 ------- 

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

421 The DCR-matched template 

422 

423 Raises 

424 ------ 

425 ValueError 

426 If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and 

427 ``wcs`` are set. 

428 """ 

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

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

431 "to calculate DCR.") 

432 if exposure is not None: 

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

434 bbox = exposure.getBBox() 

435 wcs = exposure.getInfo().getWcs() 

436 elif visitInfo is None or bbox is None or wcs is None: 

437 raise ValueError("Either exposure or visitInfo, bbox, and wcs must be set.") 

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

439 splitSubfilters=splitSubfilters) 

440 templateImage = afwImage.ImageF(bbox) 

441 refModel = self.getReferenceImage(bbox) 

442 for subfilter, dcr in enumerate(dcrShift): 

443 if amplifyModel > 1: 

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

445 else: 

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

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

448 splitThreshold=splitThreshold, order=order) 

449 return templateImage 

450 

451 def buildMatchedExposure(self, exposure=None, 

452 visitInfo=None, bbox=None, wcs=None, mask=None): 

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

454 

455 Parameters 

456 ---------- 

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

458 The input exposure to build a matched template for. 

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

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

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

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

463 Sub-region of the coadd. Ignored if ``exposure`` is set. 

464 wcs : `lsst.afw.geom.SkyWcs`, optional 

465 Coordinate system definition (wcs) for the exposure. 

466 Ignored if ``exposure`` is set. 

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

468 reference mask to use for the template image. 

469 

470 Returns 

471 ------- 

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

473 The DCR-matched template 

474 

475 Raises 

476 ------ 

477 RuntimeError 

478 If no `photcCalib` is set. 

479 """ 

480 if bbox is None: 

481 bbox = exposure.getBBox() 

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

483 bbox=bbox, wcs=wcs, mask=mask) 

484 maskedImage = afwImage.MaskedImageF(bbox) 

485 maskedImage.image = templateImage[bbox] 

486 maskedImage.mask = self.mask[bbox] 

487 maskedImage.variance = self.variance[bbox] 

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

489 # times the variance of the individual subfilters. 

490 maskedImage.variance *= self.dcrNumSubfilters 

491 templateExposure = afwImage.ExposureF(bbox, wcs) 

492 templateExposure.setMaskedImage(maskedImage[bbox]) 

493 templateExposure.setPsf(self.psf) 

494 templateExposure.setFilterLabel(self.filterLabel) 

495 if self.photoCalib is None: 

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

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

498 " you must also specify the photoCalib.") 

499 templateExposure.setPhotoCalib(self.photoCalib) 

500 return templateExposure 

501 

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

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

504 

505 Parameters 

506 ---------- 

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

508 The new DCR model images from the current iteration. 

509 The values will be modified in place. 

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

511 Sub-region of the coadd 

512 gain : `float`, optional 

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

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

515 """ 

516 # Calculate weighted averages of the images. 

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

518 newModel *= gain 

519 newModel += model[bbox] 

520 newModel /= 1. + gain 

521 

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

523 regularizationWidth=2): 

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

525 

526 Parameters 

527 ---------- 

528 subfilter : `int` 

529 Index of the current subfilter within the full band. 

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

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

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

533 iteration are modified in place. 

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

535 Sub-region to coadd 

536 regularizationFactor : `float` 

537 Maximum relative change of the model allowed between iterations. 

538 regularizationWidth : int, optional 

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

540 """ 

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

542 highThreshold = np.abs(refImage)*regularizationFactor 

543 lowThreshold = refImage/regularizationFactor 

544 newImage = newModel.array 

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

546 regularizationWidth=regularizationWidth) 

547 

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

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

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

551 

552 Parameters 

553 ---------- 

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

555 The new DCR model images from the current iteration. 

556 The values will be modified in place. 

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

558 Sub-region to coadd 

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

560 Statistics control object for coaddition. 

561 regularizationFactor : `float` 

562 Maximum relative change of the model allowed between subfilters. 

563 regularizationWidth : `int`, optional 

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

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

566 Optional alternate mask 

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

568 Mask planes to use to calculate convergence. 

569 

570 Notes 

571 ----- 

572 This implementation of frequency regularization restricts each 

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

574 image. 

575 """ 

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

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

578 # average will be the square root of that. 

579 maxDiff = np.sqrt(regularizationFactor) 

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

581 referenceImage = self.getReferenceImage(bbox) 

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

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

584 # Skip regularization if there are no valid pixels 

585 return 

586 referenceImage[badPixels] = 0. 

587 filterWidth = regularizationWidth 

588 fwhm = 2.*filterWidth 

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

590 # sqrt(Nsmooth) ~ fwhm pixels 

591 noiseLevel /= fwhm 

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

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

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

595 # variations in color. 

596 smoothRef += 3.*noiseLevel 

597 

598 lowThreshold = smoothRef/maxDiff 

599 highThreshold = smoothRef*maxDiff 

600 for model in modelImages: 

601 self.applyImageThresholds(model.array, 

602 highThreshold=highThreshold, 

603 lowThreshold=lowThreshold, 

604 regularizationWidth=regularizationWidth) 

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

606 smoothModel += 3.*noiseLevel 

607 relativeModel = smoothModel/smoothRef 

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

609 alpha = 3. 

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

611 relativeModel += alpha*(relativeModel - relativeModel2) 

612 model.array = relativeModel*referenceImage 

613 

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

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

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

617 

618 Parameters 

619 ---------- 

620 image : `lsst.afw.image.Image` 

621 The input image to evaluate the background noise properties. 

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

623 Statistics control object for coaddition. 

624 bufferSize : `int` 

625 Number of additional pixels to exclude 

626 from the edges of the bounding box. 

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

628 Mask planes to use to calculate convergence. 

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

630 Optional alternate mask 

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

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

633 

634 Returns 

635 ------- 

636 noiseCutoff : `float` 

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

638 """ 

639 if bbox is None: 

640 bbox = self.bbox 

641 if mask is None: 

642 mask = self.mask[bbox] 

643 bboxShrink = geom.Box2I(bbox) 

644 bboxShrink.grow(-bufferSize) 

645 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes) 

646 

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

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

649 return noiseCutoff 

650 

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

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

653 

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

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

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

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

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

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

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

661 value. 

662 

663 Parameters 

664 ---------- 

665 image : `numpy.ndarray` 

666 The image to apply the thresholds to. 

667 The values will be modified in place. 

668 highThreshold : `numpy.ndarray`, optional 

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

670 lowThreshold : `numpy.ndarray`, optional 

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

672 regularizationWidth : `int`, optional 

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

674 """ 

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

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

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

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

679 regularizationWidth) 

680 if highThreshold is not None: 

681 highPixels = image > highThreshold 

682 if regularizationWidth > 0: 

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

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

685 image[highPixels] = highThreshold[highPixels] 

686 if lowThreshold is not None: 

687 lowPixels = image < lowThreshold 

688 if regularizationWidth > 0: 

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

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

691 image[lowPixels] = lowThreshold[lowPixels] 

692 

693 

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

695 doPrefilter=True, order=3): 

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

697 

698 Parameters 

699 ---------- 

700 image : `numpy.ndarray` 

701 The input image to shift. 

702 dcr : `tuple` 

703 Shift calculated with ``calculateDcr``. 

704 Uses numpy axes ordering (Y, X). 

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

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

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

708 the effective wavelength of the subfilter. 

709 useInverse : `bool`, optional 

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

711 splitSubfilters : `bool`, optional 

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

713 instead of at the midpoint. Default: False 

714 splitThreshold : `float`, optional 

715 Minimum DCR difference within a subfilter required to use 

716 ``splitSubfilters`` 

717 doPrefilter : `bool`, optional 

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

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

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

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

722 precalculate the filter. 

723 order : `int`, optional 

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

725 

726 Returns 

727 ------- 

728 shiftedImage : `numpy.ndarray` 

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

730 """ 

731 if doPrefilter: 

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

733 else: 

734 prefilteredImage = image 

735 if splitSubfilters: 

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

737 if shiftAmp >= splitThreshold: 

738 if useInverse: 

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

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

741 else: 

742 shift = dcr[0] 

743 shift1 = dcr[1] 

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

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

746 shiftedImage /= 2. 

747 return shiftedImage 

748 else: 

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

750 # then just use the average shift for efficiency. 

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

752 if useInverse: 

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

754 else: 

755 shift = dcr 

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

757 return shiftedImage 

758 

759 

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

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

762 

763 Parameters 

764 ---------- 

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

766 Metadata for the exposure. 

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

768 Coordinate system definition (wcs) for the exposure. 

769 effectiveWavelength : `float` 

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

771 bandwidth : `float` 

772 The bandwidth of the current filter, in nanometers. 

773 dcrNumSubfilters : `int` 

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

775 splitSubfilters : `bool`, optional 

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

777 instead of at the midpoint. Default: False 

778 

779 Returns 

780 ------- 

781 dcrShift : `tuple` of two `float` 

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

783 Uses numpy axes ordering (Y, X). 

784 """ 

785 rotation = calculateImageParallacticAngle(visitInfo, wcs) 

786 dcrShift = [] 

787 weight = [0.75, 0.25] 

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

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

790 # midpoint of the full band 

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

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

793 observatory=visitInfo.getObservatory(), 

794 weather=visitInfo.getWeather()) 

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

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

797 observatory=visitInfo.getObservatory(), 

798 weather=visitInfo.getWeather()) 

799 if splitSubfilters: 

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

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

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

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

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

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

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

807 else: 

808 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2. 

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

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

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

812 dcrShift.append((shiftY, shiftX)) 

813 return dcrShift 

814 

815 

816def calculateImageParallacticAngle(visitInfo, wcs): 

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

818 

819 Parameters 

820 ---------- 

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

822 Metadata for the exposure. 

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

824 Coordinate system definition (wcs) for the exposure. 

825 

826 Returns 

827 ------- 

828 `lsst.geom.Angle` 

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

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

831 coordinate system. 

832 A rotation angle of 0 degrees is defined with 

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

834 A rotation angle of 90 degrees is defined with 

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

836 """ 

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

838 cd = wcs.getCdMatrix() 

839 if wcs.isFlipped: 

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

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

842 else: 

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

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

845 return rotAngle 

846 

847 

848def wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters): 

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

850 

851 Parameters 

852 ---------- 

853 effectiveWavelength : `float` 

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

855 bandwidth : `float` 

856 The bandwidth of the current filter, in nanometers. 

857 dcrNumSubfilters : `int` 

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

859 

860 Yields 

861 ------ 

862 `tuple` of two `float` 

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

864 """ 

865 lambdaMin = effectiveWavelength - bandwidth/2 

866 lambdaMax = effectiveWavelength + bandwidth/2 

867 wlStep = bandwidth/dcrNumSubfilters 

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

869 yield (wl, wl + wlStep)