lsst.ip.diffim  tickets.DM-23835-gc122a6f8d5
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  templateExposure = afwImage.ExposureF(bbox, wcs)
399  templateExposure.setMaskedImage(maskedImage[bbox])
400  templateExposure.setPsf(self.psf)
401  templateExposure.setFilter(self.filter)
402  if self.photoCalib is None:
403  raise RuntimeError("No PhotoCalib set for the DcrModel. "
404  "If the DcrModel was created from a masked image"
405  " you must also specify the photoCalib.")
406  templateExposure.setPhotoCalib(self.photoCalib)
407  return templateExposure
408 
409  def conditionDcrModel(self, modelImages, bbox, gain=1.):
410  """Average two iterations' solutions to reduce oscillations.
411 
412  Parameters
413  ----------
414  modelImages : `list` of `lsst.afw.image.Image`
415  The new DCR model images from the current iteration.
416  The values will be modified in place.
417  bbox : `lsst.afw.geom.Box2I`
418  Sub-region of the coadd
419  gain : `float`, optional
420  Relative weight to give the new solution when updating the model.
421  Defaults to 1.0, which gives equal weight to both solutions.
422  """
423  # Calculate weighted averages of the images.
424  for model, newModel in zip(self, modelImages):
425  newModel *= gain
426  newModel += model[bbox]
427  newModel /= 1. + gain
428 
429  def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor,
430  regularizationWidth=2):
431  """Restrict large variations in the model between iterations.
432 
433  Parameters
434  ----------
435  subfilter : `int`
436  Index of the current subfilter within the full band.
437  newModel : `lsst.afw.image.Image`
438  The new DCR model for one subfilter from the current iteration.
439  Values in ``newModel`` that are extreme compared with the last
440  iteration are modified in place.
441  bbox : `lsst.afw.geom.Box2I`
442  Sub-region to coadd
443  regularizationFactor : `float`
444  Maximum relative change of the model allowed between iterations.
445  regularizationWidth : int, optional
446  Minimum radius of a region to include in regularization, in pixels.
447  """
448  refImage = self[subfilter][bbox].array
449  highThreshold = np.abs(refImage)*regularizationFactor
450  lowThreshold = refImage/regularizationFactor
451  newImage = newModel.array
452  self.applyImageThresholds(newImage, highThreshold=highThreshold, lowThreshold=lowThreshold,
453  regularizationWidth=regularizationWidth)
454 
455  def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor,
456  regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
457  """Restrict large variations in the model between subfilters.
458 
459  Parameters
460  ----------
461  modelImages : `list` of `lsst.afw.image.Image`
462  The new DCR model images from the current iteration.
463  The values will be modified in place.
464  bbox : `lsst.afw.geom.Box2I`
465  Sub-region to coadd
466  statsCtrl : `lsst.afw.math.StatisticsControl`
467  Statistics control object for coaddition.
468  regularizationFactor : `float`
469  Maximum relative change of the model allowed between subfilters.
470  regularizationWidth : `int`, optional
471  Minimum radius of a region to include in regularization, in pixels.
472  mask : `lsst.afw.image.Mask`, optional
473  Optional alternate mask
474  convergenceMaskPlanes : `list` of `str`, or `str`, optional
475  Mask planes to use to calculate convergence.
476 
477  Notes
478  -----
479  This implementation of frequency regularization restricts each subfilter
480  image to be a smoothly-varying function times a reference image.
481  """
482  # ``regularizationFactor`` is the maximum change between subfilter images, so the maximum difference
483  # between one subfilter image and the average will be the square root of that.
484  maxDiff = np.sqrt(regularizationFactor)
485  noiseLevel = self.calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
486  referenceImage = self.getReferenceImage(bbox)
487  badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
488  if np.sum(~badPixels) == 0:
489  # Skip regularization if there are no valid pixels
490  return
491  referenceImage[badPixels] = 0.
492  filterWidth = regularizationWidth
493  fwhm = 2.*filterWidth
494  # The noise should be lower in the smoothed image by sqrt(Nsmooth) ~ fwhm pixels
495  noiseLevel /= fwhm
496  smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth, mode='constant')
497  # Add a three sigma offset to both the reference and model to prevent dividing by zero.
498  # Note that this will also slightly suppress faint variations in color.
499  smoothRef += 3.*noiseLevel
500 
501  lowThreshold = smoothRef/maxDiff
502  highThreshold = smoothRef*maxDiff
503  for model in modelImages:
504  self.applyImageThresholds(model.array,
505  highThreshold=highThreshold,
506  lowThreshold=lowThreshold,
507  regularizationWidth=regularizationWidth)
508  smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth, mode='constant')
509  smoothModel += 3.*noiseLevel
510  relativeModel = smoothModel/smoothRef
511  # Now sharpen the smoothed relativeModel using an alpha of 3.
512  alpha = 3.
513  relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/alpha)
514  relativeModel += alpha*(relativeModel - relativeModel2)
515  model.array = relativeModel*referenceImage
516 
517  def calculateNoiseCutoff(self, image, statsCtrl, bufferSize,
518  convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
519  """Helper function to calculate the background noise level of an image.
520 
521  Parameters
522  ----------
523  image : `lsst.afw.image.Image`
524  The input image to evaluate the background noise properties.
525  statsCtrl : `lsst.afw.math.StatisticsControl`
526  Statistics control object for coaddition.
527  bufferSize : `int`
528  Number of additional pixels to exclude
529  from the edges of the bounding box.
530  convergenceMaskPlanes : `list` of `str`, or `str`
531  Mask planes to use to calculate convergence.
532  mask : `lsst.afw.image.Mask`, Optional
533  Optional alternate mask
534  bbox : `lsst.afw.geom.Box2I`, optional
535  Sub-region of the masked image to calculate the noise level over.
536 
537  Returns
538  -------
539  noiseCutoff : `float`
540  The threshold value to treat pixels as noise in an image..
541  """
542  if bbox is None:
543  bbox = self.bbox
544  if mask is None:
545  mask = self.mask[bbox]
546  bboxShrink = geom.Box2I(bbox)
547  bboxShrink.grow(-bufferSize)
548  convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
549 
550  backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
551  noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
552  return noiseCutoff
553 
554  def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2):
555  """Restrict image values to be between upper and lower limits.
556 
557  This method flags all pixels in an image that are outside of the given
558  threshold values. The threshold values are taken from a reference image,
559  so noisy pixels are likely to get flagged. In order to exclude those
560  noisy pixels, the array of flags is eroded and dilated, which removes
561  isolated pixels outside of the thresholds from the list of pixels to be
562  modified. Pixels that remain flagged after this operation have their
563  values set to the appropriate upper or lower threshold value.
564 
565  Parameters
566  ----------
567  image : `numpy.ndarray`
568  The image to apply the thresholds to.
569  The values will be modified in place.
570  highThreshold : `numpy.ndarray`, optional
571  Array of upper limit values for each pixel of ``image``.
572  lowThreshold : `numpy.ndarray`, optional
573  Array of lower limit values for each pixel of ``image``.
574  regularizationWidth : `int`, optional
575  Minimum radius of a region to include in regularization, in pixels.
576  """
577  # Generate the structure for binary erosion and dilation, which is used to remove noise-like pixels.
578  # Groups of pixels with a radius smaller than ``regularizationWidth``
579  # will be excluded from regularization.
580  filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
581  regularizationWidth)
582  if highThreshold is not None:
583  highPixels = image > highThreshold
584  if regularizationWidth > 0:
585  # Erode and dilate ``highPixels`` to exclude noisy pixels.
586  highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
587  image[highPixels] = highThreshold[highPixels]
588  if lowThreshold is not None:
589  lowPixels = image < lowThreshold
590  if regularizationWidth > 0:
591  # Erode and dilate ``lowPixels`` to exclude noisy pixels.
592  lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
593  image[lowPixels] = lowThreshold[lowPixels]
594 
595 
596 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0.,
597  doPrefilter=True, order=3):
598  """Shift an image along the X and Y directions.
599 
600  Parameters
601  ----------
602  image : `numpy.ndarray`
603  The input image to shift.
604  dcr : `tuple`
605  Shift calculated with ``calculateDcr``.
606  Uses numpy axes ordering (Y, X).
607  If ``splitSubfilters`` is set, each element is itself a `tuple`
608  of two `float`, corresponding to the DCR shift at the two wavelengths.
609  Otherwise, each element is a `float` corresponding to the DCR shift at
610  the effective wavelength of the subfilter.
611  useInverse : `bool`, optional
612  Apply the shift in the opposite direction. Default: False
613  splitSubfilters : `bool`, optional
614  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
615  instead of at the midpoint. Default: False
616  splitThreshold : `float`, optional
617  Minimum DCR difference within a subfilter required to use ``splitSubfilters``
618  doPrefilter : `bool`, optional
619  Spline filter the image before shifting, if set. Filtering is required,
620  so only set to False if the image is already filtered.
621  Filtering takes ~20% of the time of shifting, so if `applyDcr` will be
622  called repeatedly on the same image it is more efficient to precalculate
623  the filter.
624  order : `int`, optional
625  The order of the spline interpolation, default is 3.
626 
627  Returns
628  -------
629  shiftedImage : `numpy.ndarray`
630  A copy of the input image with the specified shift applied.
631  """
632  if doPrefilter:
633  prefilteredImage = ndimage.spline_filter(image, order=order)
634  else:
635  prefilteredImage = image
636  if splitSubfilters:
637  shiftAmp = np.max(np.abs([_dcr0 - _dcr1 for _dcr0, _dcr1 in zip(dcr[0], dcr[1])]))
638  if shiftAmp >= splitThreshold:
639  if useInverse:
640  shift = [-1.*s for s in dcr[0]]
641  shift1 = [-1.*s for s in dcr[1]]
642  else:
643  shift = dcr[0]
644  shift1 = dcr[1]
645  shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=False, order=order)
646  shiftedImage += ndimage.shift(prefilteredImage, shift1, prefilter=False, order=order)
647  shiftedImage /= 2.
648  return shiftedImage
649  else:
650  # If the difference in the DCR shifts is less than the threshold,
651  # then just use the average shift for efficiency.
652  dcr = (np.mean(dcr[0]), np.mean(dcr[1]))
653  if useInverse:
654  shift = [-1.*s for s in dcr]
655  else:
656  shift = dcr
657  shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=False, order=order)
658  return shiftedImage
659 
660 
661 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
662  """Calculate the shift in pixels of an exposure due to DCR.
663 
664  Parameters
665  ----------
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.
670  filterInfo : `lsst.afw.image.Filter`
671  The filter definition, set in the current instruments' obs package.
672  dcrNumSubfilters : `int`
673  Number of sub-filters used to model chromatic effects within a band.
674  splitSubfilters : `bool`, optional
675  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
676  instead of at the midpoint. Default: False
677 
678  Returns
679  -------
680  dcrShift : `tuple` of two `float`
681  The 2D shift due to DCR, in pixels.
682  Uses numpy axes ordering (Y, X).
683  """
684  rotation = calculateImageParallacticAngle(visitInfo, wcs)
685  dcrShift = []
686  weight = [0.75, 0.25]
687  lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
688  for wl0, wl1 in wavelengthGenerator(filterInfo, dcrNumSubfilters):
689  # Note that diffRefractAmp can be negative, since it's relative to the midpoint of the full band
690  diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
691  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
692  observatory=visitInfo.getObservatory(),
693  weather=visitInfo.getWeather())
694  diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
695  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
696  observatory=visitInfo.getObservatory(),
697  weather=visitInfo.getWeather())
698  if splitSubfilters:
699  diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
700  diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
701  diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
702  diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
703  shiftX = [diffRefractPix*np.sin(rotation.asRadians()) for diffRefractPix in diffRefractArr]
704  shiftY = [diffRefractPix*np.cos(rotation.asRadians()) for diffRefractPix in diffRefractArr]
705  dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
706  else:
707  diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
708  diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
709  shiftX = diffRefractPix*np.sin(rotation.asRadians())
710  shiftY = diffRefractPix*np.cos(rotation.asRadians())
711  dcrShift.append((shiftY, shiftX))
712  return dcrShift
713 
714 
715 def calculateImageParallacticAngle(visitInfo, wcs):
716  """Calculate the total sky rotation angle of an exposure.
717 
718  Parameters
719  ----------
720  visitInfo : `lsst.afw.image.VisitInfo`
721  Metadata for the exposure.
722  wcs : `lsst.afw.geom.SkyWcs`
723  Coordinate system definition (wcs) for the exposure.
724 
725  Returns
726  -------
727  `lsst.geom.Angle`
728  The rotation of the image axis, East from North.
729  Equal to the parallactic angle plus any additional rotation of the
730  coordinate system.
731  A rotation angle of 0 degrees is defined with
732  North along the +y axis and East along the +x axis.
733  A rotation angle of 90 degrees is defined with
734  North along the +x axis and East along the -y axis.
735  """
736  parAngle = visitInfo.getBoresightParAngle().asRadians()
737  cd = wcs.getCdMatrix()
738  if wcs.isFlipped:
739  cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
740  rotAngle = (cdAngle + parAngle)*geom.radians
741  else:
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  return rotAngle
745 
746 
747 def wavelengthGenerator(filterInfo, dcrNumSubfilters):
748  """Iterate over the wavelength endpoints of subfilters.
749 
750  Parameters
751  ----------
752  filterInfo : `lsst.afw.image.Filter`
753  The filter definition, set in the current instruments' obs package.
754  dcrNumSubfilters : `int`
755  Number of sub-filters used to model chromatic effects within a band.
756 
757  Yields
758  ------
759  `tuple` of two `float`
760  The next set of wavelength endpoints for a subfilter, in nm.
761  """
762  lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
763  lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
764  wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
765  for wl in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=False):
766  yield (wl, wl + wlStep)
lsst::ip::diffim.dcrModel.DcrModel.buildMatchedTemplate
def buildMatchedTemplate(self, exposure=None, order=3, visitInfo=None, bbox=None, wcs=None, mask=None, splitSubfilters=True, splitThreshold=0., amplifyModel=1.)
Definition: dcrModel.py:306
lsst::afw::image
lsst::ip::diffim.dcrModel.DcrModel.fromImage
def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None, photoCalib=None)
Definition: dcrModel.py:62
lsst::ip::diffim.dcrModel.DcrModel.assign
def assign(self, dcrSubModel, bbox=None)
Definition: dcrModel.py:283
lsst::ip::diffim.dcrModel.DcrModel.getReferenceImage
def getReferenceImage(self, bbox=None)
Definition: dcrModel.py:267
lsst::ip::diffim.dcrModel.applyDcr
def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0., doPrefilter=True, order=3)
Definition: dcrModel.py:596
lsst::ip::diffim.dcrModel.DcrModel.variance
def variance(self)
Definition: dcrModel.py:257
lsst::ip::diffim.dcrModel.DcrModel.buildMatchedExposure
def buildMatchedExposure(self, exposure=None, visitInfo=None, bbox=None, wcs=None, mask=None)
Definition: dcrModel.py:366
lsst::ip::diffim.dcrModel.DcrModel.regularizeModelFreq
def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED")
Definition: dcrModel.py:455
lsst::ip::diffim.dcrModel.DcrModel.__len__
def __len__(self)
Definition: dcrModel.py:153
lsst::ip::diffim.dcrModel.DcrModel.applyImageThresholds
def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2)
Definition: dcrModel.py:554
lsst::ip::diffim.dcrModel.DcrModel.fromDataRef
def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, **kwargs)
Definition: dcrModel.py:109
lsst::ip::diffim.dcrModel.DcrModel.__setitem__
def __setitem__(self, subfilter, maskedImage)
Definition: dcrModel.py:188
lsst::afw::coord::refraction
lsst::ip::diffim.dcrModel.DcrModel._mask
_mask
Definition: dcrModel.py:57
lsst::ip::diffim.dcrModel.calculateImageParallacticAngle
def calculateImageParallacticAngle(visitInfo, wcs)
Definition: dcrModel.py:715
lsst::ip::diffim.dcrModel.DcrModel._filter
_filter
Definition: dcrModel.py:55
lsst::ip::diffim.dcrModel.DcrModel.psf
def psf(self)
Definition: dcrModel.py:224
lsst::ip::diffim.dcrModel.DcrModel.__init__
def __init__(self, modelImages, filterInfo=None, psf=None, mask=None, variance=None, photoCalib=None)
Definition: dcrModel.py:52
lsst::ip::diffim.dcrModel.DcrModel.bbox
def bbox(self)
Definition: dcrModel.py:235
lsst::ip::diffim.dcrModel.DcrModel.filter
def filter(self)
Definition: dcrModel.py:213
lsst::ip::diffim.dcrModel.DcrModel.mask
def mask(self)
Definition: dcrModel.py:246
lsst::ip::diffim.dcrModel.DcrModel.__getitem__
def __getitem__(self, subfilter)
Definition: dcrModel.py:163
lsst::ip::diffim.dcrModel.DcrModel.calculateNoiseCutoff
def calculateNoiseCutoff(self, image, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
Definition: dcrModel.py:517
lsst::ip::diffim.dcrModel.DcrModel.photoCalib
photoCalib
Definition: dcrModel.py:59
lsst::ip::diffim.dcrModel.calculateDcr
def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False)
Definition: dcrModel.py:661
lsst::ip::diffim.dcrModel.DcrModel.regularizeModelIter
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
Definition: dcrModel.py:429
lsst::geom
lsst::ip::diffim.dcrModel.DcrModel
Definition: dcrModel.py:32
lsst::ip::diffim.dcrModel.DcrModel.conditionDcrModel
def conditionDcrModel(self, modelImages, bbox, gain=1.)
Definition: dcrModel.py:409
lsst::ip::diffim.dcrModel.DcrModel.modelImages
modelImages
Definition: dcrModel.py:54
lsst::ip::diffim.dcrModel.DcrModel._variance
_variance
Definition: dcrModel.py:58
lsst::geom::Box2I
lsst::ip::diffim.dcrModel.DcrModel._psf
_psf
Definition: dcrModel.py:56
lsst::ip::diffim.dcrModel.wavelengthGenerator
def wavelengthGenerator(filterInfo, dcrNumSubfilters)
Definition: dcrModel.py:747
lsst::ip::diffim.dcrModel.DcrModel.dcrNumSubfilters
dcrNumSubfilters
Definition: dcrModel.py:53