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 modelImages : `list` of `lsst.afw.image.MaskedImage` 44 A list of masked images, each containing the model for one subfilter 48 The ``DcrModel`` contains an estimate of the true sky, at a higher 49 wavelength resolution than the input observations. It can be forward- 50 modeled to produce Differential Chromatic Refraction (DCR) matched 51 templates for a given ``Exposure``, and provides utilities for conditioning 52 the model in ``dcrAssembleCoadd`` to avoid oscillating solutions between 53 iterations of forward modeling or between the subfilters of the model. 56 def __init__(self, modelImages, filterInfo=None, psf=None):
63 def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None):
64 """Initialize a DcrModel by dividing a coadd between the subfilters. 68 maskedImage : `lsst.afw.image.MaskedImage` 69 Input coadded image to divide equally between the subfilters. 70 dcrNumSubfilters : `int` 71 Number of sub-filters used to model chromatic effects within a band. 72 filterInfo : `lsst.afw.image.Filter`, optional 73 The filter definition, set in the current instruments' obs package. 74 Required for any calculation of DCR, including making matched templates. 75 psf : `lsst.afw.detection.Psf`, optional 76 Point spread function (PSF) of the model. 77 Required if the ``DcrModel`` will be persisted. 81 dcrModel : `lsst.pipe.tasks.DcrModel` 82 Best fit model of the true sky after correcting chromatic effects. 86 model = maskedImage.clone()
87 badPixels = np.isnan(model.image.array) | np.isnan(model.variance.array)
88 model.image.array[badPixels] = 0.
89 model.variance.array[badPixels] = 0.
90 model.image.array /= dcrNumSubfilters
96 model.variance.array /= dcrNumSubfilters
97 model.mask.array[badPixels] = model.mask.getPlaneBitMask(
"NO_DATA")
98 modelImages = [model, ]
99 for subfilter
in range(1, dcrNumSubfilters):
100 modelImages.append(model.clone())
101 return cls(modelImages, filterInfo, psf)
104 def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, **kwargs):
105 """Load an existing DcrModel from a repository. 109 dataRef : `lsst.daf.persistence.ButlerDataRef` 110 Data reference defining the patch for coaddition and the 112 datasetType : `str`, optional 113 Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"} 114 numSubfilters : `int` 115 Number of sub-filters used to model chromatic effects within a band. 117 Additional keyword arguments to pass to look up the model in the data registry. 118 Common keywords and their types include: ``tract``:`str`, ``patch``:`str`, 119 ``bbox``:`lsst.afw.geom.Box2I` 123 dcrModel : `lsst.pipe.tasks.DcrModel` 124 Best fit model of the true sky after correcting chromatic effects. 129 for subfilter
in range(numSubfilters):
130 dcrCoadd = dataRef.get(datasetType, subfilter=subfilter,
131 numSubfilters=numSubfilters, **kwargs)
132 if filterInfo
is None:
133 filterInfo = dcrCoadd.getFilter()
135 psf = dcrCoadd.getPsf()
136 modelImages.append(dcrCoadd.maskedImage)
137 return cls(modelImages, filterInfo, psf)
140 """Return the number of subfilters. 144 dcrNumSubfilters : `int` 145 The number of DCR subfilters in the model. 150 """Iterate over the subfilters of the DCR model. 155 Index of the current ``subfilter`` within the full band. 156 Negative indices are allowed, and count in reverse order 157 from the highest ``subfilter``. 161 modelImage : `lsst.afw.image.MaskedImage` 162 The DCR model for the given ``subfilter``. 167 If the requested ``subfilter`` is greater or equal to the number 168 of subfilters in the model. 170 if np.abs(subfilter) >= len(self):
171 raise IndexError(
"subfilter out of bounds.")
175 """Update the model image for one subfilter. 180 Index of the current subfilter within the full band. 181 maskedImage : `lsst.afw.image.MaskedImage` 182 The DCR model to set for the given ``subfilter``. 187 If the requested ``subfilter`` is greater or equal to the number 188 of subfilters in the model. 190 If the bounding box of the new image does not match. 192 if np.abs(subfilter) >= len(self):
193 raise IndexError(
"subfilter out of bounds.")
194 if maskedImage.getBBox() != self.
bbox:
195 raise ValueError(
"The bounding box of a subfilter must not change.")
200 """Return the filter of the model. 204 filter : `lsst.afw.image.Filter` 205 The filter definition, set in the current instruments' obs package. 211 """Return the psf of the model. 215 psf : `lsst.afw.detection.Psf` 216 Point spread function (PSF) of the model. 222 """Return the common bounding box of each subfilter image. 226 bbox : `lsst.afw.geom.Box2I` 227 Bounding box of the DCR model. 229 return self[0].getBBox()
233 """Return the common mask of each subfilter image. 237 bbox : `lsst.afw.image.Mask` 238 Mask plane of the DCR model. 243 """Calculate a reference image from the average of the subfilter images. 247 bbox : `lsst.afw.geom.Box2I`, optional 248 Sub-region of the coadd. Returns the entire image if `None`. 252 refImage : `numpy.ndarray` 253 The reference image with no chromatic effects applied. 255 bbox = bbox
or self.
bbox 256 return np.mean([model[bbox].image.array
for model
in self], axis=0)
258 def assign(self, dcrSubModel, bbox=None):
259 """Update a sub-region of the ``DcrModel`` with new values. 263 dcrSubModel : `lsst.pipe.tasks.DcrModel` 264 New model of the true scene after correcting chromatic effects. 265 bbox : `lsst.afw.geom.Box2I`, optional 266 Sub-region of the coadd. 267 Defaults to the bounding box of ``dcrSubModel``. 272 If the new model has a different number of subfilters. 274 if len(dcrSubModel) != len(self):
275 raise ValueError(
"The number of DCR subfilters must be the same " 276 "between the old and new models.")
277 bbox = bbox
or self.
bbox 278 for model, subModel
in zip(self, dcrSubModel):
279 model.assign(subModel[bbox], bbox)
282 visitInfo=None, bbox=None, wcs=None, mask=None,
283 splitSubfilters=False):
284 """Create a DCR-matched template image for an exposure. 288 exposure : `lsst.afw.image.Exposure`, optional 289 The input exposure to build a matched template for. 290 May be omitted if all of the metadata is supplied separately 291 warpCtrl : `lsst.afw.Math.WarpingControl`, optional 292 Configuration settings for warping an image. 293 If not set, defaults to a lanczos3 warping kernel for the image, 294 and a bilinear kernel for the mask 295 visitInfo : `lsst.afw.image.VisitInfo`, optional 296 Metadata for the exposure. Ignored if ``exposure`` is set. 297 bbox : `lsst.afw.geom.Box2I`, optional 298 Sub-region of the coadd. Ignored if ``exposure`` is set. 299 wcs : `lsst.afw.geom.SkyWcs`, optional 300 Coordinate system definition (wcs) for the exposure. 301 Ignored if ``exposure`` is set. 302 mask : `lsst.afw.image.Mask`, optional 303 reference mask to use for the template image. 304 splitSubfilters : `bool`, optional 305 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 306 instead of at the midpoint. Default: False 310 templateImage : `lsst.afw.image.maskedImageF` 311 The DCR-matched template 316 If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set. 319 raise ValueError(
"'filterInfo' must be set for the DcrModel in order to calculate DCR.")
320 if exposure
is not None:
321 visitInfo = exposure.getInfo().getVisitInfo()
322 bbox = exposure.getBBox()
323 wcs = exposure.getInfo().getWcs()
324 elif visitInfo
is None or bbox
is None or wcs
is None:
325 raise ValueError(
"Either exposure or visitInfo, bbox, and wcs must be set.")
330 cacheSize=0, interpLength=
max(bbox.getDimensions()))
332 dcrShift =
calculateDcr(visitInfo, wcs, self.
filter, len(self), splitSubfilters=splitSubfilters)
333 templateImage = afwImage.MaskedImageF(bbox)
334 for subfilter, dcr
in enumerate(dcrShift):
335 templateImage +=
applyDcr(self[subfilter][bbox], dcr, warpCtrl, splitSubfilters=splitSubfilters)
337 templateImage.setMask(mask[bbox])
341 visitInfo=None, bbox=None, wcs=None, mask=None):
342 """Wrapper to create an exposure from a template image. 346 exposure : `lsst.afw.image.Exposure`, optional 347 The input exposure to build a matched template for. 348 May be omitted if all of the metadata is supplied separately 349 warpCtrl : `lsst.afw.Math.WarpingControl` 350 Configuration settings for warping an image 351 visitInfo : `lsst.afw.image.VisitInfo`, optional 352 Metadata for the exposure. Ignored if ``exposure`` is set. 353 bbox : `lsst.afw.geom.Box2I`, optional 354 Sub-region of the coadd. Ignored if ``exposure`` is set. 355 wcs : `lsst.afw.geom.SkyWcs`, optional 356 Coordinate system definition (wcs) for the exposure. 357 Ignored if ``exposure`` is set. 358 mask : `lsst.afw.image.Mask`, optional 359 reference mask to use for the template image. 363 templateExposure : `lsst.afw.image.exposureF` 364 The DCR-matched template 367 templateExposure = afwImage.ExposureF(bbox, wcs)
368 templateExposure.setMaskedImage(templateImage)
369 templateExposure.setPsf(self.
psf)
370 templateExposure.setFilter(self.
filter)
371 return templateExposure
374 """Average two iterations' solutions to reduce oscillations. 378 modelImages : `list` of `lsst.afw.image.MaskedImage` 379 The new DCR model images from the current iteration. 380 The values will be modified in place. 381 bbox : `lsst.afw.geom.Box2I` 382 Sub-region of the coadd 383 gain : `float`, optional 384 Relative weight to give the new solution when updating the model. 385 Defaults to 1.0, which gives equal weight to both solutions. 389 for model, newModel
in zip(self, modelImages):
390 newModel.image *= gain
391 newModel.image += model[bbox].image
392 newModel.image /= 1. + gain
393 newModel.variance *= gain
394 newModel.variance += model[bbox].variance
395 newModel.variance /= 1. + gain
398 regularizationWidth=2):
399 """Restrict large variations in the model between iterations. 404 Index of the current subfilter within the full band. 405 newModel : `lsst.afw.image.MaskedImage` 406 The new DCR model for one subfilter from the current iteration. 407 Values in ``newModel`` that are extreme compared with the last 408 iteration are modified in place. 409 bbox : `lsst.afw.geom.Box2I` 411 regularizationFactor : `float` 412 Maximum relative change of the model allowed between iterations. 413 regularizationWidth : int, optional 414 Minimum radius of a region to include in regularization, in pixels. 416 refImage = self[subfilter][bbox].image.array
417 highThreshold = np.abs(refImage)*regularizationFactor
418 lowThreshold = refImage/regularizationFactor
419 newImage = newModel.image.array
421 regularizationWidth=regularizationWidth)
424 regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
425 """Restrict large variations in the model between subfilters. 429 modelImages : `list` of `lsst.afw.image.MaskedImage` 430 The new DCR model images from the current iteration. 431 The values will be modified in place. 432 bbox : `lsst.afw.geom.Box2I` 434 statsCtrl : `lsst.afw.math.StatisticsControl` 435 Statistics control object for coaddition. 436 regularizationFactor : `float` 437 Maximum relative change of the model allowed between subfilters. 438 regularizationWidth : `int`, optional 439 Minimum radius of a region to include in regularization, in pixels. 440 mask : `lsst.afw.image.Mask`, optional 441 Optional alternate mask 442 convergenceMaskPlanes : `list` of `str`, or `str`, optional 443 Mask planes to use to calculate convergence. 447 This implementation of frequency regularization restricts each subfilter 448 image to be a smoothly-varying function times a reference image. 452 maxDiff = np.sqrt(regularizationFactor)
453 noiseLevel = self.
calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
455 badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
456 if np.sum(~badPixels) == 0:
459 referenceImage[badPixels] = 0.
460 filterWidth = regularizationWidth
461 fwhm = 2.*filterWidth
464 smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth) + noiseLevel
466 baseThresh = np.ones_like(referenceImage)
467 highThreshold = baseThresh*maxDiff
468 lowThreshold = baseThresh/maxDiff
469 for subfilter, model
in enumerate(modelImages):
470 smoothModel = ndimage.filters.gaussian_filter(model.image.array, filterWidth) + noiseLevel
471 relativeModel = smoothModel/smoothRef
473 relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/3.)
474 relativeModel = relativeModel + 3.*(relativeModel - relativeModel2)
476 highThreshold=highThreshold,
477 lowThreshold=lowThreshold,
478 regularizationWidth=regularizationWidth)
479 relativeModel *= referenceImage
480 modelImages[subfilter].image.array = relativeModel
483 convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
484 """Helper function to calculate the background noise level of an image. 488 maskedImage : `lsst.afw.image.MaskedImage` 489 The input image to evaluate the background noise properties. 490 statsCtrl : `lsst.afw.math.StatisticsControl` 491 Statistics control object for coaddition. 493 Number of additional pixels to exclude 494 from the edges of the bounding box. 495 convergenceMaskPlanes : `list` of `str`, or `str` 496 Mask planes to use to calculate convergence. 497 mask : `lsst.afw.image.Mask`, Optional 498 Optional alternate mask 499 bbox : `lsst.afw.geom.Box2I`, optional 500 Sub-region of the masked image to calculate the noise level over. 504 noiseCutoff : `float` 505 The threshold value to treat pixels as noise in an image.. 510 mask = maskedImage[bbox].mask
512 bboxShrink.grow(-bufferSize)
513 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
515 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
516 noiseCutoff = np.std(maskedImage[bboxShrink].image.array[backgroundPixels])
520 """Restrict image values to be between upper and lower limits. 522 This method flags all pixels in an image that are outside of the given 523 threshold values. The threshold values are taken from a reference image, 524 so noisy pixels are likely to get flagged. In order to exclude those 525 noisy pixels, the array of flags is eroded and dilated, which removes 526 isolated pixels outside of the thresholds from the list of pixels to be 527 modified. Pixels that remain flagged after this operation have their 528 values set to the appropriate upper or lower threshold value. 532 image : `numpy.ndarray` 533 The image to apply the thresholds to. 534 The values will be modified in place. 535 highThreshold : `numpy.ndarray`, optional 536 Array of upper limit values for each pixel of ``image``. 537 lowThreshold : `numpy.ndarray`, optional 538 Array of lower limit values for each pixel of ``image``. 539 regularizationWidth : `int`, optional 540 Minimum radius of a region to include in regularization, in pixels. 545 filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
547 if highThreshold
is not None:
548 highPixels = image > highThreshold
549 if regularizationWidth > 0:
551 highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
552 image[highPixels] = highThreshold[highPixels]
553 if lowThreshold
is not None:
554 lowPixels = image < lowThreshold
555 if regularizationWidth > 0:
557 lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
558 image[lowPixels] = lowThreshold[lowPixels]
561 def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False, splitSubfilters=False):
562 """Shift a masked image. 566 maskedImage : `lsst.afw.image.MaskedImage` 567 The input masked image to shift. 568 dcr : `lsst.afw.geom.Extent2I` 569 Shift calculated with ``calculateDcr``. 570 warpCtrl : `lsst.afw.math.WarpingControl` 571 Configuration settings for warping an image 572 bbox : `lsst.afw.geom.Box2I`, optional 573 Sub-region of the masked image to shift. 574 Shifts the entire image if None (Default). 575 useInverse : `bool`, optional 576 Use the reverse of ``dcr`` for the shift. Default: False 577 splitSubfilters : `bool`, optional 578 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 579 instead of at the midpoint. Default: False 583 shiftedImage : `lsst.afw.image.maskedImageF` 584 A masked image, with the pixels within the bounding box shifted. 586 padValue = afwImage.pixel.SinglePixelF(0., maskedImage.mask.getPlaneBitMask(
"NO_DATA"), 0)
588 bbox = maskedImage.getBBox()
590 shiftedImage = afwImage.MaskedImageF(bbox)
593 transform0, warpCtrl, padValue=padValue)
594 shiftedImage1 = afwImage.MaskedImageF(bbox)
597 transform1, warpCtrl, padValue=padValue)
598 shiftedImage += shiftedImage1
601 shiftedImage = afwImage.MaskedImageF(bbox)
604 transform, warpCtrl, padValue=padValue)
608 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
609 """Calculate the shift in pixels of an exposure due to DCR. 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. 617 filterInfo : `lsst.afw.image.Filter` 618 The filter definition, set in the current instruments' obs package. 619 dcrNumSubfilters : `int` 620 Number of sub-filters used to model chromatic effects within a band. 621 splitSubfilters : `bool`, optional 622 Calculate DCR for two evenly-spaced wavelengths in each subfilter, 623 instead of at the midpoint. Default: False 627 dcrShift : `lsst.afw.geom.Extent2I` 628 The 2D shift due to DCR, in pixels. 632 weight = [0.75, 0.25]
633 lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
637 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
638 observatory=visitInfo.getObservatory(),
639 weather=visitInfo.getWeather())
641 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
642 observatory=visitInfo.getObservatory(),
643 weather=visitInfo.getWeather())
645 diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
646 diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
647 diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
648 diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
649 shiftX = [diffRefractPix*np.sin(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
650 shiftY = [diffRefractPix*np.cos(rotation.asRadians())
for diffRefractPix
in diffRefractArr]
653 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
654 diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
655 shiftX = diffRefractPix*np.sin(rotation.asRadians())
656 shiftY = diffRefractPix*np.cos(rotation.asRadians())
662 """Calculate the total sky rotation angle of an exposure. 666 visitInfo : `lsst.afw.image.VisitInfo` 667 Metadata for the exposure. 668 wcs : `lsst.afw.geom.SkyWcs` 669 Coordinate system definition (wcs) for the exposure. 674 The rotation of the image axis, East from North. 675 Equal to the parallactic angle plus any additional rotation of the 677 A rotation angle of 0 degrees is defined with 678 North along the +y axis and East along the +x axis. 679 A rotation angle of 90 degrees is defined with 680 North along the +x axis and East along the -y axis. 682 parAngle = visitInfo.getBoresightParAngle().asRadians()
683 cd = wcs.getCdMatrix()
685 cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
687 cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
688 rotAngle = (cdAngle + parAngle)*radians
693 """Iterate over the wavelength endpoints of subfilters. 697 filterInfo : `lsst.afw.image.Filter` 698 The filter definition, set in the current instruments' obs package. 699 dcrNumSubfilters : `int` 700 Number of sub-filters used to model chromatic effects within a band. 704 `tuple` of two `float` 705 The next set of wavelength endpoints for a subfilter, in nm. 707 lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
708 lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
709 wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
710 for wl
in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=
False):
711 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 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()))
A variant of warpImage that uses a Transform<Point2Endpoint, Point2Endpoint> instead of a pair of WCS...
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
Parameters to control convolution.
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 applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2)
def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, kwargs)
def getReferenceImage(self, bbox=None)
def differentialRefraction(wavelength, wavelengthRef, elevation, observatory, weather=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 regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED")
def conditionDcrModel(self, modelImages, bbox, gain=1.)
An integer coordinate rectangle.
std::shared_ptr< TransformPoint2ToPoint2 > makeTransform(lsst::geom::AffineTransform const &affine)
Wrap an lsst::geom::AffineTransform as a Transform.