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=False):
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 visitInfo : `lsst.afw.image.VisitInfo`, optional 311 Metadata for the exposure. Ignored if ``exposure`` is set. 312 bbox : `lsst.afw.geom.Box2I`, optional 313 Sub-region of the coadd. Ignored if ``exposure`` is set. 314 wcs : `lsst.afw.geom.SkyWcs`, optional 315 Coordinate system definition (wcs) for the exposure. 316 Ignored if ``exposure`` is set. 317 mask : `lsst.afw.image.Mask`, optional 318 reference mask to use for the template image. 319 splitSubfilters : `bool`, optional 320 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 321 instead of at the midpoint. Default: False 325 templateImage : `lsst.afw.image.ImageF` 326 The DCR-matched template 331 If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set. 334 raise ValueError(
"'filterInfo' must be set for the DcrModel in order to calculate DCR.")
335 if exposure
is not None:
336 visitInfo = exposure.getInfo().getVisitInfo()
337 bbox = exposure.getBBox()
338 wcs = exposure.getInfo().getWcs()
339 elif visitInfo
is None or bbox
is None or wcs
is None:
340 raise ValueError(
"Either exposure or visitInfo, bbox, and wcs must be set.")
341 dcrShift =
calculateDcr(visitInfo, wcs, self.
filter, len(self), splitSubfilters=splitSubfilters)
342 templateImage = afwImage.ImageF(bbox)
343 for subfilter, dcr
in enumerate(dcrShift):
344 templateImage.array +=
applyDcr(self[subfilter][bbox].array, dcr,
345 splitSubfilters=splitSubfilters, order=order)
349 visitInfo=None, bbox=None, wcs=None, mask=None):
350 """Wrapper to create an exposure from a template image. 354 exposure : `lsst.afw.image.Exposure`, optional 355 The input exposure to build a matched template for. 356 May be omitted if all of the metadata is supplied separately 357 visitInfo : `lsst.afw.image.VisitInfo`, optional 358 Metadata for the exposure. Ignored if ``exposure`` is set. 359 bbox : `lsst.afw.geom.Box2I`, optional 360 Sub-region of the coadd. Ignored if ``exposure`` is set. 361 wcs : `lsst.afw.geom.SkyWcs`, optional 362 Coordinate system definition (wcs) for the exposure. 363 Ignored if ``exposure`` is set. 364 mask : `lsst.afw.image.Mask`, optional 365 reference mask to use for the template image. 369 templateExposure : `lsst.afw.image.exposureF` 370 The DCR-matched template 373 maskedImage = afwImage.MaskedImageF(bbox)
374 maskedImage.image = templateImage
375 maskedImage.mask = self.
mask 376 maskedImage.variance = self.
variance 377 templateExposure = afwImage.ExposureF(bbox, wcs)
378 templateExposure.setMaskedImage(templateImage)
379 templateExposure.setPsf(self.
psf)
380 templateExposure.setFilter(self.
filter)
381 return templateExposure
384 """Average two iterations' solutions to reduce oscillations. 388 modelImages : `list` of `lsst.afw.image.Image` 389 The new DCR model images from the current iteration. 390 The values will be modified in place. 391 bbox : `lsst.afw.geom.Box2I` 392 Sub-region of the coadd 393 gain : `float`, optional 394 Relative weight to give the new solution when updating the model. 395 Defaults to 1.0, which gives equal weight to both solutions. 398 for model, newModel
in zip(self, modelImages):
400 newModel += model[bbox]
401 newModel /= 1. + gain
404 regularizationWidth=2):
405 """Restrict large variations in the model between iterations. 410 Index of the current subfilter within the full band. 411 newModel : `lsst.afw.image.Image` 412 The new DCR model for one subfilter from the current iteration. 413 Values in ``newModel`` that are extreme compared with the last 414 iteration are modified in place. 415 bbox : `lsst.afw.geom.Box2I` 417 regularizationFactor : `float` 418 Maximum relative change of the model allowed between iterations. 419 regularizationWidth : int, optional 420 Minimum radius of a region to include in regularization, in pixels. 422 refImage = self[subfilter][bbox].array
423 highThreshold = np.abs(refImage)*regularizationFactor
424 lowThreshold = refImage/regularizationFactor
425 newImage = newModel.array
427 regularizationWidth=regularizationWidth)
430 regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
431 """Restrict large variations in the model between subfilters. 435 modelImages : `list` of `lsst.afw.image.Image` 436 The new DCR model images from the current iteration. 437 The values will be modified in place. 438 bbox : `lsst.afw.geom.Box2I` 440 statsCtrl : `lsst.afw.math.StatisticsControl` 441 Statistics control object for coaddition. 442 regularizationFactor : `float` 443 Maximum relative change of the model allowed between subfilters. 444 regularizationWidth : `int`, optional 445 Minimum radius of a region to include in regularization, in pixels. 446 mask : `lsst.afw.image.Mask`, optional 447 Optional alternate mask 448 convergenceMaskPlanes : `list` of `str`, or `str`, optional 449 Mask planes to use to calculate convergence. 453 This implementation of frequency regularization restricts each subfilter 454 image to be a smoothly-varying function times a reference image. 458 maxDiff = np.sqrt(regularizationFactor)
459 noiseLevel = self.
calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
461 badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
462 if np.sum(~badPixels) == 0:
465 referenceImage[badPixels] = 0.
466 filterWidth = regularizationWidth
467 fwhm = 2.*filterWidth
470 smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth) + noiseLevel
472 baseThresh = np.ones_like(referenceImage)
473 highThreshold = baseThresh*maxDiff
474 lowThreshold = baseThresh/maxDiff
475 for subfilter, model
in enumerate(modelImages):
476 smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth) + noiseLevel
477 relativeModel = smoothModel/smoothRef
479 relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/3.)
480 relativeModel = relativeModel + 3.*(relativeModel - relativeModel2)
482 highThreshold=highThreshold,
483 lowThreshold=lowThreshold,
484 regularizationWidth=regularizationWidth)
485 relativeModel *= referenceImage
486 modelImages[subfilter].array = relativeModel
489 convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
490 """Helper function to calculate the background noise level of an image. 494 image : `lsst.afw.image.Image` 495 The input image to evaluate the background noise properties. 496 statsCtrl : `lsst.afw.math.StatisticsControl` 497 Statistics control object for coaddition. 499 Number of additional pixels to exclude 500 from the edges of the bounding box. 501 convergenceMaskPlanes : `list` of `str`, or `str` 502 Mask planes to use to calculate convergence. 503 mask : `lsst.afw.image.Mask`, Optional 504 Optional alternate mask 505 bbox : `lsst.afw.geom.Box2I`, optional 506 Sub-region of the masked image to calculate the noise level over. 510 noiseCutoff : `float` 511 The threshold value to treat pixels as noise in an image.. 516 mask = self.
mask[bbox]
517 bboxShrink = afwGeom.Box2I(bbox)
518 bboxShrink.grow(-bufferSize)
519 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
521 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
522 noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
526 """Restrict image values to be between upper and lower limits. 528 This method flags all pixels in an image that are outside of the given 529 threshold values. The threshold values are taken from a reference image, 530 so noisy pixels are likely to get flagged. In order to exclude those 531 noisy pixels, the array of flags is eroded and dilated, which removes 532 isolated pixels outside of the thresholds from the list of pixels to be 533 modified. Pixels that remain flagged after this operation have their 534 values set to the appropriate upper or lower threshold value. 538 image : `numpy.ndarray` 539 The image to apply the thresholds to. 540 The values will be modified in place. 541 highThreshold : `numpy.ndarray`, optional 542 Array of upper limit values for each pixel of ``image``. 543 lowThreshold : `numpy.ndarray`, optional 544 Array of lower limit values for each pixel of ``image``. 545 regularizationWidth : `int`, optional 546 Minimum radius of a region to include in regularization, in pixels. 551 filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
553 if highThreshold
is not None:
554 highPixels = image > highThreshold
555 if regularizationWidth > 0:
557 highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
558 image[highPixels] = highThreshold[highPixels]
559 if lowThreshold
is not None:
560 lowPixels = image < lowThreshold
561 if regularizationWidth > 0:
563 lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
564 image[lowPixels] = lowThreshold[lowPixels]
567 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, **kwargs):
568 """Shift an image along the X and Y directions. 572 image : `numpy.ndarray` 573 The input image to shift. 575 Shift calculated with ``calculateDcr``. 576 Uses numpy axes ordering (Y, X). 577 If ``splitSubfilters`` is set, each element is itself a `tuple` 578 of two `float`, corresponding to the DCR shift at the two wavelengths. 579 Otherwise, each element is a `float` corresponding to the DCR shift at 580 the effective wavelength of the subfilter. 581 useInverse : `bool`, optional 582 Apply the shift in the opposite direction. Default: False 583 splitSubfilters : `bool`, optional 584 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 585 instead of at the midpoint. Default: False 587 Additional keyword parameters to pass in to 588 `scipy.ndimage.interpolation.shift` 592 shiftedImage : `numpy.ndarray` 593 A copy of the input image with the specified shift applied. 597 shift = [-1.*s
for s
in dcr[0]]
598 shift1 = [-1.*s
for s
in dcr[1]]
602 shiftedImage = ndimage.interpolation.shift(image, shift, **kwargs)
603 shiftedImage += ndimage.interpolation.shift(image, shift1, **kwargs)
607 shift = [-1.*s
for s
in dcr]
610 shiftedImage = ndimage.interpolation.shift(image, shift, **kwargs)
614 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
615 """Calculate the shift in pixels of an exposure due to DCR. 619 visitInfo : `lsst.afw.image.VisitInfo` 620 Metadata for the exposure. 621 wcs : `lsst.afw.geom.SkyWcs` 622 Coordinate system definition (wcs) for the exposure. 623 filterInfo : `lsst.afw.image.Filter` 624 The filter definition, set in the current instruments' obs package. 625 dcrNumSubfilters : `int` 626 Number of sub-filters used to model chromatic effects within a band. 627 splitSubfilters : `bool`, optional 628 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 629 instead of at the midpoint. Default: False 633 dcrShift : `tuple` of two `float` 634 The 2D shift due to DCR, in pixels. 635 Uses numpy axes ordering (Y, X). 639 weight = [0.75, 0.25]
640 lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
643 diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
644 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
645 observatory=visitInfo.getObservatory(),
646 weather=visitInfo.getWeather())
647 diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
648 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
649 observatory=visitInfo.getObservatory(),
650 weather=visitInfo.getWeather())
652 diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
653 diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
654 diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
655 diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
656 shiftX = [diffRefractPix*np.sin(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
657 shiftY = [diffRefractPix*np.cos(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
658 dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
660 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
661 diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
662 shiftX = diffRefractPix*np.sin(rotation.asRadians())
663 shiftY = diffRefractPix*np.cos(rotation.asRadians())
664 dcrShift.append((shiftY, shiftX))
669 """Calculate the total sky rotation angle of an exposure. 673 visitInfo : `lsst.afw.image.VisitInfo` 674 Metadata for the exposure. 675 wcs : `lsst.afw.geom.SkyWcs` 676 Coordinate system definition (wcs) for the exposure. 681 The rotation of the image axis, East from North. 682 Equal to the parallactic angle plus any additional rotation of the 684 A rotation angle of 0 degrees is defined with 685 North along the +y axis and East along the +x axis. 686 A rotation angle of 90 degrees is defined with 687 North along the +x axis and East along the -y axis. 689 parAngle = visitInfo.getBoresightParAngle().asRadians()
690 cd = wcs.getCdMatrix()
692 cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
694 cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
695 rotAngle = (cdAngle + parAngle)*radians
700 """Iterate over the wavelength endpoints of subfilters. 704 filterInfo : `lsst.afw.image.Filter` 705 The filter definition, set in the current instruments' obs package. 706 dcrNumSubfilters : `int` 707 Number of sub-filters used to model chromatic effects within a band. 711 `tuple` of two `float` 712 The next set of wavelength endpoints for a subfilter, in nm. 714 lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
715 lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
716 wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
717 for wl
in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=
False):
718 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 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 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.)
def buildMatchedTemplate(self, exposure=None, order=3, visitInfo=None, bbox=None, wcs=None, mask=None, splitSubfilters=False)