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.refraction 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, filterInfo=None, psf=None, 

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

54 self.dcrNumSubfilters = len(modelImages) 

55 self.modelImages = modelImages 

56 self._filterInfo = filterInfo 

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 filterInfo=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 filterInfo : `lsst.afw.image.Filter`, optional 

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

82 Note: this object will be changed in DM-21333. 

83 Required for any calculation of DCR, including making matched 

84 templates. 

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

86 Point spread function (PSF) of the model. 

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

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

89 Calibration to convert instrumental flux and 

90 flux error to nanoJansky. 

91 

92 Returns 

93 ------- 

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

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

96 """ 

97 # NANs will potentially contaminate the entire image, 

98 # depending on the shift or convolution type used. 

99 model = maskedImage.image.clone() 

100 mask = maskedImage.mask.clone() 

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

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

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

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

105 # subfilter images to construct matched templates. 

106 variance = maskedImage.variance.clone() 

107 variance /= dcrNumSubfilters 

108 model /= dcrNumSubfilters 

109 modelImages = [model, ] 

110 for subfilter in range(1, dcrNumSubfilters): 

111 modelImages.append(model.clone()) 

112 return cls(modelImages, effectiveWavelength, bandwidth, 

113 filterInfo=filterInfo, psf=psf, mask=mask, variance=variance, photoCalib=photoCalib) 

114 

115 @classmethod 

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

117 **kwargs): 

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

119 

120 Parameters 

121 ---------- 

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

123 Data reference defining the patch for coaddition and the 

124 reference Warp 

125 effectiveWavelength : `float` 

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

127 bandwidth : `float` 

128 The bandwidth of the current filter, in nanometers. 

129 datasetType : `str`, optional 

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

131 numSubfilters : `int` 

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

133 band. 

134 **kwargs 

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

136 data registry. 

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

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

139 

140 Returns 

141 ------- 

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

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

144 """ 

145 modelImages = [] 

146 filterInfo = None 

147 psf = None 

148 mask = None 

149 variance = None 

150 photoCalib = None 

151 if "subfilter" in kwargs: 

152 kwargs.pop("subfilter") 

153 for subfilter in range(numSubfilters): 

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

155 numSubfilters=numSubfilters, **kwargs) 

156 if filterInfo is None: 

157 filterInfo = dcrCoadd.getFilter() 

158 if psf is None: 

159 psf = dcrCoadd.getPsf() 

160 if mask is None: 

161 mask = dcrCoadd.mask 

162 if variance is None: 

163 variance = dcrCoadd.variance 

164 if photoCalib is None: 

165 photoCalib = dcrCoadd.getPhotoCalib() 

166 modelImages.append(dcrCoadd.image) 

167 return cls(modelImages, effectiveWavelength, bandwidth, filterInfo, psf, mask, variance, photoCalib) 

168 

169 @classmethod 

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

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

172 

173 Parameters 

174 ---------- 

175 availableCoaddRefs : `dict` of 

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

177 Dictionary of spatially relevant retrieved coadd patches, 

178 indexed by their sequential patch number. 

179 effectiveWavelength : `float` 

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

181 bandwidth : `float` 

182 The bandwidth of the current filter, in nanometers. 

183 

184 Returns 

185 ------- 

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

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

188 """ 

189 filterInfo = None 

190 psf = None 

191 mask = None 

192 variance = None 

193 photoCalib = None 

194 modelImages = [None]*len(availableCoaddRefs) 

195 

196 for coaddRef in availableCoaddRefs: 

197 subfilter = coaddRef.dataId["subfilter"] 

198 dcrCoadd = coaddRef.get() 

199 if filterInfo is None: 

200 filterInfo = dcrCoadd.getFilter() 

201 if psf is None: 

202 psf = dcrCoadd.getPsf() 

203 if mask is None: 

204 mask = dcrCoadd.mask 

205 if variance is None: 

206 variance = dcrCoadd.variance 

207 if photoCalib is None: 

208 photoCalib = dcrCoadd.getPhotoCalib() 

209 modelImages[subfilter] = dcrCoadd.image 

210 return cls(modelImages, effectiveWavelength, bandwidth, filterInfo, psf, mask, variance, photoCalib) 

211 

212 def __len__(self): 

213 """Return the number of subfilters. 

214 

215 Returns 

216 ------- 

217 dcrNumSubfilters : `int` 

218 The number of DCR subfilters in the model. 

219 """ 

220 return self.dcrNumSubfilters 

221 

222 def __getitem__(self, subfilter): 

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

224 

225 Parameters 

226 ---------- 

227 subfilter : `int` 

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

229 Negative indices are allowed, and count in reverse order 

230 from the highest ``subfilter``. 

231 

232 Returns 

233 ------- 

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

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

236 

237 Raises 

238 ------ 

239 IndexError 

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

241 of subfilters in the model. 

242 """ 

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

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

245 return self.modelImages[subfilter] 

246 

247 def __setitem__(self, subfilter, maskedImage): 

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

249 

250 Parameters 

251 ---------- 

252 subfilter : `int` 

253 Index of the current subfilter within the full band. 

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

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

256 

257 Raises 

258 ------ 

259 IndexError 

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

261 of subfilters in the model. 

262 ValueError 

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

264 """ 

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

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

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

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

269 self.modelImages[subfilter] = maskedImage 

270 

271 @property 

272 def effectiveWavelength(self): 

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

274 

275 Returns 

276 ------- 

277 effectiveWavelength : `float` 

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

279 """ 

280 return self._effectiveWavelength 

281 

282 @property 

283 def filter(self): 

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

285 

286 Returns 

287 ------- 

288 filterInfo : `lsst.afw.image.Filter` 

289 The name of the filter used for the input observations. 

290 Note: this object will be changed in DM-21333. 

291 """ 

292 return self._filterInfo 

293 

294 @property 

295 def bandwidth(self): 

296 """Return the bandwidth of the model. 

297 

298 Returns 

299 ------- 

300 bandwidth : `float` 

301 The bandwidth of the current filter, in nanometers. 

302 """ 

303 return self._bandwidth 

304 

305 @property 

306 def psf(self): 

307 """Return the psf of the model. 

308 

309 Returns 

310 ------- 

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

312 Point spread function (PSF) of the model. 

313 """ 

314 return self._psf 

315 

316 @property 

317 def bbox(self): 

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

319 

320 Returns 

321 ------- 

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

323 Bounding box of the DCR model. 

324 """ 

325 return self[0].getBBox() 

326 

327 @property 

328 def mask(self): 

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

330 

331 Returns 

332 ------- 

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

334 Mask plane of the DCR model. 

335 """ 

336 return self._mask 

337 

338 @property 

339 def variance(self): 

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

341 

342 Returns 

343 ------- 

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

345 Variance plane of the DCR model. 

346 """ 

347 return self._variance 

348 

349 def getReferenceImage(self, bbox=None): 

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

351 images. 

352 

353 Parameters 

354 ---------- 

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

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

357 

358 Returns 

359 ------- 

360 refImage : `numpy.ndarray` 

361 The reference image with no chromatic effects applied. 

362 """ 

363 bbox = bbox or self.bbox 

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

365 

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

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

368 

369 Parameters 

370 ---------- 

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

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

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

374 Sub-region of the coadd. 

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

376 

377 Raises 

378 ------ 

379 ValueError 

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

381 """ 

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

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

384 "between the old and new models.") 

385 bbox = bbox or self.bbox 

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

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

388 

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

390 visitInfo=None, bbox=None, wcs=None, mask=None, 

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

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

393 

394 Parameters 

395 ---------- 

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

397 The input exposure to build a matched template for. 

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

399 order : `int`, optional 

400 Interpolation order of the DCR shift. 

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

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

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

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

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

406 Coordinate system definition (wcs) for the exposure. 

407 Ignored if ``exposure`` is set. 

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

409 reference mask to use for the template image. 

410 splitSubfilters : `bool`, optional 

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

412 instead of at the midpoint. Default: True 

413 splitThreshold : `float`, optional 

414 Minimum DCR difference within a subfilter required to use 

415 ``splitSubfilters`` 

416 amplifyModel : `float`, optional 

417 Multiplication factor to amplify differences between model planes. 

418 Used to speed convergence of iterative forward modeling. 

419 

420 Returns 

421 ------- 

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

423 The DCR-matched template 

424 

425 Raises 

426 ------ 

427 ValueError 

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

429 ``wcs`` are set. 

430 """ 

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

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

433 "to calculate DCR.") 

434 if exposure is not None: 

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

436 bbox = exposure.getBBox() 

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

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

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

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

441 splitSubfilters=splitSubfilters) 

442 templateImage = afwImage.ImageF(bbox) 

443 refModel = self.getReferenceImage(bbox) 

444 for subfilter, dcr in enumerate(dcrShift): 

445 if amplifyModel > 1: 

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

447 else: 

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

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

450 splitThreshold=splitThreshold, order=order) 

451 return templateImage 

452 

453 def buildMatchedExposure(self, exposure=None, 

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

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

456 

457 Parameters 

458 ---------- 

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

460 The input exposure to build a matched template for. 

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

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

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

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

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

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

467 Coordinate system definition (wcs) for the exposure. 

468 Ignored if ``exposure`` is set. 

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

470 reference mask to use for the template image. 

471 

472 Returns 

473 ------- 

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

475 The DCR-matched template 

476 

477 Raises 

478 ------ 

479 RuntimeError 

480 If no `photcCalib` is set. 

481 """ 

482 if bbox is None: 

483 bbox = exposure.getBBox() 

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

485 bbox=bbox, wcs=wcs, mask=mask) 

486 maskedImage = afwImage.MaskedImageF(bbox) 

487 maskedImage.image = templateImage[bbox] 

488 maskedImage.mask = self.mask[bbox] 

489 maskedImage.variance = self.variance[bbox] 

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

491 # times the variance of the individual subfilters. 

492 maskedImage.variance *= self.dcrNumSubfilters 

493 templateExposure = afwImage.ExposureF(bbox, wcs) 

494 templateExposure.setMaskedImage(maskedImage[bbox]) 

495 templateExposure.setPsf(self.psf) 

496 templateExposure.setFilter(self.filterInfo) 

497 if self.photoCalib is None: 

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

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

500 " you must also specify the photoCalib.") 

501 templateExposure.setPhotoCalib(self.photoCalib) 

502 return templateExposure 

503 

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

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

506 

507 Parameters 

508 ---------- 

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

510 The new DCR model images from the current iteration. 

511 The values will be modified in place. 

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

513 Sub-region of the coadd 

514 gain : `float`, optional 

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

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

517 """ 

518 # Calculate weighted averages of the images. 

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

520 newModel *= gain 

521 newModel += model[bbox] 

522 newModel /= 1. + gain 

523 

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

525 regularizationWidth=2): 

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

527 

528 Parameters 

529 ---------- 

530 subfilter : `int` 

531 Index of the current subfilter within the full band. 

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

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

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

535 iteration are modified in place. 

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

537 Sub-region to coadd 

538 regularizationFactor : `float` 

539 Maximum relative change of the model allowed between iterations. 

540 regularizationWidth : int, optional 

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

542 """ 

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

544 highThreshold = np.abs(refImage)*regularizationFactor 

545 lowThreshold = refImage/regularizationFactor 

546 newImage = newModel.array 

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

548 regularizationWidth=regularizationWidth) 

549 

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

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

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

553 

554 Parameters 

555 ---------- 

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

557 The new DCR model images from the current iteration. 

558 The values will be modified in place. 

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

560 Sub-region to coadd 

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

562 Statistics control object for coaddition. 

563 regularizationFactor : `float` 

564 Maximum relative change of the model allowed between subfilters. 

565 regularizationWidth : `int`, optional 

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

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

568 Optional alternate mask 

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

570 Mask planes to use to calculate convergence. 

571 

572 Notes 

573 ----- 

574 This implementation of frequency regularization restricts each 

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

576 image. 

577 """ 

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

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

580 # average will be the square root of that. 

581 maxDiff = np.sqrt(regularizationFactor) 

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

583 referenceImage = self.getReferenceImage(bbox) 

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

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

586 # Skip regularization if there are no valid pixels 

587 return 

588 referenceImage[badPixels] = 0. 

589 filterWidth = regularizationWidth 

590 fwhm = 2.*filterWidth 

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

592 # sqrt(Nsmooth) ~ fwhm pixels 

593 noiseLevel /= fwhm 

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

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

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

597 # variations in color. 

598 smoothRef += 3.*noiseLevel 

599 

600 lowThreshold = smoothRef/maxDiff 

601 highThreshold = smoothRef*maxDiff 

602 for model in modelImages: 

603 self.applyImageThresholds(model.array, 

604 highThreshold=highThreshold, 

605 lowThreshold=lowThreshold, 

606 regularizationWidth=regularizationWidth) 

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

608 smoothModel += 3.*noiseLevel 

609 relativeModel = smoothModel/smoothRef 

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

611 alpha = 3. 

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

613 relativeModel += alpha*(relativeModel - relativeModel2) 

614 model.array = relativeModel*referenceImage 

615 

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

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

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

619 

620 Parameters 

621 ---------- 

622 image : `lsst.afw.image.Image` 

623 The input image to evaluate the background noise properties. 

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

625 Statistics control object for coaddition. 

626 bufferSize : `int` 

627 Number of additional pixels to exclude 

628 from the edges of the bounding box. 

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

630 Mask planes to use to calculate convergence. 

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

632 Optional alternate mask 

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

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

635 

636 Returns 

637 ------- 

638 noiseCutoff : `float` 

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

640 """ 

641 if bbox is None: 

642 bbox = self.bbox 

643 if mask is None: 

644 mask = self.mask[bbox] 

645 bboxShrink = geom.Box2I(bbox) 

646 bboxShrink.grow(-bufferSize) 

647 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes) 

648 

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

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

651 return noiseCutoff 

652 

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

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

655 

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

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

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

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

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

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

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

663 value. 

664 

665 Parameters 

666 ---------- 

667 image : `numpy.ndarray` 

668 The image to apply the thresholds to. 

669 The values will be modified in place. 

670 highThreshold : `numpy.ndarray`, optional 

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

672 lowThreshold : `numpy.ndarray`, optional 

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

674 regularizationWidth : `int`, optional 

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

676 """ 

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

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

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

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

681 regularizationWidth) 

682 if highThreshold is not None: 

683 highPixels = image > highThreshold 

684 if regularizationWidth > 0: 

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

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

687 image[highPixels] = highThreshold[highPixels] 

688 if lowThreshold is not None: 

689 lowPixels = image < lowThreshold 

690 if regularizationWidth > 0: 

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

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

693 image[lowPixels] = lowThreshold[lowPixels] 

694 

695 

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

697 doPrefilter=True, order=3): 

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

699 

700 Parameters 

701 ---------- 

702 image : `numpy.ndarray` 

703 The input image to shift. 

704 dcr : `tuple` 

705 Shift calculated with ``calculateDcr``. 

706 Uses numpy axes ordering (Y, X). 

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

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

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

710 the effective wavelength of the subfilter. 

711 useInverse : `bool`, optional 

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

713 splitSubfilters : `bool`, optional 

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

715 instead of at the midpoint. Default: False 

716 splitThreshold : `float`, optional 

717 Minimum DCR difference within a subfilter required to use 

718 ``splitSubfilters`` 

719 doPrefilter : `bool`, optional 

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

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

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

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

724 precalculate the filter. 

725 order : `int`, optional 

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

727 

728 Returns 

729 ------- 

730 shiftedImage : `numpy.ndarray` 

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

732 """ 

733 if doPrefilter: 

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

735 else: 

736 prefilteredImage = image 

737 if splitSubfilters: 

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

739 if shiftAmp >= splitThreshold: 

740 if useInverse: 

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

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

743 else: 

744 shift = dcr[0] 

745 shift1 = dcr[1] 

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

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

748 shiftedImage /= 2. 

749 return shiftedImage 

750 else: 

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

752 # then just use the average shift for efficiency. 

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

754 if useInverse: 

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

756 else: 

757 shift = dcr 

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

759 return shiftedImage 

760 

761 

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

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

764 

765 Parameters 

766 ---------- 

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

768 Metadata for the exposure. 

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

770 Coordinate system definition (wcs) for the exposure. 

771 effectiveWavelength : `float` 

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

773 bandwidth : `float` 

774 The bandwidth of the current filter, in nanometers. 

775 dcrNumSubfilters : `int` 

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

777 splitSubfilters : `bool`, optional 

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

779 instead of at the midpoint. Default: False 

780 

781 Returns 

782 ------- 

783 dcrShift : `tuple` of two `float` 

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

785 Uses numpy axes ordering (Y, X). 

786 """ 

787 rotation = calculateImageParallacticAngle(visitInfo, wcs) 

788 dcrShift = [] 

789 weight = [0.75, 0.25] 

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

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

792 # midpoint of the full band 

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

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

795 observatory=visitInfo.getObservatory(), 

796 weather=visitInfo.getWeather()) 

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

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

799 observatory=visitInfo.getObservatory(), 

800 weather=visitInfo.getWeather()) 

801 if splitSubfilters: 

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

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

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

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

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

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

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

809 else: 

810 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2. 

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

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

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

814 dcrShift.append((shiftY, shiftX)) 

815 return dcrShift 

816 

817 

818def calculateImageParallacticAngle(visitInfo, wcs): 

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

820 

821 Parameters 

822 ---------- 

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

824 Metadata for the exposure. 

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

826 Coordinate system definition (wcs) for the exposure. 

827 

828 Returns 

829 ------- 

830 `lsst.geom.Angle` 

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

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

833 coordinate system. 

834 A rotation angle of 0 degrees is defined with 

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

836 A rotation angle of 90 degrees is defined with 

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

838 """ 

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

840 cd = wcs.getCdMatrix() 

841 if wcs.isFlipped: 

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

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

844 else: 

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

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

847 return rotAngle 

848 

849 

850def wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters): 

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

852 

853 Parameters 

854 ---------- 

855 effectiveWavelength : `float` 

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

857 bandwidth : `float` 

858 The bandwidth of the current filter, in nanometers. 

859 dcrNumSubfilters : `int` 

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

861 

862 Yields 

863 ------ 

864 `tuple` of two `float` 

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

866 """ 

867 lambdaMin = effectiveWavelength - bandwidth/2 

868 lambdaMax = effectiveWavelength + bandwidth/2 

869 wlStep = bandwidth/dcrNumSubfilters 

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

871 yield (wl, wl + wlStep)