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 """Create a DCR-matched template image for an exposure. 297 exposure : `lsst.afw.image.Exposure`, optional 298 The input exposure to build a matched template for. 299 May be omitted if all of the metadata is supplied separately 300 warpCtrl : `lsst.afw.Math.WarpingControl`, optional 301 Configuration settings for warping an image. 302 If not set, defaults to a lanczos3 warping kernel for the image, 303 and a bilinear kernel for the mask 304 visitInfo : `lsst.afw.image.VisitInfo`, optional 305 Metadata for the exposure. Ignored if ``exposure`` is set. 306 bbox : `lsst.afw.geom.Box2I`, optional 307 Sub-region of the coadd. Ignored if ``exposure`` is set. 308 wcs : `lsst.afw.geom.SkyWcs`, optional 309 Coordinate system definition (wcs) for the exposure. 310 Ignored if ``exposure`` is set. 311 mask : `lsst.afw.image.Mask`, optional 312 reference mask to use for the template image. 316 templateImage : `lsst.afw.image.maskedImageF` 317 The DCR-matched template 322 If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set. 325 raise ValueError(
"'filterInfo' must be set for the DcrModel in order to calculate DCR.")
326 if exposure
is not None:
327 visitInfo = exposure.getInfo().getVisitInfo()
328 bbox = exposure.getBBox()
329 wcs = exposure.getInfo().getWcs()
330 elif visitInfo
is None or bbox
is None or wcs
is None:
331 raise ValueError(
"Either exposure or visitInfo, bbox, and wcs must be set.")
336 cacheSize=0, interpLength=max(bbox.getDimensions()))
339 templateImage = afwImage.MaskedImageF(bbox)
340 for subfilter, dcr
in enumerate(dcrShift):
341 templateImage +=
applyDcr(self[subfilter][bbox], dcr, warpCtrl)
343 templateImage.setMask(mask[bbox])
347 visitInfo=None, bbox=None, wcs=None, mask=None):
348 """Wrapper to create an exposure from a template image. 352 exposure : `lsst.afw.image.Exposure`, optional 353 The input exposure to build a matched template for. 354 May be omitted if all of the metadata is supplied separately 355 warpCtrl : `lsst.afw.Math.WarpingControl` 356 Configuration settings for warping an image 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 templateExposure = afwImage.ExposureF(bbox, wcs)
374 templateExposure.setMaskedImage(templateImage)
375 templateExposure.setPsf(self.
psf)
376 templateExposure.setFilter(self.
filter)
377 return templateExposure
380 """Average two iterations' solutions to reduce oscillations. 384 modelImages : `list` of `lsst.afw.image.MaskedImage` 385 The new DCR model images from the current iteration. 386 The values will be modified in place. 387 bbox : `lsst.afw.geom.Box2I` 388 Sub-region of the coadd 389 gain : `float`, optional 390 Relative weight to give the new solution when updating the model. 391 Defaults to 1.0, which gives equal weight to both solutions. 395 for model, newModel
in zip(self, modelImages):
396 newModel.image *= gain
397 newModel.image += model[bbox].image
398 newModel.image /= 1. + gain
399 newModel.variance *= gain
400 newModel.variance += model[bbox].variance
401 newModel.variance /= 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.MaskedImage` 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].image.array
423 highThreshold = np.abs(refImage)*regularizationFactor
424 lowThreshold = refImage/regularizationFactor
426 regularizationWidth=regularizationWidth)
429 regularizationWidth=2):
430 """Restrict large variations in the model between subfilters. 434 modelImages : `list` of `lsst.afw.image.MaskedImage` 435 The new DCR model images from the current iteration. 436 The values will be modified in place. 437 bbox : `lsst.afw.geom.Box2I` 439 regularizationFactor : `float` 440 Maximum relative change of the model allowed between subfilters. 441 regularizationWidth : `int`, optional 442 Minimum radius of a region to include in regularization, in pixels. 446 maxDiff = np.sqrt(regularizationFactor)
449 for model
in modelImages:
450 highThreshold = np.abs(refImage)*maxDiff
451 lowThreshold = refImage/maxDiff
453 regularizationWidth=regularizationWidth)
456 convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
457 """Helper function to calculate the background noise level of an image. 461 maskedImage : `lsst.afw.image.MaskedImage` 462 The input image to evaluate the background noise properties. 463 statsCtrl : `lsst.afw.math.StatisticsControl` 464 Statistics control object for coaddition. 466 Number of additional pixels to exclude 467 from the edges of the bounding box. 468 convergenceMaskPlanes : `list` of `str`, or `str` 469 Mask planes to use to calculate convergence. 470 mask : `lsst.afw.image.Mask`, Optional 471 Optional alternate mask 472 bbox : `lsst.afw.geom.Box2I`, optional 473 Sub-region of the masked image to calculate the noise level over. 477 noiseCutoff : `float` 478 The threshold value to treat pixels as noise in an image.. 483 mask = maskedImage[bbox].mask
484 bboxShrink = afwGeom.Box2I(bbox)
485 bboxShrink.grow(-bufferSize)
486 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
488 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
489 noiseCutoff = np.std(maskedImage[bboxShrink].image.array[backgroundPixels])
493 regularizationWidth=2):
494 """Restrict image values to be between upper and lower limits. 496 This method flags all pixels in an image that are outside of the given 497 threshold values. The threshold values are taken from a reference image, 498 so noisy pixels are likely to get flagged. In order to exclude those 499 noisy pixels, the array of flags is eroded and dilated, which removes 500 isolated pixels outside of the thresholds from the list of pixels to be 501 modified. Pixels that remain flagged after this operation have their 502 values set to the appropriate upper or lower threshold value. 506 maskedImage : `lsst.afw.image.MaskedImage` 507 The image to apply the thresholds to. 508 The image plane values will be modified in place. 509 highThreshold : `numpy.ndarray`, optional 510 Array of upper limit values for each pixel of ``maskedImage``. 511 lowThreshold : `numpy.ndarray`, optional 512 Array of lower limit values for each pixel of ``maskedImage``. 513 regularizationWidth : `int`, optional 514 Minimum radius of a region to include in regularization, in pixels. 519 image = maskedImage.image.array
520 filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
522 if highThreshold
is not None:
523 highPixels = image > highThreshold
524 if regularizationWidth > 0:
526 highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
527 image[highPixels] = highThreshold[highPixels]
528 if lowThreshold
is not None:
529 lowPixels = image < lowThreshold
530 if regularizationWidth > 0:
532 lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
533 image[lowPixels] = lowThreshold[lowPixels]
536 def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False):
537 """Shift a masked image. 541 maskedImage : `lsst.afw.image.MaskedImage` 542 The input masked image to shift. 543 dcr : `lsst.afw.geom.Extent2I` 544 Shift calculated with ``calculateDcr``. 545 warpCtrl : `lsst.afw.math.WarpingControl` 546 Configuration settings for warping an image 547 bbox : `lsst.afw.geom.Box2I`, optional 548 Sub-region of the masked image to shift. 549 Shifts the entire image if None (Default). 550 useInverse : `bool`, optional 551 Use the reverse of ``dcr`` for the shift. Default: False 555 `lsst.afw.image.maskedImageF` 556 A masked image, with the pixels within the bounding box shifted. 558 padValue = afwImage.pixel.SinglePixelF(0., maskedImage.mask.getPlaneBitMask(
"NO_DATA"), 0)
560 bbox = maskedImage.getBBox()
561 shiftedImage = afwImage.MaskedImageF(bbox)
562 transform = makeTransform(AffineTransform((-1.0
if useInverse
else 1.0)*dcr))
564 transform, warpCtrl, padValue=padValue)
569 """Calculate the shift in pixels of an exposure due to DCR. 573 visitInfo : `lsst.afw.image.VisitInfo` 574 Metadata for the exposure. 575 wcs : `lsst.afw.geom.SkyWcs` 576 Coordinate system definition (wcs) for the exposure. 577 filterInfo : `lsst.afw.image.Filter` 578 The filter definition, set in the current instruments' obs package. 579 dcrNumSubfilters : `int` 580 Number of sub-filters used to model chromatic effects within a band. 584 `lsst.afw.geom.Extent2I` 585 The 2D shift due to DCR, in pixels. 589 lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
592 diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
593 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
594 observatory=visitInfo.getObservatory(),
595 weather=visitInfo.getWeather())
596 diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
597 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
598 observatory=visitInfo.getObservatory(),
599 weather=visitInfo.getWeather())
600 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
601 diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
602 shiftX = diffRefractPix*np.sin(rotation.asRadians())
603 shiftY = diffRefractPix*np.cos(rotation.asRadians())
604 dcrShift.append(afwGeom.Extent2D(shiftX, shiftY))
609 """Calculate the total sky rotation angle of an exposure. 613 visitInfo : `lsst.afw.image.VisitInfo` 614 Metadata for the exposure. 615 wcs : `lsst.afw.geom.SkyWcs` 616 Coordinate system definition (wcs) for the exposure. 621 The rotation of the image axis, East from North. 622 Equal to the parallactic angle plus any additional rotation of the 624 A rotation angle of 0 degrees is defined with 625 North along the +y axis and East along the +x axis. 626 A rotation angle of 90 degrees is defined with 627 North along the +x axis and East along the -y axis. 629 parAngle = visitInfo.getBoresightParAngle().asRadians()
630 cd = wcs.getCdMatrix()
632 cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
634 cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
635 rotAngle = (cdAngle + parAngle)*radians
640 """Iterate over the wavelength endpoints of subfilters. 644 filterInfo : `lsst.afw.image.Filter` 645 The filter definition, set in the current instruments' obs package. 646 dcrNumSubfilters : `int` 647 Number of sub-filters used to model chromatic effects within a band. 651 `tuple` of two `float` 652 The next set of wavelength endpoints for a subfilter, in nm. 654 lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
655 lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
656 wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
657 for wl
in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=
False):
658 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 buildMatchedTemplate(self, exposure=None, warpCtrl=None, visitInfo=None, bbox=None, wcs=None, mask=None)
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 buildMatchedExposure(self, exposure=None, warpCtrl=None, visitInfo=None, bbox=None, wcs=None, mask=None)
def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters)
def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, kwargs)
def getReferenceImage(self, bbox=None)
def assign(self, dcrSubModel, bbox=None)
def conditionDcrModel(self, modelImages, bbox, gain=1.)
def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False)