LSSTApplications  20.0.0
LSSTDataManagementBasePackage
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 Gen 2 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  @classmethod
154  def fromQuantum(cls, availableCoaddRefs):
155  """Load an existing DcrModel from a Gen 3 repository.
156 
157  Parameters
158  ----------
159  availableCoaddRefs : `dict` of `int` : `lsst.daf.butler.DeferredDatasetHandle`
160  Dictionary of spatially relevant retrieved coadd patches,
161  indexed by their sequential patch number.
162 
163  Returns
164  -------
165  dcrModel : `lsst.pipe.tasks.DcrModel`
166  Best fit model of the true sky after correcting chromatic effects.
167  """
168  filterInfo = None
169  psf = None
170  mask = None
171  variance = None
172  photoCalib = None
173  modelImages = [None]*len(availableCoaddRefs)
174 
175  for coaddRef in availableCoaddRefs:
176  subfilter = coaddRef.dataId["subfilter"]
177  dcrCoadd = coaddRef.get()
178  if filterInfo is None:
179  filterInfo = dcrCoadd.getFilter()
180  if psf is None:
181  psf = dcrCoadd.getPsf()
182  if mask is None:
183  mask = dcrCoadd.mask
184  if variance is None:
185  variance = dcrCoadd.variance
186  if photoCalib is None:
187  photoCalib = dcrCoadd.getPhotoCalib()
188  modelImages[subfilter] = dcrCoadd.image
189  return cls(modelImages, filterInfo, psf, mask, variance, photoCalib)
190 
191  def __len__(self):
192  """Return the number of subfilters.
193 
194  Returns
195  -------
196  dcrNumSubfilters : `int`
197  The number of DCR subfilters in the model.
198  """
199  return self.dcrNumSubfilters
200 
201  def __getitem__(self, subfilter):
202  """Iterate over the subfilters of the DCR model.
203 
204  Parameters
205  ----------
206  subfilter : `int`
207  Index of the current ``subfilter`` within the full band.
208  Negative indices are allowed, and count in reverse order
209  from the highest ``subfilter``.
210 
211  Returns
212  -------
213  modelImage : `lsst.afw.image.Image`
214  The DCR model for the given ``subfilter``.
215 
216  Raises
217  ------
218  IndexError
219  If the requested ``subfilter`` is greater or equal to the number
220  of subfilters in the model.
221  """
222  if np.abs(subfilter) >= len(self):
223  raise IndexError("subfilter out of bounds.")
224  return self.modelImages[subfilter]
225 
226  def __setitem__(self, subfilter, maskedImage):
227  """Update the model image for one subfilter.
228 
229  Parameters
230  ----------
231  subfilter : `int`
232  Index of the current subfilter within the full band.
233  maskedImage : `lsst.afw.image.Image`
234  The DCR model to set for the given ``subfilter``.
235 
236  Raises
237  ------
238  IndexError
239  If the requested ``subfilter`` is greater or equal to the number
240  of subfilters in the model.
241  ValueError
242  If the bounding box of the new image does not match.
243  """
244  if np.abs(subfilter) >= len(self):
245  raise IndexError("subfilter out of bounds.")
246  if maskedImage.getBBox() != self.bbox:
247  raise ValueError("The bounding box of a subfilter must not change.")
248  self.modelImages[subfilter] = maskedImage
249 
250  @property
251  def filter(self):
252  """Return the filter of the model.
253 
254  Returns
255  -------
256  filter : `lsst.afw.image.Filter`
257  The filter definition, set in the current instruments' obs package.
258  """
259  return self._filter
260 
261  @property
262  def psf(self):
263  """Return the psf of the model.
264 
265  Returns
266  -------
267  psf : `lsst.afw.detection.Psf`
268  Point spread function (PSF) of the model.
269  """
270  return self._psf
271 
272  @property
273  def bbox(self):
274  """Return the common bounding box of each subfilter image.
275 
276  Returns
277  -------
278  bbox : `lsst.afw.geom.Box2I`
279  Bounding box of the DCR model.
280  """
281  return self[0].getBBox()
282 
283  @property
284  def mask(self):
285  """Return the common mask of each subfilter image.
286 
287  Returns
288  -------
289  mask : `lsst.afw.image.Mask`
290  Mask plane of the DCR model.
291  """
292  return self._mask
293 
294  @property
295  def variance(self):
296  """Return the common variance of each subfilter image.
297 
298  Returns
299  -------
300  variance : `lsst.afw.image.Image`
301  Variance plane of the DCR model.
302  """
303  return self._variance
304 
305  def getReferenceImage(self, bbox=None):
306  """Calculate a reference image from the average of the subfilter images.
307 
308  Parameters
309  ----------
310  bbox : `lsst.afw.geom.Box2I`, optional
311  Sub-region of the coadd. Returns the entire image if `None`.
312 
313  Returns
314  -------
315  refImage : `numpy.ndarray`
316  The reference image with no chromatic effects applied.
317  """
318  bbox = bbox or self.bbox
319  return np.mean([model[bbox].array for model in self], axis=0)
320 
321  def assign(self, dcrSubModel, bbox=None):
322  """Update a sub-region of the ``DcrModel`` with new values.
323 
324  Parameters
325  ----------
326  dcrSubModel : `lsst.pipe.tasks.DcrModel`
327  New model of the true scene after correcting chromatic effects.
328  bbox : `lsst.afw.geom.Box2I`, optional
329  Sub-region of the coadd.
330  Defaults to the bounding box of ``dcrSubModel``.
331 
332  Raises
333  ------
334  ValueError
335  If the new model has a different number of subfilters.
336  """
337  if len(dcrSubModel) != len(self):
338  raise ValueError("The number of DCR subfilters must be the same "
339  "between the old and new models.")
340  bbox = bbox or self.bbox
341  for model, subModel in zip(self, dcrSubModel):
342  model.assign(subModel[bbox], bbox)
343 
344  def buildMatchedTemplate(self, exposure=None, order=3,
345  visitInfo=None, bbox=None, wcs=None, mask=None,
346  splitSubfilters=True, splitThreshold=0., amplifyModel=1.):
347  """Create a DCR-matched template image for an exposure.
348 
349  Parameters
350  ----------
351  exposure : `lsst.afw.image.Exposure`, optional
352  The input exposure to build a matched template for.
353  May be omitted if all of the metadata is supplied separately
354  order : `int`, optional
355  Interpolation order of the DCR shift.
356  visitInfo : `lsst.afw.image.VisitInfo`, optional
357  Metadata for the exposure. Ignored if ``exposure`` is set.
358  bbox : `lsst.afw.geom.Box2I`, optional
359  Sub-region of the coadd. Ignored if ``exposure`` is set.
360  wcs : `lsst.afw.geom.SkyWcs`, optional
361  Coordinate system definition (wcs) for the exposure.
362  Ignored if ``exposure`` is set.
363  mask : `lsst.afw.image.Mask`, optional
364  reference mask to use for the template image.
365  splitSubfilters : `bool`, optional
366  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
367  instead of at the midpoint. Default: True
368  splitThreshold : `float`, optional
369  Minimum DCR difference within a subfilter required to use ``splitSubfilters``
370  amplifyModel : `float`, optional
371  Multiplication factor to amplify differences between model planes.
372  Used to speed convergence of iterative forward modeling.
373 
374  Returns
375  -------
376  templateImage : `lsst.afw.image.ImageF`
377  The DCR-matched template
378 
379  Raises
380  ------
381  ValueError
382  If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set.
383  """
384  if self.filter is None:
385  raise ValueError("'filterInfo' must be set for the DcrModel in order to calculate DCR.")
386  if exposure is not None:
387  visitInfo = exposure.getInfo().getVisitInfo()
388  bbox = exposure.getBBox()
389  wcs = exposure.getInfo().getWcs()
390  elif visitInfo is None or bbox is None or wcs is None:
391  raise ValueError("Either exposure or visitInfo, bbox, and wcs must be set.")
392  dcrShift = calculateDcr(visitInfo, wcs, self.filter, len(self), splitSubfilters=splitSubfilters)
393  templateImage = afwImage.ImageF(bbox)
394  refModel = self.getReferenceImage(bbox)
395  for subfilter, dcr in enumerate(dcrShift):
396  if amplifyModel > 1:
397  model = (self[subfilter][bbox].array - refModel)*amplifyModel + refModel
398  else:
399  model = self[subfilter][bbox].array
400  templateImage.array += applyDcr(model, dcr, splitSubfilters=splitSubfilters,
401  splitThreshold=splitThreshold, order=order)
402  return templateImage
403 
404  def buildMatchedExposure(self, exposure=None,
405  visitInfo=None, bbox=None, wcs=None, mask=None):
406  """Wrapper to create an exposure from a template image.
407 
408  Parameters
409  ----------
410  exposure : `lsst.afw.image.Exposure`, optional
411  The input exposure to build a matched template for.
412  May be omitted if all of the metadata is supplied separately
413  visitInfo : `lsst.afw.image.VisitInfo`, optional
414  Metadata for the exposure. Ignored if ``exposure`` is set.
415  bbox : `lsst.afw.geom.Box2I`, optional
416  Sub-region of the coadd. Ignored if ``exposure`` is set.
417  wcs : `lsst.afw.geom.SkyWcs`, optional
418  Coordinate system definition (wcs) for the exposure.
419  Ignored if ``exposure`` is set.
420  mask : `lsst.afw.image.Mask`, optional
421  reference mask to use for the template image.
422 
423  Returns
424  -------
425  templateExposure : `lsst.afw.image.exposureF`
426  The DCR-matched template
427  """
428  if bbox is None:
429  bbox = exposure.getBBox()
430  templateImage = self.buildMatchedTemplate(exposure=exposure, visitInfo=visitInfo,
431  bbox=bbox, wcs=wcs, mask=mask)
432  maskedImage = afwImage.MaskedImageF(bbox)
433  maskedImage.image = templateImage[bbox]
434  maskedImage.mask = self.mask[bbox]
435  maskedImage.variance = self.variance[bbox]
436  # The variance of the stacked image will be `dcrNumSubfilters`
437  # times the variance of the individual subfilters.
438  maskedImage.variance *= self.dcrNumSubfilters
439  templateExposure = afwImage.ExposureF(bbox, wcs)
440  templateExposure.setMaskedImage(maskedImage[bbox])
441  templateExposure.setPsf(self.psf)
442  templateExposure.setFilter(self.filter)
443  if self.photoCalib is None:
444  raise RuntimeError("No PhotoCalib set for the DcrModel. "
445  "If the DcrModel was created from a masked image"
446  " you must also specify the photoCalib.")
447  templateExposure.setPhotoCalib(self.photoCalib)
448  return templateExposure
449 
450  def conditionDcrModel(self, modelImages, bbox, gain=1.):
451  """Average two iterations' solutions to reduce oscillations.
452 
453  Parameters
454  ----------
455  modelImages : `list` of `lsst.afw.image.Image`
456  The new DCR model images from the current iteration.
457  The values will be modified in place.
458  bbox : `lsst.afw.geom.Box2I`
459  Sub-region of the coadd
460  gain : `float`, optional
461  Relative weight to give the new solution when updating the model.
462  Defaults to 1.0, which gives equal weight to both solutions.
463  """
464  # Calculate weighted averages of the images.
465  for model, newModel in zip(self, modelImages):
466  newModel *= gain
467  newModel += model[bbox]
468  newModel /= 1. + gain
469 
470  def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor,
471  regularizationWidth=2):
472  """Restrict large variations in the model between iterations.
473 
474  Parameters
475  ----------
476  subfilter : `int`
477  Index of the current subfilter within the full band.
478  newModel : `lsst.afw.image.Image`
479  The new DCR model for one subfilter from the current iteration.
480  Values in ``newModel`` that are extreme compared with the last
481  iteration are modified in place.
482  bbox : `lsst.afw.geom.Box2I`
483  Sub-region to coadd
484  regularizationFactor : `float`
485  Maximum relative change of the model allowed between iterations.
486  regularizationWidth : int, optional
487  Minimum radius of a region to include in regularization, in pixels.
488  """
489  refImage = self[subfilter][bbox].array
490  highThreshold = np.abs(refImage)*regularizationFactor
491  lowThreshold = refImage/regularizationFactor
492  newImage = newModel.array
493  self.applyImageThresholds(newImage, highThreshold=highThreshold, lowThreshold=lowThreshold,
494  regularizationWidth=regularizationWidth)
495 
496  def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor,
497  regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
498  """Restrict large variations in the model between subfilters.
499 
500  Parameters
501  ----------
502  modelImages : `list` of `lsst.afw.image.Image`
503  The new DCR model images from the current iteration.
504  The values will be modified in place.
505  bbox : `lsst.afw.geom.Box2I`
506  Sub-region to coadd
507  statsCtrl : `lsst.afw.math.StatisticsControl`
508  Statistics control object for coaddition.
509  regularizationFactor : `float`
510  Maximum relative change of the model allowed between subfilters.
511  regularizationWidth : `int`, optional
512  Minimum radius of a region to include in regularization, in pixels.
513  mask : `lsst.afw.image.Mask`, optional
514  Optional alternate mask
515  convergenceMaskPlanes : `list` of `str`, or `str`, optional
516  Mask planes to use to calculate convergence.
517 
518  Notes
519  -----
520  This implementation of frequency regularization restricts each subfilter
521  image to be a smoothly-varying function times a reference image.
522  """
523  # ``regularizationFactor`` is the maximum change between subfilter images, so the maximum difference
524  # between one subfilter image and the average will be the square root of that.
525  maxDiff = np.sqrt(regularizationFactor)
526  noiseLevel = self.calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
527  referenceImage = self.getReferenceImage(bbox)
528  badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
529  if np.sum(~badPixels) == 0:
530  # Skip regularization if there are no valid pixels
531  return
532  referenceImage[badPixels] = 0.
533  filterWidth = regularizationWidth
534  fwhm = 2.*filterWidth
535  # The noise should be lower in the smoothed image by sqrt(Nsmooth) ~ fwhm pixels
536  noiseLevel /= fwhm
537  smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth, mode='constant')
538  # Add a three sigma offset to both the reference and model to prevent dividing by zero.
539  # Note that this will also slightly suppress faint variations in color.
540  smoothRef += 3.*noiseLevel
541 
542  lowThreshold = smoothRef/maxDiff
543  highThreshold = smoothRef*maxDiff
544  for model in modelImages:
545  self.applyImageThresholds(model.array,
546  highThreshold=highThreshold,
547  lowThreshold=lowThreshold,
548  regularizationWidth=regularizationWidth)
549  smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth, mode='constant')
550  smoothModel += 3.*noiseLevel
551  relativeModel = smoothModel/smoothRef
552  # Now sharpen the smoothed relativeModel using an alpha of 3.
553  alpha = 3.
554  relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/alpha)
555  relativeModel += alpha*(relativeModel - relativeModel2)
556  model.array = relativeModel*referenceImage
557 
558  def calculateNoiseCutoff(self, image, statsCtrl, bufferSize,
559  convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
560  """Helper function to calculate the background noise level of an image.
561 
562  Parameters
563  ----------
564  image : `lsst.afw.image.Image`
565  The input image to evaluate the background noise properties.
566  statsCtrl : `lsst.afw.math.StatisticsControl`
567  Statistics control object for coaddition.
568  bufferSize : `int`
569  Number of additional pixels to exclude
570  from the edges of the bounding box.
571  convergenceMaskPlanes : `list` of `str`, or `str`
572  Mask planes to use to calculate convergence.
573  mask : `lsst.afw.image.Mask`, Optional
574  Optional alternate mask
575  bbox : `lsst.afw.geom.Box2I`, optional
576  Sub-region of the masked image to calculate the noise level over.
577 
578  Returns
579  -------
580  noiseCutoff : `float`
581  The threshold value to treat pixels as noise in an image..
582  """
583  if bbox is None:
584  bbox = self.bbox
585  if mask is None:
586  mask = self.mask[bbox]
587  bboxShrink = geom.Box2I(bbox)
588  bboxShrink.grow(-bufferSize)
589  convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
590 
591  backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
592  noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
593  return noiseCutoff
594 
595  def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2):
596  """Restrict image values to be between upper and lower limits.
597 
598  This method flags all pixels in an image that are outside of the given
599  threshold values. The threshold values are taken from a reference image,
600  so noisy pixels are likely to get flagged. In order to exclude those
601  noisy pixels, the array of flags is eroded and dilated, which removes
602  isolated pixels outside of the thresholds from the list of pixels to be
603  modified. Pixels that remain flagged after this operation have their
604  values set to the appropriate upper or lower threshold value.
605 
606  Parameters
607  ----------
608  image : `numpy.ndarray`
609  The image to apply the thresholds to.
610  The values will be modified in place.
611  highThreshold : `numpy.ndarray`, optional
612  Array of upper limit values for each pixel of ``image``.
613  lowThreshold : `numpy.ndarray`, optional
614  Array of lower limit values for each pixel of ``image``.
615  regularizationWidth : `int`, optional
616  Minimum radius of a region to include in regularization, in pixels.
617  """
618  # Generate the structure for binary erosion and dilation, which is used to remove noise-like pixels.
619  # Groups of pixels with a radius smaller than ``regularizationWidth``
620  # will be excluded from regularization.
621  filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
622  regularizationWidth)
623  if highThreshold is not None:
624  highPixels = image > highThreshold
625  if regularizationWidth > 0:
626  # Erode and dilate ``highPixels`` to exclude noisy pixels.
627  highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
628  image[highPixels] = highThreshold[highPixels]
629  if lowThreshold is not None:
630  lowPixels = image < lowThreshold
631  if regularizationWidth > 0:
632  # Erode and dilate ``lowPixels`` to exclude noisy pixels.
633  lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
634  image[lowPixels] = lowThreshold[lowPixels]
635 
636 
637 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0.,
638  doPrefilter=True, order=3):
639  """Shift an image along the X and Y directions.
640 
641  Parameters
642  ----------
643  image : `numpy.ndarray`
644  The input image to shift.
645  dcr : `tuple`
646  Shift calculated with ``calculateDcr``.
647  Uses numpy axes ordering (Y, X).
648  If ``splitSubfilters`` is set, each element is itself a `tuple`
649  of two `float`, corresponding to the DCR shift at the two wavelengths.
650  Otherwise, each element is a `float` corresponding to the DCR shift at
651  the effective wavelength of the subfilter.
652  useInverse : `bool`, optional
653  Apply the shift in the opposite direction. Default: False
654  splitSubfilters : `bool`, optional
655  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
656  instead of at the midpoint. Default: False
657  splitThreshold : `float`, optional
658  Minimum DCR difference within a subfilter required to use ``splitSubfilters``
659  doPrefilter : `bool`, optional
660  Spline filter the image before shifting, if set. Filtering is required,
661  so only set to False if the image is already filtered.
662  Filtering takes ~20% of the time of shifting, so if `applyDcr` will be
663  called repeatedly on the same image it is more efficient to precalculate
664  the filter.
665  order : `int`, optional
666  The order of the spline interpolation, default is 3.
667 
668  Returns
669  -------
670  shiftedImage : `numpy.ndarray`
671  A copy of the input image with the specified shift applied.
672  """
673  if doPrefilter:
674  prefilteredImage = ndimage.spline_filter(image, order=order)
675  else:
676  prefilteredImage = image
677  if splitSubfilters:
678  shiftAmp = np.max(np.abs([_dcr0 - _dcr1 for _dcr0, _dcr1 in zip(dcr[0], dcr[1])]))
679  if shiftAmp >= splitThreshold:
680  if useInverse:
681  shift = [-1.*s for s in dcr[0]]
682  shift1 = [-1.*s for s in dcr[1]]
683  else:
684  shift = dcr[0]
685  shift1 = dcr[1]
686  shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=False, order=order)
687  shiftedImage += ndimage.shift(prefilteredImage, shift1, prefilter=False, order=order)
688  shiftedImage /= 2.
689  return shiftedImage
690  else:
691  # If the difference in the DCR shifts is less than the threshold,
692  # then just use the average shift for efficiency.
693  dcr = (np.mean(dcr[0]), np.mean(dcr[1]))
694  if useInverse:
695  shift = [-1.*s for s in dcr]
696  else:
697  shift = dcr
698  shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=False, order=order)
699  return shiftedImage
700 
701 
702 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
703  """Calculate the shift in pixels of an exposure due to DCR.
704 
705  Parameters
706  ----------
707  visitInfo : `lsst.afw.image.VisitInfo`
708  Metadata for the exposure.
709  wcs : `lsst.afw.geom.SkyWcs`
710  Coordinate system definition (wcs) for the exposure.
711  filterInfo : `lsst.afw.image.Filter`
712  The filter definition, set in the current instruments' obs package.
713  dcrNumSubfilters : `int`
714  Number of sub-filters used to model chromatic effects within a band.
715  splitSubfilters : `bool`, optional
716  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
717  instead of at the midpoint. Default: False
718 
719  Returns
720  -------
721  dcrShift : `tuple` of two `float`
722  The 2D shift due to DCR, in pixels.
723  Uses numpy axes ordering (Y, X).
724  """
725  rotation = calculateImageParallacticAngle(visitInfo, wcs)
726  dcrShift = []
727  weight = [0.75, 0.25]
728  lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
729  for wl0, wl1 in wavelengthGenerator(filterInfo, dcrNumSubfilters):
730  # Note that diffRefractAmp can be negative, since it's relative to the midpoint of the full band
731  diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
732  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
733  observatory=visitInfo.getObservatory(),
734  weather=visitInfo.getWeather())
735  diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
736  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
737  observatory=visitInfo.getObservatory(),
738  weather=visitInfo.getWeather())
739  if splitSubfilters:
740  diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
741  diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
742  diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
743  diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
744  shiftX = [diffRefractPix*np.sin(rotation.asRadians()) for diffRefractPix in diffRefractArr]
745  shiftY = [diffRefractPix*np.cos(rotation.asRadians()) for diffRefractPix in diffRefractArr]
746  dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
747  else:
748  diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
749  diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
750  shiftX = diffRefractPix*np.sin(rotation.asRadians())
751  shiftY = diffRefractPix*np.cos(rotation.asRadians())
752  dcrShift.append((shiftY, shiftX))
753  return dcrShift
754 
755 
756 def calculateImageParallacticAngle(visitInfo, wcs):
757  """Calculate the total sky rotation angle of an exposure.
758 
759  Parameters
760  ----------
761  visitInfo : `lsst.afw.image.VisitInfo`
762  Metadata for the exposure.
763  wcs : `lsst.afw.geom.SkyWcs`
764  Coordinate system definition (wcs) for the exposure.
765 
766  Returns
767  -------
768  `lsst.geom.Angle`
769  The rotation of the image axis, East from North.
770  Equal to the parallactic angle plus any additional rotation of the
771  coordinate system.
772  A rotation angle of 0 degrees is defined with
773  North along the +y axis and East along the +x axis.
774  A rotation angle of 90 degrees is defined with
775  North along the +x axis and East along the -y axis.
776  """
777  parAngle = visitInfo.getBoresightParAngle().asRadians()
778  cd = wcs.getCdMatrix()
779  if wcs.isFlipped:
780  cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
781  rotAngle = (cdAngle + parAngle)*geom.radians
782  else:
783  cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
784  rotAngle = (cdAngle - parAngle)*geom.radians
785  return rotAngle
786 
787 
788 def wavelengthGenerator(filterInfo, dcrNumSubfilters):
789  """Iterate over the wavelength endpoints of subfilters.
790 
791  Parameters
792  ----------
793  filterInfo : `lsst.afw.image.Filter`
794  The filter definition, set in the current instruments' obs package.
795  dcrNumSubfilters : `int`
796  Number of sub-filters used to model chromatic effects within a band.
797 
798  Yields
799  ------
800  `tuple` of two `float`
801  The next set of wavelength endpoints for a subfilter, in nm.
802  """
803  lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
804  lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
805  wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
806  for wl in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=False):
807  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:344
lsst::afw::image
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
Definition: imageAlgorithm.dox:1
lsst::ip::diffim.dcrModel.DcrModel.fromImage
def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None, photoCalib=None)
Definition: dcrModel.py:62
lsst::afw::coord.refraction.differentialRefraction
def differentialRefraction(wavelength, wavelengthRef, elevation, observatory, weather=None)
Definition: refraction.py:95
lsst::ip::diffim.dcrModel.DcrModel.assign
def assign(self, dcrSubModel, bbox=None)
Definition: dcrModel.py:321
lsst::ip::diffim.dcrModel.DcrModel.getReferenceImage
def getReferenceImage(self, bbox=None)
Definition: dcrModel.py:305
lsst::ip::diffim.dcrModel.applyDcr
def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0., doPrefilter=True, order=3)
Definition: dcrModel.py:637
lsst::ip::diffim.dcrModel.DcrModel.variance
def variance(self)
Definition: dcrModel.py:295
lsst::ip::diffim.dcrModel.DcrModel.buildMatchedExposure
def buildMatchedExposure(self, exposure=None, visitInfo=None, bbox=None, wcs=None, mask=None)
Definition: dcrModel.py:404
lsst::ip::diffim.dcrModel.DcrModel.regularizeModelFreq
def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED")
Definition: dcrModel.py:496
lsst::ip::diffim.dcrModel.DcrModel.__len__
def __len__(self)
Definition: dcrModel.py:191
lsst::ip::diffim.dcrModel.DcrModel.applyImageThresholds
def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2)
Definition: dcrModel.py:595
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:226
lsst::afw::geom.transform.transformContinued.cls
cls
Definition: transformContinued.py:33
lsst::afw::coord.refraction
Definition: refraction.py:1
lsst::ip::diffim.dcrModel.DcrModel._mask
_mask
Definition: dcrModel.py:57
lsst::ip::diffim.dcrModel.calculateImageParallacticAngle
def calculateImageParallacticAngle(visitInfo, wcs)
Definition: dcrModel.py:756
lsst::ip::diffim.dcrModel.DcrModel._filter
_filter
Definition: dcrModel.py:55
lsst::ip::diffim.dcrModel.DcrModel.psf
def psf(self)
Definition: dcrModel.py:262
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:273
lsst::ip::diffim.dcrModel.DcrModel.filter
def filter(self)
Definition: dcrModel.py:251
lsst::ip::diffim.dcrModel.DcrModel.mask
def mask(self)
Definition: dcrModel.py:284
lsst::ip::diffim.dcrModel.DcrModel.__getitem__
def __getitem__(self, subfilter)
Definition: dcrModel.py:201
lsst::ip::diffim.dcrModel.DcrModel.calculateNoiseCutoff
def calculateNoiseCutoff(self, image, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
Definition: dcrModel.py:558
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:702
lsst::ip::diffim.dcrModel.DcrModel.regularizeModelIter
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
Definition: dcrModel.py:470
lsst::geom
Definition: geomOperators.dox:4
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:450
lsst::ip::diffim.dcrModel.DcrModel.modelImages
modelImages
Definition: dcrModel.py:54
lsst::ip::diffim.dcrModel.DcrModel.fromQuantum
def fromQuantum(cls, availableCoaddRefs)
Definition: dcrModel.py:154
lsst::ip::diffim.dcrModel.DcrModel._variance
_variance
Definition: dcrModel.py:58
lsst::geom::Box2I
An integer coordinate rectangle.
Definition: Box.h:55
lsst::ip::diffim.dcrModel.DcrModel._psf
_psf
Definition: dcrModel.py:56
lsst::ip::diffim.dcrModel.wavelengthGenerator
def wavelengthGenerator(filterInfo, dcrNumSubfilters)
Definition: dcrModel.py:788
lsst::ip::diffim.dcrModel.DcrModel.dcrNumSubfilters
dcrNumSubfilters
Definition: dcrModel.py:53