lsst.ip.diffim  19.0.0-13-g8db0348+6
dcrModel.py
Go to the documentation of this file.
1 # This file is part of ip_diffim.
2 #
3 # LSST Data Management System
4 # This product includes software developed by the
5 # LSST Project (http://www.lsst.org/).
6 # See COPYRIGHT file at the top of the source tree.
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 import numpy as np
24 from scipy import ndimage
25 from lsst.afw.coord.refraction import differentialRefraction
26 import lsst.afw.image as afwImage
27 import lsst.geom as geom
28 
29 __all__ = ["DcrModel", "applyDcr", "calculateDcr", "calculateImageParallacticAngle"]
30 
31 
32 class DcrModel:
33  """A model of the true sky after correcting chromatic effects.
34 
35  Attributes
36  ----------
37  dcrNumSubfilters : `int`
38  Number of sub-filters used to model chromatic effects within a band.
39  modelImages : `list` of `lsst.afw.image.Image`
40  A list of masked images, each containing the model for one subfilter
41 
42  Notes
43  -----
44  The ``DcrModel`` contains an estimate of the true sky, at a higher
45  wavelength resolution than the input observations. It can be forward-
46  modeled to produce Differential Chromatic Refraction (DCR) matched
47  templates for a given ``Exposure``, and provides utilities for conditioning
48  the model in ``dcrAssembleCoadd`` to avoid oscillating solutions between
49  iterations of forward modeling or between the subfilters of the model.
50  """
51 
52  def __init__(self, modelImages, filterInfo=None, psf=None, mask=None, variance=None, photoCalib=None):
53  self.dcrNumSubfilters = len(modelImages)
54  self.modelImages = modelImages
55  self._filter = filterInfo
56  self._psf = psf
57  self._mask = mask
58  self._variance = variance
59  self.photoCalib = photoCalib
60 
61  @classmethod
62  def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None, photoCalib=None):
63  """Initialize a DcrModel by dividing a coadd between the subfilters.
64 
65  Parameters
66  ----------
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.
77  photoCalib : `lsst.afw.image.PhotoCalib`, optional
78  Calibration to convert instrumental flux and
79  flux error to nanoJansky.
80 
81  Returns
82  -------
83  dcrModel : `lsst.pipe.tasks.DcrModel`
84  Best fit model of the true sky after correcting chromatic effects.
85 
86  Raises
87  ------
88  ValueError
89  If there are any unmasked NAN values in ``maskedImage``.
90  """
91  # NANs will potentially contaminate the entire image,
92  # depending on the shift or convolution type used.
93  model = maskedImage.image.clone()
94  mask = maskedImage.mask.clone()
95  # We divide the variance by N and not N**2 because we will assume each
96  # subfilter is independent. That means that the significance of
97  # detected sources will be lower by a factor of sqrt(N) in the
98  # subfilter images, but we will recover it when we combine the
99  # subfilter images to construct matched templates.
100  variance = maskedImage.variance.clone()
101  variance /= dcrNumSubfilters
102  model /= dcrNumSubfilters
103  modelImages = [model, ]
104  for subfilter in range(1, dcrNumSubfilters):
105  modelImages.append(model.clone())
106  return cls(modelImages, filterInfo, psf, mask, variance, photoCalib=photoCalib)
107 
108  @classmethod
109  def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, **kwargs):
110  """Load an existing DcrModel from a repository.
111 
112  Parameters
113  ----------
114  dataRef : `lsst.daf.persistence.ButlerDataRef`
115  Data reference defining the patch for coaddition and the
116  reference Warp
117  datasetType : `str`, optional
118  Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"}
119  numSubfilters : `int`
120  Number of sub-filters used to model chromatic effects within a band.
121  **kwargs
122  Additional keyword arguments to pass to look up the model in the data registry.
123  Common keywords and their types include: ``tract``:`str`, ``patch``:`str`,
124  ``bbox``:`lsst.afw.geom.Box2I`
125 
126  Returns
127  -------
128  dcrModel : `lsst.pipe.tasks.DcrModel`
129  Best fit model of the true sky after correcting chromatic effects.
130  """
131  modelImages = []
132  filterInfo = None
133  psf = None
134  mask = None
135  variance = None
136  photoCalib = None
137  for subfilter in range(numSubfilters):
138  dcrCoadd = dataRef.get(datasetType, subfilter=subfilter,
139  numSubfilters=numSubfilters, **kwargs)
140  if filterInfo is None:
141  filterInfo = dcrCoadd.getFilter()
142  if psf is None:
143  psf = dcrCoadd.getPsf()
144  if mask is None:
145  mask = dcrCoadd.mask
146  if variance is None:
147  variance = dcrCoadd.variance
148  if photoCalib is None:
149  photoCalib = dcrCoadd.getPhotoCalib()
150  modelImages.append(dcrCoadd.image)
151  return cls(modelImages, filterInfo, psf, mask, variance, photoCalib)
152 
153  def __len__(self):
154  """Return the number of subfilters.
155 
156  Returns
157  -------
158  dcrNumSubfilters : `int`
159  The number of DCR subfilters in the model.
160  """
161  return self.dcrNumSubfilters
162 
163  def __getitem__(self, subfilter):
164  """Iterate over the subfilters of the DCR model.
165 
166  Parameters
167  ----------
168  subfilter : `int`
169  Index of the current ``subfilter`` within the full band.
170  Negative indices are allowed, and count in reverse order
171  from the highest ``subfilter``.
172 
173  Returns
174  -------
175  modelImage : `lsst.afw.image.Image`
176  The DCR model for the given ``subfilter``.
177 
178  Raises
179  ------
180  IndexError
181  If the requested ``subfilter`` is greater or equal to the number
182  of subfilters in the model.
183  """
184  if np.abs(subfilter) >= len(self):
185  raise IndexError("subfilter out of bounds.")
186  return self.modelImages[subfilter]
187 
188  def __setitem__(self, subfilter, maskedImage):
189  """Update the model image for one subfilter.
190 
191  Parameters
192  ----------
193  subfilter : `int`
194  Index of the current subfilter within the full band.
195  maskedImage : `lsst.afw.image.Image`
196  The DCR model to set for the given ``subfilter``.
197 
198  Raises
199  ------
200  IndexError
201  If the requested ``subfilter`` is greater or equal to the number
202  of subfilters in the model.
203  ValueError
204  If the bounding box of the new image does not match.
205  """
206  if np.abs(subfilter) >= len(self):
207  raise IndexError("subfilter out of bounds.")
208  if maskedImage.getBBox() != self.bbox:
209  raise ValueError("The bounding box of a subfilter must not change.")
210  self.modelImages[subfilter] = maskedImage
211 
212  @property
213  def filter(self):
214  """Return the filter of the model.
215 
216  Returns
217  -------
218  filter : `lsst.afw.image.Filter`
219  The filter definition, set in the current instruments' obs package.
220  """
221  return self._filter
222 
223  @property
224  def psf(self):
225  """Return the psf of the model.
226 
227  Returns
228  -------
229  psf : `lsst.afw.detection.Psf`
230  Point spread function (PSF) of the model.
231  """
232  return self._psf
233 
234  @property
235  def bbox(self):
236  """Return the common bounding box of each subfilter image.
237 
238  Returns
239  -------
240  bbox : `lsst.afw.geom.Box2I`
241  Bounding box of the DCR model.
242  """
243  return self[0].getBBox()
244 
245  @property
246  def mask(self):
247  """Return the common mask of each subfilter image.
248 
249  Returns
250  -------
251  mask : `lsst.afw.image.Mask`
252  Mask plane of the DCR model.
253  """
254  return self._mask
255 
256  @property
257  def variance(self):
258  """Return the common variance of each subfilter image.
259 
260  Returns
261  -------
262  variance : `lsst.afw.image.Image`
263  Variance plane of the DCR model.
264  """
265  return self._variance
266 
267  def getReferenceImage(self, bbox=None):
268  """Calculate a reference image from the average of the subfilter images.
269 
270  Parameters
271  ----------
272  bbox : `lsst.afw.geom.Box2I`, optional
273  Sub-region of the coadd. Returns the entire image if `None`.
274 
275  Returns
276  -------
277  refImage : `numpy.ndarray`
278  The reference image with no chromatic effects applied.
279  """
280  bbox = bbox or self.bbox
281  return np.mean([model[bbox].array for model in self], axis=0)
282 
283  def assign(self, dcrSubModel, bbox=None):
284  """Update a sub-region of the ``DcrModel`` with new values.
285 
286  Parameters
287  ----------
288  dcrSubModel : `lsst.pipe.tasks.DcrModel`
289  New model of the true scene after correcting chromatic effects.
290  bbox : `lsst.afw.geom.Box2I`, optional
291  Sub-region of the coadd.
292  Defaults to the bounding box of ``dcrSubModel``.
293 
294  Raises
295  ------
296  ValueError
297  If the new model has a different number of subfilters.
298  """
299  if len(dcrSubModel) != len(self):
300  raise ValueError("The number of DCR subfilters must be the same "
301  "between the old and new models.")
302  bbox = bbox or self.bbox
303  for model, subModel in zip(self, dcrSubModel):
304  model.assign(subModel[bbox], bbox)
305 
306  def buildMatchedTemplate(self, exposure=None, order=3,
307  visitInfo=None, bbox=None, wcs=None, mask=None,
308  splitSubfilters=True, splitThreshold=0., amplifyModel=1.):
309  """Create a DCR-matched template image for an exposure.
310 
311  Parameters
312  ----------
313  exposure : `lsst.afw.image.Exposure`, optional
314  The input exposure to build a matched template for.
315  May be omitted if all of the metadata is supplied separately
316  order : `int`, optional
317  Interpolation order of the DCR shift.
318  visitInfo : `lsst.afw.image.VisitInfo`, optional
319  Metadata for the exposure. Ignored if ``exposure`` is set.
320  bbox : `lsst.afw.geom.Box2I`, optional
321  Sub-region of the coadd. Ignored if ``exposure`` is set.
322  wcs : `lsst.afw.geom.SkyWcs`, optional
323  Coordinate system definition (wcs) for the exposure.
324  Ignored if ``exposure`` is set.
325  mask : `lsst.afw.image.Mask`, optional
326  reference mask to use for the template image.
327  splitSubfilters : `bool`, optional
328  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
329  instead of at the midpoint. Default: True
330  splitThreshold : `float`, optional
331  Minimum DCR difference within a subfilter required to use ``splitSubfilters``
332  amplifyModel : `float`, optional
333  Multiplication factor to amplify differences between model planes.
334  Used to speed convergence of iterative forward modeling.
335 
336  Returns
337  -------
338  templateImage : `lsst.afw.image.ImageF`
339  The DCR-matched template
340 
341  Raises
342  ------
343  ValueError
344  If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set.
345  """
346  if self.filter is None:
347  raise ValueError("'filterInfo' must be set for the DcrModel in order to calculate DCR.")
348  if exposure is not None:
349  visitInfo = exposure.getInfo().getVisitInfo()
350  bbox = exposure.getBBox()
351  wcs = exposure.getInfo().getWcs()
352  elif visitInfo is None or bbox is None or wcs is None:
353  raise ValueError("Either exposure or visitInfo, bbox, and wcs must be set.")
354  dcrShift = calculateDcr(visitInfo, wcs, self.filter, len(self), splitSubfilters=splitSubfilters)
355  templateImage = afwImage.ImageF(bbox)
356  refModel = self.getReferenceImage(bbox)
357  for subfilter, dcr in enumerate(dcrShift):
358  if amplifyModel > 1:
359  model = (self[subfilter][bbox].array - refModel)*amplifyModel + refModel
360  else:
361  model = self[subfilter][bbox].array
362  templateImage.array += applyDcr(model, dcr, splitSubfilters=splitSubfilters,
363  splitThreshold=splitThreshold, order=order)
364  return templateImage
365 
366  def buildMatchedExposure(self, exposure=None,
367  visitInfo=None, bbox=None, wcs=None, mask=None):
368  """Wrapper to create an exposure from a template image.
369 
370  Parameters
371  ----------
372  exposure : `lsst.afw.image.Exposure`, optional
373  The input exposure to build a matched template for.
374  May be omitted if all of the metadata is supplied separately
375  visitInfo : `lsst.afw.image.VisitInfo`, optional
376  Metadata for the exposure. Ignored if ``exposure`` is set.
377  bbox : `lsst.afw.geom.Box2I`, optional
378  Sub-region of the coadd. Ignored if ``exposure`` is set.
379  wcs : `lsst.afw.geom.SkyWcs`, optional
380  Coordinate system definition (wcs) for the exposure.
381  Ignored if ``exposure`` is set.
382  mask : `lsst.afw.image.Mask`, optional
383  reference mask to use for the template image.
384 
385  Returns
386  -------
387  templateExposure : `lsst.afw.image.exposureF`
388  The DCR-matched template
389  """
390  if bbox is None:
391  bbox = exposure.getBBox()
392  templateImage = self.buildMatchedTemplate(exposure=exposure, visitInfo=visitInfo,
393  bbox=bbox, wcs=wcs, mask=mask)
394  maskedImage = afwImage.MaskedImageF(bbox)
395  maskedImage.image = templateImage[bbox]
396  maskedImage.mask = self.mask[bbox]
397  maskedImage.variance = self.variance[bbox]
398  # The variance of the stacked image will be `dcrNumSubfilters`
399  # times the variance of the individual subfilters.
400  maskedImage.variance *= self.dcrNumSubfilters
401  templateExposure = afwImage.ExposureF(bbox, wcs)
402  templateExposure.setMaskedImage(maskedImage[bbox])
403  templateExposure.setPsf(self.psf)
404  templateExposure.setFilter(self.filter)
405  if self.photoCalib is None:
406  raise RuntimeError("No PhotoCalib set for the DcrModel. "
407  "If the DcrModel was created from a masked image"
408  " you must also specify the photoCalib.")
409  templateExposure.setPhotoCalib(self.photoCalib)
410  return templateExposure
411 
412  def conditionDcrModel(self, modelImages, bbox, gain=1.):
413  """Average two iterations' solutions to reduce oscillations.
414 
415  Parameters
416  ----------
417  modelImages : `list` of `lsst.afw.image.Image`
418  The new DCR model images from the current iteration.
419  The values will be modified in place.
420  bbox : `lsst.afw.geom.Box2I`
421  Sub-region of the coadd
422  gain : `float`, optional
423  Relative weight to give the new solution when updating the model.
424  Defaults to 1.0, which gives equal weight to both solutions.
425  """
426  # Calculate weighted averages of the images.
427  for model, newModel in zip(self, modelImages):
428  newModel *= gain
429  newModel += model[bbox]
430  newModel /= 1. + gain
431 
432  def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor,
433  regularizationWidth=2):
434  """Restrict large variations in the model between iterations.
435 
436  Parameters
437  ----------
438  subfilter : `int`
439  Index of the current subfilter within the full band.
440  newModel : `lsst.afw.image.Image`
441  The new DCR model for one subfilter from the current iteration.
442  Values in ``newModel`` that are extreme compared with the last
443  iteration are modified in place.
444  bbox : `lsst.afw.geom.Box2I`
445  Sub-region to coadd
446  regularizationFactor : `float`
447  Maximum relative change of the model allowed between iterations.
448  regularizationWidth : int, optional
449  Minimum radius of a region to include in regularization, in pixels.
450  """
451  refImage = self[subfilter][bbox].array
452  highThreshold = np.abs(refImage)*regularizationFactor
453  lowThreshold = refImage/regularizationFactor
454  newImage = newModel.array
455  self.applyImageThresholds(newImage, highThreshold=highThreshold, lowThreshold=lowThreshold,
456  regularizationWidth=regularizationWidth)
457 
458  def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor,
459  regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
460  """Restrict large variations in the model between subfilters.
461 
462  Parameters
463  ----------
464  modelImages : `list` of `lsst.afw.image.Image`
465  The new DCR model images from the current iteration.
466  The values will be modified in place.
467  bbox : `lsst.afw.geom.Box2I`
468  Sub-region to coadd
469  statsCtrl : `lsst.afw.math.StatisticsControl`
470  Statistics control object for coaddition.
471  regularizationFactor : `float`
472  Maximum relative change of the model allowed between subfilters.
473  regularizationWidth : `int`, optional
474  Minimum radius of a region to include in regularization, in pixels.
475  mask : `lsst.afw.image.Mask`, optional
476  Optional alternate mask
477  convergenceMaskPlanes : `list` of `str`, or `str`, optional
478  Mask planes to use to calculate convergence.
479 
480  Notes
481  -----
482  This implementation of frequency regularization restricts each subfilter
483  image to be a smoothly-varying function times a reference image.
484  """
485  # ``regularizationFactor`` is the maximum change between subfilter images, so the maximum difference
486  # between one subfilter image and the average will be the square root of that.
487  maxDiff = np.sqrt(regularizationFactor)
488  noiseLevel = self.calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
489  referenceImage = self.getReferenceImage(bbox)
490  badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
491  if np.sum(~badPixels) == 0:
492  # Skip regularization if there are no valid pixels
493  return
494  referenceImage[badPixels] = 0.
495  filterWidth = regularizationWidth
496  fwhm = 2.*filterWidth
497  # The noise should be lower in the smoothed image by sqrt(Nsmooth) ~ fwhm pixels
498  noiseLevel /= fwhm
499  smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth, mode='constant')
500  # Add a three sigma offset to both the reference and model to prevent dividing by zero.
501  # Note that this will also slightly suppress faint variations in color.
502  smoothRef += 3.*noiseLevel
503 
504  lowThreshold = smoothRef/maxDiff
505  highThreshold = smoothRef*maxDiff
506  for model in modelImages:
507  self.applyImageThresholds(model.array,
508  highThreshold=highThreshold,
509  lowThreshold=lowThreshold,
510  regularizationWidth=regularizationWidth)
511  smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth, mode='constant')
512  smoothModel += 3.*noiseLevel
513  relativeModel = smoothModel/smoothRef
514  # Now sharpen the smoothed relativeModel using an alpha of 3.
515  alpha = 3.
516  relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/alpha)
517  relativeModel += alpha*(relativeModel - relativeModel2)
518  model.array = relativeModel*referenceImage
519 
520  def calculateNoiseCutoff(self, image, statsCtrl, bufferSize,
521  convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
522  """Helper function to calculate the background noise level of an image.
523 
524  Parameters
525  ----------
526  image : `lsst.afw.image.Image`
527  The input image to evaluate the background noise properties.
528  statsCtrl : `lsst.afw.math.StatisticsControl`
529  Statistics control object for coaddition.
530  bufferSize : `int`
531  Number of additional pixels to exclude
532  from the edges of the bounding box.
533  convergenceMaskPlanes : `list` of `str`, or `str`
534  Mask planes to use to calculate convergence.
535  mask : `lsst.afw.image.Mask`, Optional
536  Optional alternate mask
537  bbox : `lsst.afw.geom.Box2I`, optional
538  Sub-region of the masked image to calculate the noise level over.
539 
540  Returns
541  -------
542  noiseCutoff : `float`
543  The threshold value to treat pixels as noise in an image..
544  """
545  if bbox is None:
546  bbox = self.bbox
547  if mask is None:
548  mask = self.mask[bbox]
549  bboxShrink = geom.Box2I(bbox)
550  bboxShrink.grow(-bufferSize)
551  convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
552 
553  backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
554  noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
555  return noiseCutoff
556 
557  def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2):
558  """Restrict image values to be between upper and lower limits.
559 
560  This method flags all pixels in an image that are outside of the given
561  threshold values. The threshold values are taken from a reference image,
562  so noisy pixels are likely to get flagged. In order to exclude those
563  noisy pixels, the array of flags is eroded and dilated, which removes
564  isolated pixels outside of the thresholds from the list of pixels to be
565  modified. Pixels that remain flagged after this operation have their
566  values set to the appropriate upper or lower threshold value.
567 
568  Parameters
569  ----------
570  image : `numpy.ndarray`
571  The image to apply the thresholds to.
572  The values will be modified in place.
573  highThreshold : `numpy.ndarray`, optional
574  Array of upper limit values for each pixel of ``image``.
575  lowThreshold : `numpy.ndarray`, optional
576  Array of lower limit values for each pixel of ``image``.
577  regularizationWidth : `int`, optional
578  Minimum radius of a region to include in regularization, in pixels.
579  """
580  # Generate the structure for binary erosion and dilation, which is used to remove noise-like pixels.
581  # Groups of pixels with a radius smaller than ``regularizationWidth``
582  # will be excluded from regularization.
583  filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
584  regularizationWidth)
585  if highThreshold is not None:
586  highPixels = image > highThreshold
587  if regularizationWidth > 0:
588  # Erode and dilate ``highPixels`` to exclude noisy pixels.
589  highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
590  image[highPixels] = highThreshold[highPixels]
591  if lowThreshold is not None:
592  lowPixels = image < lowThreshold
593  if regularizationWidth > 0:
594  # Erode and dilate ``lowPixels`` to exclude noisy pixels.
595  lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
596  image[lowPixels] = lowThreshold[lowPixels]
597 
598 
599 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0.,
600  doPrefilter=True, order=3):
601  """Shift an image along the X and Y directions.
602 
603  Parameters
604  ----------
605  image : `numpy.ndarray`
606  The input image to shift.
607  dcr : `tuple`
608  Shift calculated with ``calculateDcr``.
609  Uses numpy axes ordering (Y, X).
610  If ``splitSubfilters`` is set, each element is itself a `tuple`
611  of two `float`, corresponding to the DCR shift at the two wavelengths.
612  Otherwise, each element is a `float` corresponding to the DCR shift at
613  the effective wavelength of the subfilter.
614  useInverse : `bool`, optional
615  Apply the shift in the opposite direction. Default: False
616  splitSubfilters : `bool`, optional
617  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
618  instead of at the midpoint. Default: False
619  splitThreshold : `float`, optional
620  Minimum DCR difference within a subfilter required to use ``splitSubfilters``
621  doPrefilter : `bool`, optional
622  Spline filter the image before shifting, if set. Filtering is required,
623  so only set to False if the image is already filtered.
624  Filtering takes ~20% of the time of shifting, so if `applyDcr` will be
625  called repeatedly on the same image it is more efficient to precalculate
626  the filter.
627  order : `int`, optional
628  The order of the spline interpolation, default is 3.
629 
630  Returns
631  -------
632  shiftedImage : `numpy.ndarray`
633  A copy of the input image with the specified shift applied.
634  """
635  if doPrefilter:
636  prefilteredImage = ndimage.spline_filter(image, order=order)
637  else:
638  prefilteredImage = image
639  if splitSubfilters:
640  shiftAmp = np.max(np.abs([_dcr0 - _dcr1 for _dcr0, _dcr1 in zip(dcr[0], dcr[1])]))
641  if shiftAmp >= splitThreshold:
642  if useInverse:
643  shift = [-1.*s for s in dcr[0]]
644  shift1 = [-1.*s for s in dcr[1]]
645  else:
646  shift = dcr[0]
647  shift1 = dcr[1]
648  shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=False, order=order)
649  shiftedImage += ndimage.shift(prefilteredImage, shift1, prefilter=False, order=order)
650  shiftedImage /= 2.
651  return shiftedImage
652  else:
653  # If the difference in the DCR shifts is less than the threshold,
654  # then just use the average shift for efficiency.
655  dcr = (np.mean(dcr[0]), np.mean(dcr[1]))
656  if useInverse:
657  shift = [-1.*s for s in dcr]
658  else:
659  shift = dcr
660  shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=False, order=order)
661  return shiftedImage
662 
663 
664 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
665  """Calculate the shift in pixels of an exposure due to DCR.
666 
667  Parameters
668  ----------
669  visitInfo : `lsst.afw.image.VisitInfo`
670  Metadata for the exposure.
671  wcs : `lsst.afw.geom.SkyWcs`
672  Coordinate system definition (wcs) for the exposure.
673  filterInfo : `lsst.afw.image.Filter`
674  The filter definition, set in the current instruments' obs package.
675  dcrNumSubfilters : `int`
676  Number of sub-filters used to model chromatic effects within a band.
677  splitSubfilters : `bool`, optional
678  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
679  instead of at the midpoint. Default: False
680 
681  Returns
682  -------
683  dcrShift : `tuple` of two `float`
684  The 2D shift due to DCR, in pixels.
685  Uses numpy axes ordering (Y, X).
686  """
687  rotation = calculateImageParallacticAngle(visitInfo, wcs)
688  dcrShift = []
689  weight = [0.75, 0.25]
690  lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
691  for wl0, wl1 in wavelengthGenerator(filterInfo, dcrNumSubfilters):
692  # Note that diffRefractAmp can be negative, since it's relative to the midpoint of the full band
693  diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
694  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
695  observatory=visitInfo.getObservatory(),
696  weather=visitInfo.getWeather())
697  diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
698  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
699  observatory=visitInfo.getObservatory(),
700  weather=visitInfo.getWeather())
701  if splitSubfilters:
702  diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
703  diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
704  diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
705  diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
706  shiftX = [diffRefractPix*np.sin(rotation.asRadians()) for diffRefractPix in diffRefractArr]
707  shiftY = [diffRefractPix*np.cos(rotation.asRadians()) for diffRefractPix in diffRefractArr]
708  dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
709  else:
710  diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
711  diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
712  shiftX = diffRefractPix*np.sin(rotation.asRadians())
713  shiftY = diffRefractPix*np.cos(rotation.asRadians())
714  dcrShift.append((shiftY, shiftX))
715  return dcrShift
716 
717 
718 def calculateImageParallacticAngle(visitInfo, wcs):
719  """Calculate the total sky rotation angle of an exposure.
720 
721  Parameters
722  ----------
723  visitInfo : `lsst.afw.image.VisitInfo`
724  Metadata for the exposure.
725  wcs : `lsst.afw.geom.SkyWcs`
726  Coordinate system definition (wcs) for the exposure.
727 
728  Returns
729  -------
730  `lsst.geom.Angle`
731  The rotation of the image axis, East from North.
732  Equal to the parallactic angle plus any additional rotation of the
733  coordinate system.
734  A rotation angle of 0 degrees is defined with
735  North along the +y axis and East along the +x axis.
736  A rotation angle of 90 degrees is defined with
737  North along the +x axis and East along the -y axis.
738  """
739  parAngle = visitInfo.getBoresightParAngle().asRadians()
740  cd = wcs.getCdMatrix()
741  if wcs.isFlipped:
742  cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
743  rotAngle = (cdAngle + parAngle)*geom.radians
744  else:
745  cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
746  rotAngle = (cdAngle - parAngle)*geom.radians
747  return rotAngle
748 
749 
750 def wavelengthGenerator(filterInfo, dcrNumSubfilters):
751  """Iterate over the wavelength endpoints of subfilters.
752 
753  Parameters
754  ----------
755  filterInfo : `lsst.afw.image.Filter`
756  The filter definition, set in the current instruments' obs package.
757  dcrNumSubfilters : `int`
758  Number of sub-filters used to model chromatic effects within a band.
759 
760  Yields
761  ------
762  `tuple` of two `float`
763  The next set of wavelength endpoints for a subfilter, in nm.
764  """
765  lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
766  lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
767  wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
768  for wl in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=False):
769  yield (wl, wl + wlStep)
def __setitem__(self, subfilter, maskedImage)
Definition: dcrModel.py:188
def calculateImageParallacticAngle(visitInfo, wcs)
Definition: dcrModel.py:718
def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0., doPrefilter=True, order=3)
Definition: dcrModel.py:600
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
Definition: dcrModel.py:433
def __getitem__(self, subfilter)
Definition: dcrModel.py:163
def calculateNoiseCutoff(self, image, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
Definition: dcrModel.py:521
def wavelengthGenerator(filterInfo, dcrNumSubfilters)
Definition: dcrModel.py:750
def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2)
Definition: dcrModel.py:557
def buildMatchedExposure(self, exposure=None, visitInfo=None, bbox=None, wcs=None, mask=None)
Definition: dcrModel.py:367
def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, kwargs)
Definition: dcrModel.py:109
def getReferenceImage(self, bbox=None)
Definition: dcrModel.py:267
def buildMatchedTemplate(self, exposure=None, order=3, visitInfo=None, bbox=None, wcs=None, mask=None, splitSubfilters=True, splitThreshold=0., amplifyModel=1.)
Definition: dcrModel.py:308
def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None, photoCalib=None)
Definition: dcrModel.py:62
def assign(self, dcrSubModel, bbox=None)
Definition: dcrModel.py:283
def __init__(self, modelImages, filterInfo=None, psf=None, mask=None, variance=None, photoCalib=None)
Definition: dcrModel.py:52
def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False)
Definition: dcrModel.py:664
def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED")
Definition: dcrModel.py:459
def conditionDcrModel(self, modelImages, bbox, gain=1.)
Definition: dcrModel.py:412