32 __all__ = [
"DcrModel",
"applyDcr",
"calculateDcr",
"calculateImageParallacticAngle"]
36 """A model of the true sky after correcting chromatic effects. 40 dcrNumSubfilters : `int` 41 Number of sub-filters used to model chromatic effects within a band. 42 filterInfo : `lsst.afw.image.Filter` 43 The filter definition, set in the current instruments' obs package. 44 modelImages : `list` of `lsst.afw.image.MaskedImage` 45 A list of masked images, each containing the model for one subfilter 49 modelImages : `list` of `lsst.afw.image.MaskedImage` 50 A list of masked images, each containing the model for one subfilter. 51 filterInfo : `lsst.afw.image.Filter`, optional 52 The filter definition, set in the current instruments' obs package. 53 Required for any calculation of DCR, including making matched templates. 57 The ``DcrModel`` contains an estimate of the true sky, at a higher 58 wavelength resolution than the input observations. It can be forward- 59 modeled to produce Differential Chromatic Refraction (DCR) matched 60 templates for a given ``Exposure``, and provides utilities for conditioning 61 the model in ``dcrAssembleCoadd`` to avoid oscillating solutions between 62 iterations of forward modeling or between the subfilters of the model. 65 def __init__(self, modelImages, filterInfo=None):
71 def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None):
72 """Initialize a DcrModel by dividing a coadd between the subfilters. 76 maskedImage : `lsst.afw.image.MaskedImage` 77 Input coadded image to divide equally between the subfilters. 78 dcrNumSubfilters : `int` 79 Number of sub-filters used to model chromatic effects within a band. 80 filterInfo : None, optional 81 The filter definition, set in the current instruments' obs package. 82 Required for any calculation of DCR, including making matched templates. 86 dcrModel : `lsst.pipe.tasks.DcrModel` 87 Best fit model of the true sky after correcting chromatic effects. 91 model = maskedImage.clone()
92 badPixels = np.isnan(model.image.array) | np.isnan(model.variance.array)
93 model.image.array[badPixels] = 0.
94 model.variance.array[badPixels] = 0.
95 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)
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 dcrNumSubfilters : `int` 113 Number of sub-filters used to model chromatic effects within a band. 117 dcrModel : `lsst.pipe.tasks.DcrModel` 118 Best fit model of the true sky after correcting chromatic effects. 122 for subfilter
in range(dcrNumSubfilters):
123 dcrCoadd = dataRef.get(
"dcrCoadd", subfilter=subfilter, numSubfilters=dcrNumSubfilters)
124 if filterInfo
is None:
125 filterInfo = dcrCoadd.getFilter()
126 modelImages.append(dcrCoadd.maskedImage)
127 return cls(modelImages, filterInfo)
130 """Return the number of subfilters. 134 dcrNumSubfilters : `int` 135 The number of DCR subfilters in the model. 140 """Iterate over the subfilters of the DCR model. 145 Index of the current ``subfilter`` within the full band. 146 Negative indices are allowed, and count in reverse order 147 from the highest ``subfilter``. 151 modelImage : `lsst.afw.image.MaskedImage` 152 The DCR model for the given ``subfilter``. 157 If the requested ``subfilter`` is greater or equal to the number 158 of subfilters in the model. 160 if np.abs(subfilter) >= len(self):
161 raise IndexError(
"subfilter out of bounds.")
165 """Update the model image for one subfilter. 170 Index of the current subfilter within the full band. 171 maskedImage : `lsst.afw.image.MaskedImage` 172 The DCR model to set for the given ``subfilter``. 177 If the requested ``subfilter`` is greater or equal to the number 178 of subfilters in the model. 180 If the bounding box of the new image does not match. 182 if np.abs(subfilter) >= len(self):
183 raise IndexError(
"subfilter out of bounds.")
184 if maskedImage.getBBox() != self.
getBBox():
185 raise ValueError(
"The bounding box of a subfilter must not change.")
189 """Return the common bounding box of each subfilter image. 193 bbox : `lsst.afw.geom.Box2I` 194 Bounding box of the DCR model. 199 """Create a simple template from the DCR model. 203 bbox : `lsst.afw.geom.Box2I`, optional 204 Sub-region of the coadd. Returns the entire image if None. 208 templateImage : `numpy.ndarray` 209 The template with no chromatic effects applied. 211 return np.mean([model[bbox].image.array
for model
in self], axis=0)
213 def assign(self, dcrSubModel, bbox=None):
214 """Update a sub-region of the ``DcrModel`` with new values. 218 dcrSubModel : `lsst.pipe.tasks.DcrModel` 219 New model of the true scene after correcting chromatic effects. 220 bbox : `lsst.afw.geom.Box2I`, optional 221 Sub-region of the coadd. 222 Defaults to the bounding box of ``dcrSubModel``. 227 If the new model has a different number of subfilters. 229 if len(dcrSubModel) != len(self):
230 raise ValueError(
"The number of DCR subfilters must be the same " 231 "between the old and new models.")
234 for model, subModel
in zip(self, dcrSubModel):
235 model.assign(subModel[bbox], bbox)
238 """Create a DCR-matched template for an exposure. 242 warpCtrl : `lsst.afw.math.WarpingControl` 243 Configuration settings for warping an image 244 exposure : `lsst.afw.image.Exposure`, optional 245 The input exposure to build a matched template for. 246 May be omitted if all of the metadata is supplied separately 247 visitInfo : `lsst.afw.image.VisitInfo`, optional 248 Metadata for the exposure. Ignored if ``exposure`` is set. 249 bbox : `lsst.afw.geom.Box2I`, optional 250 Sub-region of the coadd. Ignored if ``exposure`` is set. 251 wcs : `lsst.afw.geom.SkyWcs`, optional 252 Coordinate system definition (wcs) for the exposure. 253 Ignored if ``exposure`` is set. 254 mask : `lsst.afw.image.Mask`, optional 255 reference mask to use for the template image. 256 Ignored if ``exposure`` is set. 260 templateImage : `lsst.afw.image.maskedImageF` 261 The DCR-matched template 266 If ``filterInfo`` is not set. 268 If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set. 271 raise ValueError(
"'filterInfo' must be set for the DcrModel in order to calculate DCR.")
272 if exposure
is not None:
273 visitInfo = exposure.getInfo().getVisitInfo()
274 bbox = exposure.getBBox()
275 wcs = exposure.getInfo().getWcs()
276 elif visitInfo
is None or bbox
is None or wcs
is None:
277 raise ValueError(
"Either exposure or visitInfo, bbox, and wcs must be set.")
279 templateImage = afwImage.MaskedImageF(bbox)
280 for subfilter, dcr
in enumerate(dcrShift):
281 templateImage +=
applyDcr(self[subfilter][bbox], dcr, warpCtrl)
283 templateImage.setMask(mask[bbox])
287 """Average two iterations' solutions to reduce oscillations. 292 Index of the current subfilter within the full band. 293 newModel : `lsst.afw.image.MaskedImage` 294 The new DCR model for one subfilter from the current iteration. 295 Values in ``newModel`` that are extreme compared with the last 296 iteration are modified in place. 297 bbox : `lsst.afw.geom.Box2I` 298 Sub-region of the coadd 299 gain : `float`, optional 300 Additional weight to apply to the model from the current iteration. 301 Defaults to 1.0, which gives equal weight to both solutions. 305 newModel.image *= gain
306 newModel.image += self[subfilter][bbox].image
307 newModel.image /= 1. + gain
308 newModel.variance *= gain
309 newModel.variance += self[subfilter][bbox].variance
310 newModel.variance /= 1. + gain
312 def clampModel(self, subfilter, newModel, bbox, statsCtrl, regularizeSigma, modelClampFactor,
313 convergenceMaskPlanes="DETECTED"):
314 """Restrict large variations in the model between iterations. 319 Index of the current subfilter within the full band. 320 newModel : `lsst.afw.image.MaskedImage` 321 The new DCR model for one subfilter from the current iteration. 322 Values in ``newModel`` that are extreme compared with the last 323 iteration are modified in place. 324 bbox : `lsst.afw.geom.Box2I` 326 statsCtrl : `lsst.afw.math.StatisticsControl` 327 Statistics control object for coadd 328 regularizeSigma : `float` 329 Threshold to exclude noise-like pixels from regularization. 330 modelClampFactor : `float` 331 Maximum relative change of the model allowed between iterations. 332 convergenceMaskPlanes : `list` of `str`, or `str`, optional 333 Mask planes to use to calculate convergence. 334 Default value is set in ``calculateNoiseCutoff`` if not supplied. 336 newImage = newModel.image.array
337 oldImage = self[subfilter][bbox].image.array
339 convergenceMaskPlanes=convergenceMaskPlanes)
341 nanPixels = np.isnan(newImage)
342 newImage[nanPixels] = 0.
343 infPixels = np.isinf(newImage)
344 newImage[infPixels] = oldImage[infPixels]*modelClampFactor
346 clampPixels = np.abs(newImage - oldImage) > (np.abs(oldImage*(modelClampFactor - 1)) +
350 highPixels = newImage > oldImage
351 newImage[clampPixels & highPixels] = oldImage[clampPixels & highPixels]*modelClampFactor
352 newImage[clampPixels & ~highPixels] = oldImage[clampPixels & ~highPixels]/modelClampFactor
354 def regularizeModel(self, bbox, mask, statsCtrl, regularizeSigma, clampFrequency,
355 convergenceMaskPlanes="DETECTED"):
356 """Restrict large variations in the model between subfilters. 358 Any flux subtracted by the restriction is accumulated from all 359 subfilters, and divided evenly to each afterwards in order to preserve 364 bbox : `lsst.afw.geom.Box2I` 366 mask : `lsst.afw.image.Mask` 367 Reference mask to use for all model planes. 368 statsCtrl : `lsst.afw.math.StatisticsControl` 369 Statistics control object for coadd 370 regularizeSigma : `float` 371 Threshold to exclude noise-like pixels from regularization. 372 clampFrequency : `float` 373 Maximum relative change of the model allowed between subfilters. 374 convergenceMaskPlanes : `list` of `str`, or `str`, optional 375 Mask planes to use to calculate convergence. (Default is "DETECTED") 376 Default value is set in ``calculateNoiseCutoff`` if not supplied. 379 excess = np.zeros_like(templateImage)
382 convergenceMaskPlanes=convergenceMaskPlanes,
384 modelVals = model.image.array
385 highPixels = (modelVals > (templateImage*clampFrequency + noiseCutoff))
386 excess[highPixels] += modelVals[highPixels] - templateImage[highPixels]*clampFrequency
387 modelVals[highPixels] = templateImage[highPixels]*clampFrequency
388 lowPixels = (modelVals < templateImage/clampFrequency - noiseCutoff)
389 excess[lowPixels] += modelVals[lowPixels] - templateImage[lowPixels]/clampFrequency
390 modelVals[lowPixels] = templateImage[lowPixels]/clampFrequency
393 model.image.array += excess
396 convergenceMaskPlanes="DETECTED", mask=None):
397 """Helper function to calculate the background noise level of an image. 401 maskedImage : `lsst.afw.image.MaskedImage` 402 The input image to evaluate the background noise properties. 403 statsCtrl : `lsst.afw.math.StatisticsControl` 404 Statistics control object for coadd 405 regularizeSigma : `float` 406 Threshold to exclude noise-like pixels from regularization. 407 convergenceMaskPlanes : `list` of `str`, or `str` 408 Mask planes to use to calculate convergence. 409 mask : `lsst.afw.image.Mask`, Optional 410 Optional alternate mask 414 noiseCutoff : `float` 415 The threshold value to treat pixels as noise in an image.. 417 convergeMask = maskedImage.mask.getPlaneBitMask(convergenceMaskPlanes)
419 mask = maskedImage.mask
420 backgroundPixels = mask.array & (statsCtrl.getAndMask() | convergeMask) == 0
421 noiseCutoff = regularizeSigma*np.std(maskedImage.image.array[backgroundPixels])
425 def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False):
426 """Shift a masked image. 430 maskedImage : `lsst.afw.image.MaskedImage` 431 The input masked image to shift. 432 dcr : `lsst.afw.geom.Extent2I` 433 Shift calculated with ``calculateDcr``. 434 warpCtrl : `lsst.afw.math.WarpingControl` 435 Configuration settings for warping an image 436 bbox : `lsst.afw.geom.Box2I`, optional 437 Sub-region of the masked image to shift. 438 Shifts the entire image if None (Default). 439 useInverse : `bool`, optional 440 Use the reverse of ``dcr`` for the shift. Default: False 444 `lsst.afw.image.maskedImageF` 445 A masked image, with the pixels within the bounding box shifted. 447 padValue = afwImage.pixel.SinglePixelF(0., maskedImage.mask.getPlaneBitMask(
"NO_DATA"), 0)
449 bbox = maskedImage.getBBox()
450 shiftedImage = afwImage.MaskedImageF(bbox)
451 transform = makeTransform(AffineTransform((-1.0
if useInverse
else 1.0)*dcr))
452 afwMath.warpImage(shiftedImage, maskedImage[bbox],
453 transform, warpCtrl, padValue=padValue)
458 """Calculate the shift in pixels of an exposure due to DCR. 462 visitInfo : `lsst.afw.image.VisitInfo` 463 Metadata for the exposure. 464 wcs : `lsst.afw.geom.SkyWcs` 465 Coordinate system definition (wcs) for the exposure. 466 filterInfo : `lsst.afw.image.Filter` 467 The filter definition, set in the current instruments' obs package. 468 dcrNumSubfilters : `int` 469 Number of sub-filters used to model chromatic effects within a band. 473 `lsst.afw.geom.Extent2I` 474 The 2D shift due to DCR, in pixels. 478 lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
481 diffRefractAmp0 = differentialRefraction(wl0, lambdaEff,
482 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
483 observatory=visitInfo.getObservatory(),
484 weather=visitInfo.getWeather())
485 diffRefractAmp1 = differentialRefraction(wl1, lambdaEff,
486 elevation=visitInfo.getBoresightAzAlt().getLatitude(),
487 observatory=visitInfo.getObservatory(),
488 weather=visitInfo.getWeather())
489 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
490 diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
491 dcrShift.append(afwGeom.Extent2D(diffRefractPix*np.cos(rotation.asRadians()),
492 diffRefractPix*np.sin(rotation.asRadians())))
497 """Calculate the total sky rotation angle of an exposure. 501 visitInfo : `lsst.afw.image.VisitInfo` 502 Metadata for the exposure. 503 wcs : `lsst.afw.geom.SkyWcs` 504 Coordinate system definition (wcs) for the exposure. 509 The rotation of the image axis, East from North. 510 Equal to the parallactic angle plus any additional rotation of the 512 A rotation angle of 0 degrees is defined with 513 North along the +y axis and East along the +x axis. 514 A rotation angle of 90 degrees is defined with 515 North along the +x axis and East along the -y axis. 517 parAngle = visitInfo.getBoresightParAngle().asRadians()
518 cd = wcs.getCdMatrix()
519 cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
520 rotAngle = (cdAngle + parAngle)*radians
525 """Iterate over the wavelength endpoints of subfilters. 529 filterInfo : `lsst.afw.image.Filter` 530 The filter definition, set in the current instruments' obs package. 531 dcrNumSubfilters : `int` 532 Number of sub-filters used to model chromatic effects within a band. 536 `tuple` of two `float` 537 The next set of wavelength endpoints for a subfilter, in nm. 539 lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
540 lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
541 wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
542 for wl
in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=
False):
543 yield (wl, wl + wlStep)
def calculateNoiseCutoff(self, maskedImage, statsCtrl, regularizeSigma, convergenceMaskPlanes="DETECTED", mask=None)
def conditionDcrModel(self, subfilter, newModel, bbox, gain=1.)
def __init__(self, modelImages, filterInfo=None)
def regularizeModel(self, bbox, mask, statsCtrl, regularizeSigma, clampFrequency, convergenceMaskPlanes="DETECTED")
def clampModel(self, subfilter, newModel, bbox, statsCtrl, regularizeSigma, modelClampFactor, convergenceMaskPlanes="DETECTED")
def fromDataRef(cls, dataRef, dcrNumSubfilters)
def calculateImageParallacticAngle(visitInfo, wcs)
def __getitem__(self, subfilter)
def __setitem__(self, subfilter, maskedImage)
def assign(self, dcrSubModel, bbox=None)
def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False)
def wavelengthGenerator(filterInfo, dcrNumSubfilters)
def buildMatchedTemplate(self, warpCtrl, exposure=None, visitInfo=None, bbox=None, wcs=None, mask=None)
def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None)
def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters)
def getReferenceImage(self, bbox=None)