24 from scipy
import ndimage
30 __all__ = [
"DcrModel",
"applyDcr",
"calculateDcr",
"calculateImageParallacticAngle"]
34 """A model of the true sky after correcting chromatic effects. 38 dcrNumSubfilters : `int` 39 Number of sub-filters used to model chromatic effects within a band. 40 modelImages : `list` of `lsst.afw.image.Image` 41 A list of masked images, each containing the model for one subfilter 45 The ``DcrModel`` contains an estimate of the true sky, at a higher 46 wavelength resolution than the input observations. It can be forward- 47 modeled to produce Differential Chromatic Refraction (DCR) matched 48 templates for a given ``Exposure``, and provides utilities for conditioning 49 the model in ``dcrAssembleCoadd`` to avoid oscillating solutions between 50 iterations of forward modeling or between the subfilters of the model. 53 def __init__(self, modelImages, filterInfo=None, psf=None, mask=None, variance=None):
62 def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None):
63 """Initialize a DcrModel by dividing a coadd between the subfilters. 67 maskedImage : `lsst.afw.image.MaskedImage` 68 Input coadded image to divide equally between the subfilters. 69 dcrNumSubfilters : `int` 70 Number of sub-filters used to model chromatic effects within a band. 71 filterInfo : `lsst.afw.image.Filter`, optional 72 The filter definition, set in the current instruments' obs package. 73 Required for any calculation of DCR, including making matched templates. 74 psf : `lsst.afw.detection.Psf`, optional 75 Point spread function (PSF) of the model. 76 Required if the ``DcrModel`` will be persisted. 80 dcrModel : `lsst.pipe.tasks.DcrModel` 81 Best fit model of the true sky after correcting chromatic effects. 86 If there are any unmasked NAN values in ``maskedImage``. 90 model = maskedImage.image.clone()
91 mask = maskedImage.mask.clone()
97 variance = maskedImage.variance.clone()
98 variance /= dcrNumSubfilters
99 model /= dcrNumSubfilters
100 modelImages = [model, ]
101 for subfilter
in range(1, dcrNumSubfilters):
102 modelImages.append(model.clone())
103 return cls(modelImages, filterInfo, psf, mask, variance)
106 def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, **kwargs):
107 """Load an existing DcrModel from a repository. 111 dataRef : `lsst.daf.persistence.ButlerDataRef` 112 Data reference defining the patch for coaddition and the 114 datasetType : `str`, optional 115 Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"} 116 numSubfilters : `int` 117 Number of sub-filters used to model chromatic effects within a band. 119 Additional keyword arguments to pass to look up the model in the data registry. 120 Common keywords and their types include: ``tract``:`str`, ``patch``:`str`, 121 ``bbox``:`lsst.afw.geom.Box2I` 125 dcrModel : `lsst.pipe.tasks.DcrModel` 126 Best fit model of the true sky after correcting chromatic effects. 133 for subfilter
in range(numSubfilters):
134 dcrCoadd = dataRef.get(datasetType, subfilter=subfilter,
135 numSubfilters=numSubfilters, **kwargs)
136 if filterInfo
is None:
137 filterInfo = dcrCoadd.getFilter()
139 psf = dcrCoadd.getPsf()
143 variance = dcrCoadd.variance
144 modelImages.append(dcrCoadd.image)
145 return cls(modelImages, filterInfo, psf, mask, variance)
148 """Return the number of subfilters. 152 dcrNumSubfilters : `int` 153 The number of DCR subfilters in the model. 158 """Iterate over the subfilters of the DCR model. 163 Index of the current ``subfilter`` within the full band. 164 Negative indices are allowed, and count in reverse order 165 from the highest ``subfilter``. 169 modelImage : `lsst.afw.image.Image` 170 The DCR model for the given ``subfilter``. 175 If the requested ``subfilter`` is greater or equal to the number 176 of subfilters in the model. 178 if np.abs(subfilter) >= len(self):
179 raise IndexError(
"subfilter out of bounds.")
183 """Update the model image for one subfilter. 188 Index of the current subfilter within the full band. 189 maskedImage : `lsst.afw.image.Image` 190 The DCR model to set for the given ``subfilter``. 195 If the requested ``subfilter`` is greater or equal to the number 196 of subfilters in the model. 198 If the bounding box of the new image does not match. 200 if np.abs(subfilter) >= len(self):
201 raise IndexError(
"subfilter out of bounds.")
202 if maskedImage.getBBox() != self.
bbox:
203 raise ValueError(
"The bounding box of a subfilter must not change.")
208 """Return the filter of the model. 212 filter : `lsst.afw.image.Filter` 213 The filter definition, set in the current instruments' obs package. 219 """Return the psf of the model. 223 psf : `lsst.afw.detection.Psf` 224 Point spread function (PSF) of the model. 230 """Return the common bounding box of each subfilter image. 234 bbox : `lsst.afw.geom.Box2I` 235 Bounding box of the DCR model. 237 return self[0].getBBox()
241 """Return the common mask of each subfilter image. 245 mask : `lsst.afw.image.Mask` 246 Mask plane of the DCR model. 252 """Return the common variance of each subfilter image. 256 variance : `lsst.afw.image.Image` 257 Variance plane of the DCR model. 262 """Calculate a reference image from the average of the subfilter images. 266 bbox : `lsst.afw.geom.Box2I`, optional 267 Sub-region of the coadd. Returns the entire image if `None`. 271 refImage : `numpy.ndarray` 272 The reference image with no chromatic effects applied. 274 bbox = bbox
or self.
bbox 275 return np.mean([model[bbox].array
for model
in self], axis=0)
277 def assign(self, dcrSubModel, bbox=None):
278 """Update a sub-region of the ``DcrModel`` with new values. 282 dcrSubModel : `lsst.pipe.tasks.DcrModel` 283 New model of the true scene after correcting chromatic effects. 284 bbox : `lsst.afw.geom.Box2I`, optional 285 Sub-region of the coadd. 286 Defaults to the bounding box of ``dcrSubModel``. 291 If the new model has a different number of subfilters. 293 if len(dcrSubModel) != len(self):
294 raise ValueError(
"The number of DCR subfilters must be the same " 295 "between the old and new models.")
296 bbox = bbox
or self.
bbox 297 for model, subModel
in zip(self, dcrSubModel):
298 model.assign(subModel[bbox], bbox)
301 visitInfo=None, bbox=None, wcs=None, mask=None,
302 splitSubfilters=True, amplifyModel=1.):
303 """Create a DCR-matched template image for an exposure. 307 exposure : `lsst.afw.image.Exposure`, optional 308 The input exposure to build a matched template for. 309 May be omitted if all of the metadata is supplied separately 310 order : `int`, optional 311 Interpolation order of the DCR shift. 312 visitInfo : `lsst.afw.image.VisitInfo`, optional 313 Metadata for the exposure. Ignored if ``exposure`` is set. 314 bbox : `lsst.afw.geom.Box2I`, optional 315 Sub-region of the coadd. Ignored if ``exposure`` is set. 316 wcs : `lsst.afw.geom.SkyWcs`, optional 317 Coordinate system definition (wcs) for the exposure. 318 Ignored if ``exposure`` is set. 319 mask : `lsst.afw.image.Mask`, optional 320 reference mask to use for the template image. 321 splitSubfilters : `bool`, optional 322 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 323 instead of at the midpoint. Default: True 324 amplifyModel : `float`, optional 325 Multiplication factor to amplify differences between model planes. 326 Used to speed convergence of iterative forward modeling. 330 templateImage : `lsst.afw.image.ImageF` 331 The DCR-matched template 336 If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set. 339 raise ValueError(
"'filterInfo' must be set for the DcrModel in order to calculate DCR.")
340 if exposure
is not None:
341 visitInfo = exposure.getInfo().getVisitInfo()
342 bbox = exposure.getBBox()
343 wcs = exposure.getInfo().getWcs()
344 elif visitInfo
is None or bbox
is None or wcs
is None:
345 raise ValueError(
"Either exposure or visitInfo, bbox, and wcs must be set.")
346 dcrShift =
calculateDcr(visitInfo, wcs, self.
filter, len(self), splitSubfilters=splitSubfilters)
347 templateImage = afwImage.ImageF(bbox)
349 for subfilter, dcr
in enumerate(dcrShift):
351 model = (self[subfilter][bbox].array - refModel)*amplifyModel + refModel
353 model = self[subfilter][bbox].array
354 templateImage.array +=
applyDcr(model, dcr, splitSubfilters=splitSubfilters, order=order)
358 visitInfo=None, bbox=None, wcs=None, mask=None):
359 """Wrapper to create an exposure from a template image. 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 visitInfo : `lsst.afw.image.VisitInfo`, optional 367 Metadata for the exposure. Ignored if ``exposure`` is set. 368 bbox : `lsst.afw.geom.Box2I`, optional 369 Sub-region of the coadd. Ignored if ``exposure`` is set. 370 wcs : `lsst.afw.geom.SkyWcs`, optional 371 Coordinate system definition (wcs) for the exposure. 372 Ignored if ``exposure`` is set. 373 mask : `lsst.afw.image.Mask`, optional 374 reference mask to use for the template image. 378 templateExposure : `lsst.afw.image.exposureF` 379 The DCR-matched template 382 bbox = exposure.getBBox()
384 bbox=bbox, wcs=wcs, mask=mask)
385 maskedImage = afwImage.MaskedImageF(bbox)
386 maskedImage.image = templateImage[bbox]
387 maskedImage.mask = self.
mask[bbox]
388 maskedImage.variance = self.
variance[bbox]
389 templateExposure = afwImage.ExposureF(bbox, wcs)
390 templateExposure.setMaskedImage(maskedImage[bbox])
391 templateExposure.setPsf(self.
psf)
392 templateExposure.setFilter(self.
filter)
393 return templateExposure
396 """Average two iterations' solutions to reduce oscillations. 400 modelImages : `list` of `lsst.afw.image.Image` 401 The new DCR model images from the current iteration. 402 The values will be modified in place. 403 bbox : `lsst.afw.geom.Box2I` 404 Sub-region of the coadd 405 gain : `float`, optional 406 Relative weight to give the new solution when updating the model. 407 Defaults to 1.0, which gives equal weight to both solutions. 410 for model, newModel
in zip(self, modelImages):
412 newModel += model[bbox]
413 newModel /= 1. + gain
416 regularizationWidth=2):
417 """Restrict large variations in the model between iterations. 422 Index of the current subfilter within the full band. 423 newModel : `lsst.afw.image.Image` 424 The new DCR model for one subfilter from the current iteration. 425 Values in ``newModel`` that are extreme compared with the last 426 iteration are modified in place. 427 bbox : `lsst.afw.geom.Box2I` 429 regularizationFactor : `float` 430 Maximum relative change of the model allowed between iterations. 431 regularizationWidth : int, optional 432 Minimum radius of a region to include in regularization, in pixels. 434 refImage = self[subfilter][bbox].array
435 highThreshold = np.abs(refImage)*regularizationFactor
436 lowThreshold = refImage/regularizationFactor
437 newImage = newModel.array
439 regularizationWidth=regularizationWidth)
442 regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
443 """Restrict large variations in the model between subfilters. 447 modelImages : `list` of `lsst.afw.image.Image` 448 The new DCR model images from the current iteration. 449 The values will be modified in place. 450 bbox : `lsst.afw.geom.Box2I` 452 statsCtrl : `lsst.afw.math.StatisticsControl` 453 Statistics control object for coaddition. 454 regularizationFactor : `float` 455 Maximum relative change of the model allowed between subfilters. 456 regularizationWidth : `int`, optional 457 Minimum radius of a region to include in regularization, in pixels. 458 mask : `lsst.afw.image.Mask`, optional 459 Optional alternate mask 460 convergenceMaskPlanes : `list` of `str`, or `str`, optional 461 Mask planes to use to calculate convergence. 465 This implementation of frequency regularization restricts each subfilter 466 image to be a smoothly-varying function times a reference image. 470 maxDiff = np.sqrt(regularizationFactor)
471 noiseLevel = self.
calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
473 badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
474 if np.sum(~badPixels) == 0:
477 referenceImage[badPixels] = 0.
478 filterWidth = regularizationWidth
479 fwhm = 2.*filterWidth
482 smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth, mode=
'constant')
485 smoothRef += 3.*noiseLevel
487 lowThreshold = smoothRef/maxDiff
488 highThreshold = smoothRef*maxDiff
489 for model
in modelImages:
491 highThreshold=highThreshold,
492 lowThreshold=lowThreshold,
493 regularizationWidth=regularizationWidth)
494 smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth, mode=
'constant')
495 smoothModel += 3.*noiseLevel
496 relativeModel = smoothModel/smoothRef
499 relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/alpha)
500 relativeModel += alpha*(relativeModel - relativeModel2)
501 model.array = relativeModel*referenceImage
504 convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
505 """Helper function to calculate the background noise level of an image. 509 image : `lsst.afw.image.Image` 510 The input image to evaluate the background noise properties. 511 statsCtrl : `lsst.afw.math.StatisticsControl` 512 Statistics control object for coaddition. 514 Number of additional pixels to exclude 515 from the edges of the bounding box. 516 convergenceMaskPlanes : `list` of `str`, or `str` 517 Mask planes to use to calculate convergence. 518 mask : `lsst.afw.image.Mask`, Optional 519 Optional alternate mask 520 bbox : `lsst.afw.geom.Box2I`, optional 521 Sub-region of the masked image to calculate the noise level over. 525 noiseCutoff : `float` 526 The threshold value to treat pixels as noise in an image.. 531 mask = self.
mask[bbox]
533 bboxShrink.grow(-bufferSize)
534 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
536 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
537 noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
541 """Restrict image values to be between upper and lower limits. 543 This method flags all pixels in an image that are outside of the given 544 threshold values. The threshold values are taken from a reference image, 545 so noisy pixels are likely to get flagged. In order to exclude those 546 noisy pixels, the array of flags is eroded and dilated, which removes 547 isolated pixels outside of the thresholds from the list of pixels to be 548 modified. Pixels that remain flagged after this operation have their 549 values set to the appropriate upper or lower threshold value. 553 image : `numpy.ndarray` 554 The image to apply the thresholds to. 555 The values will be modified in place. 556 highThreshold : `numpy.ndarray`, optional 557 Array of upper limit values for each pixel of ``image``. 558 lowThreshold : `numpy.ndarray`, optional 559 Array of lower limit values for each pixel of ``image``. 560 regularizationWidth : `int`, optional 561 Minimum radius of a region to include in regularization, in pixels. 566 filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
568 if highThreshold
is not None:
569 highPixels = image > highThreshold
570 if regularizationWidth > 0:
572 highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
573 image[highPixels] = highThreshold[highPixels]
574 if lowThreshold
is not None:
575 lowPixels = image < lowThreshold
576 if regularizationWidth > 0:
578 lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
579 image[lowPixels] = lowThreshold[lowPixels]
582 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, **kwargs):
583 """Shift an image along the X and Y directions. 587 image : `numpy.ndarray` 588 The input image to shift. 590 Shift calculated with ``calculateDcr``. 591 Uses numpy axes ordering (Y, X). 592 If ``splitSubfilters`` is set, each element is itself a `tuple` 593 of two `float`, corresponding to the DCR shift at the two wavelengths. 594 Otherwise, each element is a `float` corresponding to the DCR shift at 595 the effective wavelength of the subfilter. 596 useInverse : `bool`, optional 597 Apply the shift in the opposite direction. Default: False 598 splitSubfilters : `bool`, optional 599 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 600 instead of at the midpoint. Default: False 602 Additional keyword parameters to pass in to 603 `scipy.ndimage.interpolation.shift` 607 shiftedImage : `numpy.ndarray` 608 A copy of the input image with the specified shift applied. 612 shift = [-1.*s
for s
in dcr[0]]
613 shift1 = [-1.*s
for s
in dcr[1]]
617 shiftedImage = ndimage.interpolation.shift(image, shift, **kwargs)
618 shiftedImage += ndimage.interpolation.shift(image, shift1, **kwargs)
622 shift = [-1.*s
for s
in dcr]
625 shiftedImage = ndimage.interpolation.shift(image, shift, **kwargs)
629 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
630 """Calculate the shift in pixels of an exposure due to DCR. 634 visitInfo : `lsst.afw.image.VisitInfo` 635 Metadata for the exposure. 636 wcs : `lsst.afw.geom.SkyWcs` 637 Coordinate system definition (wcs) for the exposure. 638 filterInfo : `lsst.afw.image.Filter` 639 The filter definition, set in the current instruments' obs package. 640 dcrNumSubfilters : `int` 641 Number of sub-filters used to model chromatic effects within a band. 642 splitSubfilters : `bool`, optional 643 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 644 instead of at the midpoint. Default: False 648 dcrShift : `tuple` of two `float` 649 The 2D shift due to DCR, in pixels. 650 Uses numpy axes ordering (Y, X). 654 weight = [0.75, 0.25]
655 lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
659 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
660 observatory=visitInfo.getObservatory(),
661 weather=visitInfo.getWeather())
663 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
664 observatory=visitInfo.getObservatory(),
665 weather=visitInfo.getWeather())
667 diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
668 diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
669 diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
670 diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
671 shiftX = [diffRefractPix*np.sin(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
672 shiftY = [diffRefractPix*np.cos(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
673 dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
675 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
676 diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
677 shiftX = diffRefractPix*np.sin(rotation.asRadians())
678 shiftY = diffRefractPix*np.cos(rotation.asRadians())
679 dcrShift.append((shiftY, shiftX))
684 """Calculate the total sky rotation angle of an exposure. 688 visitInfo : `lsst.afw.image.VisitInfo` 689 Metadata for the exposure. 690 wcs : `lsst.afw.geom.SkyWcs` 691 Coordinate system definition (wcs) for the exposure. 696 The rotation of the image axis, East from North. 697 Equal to the parallactic angle plus any additional rotation of the 699 A rotation angle of 0 degrees is defined with 700 North along the +y axis and East along the +x axis. 701 A rotation angle of 90 degrees is defined with 702 North along the +x axis and East along the -y axis. 704 parAngle = visitInfo.getBoresightParAngle().asRadians()
705 cd = wcs.getCdMatrix()
707 cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
709 cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
710 rotAngle = (cdAngle + parAngle)*radians
715 """Iterate over the wavelength endpoints of subfilters. 719 filterInfo : `lsst.afw.image.Filter` 720 The filter definition, set in the current instruments' obs package. 721 dcrNumSubfilters : `int` 722 Number of sub-filters used to model chromatic effects within a band. 726 `tuple` of two `float` 727 The next set of wavelength endpoints for a subfilter, in nm. 729 lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
730 lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
731 wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
732 for wl
in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=
False):
733 yield (wl, wl + wlStep)
def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None)
def __setitem__(self, subfilter, maskedImage)
def calculateImageParallacticAngle(visitInfo, wcs)
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
def buildMatchedTemplate(self, exposure=None, order=3, visitInfo=None, bbox=None, wcs=None, mask=None, splitSubfilters=True, amplifyModel=1.)
def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, kwargs)
def __getitem__(self, subfilter)
def calculateNoiseCutoff(self, image, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
def __init__(self, modelImages, filterInfo=None, psf=None, mask=None, variance=None)
def wavelengthGenerator(filterInfo, dcrNumSubfilters)
def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2)
def buildMatchedExposure(self, exposure=None, visitInfo=None, bbox=None, wcs=None, mask=None)
def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, kwargs)
def getReferenceImage(self, bbox=None)
def differentialRefraction(wavelength, wavelengthRef, elevation, observatory, weather=None)
def assign(self, dcrSubModel, bbox=None)
def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False)
def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED")
def conditionDcrModel(self, modelImages, bbox, gain=1.)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
An integer coordinate rectangle.