24 from scipy
import ndimage
33 __all__ = [
"DcrModel",
"applyDcr",
"calculateDcr",
"calculateImageParallacticAngle"]
37 """A model of the true sky after correcting chromatic effects. 41 dcrNumSubfilters : `int` 42 Number of sub-filters used to model chromatic effects within a band. 43 filterInfo : `lsst.afw.image.Filter` 44 The filter definition, set in the current instruments' obs package. 45 modelImages : `list` of `lsst.afw.image.MaskedImage` 46 A list of masked images, each containing the model for one subfilter 50 modelImages : `list` of `lsst.afw.image.MaskedImage` 51 A list of masked images, each containing the model for one subfilter. 52 filterInfo : `lsst.afw.image.Filter`, optional 53 The filter definition, set in the current instruments' obs package. 54 Required for any calculation of DCR, including making matched templates. 58 The ``DcrModel`` contains an estimate of the true sky, at a higher 59 wavelength resolution than the input observations. It can be forward- 60 modeled to produce Differential Chromatic Refraction (DCR) matched 61 templates for a given ``Exposure``, and provides utilities for conditioning 62 the model in ``dcrAssembleCoadd`` to avoid oscillating solutions between 63 iterations of forward modeling or between the subfilters of the model. 66 def __init__(self, modelImages, filterInfo=None, psf=None):
73 def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None):
74 """Initialize a DcrModel by dividing a coadd between the subfilters. 78 maskedImage : `lsst.afw.image.MaskedImage` 79 Input coadded image to divide equally between the subfilters. 80 dcrNumSubfilters : `int` 81 Number of sub-filters used to model chromatic effects within a band. 82 filterInfo : `lsst.afw.image.Filter`, optional 83 The filter definition, set in the current instruments' obs package. 84 Required for any calculation of DCR, including making matched templates. 85 psf : `lsst.afw.detection.Psf`, optional 86 Point spread function (PSF) of the model. 87 Required if the ``DcrModel`` will be persisted. 91 dcrModel : `lsst.pipe.tasks.DcrModel` 92 Best fit model of the true sky after correcting chromatic effects. 96 model = maskedImage.clone()
97 badPixels = np.isnan(model.image.array) | np.isnan(model.variance.array)
98 model.image.array[badPixels] = 0.
99 model.variance.array[badPixels] = 0.
100 model.image.array /= dcrNumSubfilters
106 model.variance.array /= dcrNumSubfilters
107 model.mask.array[badPixels] = model.mask.getPlaneBitMask(
"NO_DATA")
108 modelImages = [model, ]
109 for subfilter
in range(1, dcrNumSubfilters):
110 modelImages.append(model.clone())
111 return cls(modelImages, filterInfo, psf)
114 def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, **kwargs):
115 """Load an existing DcrModel from a repository. 119 dataRef : `lsst.daf.persistence.ButlerDataRef` 120 Data reference defining the patch for coaddition and the 122 datasetType : `str`, optional 123 Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"} 124 numSubfilters : `int` 125 Number of sub-filters used to model chromatic effects within a band. 127 Additional keyword arguments to pass to look up the model in the data registry. 128 Common keywords and their types include: ``tract``:`str`, ``patch``:`str`, 129 ``bbox``:`lsst.afw.geom.Box2I` 133 dcrModel : `lsst.pipe.tasks.DcrModel` 134 Best fit model of the true sky after correcting chromatic effects. 139 for subfilter
in range(numSubfilters):
140 dcrCoadd = dataRef.get(datasetType, subfilter=subfilter,
141 numSubfilters=numSubfilters, **kwargs)
142 if filterInfo
is None:
143 filterInfo = dcrCoadd.getFilter()
145 psf = dcrCoadd.getPsf()
146 modelImages.append(dcrCoadd.maskedImage)
147 return cls(modelImages, filterInfo, psf)
150 """Return the number of subfilters. 154 dcrNumSubfilters : `int` 155 The number of DCR subfilters in the model. 160 """Iterate over the subfilters of the DCR model. 165 Index of the current ``subfilter`` within the full band. 166 Negative indices are allowed, and count in reverse order 167 from the highest ``subfilter``. 171 modelImage : `lsst.afw.image.MaskedImage` 172 The DCR model for the given ``subfilter``. 177 If the requested ``subfilter`` is greater or equal to the number 178 of subfilters in the model. 180 if np.abs(subfilter) >= len(self):
181 raise IndexError(
"subfilter out of bounds.")
185 """Update the model image for one subfilter. 190 Index of the current subfilter within the full band. 191 maskedImage : `lsst.afw.image.MaskedImage` 192 The DCR model to set for the given ``subfilter``. 197 If the requested ``subfilter`` is greater or equal to the number 198 of subfilters in the model. 200 If the bounding box of the new image does not match. 202 if np.abs(subfilter) >= len(self):
203 raise IndexError(
"subfilter out of bounds.")
204 if maskedImage.getBBox() != self.
bbox:
205 raise ValueError(
"The bounding box of a subfilter must not change.")
210 """Return the filter of the model. 214 filter : `lsst.afw.image.Filter` 215 The filter definition, set in the current instruments' obs package. 221 """Return the psf of the model. 225 psf : `lsst.afw.detection.Psf` 226 Point spread function (PSF) of the model. 232 """Return the common bounding box of each subfilter image. 236 bbox : `lsst.afw.geom.Box2I` 237 Bounding box of the DCR model. 239 return self[0].getBBox()
243 """Return the common mask of each subfilter image. 247 bbox : `lsst.afw.image.Mask` 248 Mask plane of the DCR model. 253 """Create a simple template from the DCR model. 257 bbox : `lsst.afw.geom.Box2I`, optional 258 Sub-region of the coadd. Returns the entire image if `None`. 262 templateImage : `numpy.ndarray` 263 The template with no chromatic effects applied. 265 bbox = bbox
or self.
bbox 266 return np.mean([model[bbox].image.array
for model
in self], axis=0)
268 def assign(self, dcrSubModel, bbox=None):
269 """Update a sub-region of the ``DcrModel`` with new values. 273 dcrSubModel : `lsst.pipe.tasks.DcrModel` 274 New model of the true scene after correcting chromatic effects. 275 bbox : `lsst.afw.geom.Box2I`, optional 276 Sub-region of the coadd. 277 Defaults to the bounding box of ``dcrSubModel``. 282 If the new model has a different number of subfilters. 284 if len(dcrSubModel) != len(self):
285 raise ValueError(
"The number of DCR subfilters must be the same " 286 "between the old and new models.")
287 bbox = bbox
or self.
bbox 288 for model, subModel
in zip(self, dcrSubModel):
289 model.assign(subModel[bbox], bbox)
292 visitInfo=None, bbox=None, wcs=None, mask=None,
293 splitSubfilters=False):
294 """Create a DCR-matched template image for an exposure. 298 exposure : `lsst.afw.image.Exposure`, optional 299 The input exposure to build a matched template for. 300 May be omitted if all of the metadata is supplied separately 301 warpCtrl : `lsst.afw.Math.WarpingControl`, optional 302 Configuration settings for warping an image. 303 If not set, defaults to a lanczos3 warping kernel for the image, 304 and a bilinear kernel for the mask 305 visitInfo : `lsst.afw.image.VisitInfo`, optional 306 Metadata for the exposure. Ignored if ``exposure`` is set. 307 bbox : `lsst.afw.geom.Box2I`, optional 308 Sub-region of the coadd. Ignored if ``exposure`` is set. 309 wcs : `lsst.afw.geom.SkyWcs`, optional 310 Coordinate system definition (wcs) for the exposure. 311 Ignored if ``exposure`` is set. 312 mask : `lsst.afw.image.Mask`, optional 313 reference mask to use for the template image. 314 splitSubfilters : `bool`, optional 315 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 316 instead of at the midpoint. Default: False 320 templateImage : `lsst.afw.image.maskedImageF` 321 The DCR-matched template 326 If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set. 329 raise ValueError(
"'filterInfo' must be set for the DcrModel in order to calculate DCR.")
330 if exposure
is not None:
331 visitInfo = exposure.getInfo().getVisitInfo()
332 bbox = exposure.getBBox()
333 wcs = exposure.getInfo().getWcs()
334 elif visitInfo
is None or bbox
is None or wcs
is None:
335 raise ValueError(
"Either exposure or visitInfo, bbox, and wcs must be set.")
340 cacheSize=0, interpLength=max(bbox.getDimensions()))
342 dcrShift =
calculateDcr(visitInfo, wcs, self.
filter, len(self), splitSubfilters=splitSubfilters)
343 templateImage = afwImage.MaskedImageF(bbox)
344 for subfilter, dcr
in enumerate(dcrShift):
345 templateImage +=
applyDcr(self[subfilter][bbox], dcr, warpCtrl, splitSubfilters=splitSubfilters)
347 templateImage.setMask(mask[bbox])
351 visitInfo=None, bbox=None, wcs=None, mask=None):
352 """Wrapper to create an exposure from a template image. 356 exposure : `lsst.afw.image.Exposure`, optional 357 The input exposure to build a matched template for. 358 May be omitted if all of the metadata is supplied separately 359 warpCtrl : `lsst.afw.Math.WarpingControl` 360 Configuration settings for warping an image 361 visitInfo : `lsst.afw.image.VisitInfo`, optional 362 Metadata for the exposure. Ignored if ``exposure`` is set. 363 bbox : `lsst.afw.geom.Box2I`, optional 364 Sub-region of the coadd. Ignored if ``exposure`` is set. 365 wcs : `lsst.afw.geom.SkyWcs`, optional 366 Coordinate system definition (wcs) for the exposure. 367 Ignored if ``exposure`` is set. 368 mask : `lsst.afw.image.Mask`, optional 369 reference mask to use for the template image. 373 templateExposure : `lsst.afw.image.exposureF` 374 The DCR-matched template 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.MaskedImage` 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. 399 for model, newModel
in zip(self, modelImages):
400 newModel.image *= gain
401 newModel.image += model[bbox].image
402 newModel.image /= 1. + gain
403 newModel.variance *= gain
404 newModel.variance += model[bbox].variance
405 newModel.variance /= 1. + gain
408 regularizationWidth=2):
409 """Restrict large variations in the model between iterations. 414 Index of the current subfilter within the full band. 415 newModel : `lsst.afw.image.MaskedImage` 416 The new DCR model for one subfilter from the current iteration. 417 Values in ``newModel`` that are extreme compared with the last 418 iteration are modified in place. 419 bbox : `lsst.afw.geom.Box2I` 421 regularizationFactor : `float` 422 Maximum relative change of the model allowed between iterations. 423 regularizationWidth : int, optional 424 Minimum radius of a region to include in regularization, in pixels. 426 refImage = self[subfilter][bbox].image.array
427 highThreshold = np.abs(refImage)*regularizationFactor
428 lowThreshold = refImage/regularizationFactor
430 regularizationWidth=regularizationWidth)
433 regularizationWidth=2):
434 """Restrict large variations in the model between subfilters. 438 modelImages : `list` of `lsst.afw.image.MaskedImage` 439 The new DCR model images from the current iteration. 440 The values will be modified in place. 441 bbox : `lsst.afw.geom.Box2I` 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. 450 maxDiff = np.sqrt(regularizationFactor)
453 for model
in modelImages:
454 highThreshold = np.abs(refImage)*maxDiff
455 lowThreshold = refImage/maxDiff
457 regularizationWidth=regularizationWidth)
460 convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
461 """Helper function to calculate the background noise level of an image. 465 maskedImage : `lsst.afw.image.MaskedImage` 466 The input image to evaluate the background noise properties. 467 statsCtrl : `lsst.afw.math.StatisticsControl` 468 Statistics control object for coaddition. 470 Number of additional pixels to exclude 471 from the edges of the bounding box. 472 convergenceMaskPlanes : `list` of `str`, or `str` 473 Mask planes to use to calculate convergence. 474 mask : `lsst.afw.image.Mask`, Optional 475 Optional alternate mask 476 bbox : `lsst.afw.geom.Box2I`, optional 477 Sub-region of the masked image to calculate the noise level over. 481 noiseCutoff : `float` 482 The threshold value to treat pixels as noise in an image.. 487 mask = maskedImage[bbox].mask
488 bboxShrink = afwGeom.Box2I(bbox)
489 bboxShrink.grow(-bufferSize)
490 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
492 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
493 noiseCutoff = np.std(maskedImage[bboxShrink].image.array[backgroundPixels])
497 regularizationWidth=2):
498 """Restrict image values to be between upper and lower limits. 500 This method flags all pixels in an image that are outside of the given 501 threshold values. The threshold values are taken from a reference image, 502 so noisy pixels are likely to get flagged. In order to exclude those 503 noisy pixels, the array of flags is eroded and dilated, which removes 504 isolated pixels outside of the thresholds from the list of pixels to be 505 modified. Pixels that remain flagged after this operation have their 506 values set to the appropriate upper or lower threshold value. 510 maskedImage : `lsst.afw.image.MaskedImage` 511 The image to apply the thresholds to. 512 The image plane values will be modified in place. 513 highThreshold : `numpy.ndarray`, optional 514 Array of upper limit values for each pixel of ``maskedImage``. 515 lowThreshold : `numpy.ndarray`, optional 516 Array of lower limit values for each pixel of ``maskedImage``. 517 regularizationWidth : `int`, optional 518 Minimum radius of a region to include in regularization, in pixels. 523 image = maskedImage.image.array
524 filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
526 if highThreshold
is not None:
527 highPixels = image > highThreshold
528 if regularizationWidth > 0:
530 highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
531 image[highPixels] = highThreshold[highPixels]
532 if lowThreshold
is not None:
533 lowPixels = image < lowThreshold
534 if regularizationWidth > 0:
536 lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
537 image[lowPixels] = lowThreshold[lowPixels]
540 def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False, splitSubfilters=False):
541 """Shift a masked image. 545 maskedImage : `lsst.afw.image.MaskedImage` 546 The input masked image to shift. 547 dcr : `lsst.afw.geom.Extent2I` 548 Shift calculated with ``calculateDcr``. 549 warpCtrl : `lsst.afw.math.WarpingControl` 550 Configuration settings for warping an image 551 bbox : `lsst.afw.geom.Box2I`, optional 552 Sub-region of the masked image to shift. 553 Shifts the entire image if None (Default). 554 useInverse : `bool`, optional 555 Use the reverse of ``dcr`` for the shift. Default: False 556 splitSubfilters : `bool`, optional 557 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 558 instead of at the midpoint. Default: False 562 shiftedImage : `lsst.afw.image.maskedImageF` 563 A masked image, with the pixels within the bounding box shifted. 565 padValue = afwImage.pixel.SinglePixelF(0., maskedImage.mask.getPlaneBitMask(
"NO_DATA"), 0)
567 bbox = maskedImage.getBBox()
569 shiftedImage = afwImage.MaskedImageF(bbox)
570 transform0 = makeTransform(AffineTransform((-1.0
if useInverse
else 1.0)*dcr[0]))
572 transform0, warpCtrl, padValue=padValue)
573 shiftedImage1 = afwImage.MaskedImageF(bbox)
574 transform1 = makeTransform(AffineTransform((-1.0
if useInverse
else 1.0)*dcr[1]))
576 transform1, warpCtrl, padValue=padValue)
577 shiftedImage += shiftedImage1
580 shiftedImage = afwImage.MaskedImageF(bbox)
581 transform = makeTransform(AffineTransform((-1.0
if useInverse
else 1.0)*dcr))
583 transform, warpCtrl, padValue=padValue)
587 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
588 """Calculate the shift in pixels of an exposure due to DCR. 592 visitInfo : `lsst.afw.image.VisitInfo` 593 Metadata for the exposure. 594 wcs : `lsst.afw.geom.SkyWcs` 595 Coordinate system definition (wcs) for the exposure. 596 filterInfo : `lsst.afw.image.Filter` 597 The filter definition, set in the current instruments' obs package. 598 dcrNumSubfilters : `int` 599 Number of sub-filters used to model chromatic effects within a band. 600 splitSubfilters : `bool`, optional 601 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 602 instead of at the midpoint. Default: False 606 dcrShift : `lsst.afw.geom.Extent2I` 607 The 2D shift due to DCR, in pixels. 611 weight = [0.75, 0.25]
612 lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
615 diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
616 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
617 observatory=visitInfo.getObservatory(),
618 weather=visitInfo.getWeather())
619 diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
620 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
621 observatory=visitInfo.getObservatory(),
622 weather=visitInfo.getWeather())
624 diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
625 diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
626 diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
627 diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
628 shiftX = [diffRefractPix*np.sin(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
629 shiftY = [diffRefractPix*np.cos(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
630 dcrShift.append((afwGeom.Extent2D(shiftX[0], shiftY[0]), afwGeom.Extent2D(shiftX[1], shiftY[1])))
632 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
633 diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
634 shiftX = diffRefractPix*np.sin(rotation.asRadians())
635 shiftY = diffRefractPix*np.cos(rotation.asRadians())
636 dcrShift.append(afwGeom.Extent2D(shiftX, shiftY))
641 """Calculate the total sky rotation angle of an exposure. 645 visitInfo : `lsst.afw.image.VisitInfo` 646 Metadata for the exposure. 647 wcs : `lsst.afw.geom.SkyWcs` 648 Coordinate system definition (wcs) for the exposure. 653 The rotation of the image axis, East from North. 654 Equal to the parallactic angle plus any additional rotation of the 656 A rotation angle of 0 degrees is defined with 657 North along the +y axis and East along the +x axis. 658 A rotation angle of 90 degrees is defined with 659 North along the +x axis and East along the -y axis. 661 parAngle = visitInfo.getBoresightParAngle().asRadians()
662 cd = wcs.getCdMatrix()
664 cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
666 cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
667 rotAngle = (cdAngle + parAngle)*radians
672 """Iterate over the wavelength endpoints of subfilters. 676 filterInfo : `lsst.afw.image.Filter` 677 The filter definition, set in the current instruments' obs package. 678 dcrNumSubfilters : `int` 679 Number of sub-filters used to model chromatic effects within a band. 683 `tuple` of two `float` 684 The next set of wavelength endpoints for a subfilter, in nm. 686 lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
687 lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
688 wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
689 for wl
in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=
False):
690 yield (wl, wl + wlStep)
def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None)
def calculateNoiseCutoff(self, maskedImage, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
def __setitem__(self, subfilter, maskedImage)
def applyImageThresholds(self, maskedImage, highThreshold=None, lowThreshold=None, regularizationWidth=2)
def calculateImageParallacticAngle(visitInfo, wcs)
int warpImage(DestImageT &destImage, SrcImageT const &srcImage, geom::TransformPoint2ToPoint2 const &srcToDest, WarpingControl const &control, typename DestImageT::SinglePixel padValue=lsst::afw::math::edgePixel< DestImageT >(typename lsst::afw::image::detail::image_traits< DestImageT >::image_category()))
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
def regularizeModelFreq(self, modelImages, bbox, regularizationFactor, regularizationWidth=2)
def __getitem__(self, subfilter)
def __init__(self, modelImages, filterInfo=None, psf=None)
def wavelengthGenerator(filterInfo, dcrNumSubfilters)
def buildMatchedTemplate(self, exposure=None, warpCtrl=None, visitInfo=None, bbox=None, wcs=None, mask=None, splitSubfilters=False)
def buildMatchedExposure(self, exposure=None, warpCtrl=None, visitInfo=None, bbox=None, wcs=None, mask=None)
def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, kwargs)
def getReferenceImage(self, bbox=None)
def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False, splitSubfilters=False)
def assign(self, dcrSubModel, bbox=None)
def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False)
def conditionDcrModel(self, modelImages, bbox, gain=1.)