LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
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.geom as afwGeom
27 from lsst.afw.geom import AffineTransform
28 from lsst.afw.geom import makeTransform
29 import lsst.afw.image as afwImage
30 import lsst.afw.math as afwMath
31 from lsst.geom import radians
32 
33 __all__ = ["DcrModel", "applyDcr", "calculateDcr", "calculateImageParallacticAngle"]
34 
35 
36 class DcrModel:
37  """A model of the true sky after correcting chromatic effects.
38 
39  Attributes
40  ----------
41  dcrNumSubfilters : `int`
42  Number of sub-filters used to model chromatic effects within a band.
43  modelImages : `list` of `lsst.afw.image.MaskedImage`
44  A list of masked images, each containing the model for one subfilter
45 
46  Notes
47  -----
48  The ``DcrModel`` contains an estimate of the true sky, at a higher
49  wavelength resolution than the input observations. It can be forward-
50  modeled to produce Differential Chromatic Refraction (DCR) matched
51  templates for a given ``Exposure``, and provides utilities for conditioning
52  the model in ``dcrAssembleCoadd`` to avoid oscillating solutions between
53  iterations of forward modeling or between the subfilters of the model.
54  """
55 
56  def __init__(self, modelImages, filterInfo=None, psf=None):
57  self.dcrNumSubfilters = len(modelImages)
58  self.modelImages = modelImages
59  self._filter = filterInfo
60  self._psf = psf
61 
62  @classmethod
63  def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None):
64  """Initialize a DcrModel by dividing a coadd between the subfilters.
65 
66  Parameters
67  ----------
68  maskedImage : `lsst.afw.image.MaskedImage`
69  Input coadded image to divide equally between the subfilters.
70  dcrNumSubfilters : `int`
71  Number of sub-filters used to model chromatic effects within a band.
72  filterInfo : `lsst.afw.image.Filter`, optional
73  The filter definition, set in the current instruments' obs package.
74  Required for any calculation of DCR, including making matched templates.
75  psf : `lsst.afw.detection.Psf`, optional
76  Point spread function (PSF) of the model.
77  Required if the ``DcrModel`` will be persisted.
78 
79  Returns
80  -------
81  dcrModel : `lsst.pipe.tasks.DcrModel`
82  Best fit model of the true sky after correcting chromatic effects.
83  """
84  # NANs will potentially contaminate the entire image,
85  # depending on the shift or convolution type used.
86  model = maskedImage.clone()
87  badPixels = np.isnan(model.image.array) | np.isnan(model.variance.array)
88  model.image.array[badPixels] = 0.
89  model.variance.array[badPixels] = 0.
90  model.image.array /= dcrNumSubfilters
91  # We divide the variance by N and not N**2 because we will assume each
92  # subfilter is independent. That means that the significance of
93  # detected sources will be lower by a factor of sqrt(N) in the
94  # subfilter images, but we will recover it when we combine the
95  # subfilter images to construct matched templates.
96  model.variance.array /= dcrNumSubfilters
97  model.mask.array[badPixels] = model.mask.getPlaneBitMask("NO_DATA")
98  modelImages = [model, ]
99  for subfilter in range(1, dcrNumSubfilters):
100  modelImages.append(model.clone())
101  return cls(modelImages, filterInfo, psf)
102 
103  @classmethod
104  def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, **kwargs):
105  """Load an existing DcrModel from a repository.
106 
107  Parameters
108  ----------
109  dataRef : `lsst.daf.persistence.ButlerDataRef`
110  Data reference defining the patch for coaddition and the
111  reference Warp
112  datasetType : `str`, optional
113  Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"}
114  numSubfilters : `int`
115  Number of sub-filters used to model chromatic effects within a band.
116  **kwargs
117  Additional keyword arguments to pass to look up the model in the data registry.
118  Common keywords and their types include: ``tract``:`str`, ``patch``:`str`,
119  ``bbox``:`lsst.afw.geom.Box2I`
120 
121  Returns
122  -------
123  dcrModel : `lsst.pipe.tasks.DcrModel`
124  Best fit model of the true sky after correcting chromatic effects.
125  """
126  modelImages = []
127  filterInfo = None
128  psf = None
129  for subfilter in range(numSubfilters):
130  dcrCoadd = dataRef.get(datasetType, subfilter=subfilter,
131  numSubfilters=numSubfilters, **kwargs)
132  if filterInfo is None:
133  filterInfo = dcrCoadd.getFilter()
134  if psf is None:
135  psf = dcrCoadd.getPsf()
136  modelImages.append(dcrCoadd.maskedImage)
137  return cls(modelImages, filterInfo, psf)
138 
139  def __len__(self):
140  """Return the number of subfilters.
141 
142  Returns
143  -------
144  dcrNumSubfilters : `int`
145  The number of DCR subfilters in the model.
146  """
147  return self.dcrNumSubfilters
148 
149  def __getitem__(self, subfilter):
150  """Iterate over the subfilters of the DCR model.
151 
152  Parameters
153  ----------
154  subfilter : `int`
155  Index of the current ``subfilter`` within the full band.
156  Negative indices are allowed, and count in reverse order
157  from the highest ``subfilter``.
158 
159  Returns
160  -------
161  modelImage : `lsst.afw.image.MaskedImage`
162  The DCR model for the given ``subfilter``.
163 
164  Raises
165  ------
166  IndexError
167  If the requested ``subfilter`` is greater or equal to the number
168  of subfilters in the model.
169  """
170  if np.abs(subfilter) >= len(self):
171  raise IndexError("subfilter out of bounds.")
172  return self.modelImages[subfilter]
173 
174  def __setitem__(self, subfilter, maskedImage):
175  """Update the model image for one subfilter.
176 
177  Parameters
178  ----------
179  subfilter : `int`
180  Index of the current subfilter within the full band.
181  maskedImage : `lsst.afw.image.MaskedImage`
182  The DCR model to set for the given ``subfilter``.
183 
184  Raises
185  ------
186  IndexError
187  If the requested ``subfilter`` is greater or equal to the number
188  of subfilters in the model.
189  ValueError
190  If the bounding box of the new image does not match.
191  """
192  if np.abs(subfilter) >= len(self):
193  raise IndexError("subfilter out of bounds.")
194  if maskedImage.getBBox() != self.bbox:
195  raise ValueError("The bounding box of a subfilter must not change.")
196  self.modelImages[subfilter] = maskedImage
197 
198  @property
199  def filter(self):
200  """Return the filter of the model.
201 
202  Returns
203  -------
204  filter : `lsst.afw.image.Filter`
205  The filter definition, set in the current instruments' obs package.
206  """
207  return self._filter
208 
209  @property
210  def psf(self):
211  """Return the psf of the model.
212 
213  Returns
214  -------
215  psf : `lsst.afw.detection.Psf`
216  Point spread function (PSF) of the model.
217  """
218  return self._psf
219 
220  @property
221  def bbox(self):
222  """Return the common bounding box of each subfilter image.
223 
224  Returns
225  -------
226  bbox : `lsst.afw.geom.Box2I`
227  Bounding box of the DCR model.
228  """
229  return self[0].getBBox()
230 
231  @property
232  def mask(self):
233  """Return the common mask of each subfilter image.
234 
235  Returns
236  -------
237  bbox : `lsst.afw.image.Mask`
238  Mask plane of the DCR model.
239  """
240  return self[0].mask
241 
242  def getReferenceImage(self, bbox=None):
243  """Calculate a reference image from the average of the subfilter images.
244 
245  Parameters
246  ----------
247  bbox : `lsst.afw.geom.Box2I`, optional
248  Sub-region of the coadd. Returns the entire image if `None`.
249 
250  Returns
251  -------
252  refImage : `numpy.ndarray`
253  The reference image with no chromatic effects applied.
254  """
255  bbox = bbox or self.bbox
256  return np.mean([model[bbox].image.array for model in self], axis=0)
257 
258  def assign(self, dcrSubModel, bbox=None):
259  """Update a sub-region of the ``DcrModel`` with new values.
260 
261  Parameters
262  ----------
263  dcrSubModel : `lsst.pipe.tasks.DcrModel`
264  New model of the true scene after correcting chromatic effects.
265  bbox : `lsst.afw.geom.Box2I`, optional
266  Sub-region of the coadd.
267  Defaults to the bounding box of ``dcrSubModel``.
268 
269  Raises
270  ------
271  ValueError
272  If the new model has a different number of subfilters.
273  """
274  if len(dcrSubModel) != len(self):
275  raise ValueError("The number of DCR subfilters must be the same "
276  "between the old and new models.")
277  bbox = bbox or self.bbox
278  for model, subModel in zip(self, dcrSubModel):
279  model.assign(subModel[bbox], bbox)
280 
281  def buildMatchedTemplate(self, exposure=None, warpCtrl=None,
282  visitInfo=None, bbox=None, wcs=None, mask=None,
283  splitSubfilters=False):
284  """Create a DCR-matched template image for an exposure.
285 
286  Parameters
287  ----------
288  exposure : `lsst.afw.image.Exposure`, optional
289  The input exposure to build a matched template for.
290  May be omitted if all of the metadata is supplied separately
291  warpCtrl : `lsst.afw.Math.WarpingControl`, optional
292  Configuration settings for warping an image.
293  If not set, defaults to a lanczos3 warping kernel for the image,
294  and a bilinear kernel for the mask
295  visitInfo : `lsst.afw.image.VisitInfo`, optional
296  Metadata for the exposure. Ignored if ``exposure`` is set.
297  bbox : `lsst.afw.geom.Box2I`, optional
298  Sub-region of the coadd. Ignored if ``exposure`` is set.
299  wcs : `lsst.afw.geom.SkyWcs`, optional
300  Coordinate system definition (wcs) for the exposure.
301  Ignored if ``exposure`` is set.
302  mask : `lsst.afw.image.Mask`, optional
303  reference mask to use for the template image.
304  splitSubfilters : `bool`, optional
305  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
306  instead of at the midpoint. Default: False
307 
308  Returns
309  -------
310  templateImage : `lsst.afw.image.maskedImageF`
311  The DCR-matched template
312 
313  Raises
314  ------
315  ValueError
316  If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and ``wcs`` are set.
317  """
318  if self.filter is None:
319  raise ValueError("'filterInfo' must be set for the DcrModel in order to calculate DCR.")
320  if exposure is not None:
321  visitInfo = exposure.getInfo().getVisitInfo()
322  bbox = exposure.getBBox()
323  wcs = exposure.getInfo().getWcs()
324  elif visitInfo is None or bbox is None or wcs is None:
325  raise ValueError("Either exposure or visitInfo, bbox, and wcs must be set.")
326  if warpCtrl is None:
327  # Turn off the warping cache, since we set the linear interpolation length to the entire subregion
328  # This warper is only used for applying DCR shifts, which are assumed to be uniform across a patch
329  warpCtrl = afwMath.WarpingControl("lanczos3", "bilinear",
330  cacheSize=0, interpLength=max(bbox.getDimensions()))
331 
332  dcrShift = calculateDcr(visitInfo, wcs, self.filter, len(self), splitSubfilters=splitSubfilters)
333  templateImage = afwImage.MaskedImageF(bbox)
334  for subfilter, dcr in enumerate(dcrShift):
335  templateImage += applyDcr(self[subfilter][bbox], dcr, warpCtrl, splitSubfilters=splitSubfilters)
336  if mask is not None:
337  templateImage.setMask(mask[bbox])
338  return templateImage
339 
340  def buildMatchedExposure(self, exposure=None, warpCtrl=None,
341  visitInfo=None, bbox=None, wcs=None, mask=None):
342  """Wrapper to create an exposure from a template image.
343 
344  Parameters
345  ----------
346  exposure : `lsst.afw.image.Exposure`, optional
347  The input exposure to build a matched template for.
348  May be omitted if all of the metadata is supplied separately
349  warpCtrl : `lsst.afw.Math.WarpingControl`
350  Configuration settings for warping an image
351  visitInfo : `lsst.afw.image.VisitInfo`, optional
352  Metadata for the exposure. Ignored if ``exposure`` is set.
353  bbox : `lsst.afw.geom.Box2I`, optional
354  Sub-region of the coadd. Ignored if ``exposure`` is set.
355  wcs : `lsst.afw.geom.SkyWcs`, optional
356  Coordinate system definition (wcs) for the exposure.
357  Ignored if ``exposure`` is set.
358  mask : `lsst.afw.image.Mask`, optional
359  reference mask to use for the template image.
360 
361  Returns
362  -------
363  templateExposure : `lsst.afw.image.exposureF`
364  The DCR-matched template
365  """
366  templateImage = self.buildMatchedTemplate(exposure, warpCtrl, visitInfo, bbox, wcs, mask)
367  templateExposure = afwImage.ExposureF(bbox, wcs)
368  templateExposure.setMaskedImage(templateImage)
369  templateExposure.setPsf(self.psf)
370  templateExposure.setFilter(self.filter)
371  return templateExposure
372 
373  def conditionDcrModel(self, modelImages, bbox, gain=1.):
374  """Average two iterations' solutions to reduce oscillations.
375 
376  Parameters
377  ----------
378  modelImages : `list` of `lsst.afw.image.MaskedImage`
379  The new DCR model images from the current iteration.
380  The values will be modified in place.
381  bbox : `lsst.afw.geom.Box2I`
382  Sub-region of the coadd
383  gain : `float`, optional
384  Relative weight to give the new solution when updating the model.
385  Defaults to 1.0, which gives equal weight to both solutions.
386  """
387  # Calculate weighted averages of the image and variance planes.
388  # Note that ``newModel *= gain`` would multiply the variance by ``gain**2``
389  for model, newModel in zip(self, modelImages):
390  newModel.image *= gain
391  newModel.image += model[bbox].image
392  newModel.image /= 1. + gain
393  newModel.variance *= gain
394  newModel.variance += model[bbox].variance
395  newModel.variance /= 1. + gain
396 
397  def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor,
398  regularizationWidth=2):
399  """Restrict large variations in the model between iterations.
400 
401  Parameters
402  ----------
403  subfilter : `int`
404  Index of the current subfilter within the full band.
405  newModel : `lsst.afw.image.MaskedImage`
406  The new DCR model for one subfilter from the current iteration.
407  Values in ``newModel`` that are extreme compared with the last
408  iteration are modified in place.
409  bbox : `lsst.afw.geom.Box2I`
410  Sub-region to coadd
411  regularizationFactor : `float`
412  Maximum relative change of the model allowed between iterations.
413  regularizationWidth : int, optional
414  Minimum radius of a region to include in regularization, in pixels.
415  """
416  refImage = self[subfilter][bbox].image.array
417  highThreshold = np.abs(refImage)*regularizationFactor
418  lowThreshold = refImage/regularizationFactor
419  newImage = newModel.image.array
420  self.applyImageThresholds(newImage, highThreshold=highThreshold, lowThreshold=lowThreshold,
421  regularizationWidth=regularizationWidth)
422 
423  def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor,
424  regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
425  """Restrict large variations in the model between subfilters.
426 
427  Parameters
428  ----------
429  modelImages : `list` of `lsst.afw.image.MaskedImage`
430  The new DCR model images from the current iteration.
431  The values will be modified in place.
432  bbox : `lsst.afw.geom.Box2I`
433  Sub-region to coadd
434  statsCtrl : `lsst.afw.math.StatisticsControl`
435  Statistics control object for coaddition.
436  regularizationFactor : `float`
437  Maximum relative change of the model allowed between subfilters.
438  regularizationWidth : `int`, optional
439  Minimum radius of a region to include in regularization, in pixels.
440  mask : `lsst.afw.image.Mask`, optional
441  Optional alternate mask
442  convergenceMaskPlanes : `list` of `str`, or `str`, optional
443  Mask planes to use to calculate convergence.
444 
445  Notes
446  -----
447  This implementation of frequency regularization restricts each subfilter
448  image to be a smoothly-varying function times a reference image.
449  """
450  # ``regularizationFactor`` is the maximum change between subfilter images, so the maximum difference
451  # between one subfilter image and the average will be the square root of that.
452  maxDiff = np.sqrt(regularizationFactor)
453  noiseLevel = self.calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
454  referenceImage = self.getReferenceImage(bbox)
455  badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
456  if np.sum(~badPixels) == 0:
457  # Skip regularization if there are no valid pixels
458  return
459  referenceImage[badPixels] = 0.
460  filterWidth = regularizationWidth
461  fwhm = 2.*filterWidth
462  # The noise should be lower in the smoothed image by sqrt(Nsmooth) ~ fwhm pixels
463  noiseLevel /= fwhm
464  smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth) + noiseLevel
465 
466  baseThresh = np.ones_like(referenceImage)
467  highThreshold = baseThresh*maxDiff
468  lowThreshold = baseThresh/maxDiff
469  for subfilter, model in enumerate(modelImages):
470  smoothModel = ndimage.filters.gaussian_filter(model.image.array, filterWidth) + noiseLevel
471  relativeModel = smoothModel/smoothRef
472  # Now sharpen the smoothed relativeModel using an alpha of 3.
473  relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/3.)
474  relativeModel = relativeModel + 3.*(relativeModel - relativeModel2)
475  self.applyImageThresholds(relativeModel,
476  highThreshold=highThreshold,
477  lowThreshold=lowThreshold,
478  regularizationWidth=regularizationWidth)
479  relativeModel *= referenceImage
480  modelImages[subfilter].image.array = relativeModel
481 
482  def calculateNoiseCutoff(self, maskedImage, statsCtrl, bufferSize,
483  convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
484  """Helper function to calculate the background noise level of an image.
485 
486  Parameters
487  ----------
488  maskedImage : `lsst.afw.image.MaskedImage`
489  The input image to evaluate the background noise properties.
490  statsCtrl : `lsst.afw.math.StatisticsControl`
491  Statistics control object for coaddition.
492  bufferSize : `int`
493  Number of additional pixels to exclude
494  from the edges of the bounding box.
495  convergenceMaskPlanes : `list` of `str`, or `str`
496  Mask planes to use to calculate convergence.
497  mask : `lsst.afw.image.Mask`, Optional
498  Optional alternate mask
499  bbox : `lsst.afw.geom.Box2I`, optional
500  Sub-region of the masked image to calculate the noise level over.
501 
502  Returns
503  -------
504  noiseCutoff : `float`
505  The threshold value to treat pixels as noise in an image..
506  """
507  if bbox is None:
508  bbox = self.bbox
509  if mask is None:
510  mask = maskedImage[bbox].mask
511  bboxShrink = afwGeom.Box2I(bbox)
512  bboxShrink.grow(-bufferSize)
513  convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
514 
515  backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
516  noiseCutoff = np.std(maskedImage[bboxShrink].image.array[backgroundPixels])
517  return noiseCutoff
518 
519  def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2):
520  """Restrict image values to be between upper and lower limits.
521 
522  This method flags all pixels in an image that are outside of the given
523  threshold values. The threshold values are taken from a reference image,
524  so noisy pixels are likely to get flagged. In order to exclude those
525  noisy pixels, the array of flags is eroded and dilated, which removes
526  isolated pixels outside of the thresholds from the list of pixels to be
527  modified. Pixels that remain flagged after this operation have their
528  values set to the appropriate upper or lower threshold value.
529 
530  Parameters
531  ----------
532  image : `numpy.ndarray`
533  The image to apply the thresholds to.
534  The values will be modified in place.
535  highThreshold : `numpy.ndarray`, optional
536  Array of upper limit values for each pixel of ``image``.
537  lowThreshold : `numpy.ndarray`, optional
538  Array of lower limit values for each pixel of ``image``.
539  regularizationWidth : `int`, optional
540  Minimum radius of a region to include in regularization, in pixels.
541  """
542  # Generate the structure for binary erosion and dilation, which is used to remove noise-like pixels.
543  # Groups of pixels with a radius smaller than ``regularizationWidth``
544  # will be excluded from regularization.
545  filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
546  regularizationWidth)
547  if highThreshold is not None:
548  highPixels = image > highThreshold
549  if regularizationWidth > 0:
550  # Erode and dilate ``highPixels`` to exclude noisy pixels.
551  highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
552  image[highPixels] = highThreshold[highPixels]
553  if lowThreshold is not None:
554  lowPixels = image < lowThreshold
555  if regularizationWidth > 0:
556  # Erode and dilate ``lowPixels`` to exclude noisy pixels.
557  lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
558  image[lowPixels] = lowThreshold[lowPixels]
559 
560 
561 def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False, splitSubfilters=False):
562  """Shift a masked image.
563 
564  Parameters
565  ----------
566  maskedImage : `lsst.afw.image.MaskedImage`
567  The input masked image to shift.
568  dcr : `lsst.afw.geom.Extent2I`
569  Shift calculated with ``calculateDcr``.
570  warpCtrl : `lsst.afw.math.WarpingControl`
571  Configuration settings for warping an image
572  bbox : `lsst.afw.geom.Box2I`, optional
573  Sub-region of the masked image to shift.
574  Shifts the entire image if None (Default).
575  useInverse : `bool`, optional
576  Use the reverse of ``dcr`` for the shift. Default: False
577  splitSubfilters : `bool`, optional
578  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
579  instead of at the midpoint. Default: False
580 
581  Returns
582  -------
583  shiftedImage : `lsst.afw.image.maskedImageF`
584  A masked image, with the pixels within the bounding box shifted.
585  """
586  padValue = afwImage.pixel.SinglePixelF(0., maskedImage.mask.getPlaneBitMask("NO_DATA"), 0)
587  if bbox is None:
588  bbox = maskedImage.getBBox()
589  if splitSubfilters:
590  shiftedImage = afwImage.MaskedImageF(bbox)
591  transform0 = makeTransform(AffineTransform((-1.0 if useInverse else 1.0)*dcr[0]))
592  afwMath.warpImage(shiftedImage, maskedImage[bbox],
593  transform0, warpCtrl, padValue=padValue)
594  shiftedImage1 = afwImage.MaskedImageF(bbox)
595  transform1 = makeTransform(AffineTransform((-1.0 if useInverse else 1.0)*dcr[1]))
596  afwMath.warpImage(shiftedImage1, maskedImage[bbox],
597  transform1, warpCtrl, padValue=padValue)
598  shiftedImage += shiftedImage1
599  shiftedImage /= 2.
600  else:
601  shiftedImage = afwImage.MaskedImageF(bbox)
602  transform = makeTransform(AffineTransform((-1.0 if useInverse else 1.0)*dcr))
603  afwMath.warpImage(shiftedImage, maskedImage[bbox],
604  transform, warpCtrl, padValue=padValue)
605  return shiftedImage
606 
607 
608 def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False):
609  """Calculate the shift in pixels of an exposure due to DCR.
610 
611  Parameters
612  ----------
613  visitInfo : `lsst.afw.image.VisitInfo`
614  Metadata for the exposure.
615  wcs : `lsst.afw.geom.SkyWcs`
616  Coordinate system definition (wcs) for the exposure.
617  filterInfo : `lsst.afw.image.Filter`
618  The filter definition, set in the current instruments' obs package.
619  dcrNumSubfilters : `int`
620  Number of sub-filters used to model chromatic effects within a band.
621  splitSubfilters : `bool`, optional
622  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
623  instead of at the midpoint. Default: False
624 
625  Returns
626  -------
627  dcrShift : `lsst.afw.geom.Extent2I`
628  The 2D shift due to DCR, in pixels.
629  """
630  rotation = calculateImageParallacticAngle(visitInfo, wcs)
631  dcrShift = []
632  weight = [0.75, 0.25]
633  lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
634  for wl0, wl1 in wavelengthGenerator(filterInfo, dcrNumSubfilters):
635  # Note that diffRefractAmp can be negative, since it's relative to the midpoint of the full band
636  diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
637  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
638  observatory=visitInfo.getObservatory(),
639  weather=visitInfo.getWeather())
640  diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
641  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
642  observatory=visitInfo.getObservatory(),
643  weather=visitInfo.getWeather())
644  if splitSubfilters:
645  diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
646  diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
647  diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
648  diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
649  shiftX = [diffRefractPix*np.sin(rotation.asRadians()) for diffRefractPix in diffRefractArr]
650  shiftY = [diffRefractPix*np.cos(rotation.asRadians()) for diffRefractPix in diffRefractArr]
651  dcrShift.append((afwGeom.Extent2D(shiftX[0], shiftY[0]), afwGeom.Extent2D(shiftX[1], shiftY[1])))
652  else:
653  diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
654  diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
655  shiftX = diffRefractPix*np.sin(rotation.asRadians())
656  shiftY = diffRefractPix*np.cos(rotation.asRadians())
657  dcrShift.append(afwGeom.Extent2D(shiftX, shiftY))
658  return dcrShift
659 
660 
661 def calculateImageParallacticAngle(visitInfo, wcs):
662  """Calculate the total sky rotation angle of an exposure.
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 
671  Returns
672  -------
673  `lsst.geom.Angle`
674  The rotation of the image axis, East from North.
675  Equal to the parallactic angle plus any additional rotation of the
676  coordinate system.
677  A rotation angle of 0 degrees is defined with
678  North along the +y axis and East along the +x axis.
679  A rotation angle of 90 degrees is defined with
680  North along the +x axis and East along the -y axis.
681  """
682  parAngle = visitInfo.getBoresightParAngle().asRadians()
683  cd = wcs.getCdMatrix()
684  if wcs.isFlipped:
685  cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
686  else:
687  cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
688  rotAngle = (cdAngle + parAngle)*radians
689  return rotAngle
690 
691 
692 def wavelengthGenerator(filterInfo, dcrNumSubfilters):
693  """Iterate over the wavelength endpoints of subfilters.
694 
695  Parameters
696  ----------
697  filterInfo : `lsst.afw.image.Filter`
698  The filter definition, set in the current instruments' obs package.
699  dcrNumSubfilters : `int`
700  Number of sub-filters used to model chromatic effects within a band.
701 
702  Yields
703  ------
704  `tuple` of two `float`
705  The next set of wavelength endpoints for a subfilter, in nm.
706  """
707  lambdaMin = filterInfo.getFilterProperty().getLambdaMin()
708  lambdaMax = filterInfo.getFilterProperty().getLambdaMax()
709  wlStep = (lambdaMax - lambdaMin)/dcrNumSubfilters
710  for wl in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=False):
711  yield (wl, wl + wlStep)
def fromImage(cls, maskedImage, dcrNumSubfilters, filterInfo=None, psf=None)
Definition: dcrModel.py:63
def calculateNoiseCutoff(self, maskedImage, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
Definition: dcrModel.py:483
def __setitem__(self, subfilter, maskedImage)
Definition: dcrModel.py:174
An affine coordinate transformation consisting of a linear transformation and an offset.
def calculateImageParallacticAngle(visitInfo, wcs)
Definition: dcrModel.py:661
int warpImage(DestImageT &destImage, SrcImageT const &srcImage, geom::TransformPoint2ToPoint2 const &srcToDest, WarpingControl const &control, typename DestImageT::SinglePixel padValue=lsst::afw::math::edgePixel< DestImageT >(typename lsst::afw::image::detail::image_traits< DestImageT >::image_category()))
A variant of warpImage that uses a Transform<Point2Endpoint, Point2Endpoint> instead of a pair of WCS...
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
Definition: dcrModel.py:398
Parameters to control convolution.
Definition: warpExposure.h:250
def __getitem__(self, subfilter)
Definition: dcrModel.py:149
def __init__(self, modelImages, filterInfo=None, psf=None)
Definition: dcrModel.py:56
def wavelengthGenerator(filterInfo, dcrNumSubfilters)
Definition: dcrModel.py:692
def buildMatchedTemplate(self, exposure=None, warpCtrl=None, visitInfo=None, bbox=None, wcs=None, mask=None, splitSubfilters=False)
Definition: dcrModel.py:283
def buildMatchedExposure(self, exposure=None, warpCtrl=None, visitInfo=None, bbox=None, wcs=None, mask=None)
Definition: dcrModel.py:341
def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2)
Definition: dcrModel.py:519
def fromDataRef(cls, dataRef, datasetType="dcrCoadd", numSubfilters=None, kwargs)
Definition: dcrModel.py:104
int max
def getReferenceImage(self, bbox=None)
Definition: dcrModel.py:242
def differentialRefraction(wavelength, wavelengthRef, elevation, observatory, weather=None)
Definition: refraction.py:95
def applyDcr(maskedImage, dcr, warpCtrl, bbox=None, useInverse=False, splitSubfilters=False)
Definition: dcrModel.py:561
def assign(self, dcrSubModel, bbox=None)
Definition: dcrModel.py:258
def calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters, splitSubfilters=False)
Definition: dcrModel.py:608
def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED")
Definition: dcrModel.py:424
def conditionDcrModel(self, modelImages, bbox, gain=1.)
Definition: dcrModel.py:373
An integer coordinate rectangle.
Definition: Box.h:54
std::shared_ptr< TransformPoint2ToPoint2 > makeTransform(lsst::geom::AffineTransform const &affine)
Wrap an lsst::geom::AffineTransform as a Transform.