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 bbox=bbox, wcs=wcs, mask=mask)
374 maskedImage = afwImage.MaskedImageF(bbox)
375 maskedImage.image = templateImage
376 maskedImage.mask = self.
mask 377 maskedImage.variance = self.
variance 378 templateExposure = afwImage.ExposureF(bbox, wcs)
379 templateExposure.setMaskedImage(maskedImage)
380 templateExposure.setPsf(self.
psf)
381 templateExposure.setFilter(self.
filter)
382 return templateExposure
385 """Average two iterations' solutions to reduce oscillations. 389 modelImages : `list` of `lsst.afw.image.Image` 390 The new DCR model images from the current iteration. 391 The values will be modified in place. 392 bbox : `lsst.afw.geom.Box2I` 393 Sub-region of the coadd 394 gain : `float`, optional 395 Relative weight to give the new solution when updating the model. 396 Defaults to 1.0, which gives equal weight to both solutions. 399 for model, newModel
in zip(self, modelImages):
401 newModel += model[bbox]
402 newModel /= 1. + gain
405 regularizationWidth=2):
406 """Restrict large variations in the model between iterations. 411 Index of the current subfilter within the full band. 412 newModel : `lsst.afw.image.Image` 413 The new DCR model for one subfilter from the current iteration. 414 Values in ``newModel`` that are extreme compared with the last 415 iteration are modified in place. 416 bbox : `lsst.afw.geom.Box2I` 418 regularizationFactor : `float` 419 Maximum relative change of the model allowed between iterations. 420 regularizationWidth : int, optional 421 Minimum radius of a region to include in regularization, in pixels. 423 refImage = self[subfilter][bbox].array
424 highThreshold = np.abs(refImage)*regularizationFactor
425 lowThreshold = refImage/regularizationFactor
426 newImage = newModel.array
428 regularizationWidth=regularizationWidth)
431 regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
432 """Restrict large variations in the model between subfilters. 436 modelImages : `list` of `lsst.afw.image.Image` 437 The new DCR model images from the current iteration. 438 The values will be modified in place. 439 bbox : `lsst.afw.geom.Box2I` 441 statsCtrl : `lsst.afw.math.StatisticsControl` 442 Statistics control object for coaddition. 443 regularizationFactor : `float` 444 Maximum relative change of the model allowed between subfilters. 445 regularizationWidth : `int`, optional 446 Minimum radius of a region to include in regularization, in pixels. 447 mask : `lsst.afw.image.Mask`, optional 448 Optional alternate mask 449 convergenceMaskPlanes : `list` of `str`, or `str`, optional 450 Mask planes to use to calculate convergence. 454 This implementation of frequency regularization restricts each subfilter 455 image to be a smoothly-varying function times a reference image. 459 maxDiff = np.sqrt(regularizationFactor)
460 noiseLevel = self.
calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
462 badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
463 if np.sum(~badPixels) == 0:
466 referenceImage[badPixels] = 0.
467 filterWidth = regularizationWidth
468 fwhm = 2.*filterWidth
471 smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth) + noiseLevel
473 baseThresh = np.ones_like(referenceImage)
474 highThreshold = baseThresh*maxDiff
475 lowThreshold = baseThresh/maxDiff
476 for subfilter, model
in enumerate(modelImages):
477 smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth) + noiseLevel
478 relativeModel = smoothModel/smoothRef
480 relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/3.)
481 relativeModel = relativeModel + 3.*(relativeModel - relativeModel2)
483 highThreshold=highThreshold,
484 lowThreshold=lowThreshold,
485 regularizationWidth=regularizationWidth)
486 relativeModel *= referenceImage
487 modelImages[subfilter].array = relativeModel
490 convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
491 """Helper function to calculate the background noise level of an image. 495 image : `lsst.afw.image.Image` 496 The input image to evaluate the background noise properties. 497 statsCtrl : `lsst.afw.math.StatisticsControl` 498 Statistics control object for coaddition. 500 Number of additional pixels to exclude 501 from the edges of the bounding box. 502 convergenceMaskPlanes : `list` of `str`, or `str` 503 Mask planes to use to calculate convergence. 504 mask : `lsst.afw.image.Mask`, Optional 505 Optional alternate mask 506 bbox : `lsst.afw.geom.Box2I`, optional 507 Sub-region of the masked image to calculate the noise level over. 511 noiseCutoff : `float` 512 The threshold value to treat pixels as noise in an image.. 517 mask = self.
mask[bbox]
518 bboxShrink = afwGeom.Box2I(bbox)
519 bboxShrink.grow(-bufferSize)
520 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
522 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
523 noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
527 """Restrict image values to be between upper and lower limits. 529 This method flags all pixels in an image that are outside of the given 530 threshold values. The threshold values are taken from a reference image, 531 so noisy pixels are likely to get flagged. In order to exclude those 532 noisy pixels, the array of flags is eroded and dilated, which removes 533 isolated pixels outside of the thresholds from the list of pixels to be 534 modified. Pixels that remain flagged after this operation have their 535 values set to the appropriate upper or lower threshold value. 539 image : `numpy.ndarray` 540 The image to apply the thresholds to. 541 The values will be modified in place. 542 highThreshold : `numpy.ndarray`, optional 543 Array of upper limit values for each pixel of ``image``. 544 lowThreshold : `numpy.ndarray`, optional 545 Array of lower limit values for each pixel of ``image``. 546 regularizationWidth : `int`, optional 547 Minimum radius of a region to include in regularization, in pixels. 552 filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
554 if highThreshold
is not None:
555 highPixels = image > highThreshold
556 if regularizationWidth > 0:
558 highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
559 image[highPixels] = highThreshold[highPixels]
560 if lowThreshold
is not None:
561 lowPixels = image < lowThreshold
562 if regularizationWidth > 0:
564 lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
565 image[lowPixels] = lowThreshold[lowPixels]
568 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, **kwargs):
569 """Shift an image along the X and Y directions. 573 image : `numpy.ndarray` 574 The input image to shift. 576 Shift calculated with ``calculateDcr``. 577 Uses numpy axes ordering (Y, X). 578 If ``splitSubfilters`` is set, each element is itself a `tuple` 579 of two `float`, corresponding to the DCR shift at the two wavelengths. 580 Otherwise, each element is a `float` corresponding to the DCR shift at 581 the effective wavelength of the subfilter. 582 useInverse : `bool`, optional 583 Apply the shift in the opposite direction. Default: False 584 splitSubfilters : `bool`, optional 585 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 586 instead of at the midpoint. Default: False 588 Additional keyword parameters to pass in to 589 `scipy.ndimage.interpolation.shift` 593 shiftedImage : `numpy.ndarray` 594 A copy of the input image with the specified shift applied. 598 shift = [-1.*s
for s
in dcr[0]]
599 shift1 = [-1.*s
for s
in dcr[1]]
603 shiftedImage = ndimage.interpolation.shift(image, shift, **kwargs)
604 shiftedImage += ndimage.interpolation.shift(image, shift1, **kwargs)
608 shift = [-1.*s
for s
in dcr]
611 shiftedImage = ndimage.interpolation.shift(image, shift, **kwargs)
615 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
616 """Calculate the shift in pixels of an exposure due to DCR. 620 visitInfo : `lsst.afw.image.VisitInfo` 621 Metadata for the exposure. 622 wcs : `lsst.afw.geom.SkyWcs` 623 Coordinate system definition (wcs) for the exposure. 624 filterInfo : `lsst.afw.image.Filter` 625 The filter definition, set in the current instruments' obs package. 626 dcrNumSubfilters : `int` 627 Number of sub-filters used to model chromatic effects within a band. 628 splitSubfilters : `bool`, optional 629 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 630 instead of at the midpoint. Default: False 634 dcrShift : `tuple` of two `float` 635 The 2D shift due to DCR, in pixels. 636 Uses numpy axes ordering (Y, X). 640 weight = [0.75, 0.25]
641 lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
644 diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
645 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
646 observatory=visitInfo.getObservatory(),
647 weather=visitInfo.getWeather())
648 diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
649 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
650 observatory=visitInfo.getObservatory(),
651 weather=visitInfo.getWeather())
653 diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
654 diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
655 diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
656 diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
657 shiftX = [diffRefractPix*np.sin(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
658 shiftY = [diffRefractPix*np.cos(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
659 dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
661 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
662 diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
663 shiftX = diffRefractPix*np.sin(rotation.asRadians())
664 shiftY = diffRefractPix*np.cos(rotation.asRadians())
665 dcrShift.append((shiftY, shiftX))
670 """Calculate the total sky rotation angle of an exposure. 674 visitInfo : `lsst.afw.image.VisitInfo` 675 Metadata for the exposure. 676 wcs : `lsst.afw.geom.SkyWcs` 677 Coordinate system definition (wcs) for the exposure. 682 The rotation of the image axis, East from North. 683 Equal to the parallactic angle plus any additional rotation of the 685 A rotation angle of 0 degrees is defined with 686 North along the +y axis and East along the +x axis. 687 A rotation angle of 90 degrees is defined with 688 North along the +x axis and East along the -y axis. 690 parAngle = visitInfo.getBoresightParAngle().asRadians()
691 cd = wcs.getCdMatrix()
693 cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
695 cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
696 rotAngle = (cdAngle + parAngle)*radians
701 """Iterate over the wavelength endpoints of subfilters. 705 filterInfo : `lsst.afw.image.Filter` 706 The filter definition, set in the current instruments' obs package. 707 dcrNumSubfilters : `int` 708 Number of sub-filters used to model chromatic effects within a band. 712 `tuple` of two `float` 713 The next set of wavelength endpoints for a subfilter, in nm. 715 lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
716 lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
717 wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
718 for wl
in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=
False):
719 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)