LSSTApplications  19.0.0-14-gb0260a2+72efe9b372,20.0.0+7927753e06,20.0.0+8829bf0056,20.0.0+995114c5d2,20.0.0+b6f4b2abd1,20.0.0+bddc4f4cbe,20.0.0-1-g253301a+8829bf0056,20.0.0-1-g2b7511a+0d71a2d77f,20.0.0-1-g5b95a8c+7461dd0434,20.0.0-12-g321c96ea+23efe4bbff,20.0.0-16-gfab17e72e+fdf35455f6,20.0.0-2-g0070d88+ba3ffc8f0b,20.0.0-2-g4dae9ad+ee58a624b3,20.0.0-2-g61b8584+5d3db074ba,20.0.0-2-gb780d76+d529cf1a41,20.0.0-2-ged6426c+226a441f5f,20.0.0-2-gf072044+8829bf0056,20.0.0-2-gf1f7952+ee58a624b3,20.0.0-20-geae50cf+e37fec0aee,20.0.0-25-g3dcad98+544a109665,20.0.0-25-g5eafb0f+ee58a624b3,20.0.0-27-g64178ef+f1f297b00a,20.0.0-3-g4cc78c6+e0676b0dc8,20.0.0-3-g8f21e14+4fd2c12c9a,20.0.0-3-gbd60e8c+187b78b4b8,20.0.0-3-gbecbe05+48431fa087,20.0.0-38-ge4adf513+a12e1f8e37,20.0.0-4-g97dc21a+544a109665,20.0.0-4-gb4befbc+087873070b,20.0.0-4-gf910f65+5d3db074ba,20.0.0-5-gdfe0fee+199202a608,20.0.0-5-gfbfe500+d529cf1a41,20.0.0-6-g64f541c+d529cf1a41,20.0.0-6-g9a5b7a1+a1cd37312e,20.0.0-68-ga3f3dda+5fca18c6a4,20.0.0-9-g4aef684+e18322736b,w.2020.45
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, effectiveWavelength, bandwidth, filterInfo=None, psf=None,
53  mask=None, variance=None, photoCalib=None):
54  self.dcrNumSubfilters = len(modelImages)
55  self.modelImages = modelImages
56  self._filterInfo = filterInfo
57  self._effectiveWavelength = effectiveWavelength
58  self._bandwidth = bandwidth
59  self._psf = psf
60  self._mask = mask
61  self._variance = variance
62  self.photoCalib = photoCalib
63 
64  @classmethod
65  def fromImage(cls, maskedImage, dcrNumSubfilters, effectiveWavelength, bandwidth,
66  filterInfo=None, psf=None, photoCalib=None):
67  """Initialize a DcrModel by dividing a coadd between the subfilters.
68 
69  Parameters
70  ----------
71  maskedImage : `lsst.afw.image.MaskedImage`
72  Input coadded image to divide equally between the subfilters.
73  dcrNumSubfilters : `int`
74  Number of sub-filters used to model chromatic effects within a
75  band.
76  effectiveWavelength : `float`
77  The effective wavelengths of the current filter, in nanometers.
78  bandwidth : `float`
79  The bandwidth of the current filter, in nanometers.
80  filterInfo : `lsst.afw.image.Filter`, optional
81  The filter definition, set in the current instruments' obs package.
82  Note: this object will be changed in DM-21333.
83  Required for any calculation of DCR, including making matched
84  templates.
85  psf : `lsst.afw.detection.Psf`, optional
86  Point spread function (PSF) of the model.
87  Required if the ``DcrModel`` will be persisted.
88  photoCalib : `lsst.afw.image.PhotoCalib`, optional
89  Calibration to convert instrumental flux and
90  flux error to nanoJansky.
91 
92  Returns
93  -------
94  dcrModel : `lsst.pipe.tasks.DcrModel`
95  Best fit model of the true sky after correcting chromatic effects.
96  """
97  # NANs will potentially contaminate the entire image,
98  # depending on the shift or convolution type used.
99  model = maskedImage.image.clone()
100  mask = maskedImage.mask.clone()
101  # We divide the variance by N and not N**2 because we will assume each
102  # subfilter is independent. That means that the significance of
103  # detected sources will be lower by a factor of sqrt(N) in the
104  # subfilter images, but we will recover it when we combine the
105  # subfilter images to construct matched templates.
106  variance = maskedImage.variance.clone()
107  variance /= dcrNumSubfilters
108  model /= dcrNumSubfilters
109  modelImages = [model, ]
110  for subfilter in range(1, dcrNumSubfilters):
111  modelImages.append(model.clone())
112  return cls(modelImages, effectiveWavelength, bandwidth,
113  filterInfo=filterInfo, psf=psf, mask=mask, variance=variance, photoCalib=photoCalib)
114 
115  @classmethod
116  def fromDataRef(cls, dataRef, effectiveWavelength, bandwidth, datasetType="dcrCoadd", numSubfilters=None,
117  **kwargs):
118  """Load an existing DcrModel from a Gen 2 repository.
119 
120  Parameters
121  ----------
122  dataRef : `lsst.daf.persistence.ButlerDataRef`
123  Data reference defining the patch for coaddition and the
124  reference Warp
125  effectiveWavelength : `float`
126  The effective wavelengths of the current filter, in nanometers.
127  bandwidth : `float`
128  The bandwidth of the current filter, in nanometers.
129  datasetType : `str`, optional
130  Name of the DcrModel in the registry {"dcrCoadd", "dcrCoadd_sub"}
131  numSubfilters : `int`
132  Number of sub-filters used to model chromatic effects within a
133  band.
134  **kwargs
135  Additional keyword arguments to pass to look up the model in the
136  data registry.
137  Common keywords and their types include: ``tract``:`str`,
138  ``patch``:`str`, ``bbox``:`lsst.afw.geom.Box2I`
139 
140  Returns
141  -------
142  dcrModel : `lsst.pipe.tasks.DcrModel`
143  Best fit model of the true sky after correcting chromatic effects.
144  """
145  modelImages = []
146  filterInfo = None
147  psf = None
148  mask = None
149  variance = None
150  photoCalib = None
151  if "subfilter" in kwargs:
152  kwargs.pop("subfilter")
153  for subfilter in range(numSubfilters):
154  dcrCoadd = dataRef.get(datasetType, subfilter=subfilter,
155  numSubfilters=numSubfilters, **kwargs)
156  if filterInfo is None:
157  filterInfo = dcrCoadd.getFilter()
158  if psf is None:
159  psf = dcrCoadd.getPsf()
160  if mask is None:
161  mask = dcrCoadd.mask
162  if variance is None:
163  variance = dcrCoadd.variance
164  if photoCalib is None:
165  photoCalib = dcrCoadd.getPhotoCalib()
166  modelImages.append(dcrCoadd.image)
167  return cls(modelImages, effectiveWavelength, bandwidth, filterInfo, psf, mask, variance, photoCalib)
168 
169  @classmethod
170  def fromQuantum(cls, availableCoaddRefs, effectiveWavelength, bandwidth):
171  """Load an existing DcrModel from a Gen 3 repository.
172 
173  Parameters
174  ----------
175  availableCoaddRefs : `dict` of
176  `int` : `lsst.daf.butler.DeferredDatasetHandle`
177  Dictionary of spatially relevant retrieved coadd patches,
178  indexed by their sequential patch number.
179  effectiveWavelength : `float`
180  The effective wavelengths of the current filter, in nanometers.
181  bandwidth : `float`
182  The bandwidth of the current filter, in nanometers.
183 
184  Returns
185  -------
186  dcrModel : `lsst.pipe.tasks.DcrModel`
187  Best fit model of the true sky after correcting chromatic effects.
188  """
189  filterInfo = None
190  psf = None
191  mask = None
192  variance = None
193  photoCalib = None
194  modelImages = [None]*len(availableCoaddRefs)
195 
196  for coaddRef in availableCoaddRefs:
197  subfilter = coaddRef.dataId["subfilter"]
198  dcrCoadd = coaddRef.get()
199  if filterInfo is None:
200  filterInfo = dcrCoadd.getFilter()
201  if psf is None:
202  psf = dcrCoadd.getPsf()
203  if mask is None:
204  mask = dcrCoadd.mask
205  if variance is None:
206  variance = dcrCoadd.variance
207  if photoCalib is None:
208  photoCalib = dcrCoadd.getPhotoCalib()
209  modelImages[subfilter] = dcrCoadd.image
210  return cls(modelImages, effectiveWavelength, bandwidth, filterInfo, psf, mask, variance, photoCalib)
211 
212  def __len__(self):
213  """Return the number of subfilters.
214 
215  Returns
216  -------
217  dcrNumSubfilters : `int`
218  The number of DCR subfilters in the model.
219  """
220  return self.dcrNumSubfilters
221 
222  def __getitem__(self, subfilter):
223  """Iterate over the subfilters of the DCR model.
224 
225  Parameters
226  ----------
227  subfilter : `int`
228  Index of the current ``subfilter`` within the full band.
229  Negative indices are allowed, and count in reverse order
230  from the highest ``subfilter``.
231 
232  Returns
233  -------
234  modelImage : `lsst.afw.image.Image`
235  The DCR model for the given ``subfilter``.
236 
237  Raises
238  ------
239  IndexError
240  If the requested ``subfilter`` is greater or equal to the number
241  of subfilters in the model.
242  """
243  if np.abs(subfilter) >= len(self):
244  raise IndexError("subfilter out of bounds.")
245  return self.modelImages[subfilter]
246 
247  def __setitem__(self, subfilter, maskedImage):
248  """Update the model image for one subfilter.
249 
250  Parameters
251  ----------
252  subfilter : `int`
253  Index of the current subfilter within the full band.
254  maskedImage : `lsst.afw.image.Image`
255  The DCR model to set for the given ``subfilter``.
256 
257  Raises
258  ------
259  IndexError
260  If the requested ``subfilter`` is greater or equal to the number
261  of subfilters in the model.
262  ValueError
263  If the bounding box of the new image does not match.
264  """
265  if np.abs(subfilter) >= len(self):
266  raise IndexError("subfilter out of bounds.")
267  if maskedImage.getBBox() != self.bbox:
268  raise ValueError("The bounding box of a subfilter must not change.")
269  self.modelImages[subfilter] = maskedImage
270 
271  @property
273  """Return the effective wavelength of the model.
274 
275  Returns
276  -------
277  effectiveWavelength : `float`
278  The effective wavelength of the current filter, in nanometers.
279  """
280  return self._effectiveWavelength
281 
282  @property
283  def filter(self):
284  """Return the filter label for the model.
285 
286  Returns
287  -------
288  filterInfo : `lsst.afw.image.Filter`
289  The name of the filter used for the input observations.
290  Note: this object will be changed in DM-21333.
291  """
292  return self._filterInfo
293 
294  @property
295  def bandwidth(self):
296  """Return the bandwidth of the model.
297 
298  Returns
299  -------
300  bandwidth : `float`
301  The bandwidth of the current filter, in nanometers.
302  """
303  return self._bandwidth
304 
305  @property
306  def psf(self):
307  """Return the psf of the model.
308 
309  Returns
310  -------
311  psf : `lsst.afw.detection.Psf`
312  Point spread function (PSF) of the model.
313  """
314  return self._psf
315 
316  @property
317  def bbox(self):
318  """Return the common bounding box of each subfilter image.
319 
320  Returns
321  -------
322  bbox : `lsst.afw.geom.Box2I`
323  Bounding box of the DCR model.
324  """
325  return self[0].getBBox()
326 
327  @property
328  def mask(self):
329  """Return the common mask of each subfilter image.
330 
331  Returns
332  -------
333  mask : `lsst.afw.image.Mask`
334  Mask plane of the DCR model.
335  """
336  return self._mask
337 
338  @property
339  def variance(self):
340  """Return the common variance of each subfilter image.
341 
342  Returns
343  -------
344  variance : `lsst.afw.image.Image`
345  Variance plane of the DCR model.
346  """
347  return self._variance
348 
349  def getReferenceImage(self, bbox=None):
350  """Calculate a reference image from the average of the subfilter
351  images.
352 
353  Parameters
354  ----------
355  bbox : `lsst.afw.geom.Box2I`, optional
356  Sub-region of the coadd. Returns the entire image if `None`.
357 
358  Returns
359  -------
360  refImage : `numpy.ndarray`
361  The reference image with no chromatic effects applied.
362  """
363  bbox = bbox or self.bbox
364  return np.mean([model[bbox].array for model in self], axis=0)
365 
366  def assign(self, dcrSubModel, bbox=None):
367  """Update a sub-region of the ``DcrModel`` with new values.
368 
369  Parameters
370  ----------
371  dcrSubModel : `lsst.pipe.tasks.DcrModel`
372  New model of the true scene after correcting chromatic effects.
373  bbox : `lsst.afw.geom.Box2I`, optional
374  Sub-region of the coadd.
375  Defaults to the bounding box of ``dcrSubModel``.
376 
377  Raises
378  ------
379  ValueError
380  If the new model has a different number of subfilters.
381  """
382  if len(dcrSubModel) != len(self):
383  raise ValueError("The number of DCR subfilters must be the same "
384  "between the old and new models.")
385  bbox = bbox or self.bbox
386  for model, subModel in zip(self, dcrSubModel):
387  model.assign(subModel[bbox], bbox)
388 
389  def buildMatchedTemplate(self, exposure=None, order=3,
390  visitInfo=None, bbox=None, wcs=None, mask=None,
391  splitSubfilters=True, splitThreshold=0., amplifyModel=1.):
392  """Create a DCR-matched template image for an exposure.
393 
394  Parameters
395  ----------
396  exposure : `lsst.afw.image.Exposure`, optional
397  The input exposure to build a matched template for.
398  May be omitted if all of the metadata is supplied separately
399  order : `int`, optional
400  Interpolation order of the DCR shift.
401  visitInfo : `lsst.afw.image.VisitInfo`, optional
402  Metadata for the exposure. Ignored if ``exposure`` is set.
403  bbox : `lsst.afw.geom.Box2I`, optional
404  Sub-region of the coadd. Ignored if ``exposure`` is set.
405  wcs : `lsst.afw.geom.SkyWcs`, optional
406  Coordinate system definition (wcs) for the exposure.
407  Ignored if ``exposure`` is set.
408  mask : `lsst.afw.image.Mask`, optional
409  reference mask to use for the template image.
410  splitSubfilters : `bool`, optional
411  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
412  instead of at the midpoint. Default: True
413  splitThreshold : `float`, optional
414  Minimum DCR difference within a subfilter required to use
415  ``splitSubfilters``
416  amplifyModel : `float`, optional
417  Multiplication factor to amplify differences between model planes.
418  Used to speed convergence of iterative forward modeling.
419 
420  Returns
421  -------
422  templateImage : `lsst.afw.image.ImageF`
423  The DCR-matched template
424 
425  Raises
426  ------
427  ValueError
428  If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and
429  ``wcs`` are set.
430  """
431  if self.effectiveWavelength is None or self.bandwidth is None:
432  raise ValueError("'effectiveWavelength' and 'bandwidth' must be set for the DcrModel in order "
433  "to calculate DCR.")
434  if exposure is not None:
435  visitInfo = exposure.getInfo().getVisitInfo()
436  bbox = exposure.getBBox()
437  wcs = exposure.getInfo().getWcs()
438  elif visitInfo is None or bbox is None or wcs is None:
439  raise ValueError("Either exposure or visitInfo, bbox, and wcs must be set.")
440  dcrShift = calculateDcr(visitInfo, wcs, self.effectiveWavelength, self.bandwidth, len(self),
441  splitSubfilters=splitSubfilters)
442  templateImage = afwImage.ImageF(bbox)
443  refModel = self.getReferenceImage(bbox)
444  for subfilter, dcr in enumerate(dcrShift):
445  if amplifyModel > 1:
446  model = (self[subfilter][bbox].array - refModel)*amplifyModel + refModel
447  else:
448  model = self[subfilter][bbox].array
449  templateImage.array += applyDcr(model, dcr, splitSubfilters=splitSubfilters,
450  splitThreshold=splitThreshold, order=order)
451  return templateImage
452 
453  def buildMatchedExposure(self, exposure=None,
454  visitInfo=None, bbox=None, wcs=None, mask=None):
455  """Wrapper to create an exposure from a template image.
456 
457  Parameters
458  ----------
459  exposure : `lsst.afw.image.Exposure`, optional
460  The input exposure to build a matched template for.
461  May be omitted if all of the metadata is supplied separately
462  visitInfo : `lsst.afw.image.VisitInfo`, optional
463  Metadata for the exposure. Ignored if ``exposure`` is set.
464  bbox : `lsst.afw.geom.Box2I`, optional
465  Sub-region of the coadd. Ignored if ``exposure`` is set.
466  wcs : `lsst.afw.geom.SkyWcs`, optional
467  Coordinate system definition (wcs) for the exposure.
468  Ignored if ``exposure`` is set.
469  mask : `lsst.afw.image.Mask`, optional
470  reference mask to use for the template image.
471 
472  Returns
473  -------
474  templateExposure : `lsst.afw.image.exposureF`
475  The DCR-matched template
476 
477  Raises
478  ------
479  RuntimeError
480  If no `photcCalib` is set.
481  """
482  if bbox is None:
483  bbox = exposure.getBBox()
484  templateImage = self.buildMatchedTemplate(exposure=exposure, visitInfo=visitInfo,
485  bbox=bbox, wcs=wcs, mask=mask)
486  maskedImage = afwImage.MaskedImageF(bbox)
487  maskedImage.image = templateImage[bbox]
488  maskedImage.mask = self.mask[bbox]
489  maskedImage.variance = self.variance[bbox]
490  # The variance of the stacked image will be `dcrNumSubfilters`
491  # times the variance of the individual subfilters.
492  maskedImage.variance *= self.dcrNumSubfilters
493  templateExposure = afwImage.ExposureF(bbox, wcs)
494  templateExposure.setMaskedImage(maskedImage[bbox])
495  templateExposure.setPsf(self.psf)
496  templateExposure.setFilter(self.filterInfo)
497  if self.photoCalib is None:
498  raise RuntimeError("No PhotoCalib set for the DcrModel. "
499  "If the DcrModel was created from a masked image"
500  " you must also specify the photoCalib.")
501  templateExposure.setPhotoCalib(self.photoCalib)
502  return templateExposure
503 
504  def conditionDcrModel(self, modelImages, bbox, gain=1.):
505  """Average two iterations' solutions to reduce oscillations.
506 
507  Parameters
508  ----------
509  modelImages : `list` of `lsst.afw.image.Image`
510  The new DCR model images from the current iteration.
511  The values will be modified in place.
512  bbox : `lsst.afw.geom.Box2I`
513  Sub-region of the coadd
514  gain : `float`, optional
515  Relative weight to give the new solution when updating the model.
516  Defaults to 1.0, which gives equal weight to both solutions.
517  """
518  # Calculate weighted averages of the images.
519  for model, newModel in zip(self, modelImages):
520  newModel *= gain
521  newModel += model[bbox]
522  newModel /= 1. + gain
523 
524  def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor,
525  regularizationWidth=2):
526  """Restrict large variations in the model between iterations.
527 
528  Parameters
529  ----------
530  subfilter : `int`
531  Index of the current subfilter within the full band.
532  newModel : `lsst.afw.image.Image`
533  The new DCR model for one subfilter from the current iteration.
534  Values in ``newModel`` that are extreme compared with the last
535  iteration are modified in place.
536  bbox : `lsst.afw.geom.Box2I`
537  Sub-region to coadd
538  regularizationFactor : `float`
539  Maximum relative change of the model allowed between iterations.
540  regularizationWidth : int, optional
541  Minimum radius of a region to include in regularization, in pixels.
542  """
543  refImage = self[subfilter][bbox].array
544  highThreshold = np.abs(refImage)*regularizationFactor
545  lowThreshold = refImage/regularizationFactor
546  newImage = newModel.array
547  self.applyImageThresholds(newImage, highThreshold=highThreshold, lowThreshold=lowThreshold,
548  regularizationWidth=regularizationWidth)
549 
550  def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor,
551  regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
552  """Restrict large variations in the model between subfilters.
553 
554  Parameters
555  ----------
556  modelImages : `list` of `lsst.afw.image.Image`
557  The new DCR model images from the current iteration.
558  The values will be modified in place.
559  bbox : `lsst.afw.geom.Box2I`
560  Sub-region to coadd
561  statsCtrl : `lsst.afw.math.StatisticsControl`
562  Statistics control object for coaddition.
563  regularizationFactor : `float`
564  Maximum relative change of the model allowed between subfilters.
565  regularizationWidth : `int`, optional
566  Minimum radius of a region to include in regularization, in pixels.
567  mask : `lsst.afw.image.Mask`, optional
568  Optional alternate mask
569  convergenceMaskPlanes : `list` of `str`, or `str`, optional
570  Mask planes to use to calculate convergence.
571 
572  Notes
573  -----
574  This implementation of frequency regularization restricts each
575  subfilter image to be a smoothly-varying function times a reference
576  image.
577  """
578  # ``regularizationFactor`` is the maximum change between subfilter
579  # images, so the maximum difference between one subfilter image and the
580  # average will be the square root of that.
581  maxDiff = np.sqrt(regularizationFactor)
582  noiseLevel = self.calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
583  referenceImage = self.getReferenceImage(bbox)
584  badPixels = np.isnan(referenceImage) | (referenceImage <= 0.)
585  if np.sum(~badPixels) == 0:
586  # Skip regularization if there are no valid pixels
587  return
588  referenceImage[badPixels] = 0.
589  filterWidth = regularizationWidth
590  fwhm = 2.*filterWidth
591  # The noise should be lower in the smoothed image by
592  # sqrt(Nsmooth) ~ fwhm pixels
593  noiseLevel /= fwhm
594  smoothRef = ndimage.filters.gaussian_filter(referenceImage, filterWidth, mode='constant')
595  # Add a three sigma offset to both the reference and model to prevent
596  # dividing by zero. Note that this will also slightly suppress faint
597  # variations in color.
598  smoothRef += 3.*noiseLevel
599 
600  lowThreshold = smoothRef/maxDiff
601  highThreshold = smoothRef*maxDiff
602  for model in modelImages:
603  self.applyImageThresholds(model.array,
604  highThreshold=highThreshold,
605  lowThreshold=lowThreshold,
606  regularizationWidth=regularizationWidth)
607  smoothModel = ndimage.filters.gaussian_filter(model.array, filterWidth, mode='constant')
608  smoothModel += 3.*noiseLevel
609  relativeModel = smoothModel/smoothRef
610  # Now sharpen the smoothed relativeModel using an alpha of 3.
611  alpha = 3.
612  relativeModel2 = ndimage.filters.gaussian_filter(relativeModel, filterWidth/alpha)
613  relativeModel += alpha*(relativeModel - relativeModel2)
614  model.array = relativeModel*referenceImage
615 
616  def calculateNoiseCutoff(self, image, statsCtrl, bufferSize,
617  convergenceMaskPlanes="DETECTED", mask=None, bbox=None):
618  """Helper function to calculate the background noise level of an image.
619 
620  Parameters
621  ----------
622  image : `lsst.afw.image.Image`
623  The input image to evaluate the background noise properties.
624  statsCtrl : `lsst.afw.math.StatisticsControl`
625  Statistics control object for coaddition.
626  bufferSize : `int`
627  Number of additional pixels to exclude
628  from the edges of the bounding box.
629  convergenceMaskPlanes : `list` of `str`, or `str`
630  Mask planes to use to calculate convergence.
631  mask : `lsst.afw.image.Mask`, Optional
632  Optional alternate mask
633  bbox : `lsst.afw.geom.Box2I`, optional
634  Sub-region of the masked image to calculate the noise level over.
635 
636  Returns
637  -------
638  noiseCutoff : `float`
639  The threshold value to treat pixels as noise in an image..
640  """
641  if bbox is None:
642  bbox = self.bbox
643  if mask is None:
644  mask = self.mask[bbox]
645  bboxShrink = geom.Box2I(bbox)
646  bboxShrink.grow(-bufferSize)
647  convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
648 
649  backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
650  noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
651  return noiseCutoff
652 
653  def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2):
654  """Restrict image values to be between upper and lower limits.
655 
656  This method flags all pixels in an image that are outside of the given
657  threshold values. The threshold values are taken from a reference
658  image, so noisy pixels are likely to get flagged. In order to exclude
659  those noisy pixels, the array of flags is eroded and dilated, which
660  removes isolated pixels outside of the thresholds from the list of
661  pixels to be modified. Pixels that remain flagged after this operation
662  have their values set to the appropriate upper or lower threshold
663  value.
664 
665  Parameters
666  ----------
667  image : `numpy.ndarray`
668  The image to apply the thresholds to.
669  The values will be modified in place.
670  highThreshold : `numpy.ndarray`, optional
671  Array of upper limit values for each pixel of ``image``.
672  lowThreshold : `numpy.ndarray`, optional
673  Array of lower limit values for each pixel of ``image``.
674  regularizationWidth : `int`, optional
675  Minimum radius of a region to include in regularization, in pixels.
676  """
677  # Generate the structure for binary erosion and dilation, which is used
678  # to remove noise-like pixels. Groups of pixels with a radius smaller
679  # than ``regularizationWidth`` will be excluded from regularization.
680  filterStructure = ndimage.iterate_structure(ndimage.generate_binary_structure(2, 1),
681  regularizationWidth)
682  if highThreshold is not None:
683  highPixels = image > highThreshold
684  if regularizationWidth > 0:
685  # Erode and dilate ``highPixels`` to exclude noisy pixels.
686  highPixels = ndimage.morphology.binary_opening(highPixels, structure=filterStructure)
687  image[highPixels] = highThreshold[highPixels]
688  if lowThreshold is not None:
689  lowPixels = image < lowThreshold
690  if regularizationWidth > 0:
691  # Erode and dilate ``lowPixels`` to exclude noisy pixels.
692  lowPixels = ndimage.morphology.binary_opening(lowPixels, structure=filterStructure)
693  image[lowPixels] = lowThreshold[lowPixels]
694 
695 
696 def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0.,
697  doPrefilter=True, order=3):
698  """Shift an image along the X and Y directions.
699 
700  Parameters
701  ----------
702  image : `numpy.ndarray`
703  The input image to shift.
704  dcr : `tuple`
705  Shift calculated with ``calculateDcr``.
706  Uses numpy axes ordering (Y, X).
707  If ``splitSubfilters`` is set, each element is itself a `tuple`
708  of two `float`, corresponding to the DCR shift at the two wavelengths.
709  Otherwise, each element is a `float` corresponding to the DCR shift at
710  the effective wavelength of the subfilter.
711  useInverse : `bool`, optional
712  Apply the shift in the opposite direction. Default: False
713  splitSubfilters : `bool`, optional
714  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
715  instead of at the midpoint. Default: False
716  splitThreshold : `float`, optional
717  Minimum DCR difference within a subfilter required to use
718  ``splitSubfilters``
719  doPrefilter : `bool`, optional
720  Spline filter the image before shifting, if set. Filtering is required,
721  so only set to False if the image is already filtered.
722  Filtering takes ~20% of the time of shifting, so if `applyDcr` will be
723  called repeatedly on the same image it is more efficient to
724  precalculate the filter.
725  order : `int`, optional
726  The order of the spline interpolation, default is 3.
727 
728  Returns
729  -------
730  shiftedImage : `numpy.ndarray`
731  A copy of the input image with the specified shift applied.
732  """
733  if doPrefilter:
734  prefilteredImage = ndimage.spline_filter(image, order=order)
735  else:
736  prefilteredImage = image
737  if splitSubfilters:
738  shiftAmp = np.max(np.abs([_dcr0 - _dcr1 for _dcr0, _dcr1 in zip(dcr[0], dcr[1])]))
739  if shiftAmp >= splitThreshold:
740  if useInverse:
741  shift = [-1.*s for s in dcr[0]]
742  shift1 = [-1.*s for s in dcr[1]]
743  else:
744  shift = dcr[0]
745  shift1 = dcr[1]
746  shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=False, order=order)
747  shiftedImage += ndimage.shift(prefilteredImage, shift1, prefilter=False, order=order)
748  shiftedImage /= 2.
749  return shiftedImage
750  else:
751  # If the difference in the DCR shifts is less than the threshold,
752  # then just use the average shift for efficiency.
753  dcr = (np.mean(dcr[0]), np.mean(dcr[1]))
754  if useInverse:
755  shift = [-1.*s for s in dcr]
756  else:
757  shift = dcr
758  shiftedImage = ndimage.shift(prefilteredImage, shift, prefilter=False, order=order)
759  return shiftedImage
760 
761 
762 def calculateDcr(visitInfo, wcs, effectiveWavelength, bandwidth, dcrNumSubfilters, splitSubfilters=False):
763  """Calculate the shift in pixels of an exposure due to DCR.
764 
765  Parameters
766  ----------
767  visitInfo : `lsst.afw.image.VisitInfo`
768  Metadata for the exposure.
769  wcs : `lsst.afw.geom.SkyWcs`
770  Coordinate system definition (wcs) for the exposure.
771  effectiveWavelength : `float`
772  The effective wavelengths of the current filter, in nanometers.
773  bandwidth : `float`
774  The bandwidth of the current filter, in nanometers.
775  dcrNumSubfilters : `int`
776  Number of sub-filters used to model chromatic effects within a band.
777  splitSubfilters : `bool`, optional
778  Calculate DCR for two evenly-spaced wavelengths in each subfilter,
779  instead of at the midpoint. Default: False
780 
781  Returns
782  -------
783  dcrShift : `tuple` of two `float`
784  The 2D shift due to DCR, in pixels.
785  Uses numpy axes ordering (Y, X).
786  """
787  rotation = calculateImageParallacticAngle(visitInfo, wcs)
788  dcrShift = []
789  weight = [0.75, 0.25]
790  for wl0, wl1 in wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters):
791  # Note that diffRefractAmp can be negative, since it's relative to the
792  # midpoint of the full band
793  diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=effectiveWavelength,
794  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
795  observatory=visitInfo.getObservatory(),
796  weather=visitInfo.getWeather())
797  diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=effectiveWavelength,
798  elevation=visitInfo.getBoresightAzAlt().getLatitude(),
799  observatory=visitInfo.getObservatory(),
800  weather=visitInfo.getWeather())
801  if splitSubfilters:
802  diffRefractPix0 = diffRefractAmp0.asArcseconds()/wcs.getPixelScale().asArcseconds()
803  diffRefractPix1 = diffRefractAmp1.asArcseconds()/wcs.getPixelScale().asArcseconds()
804  diffRefractArr = [diffRefractPix0*weight[0] + diffRefractPix1*weight[1],
805  diffRefractPix0*weight[1] + diffRefractPix1*weight[0]]
806  shiftX = [diffRefractPix*np.sin(rotation.asRadians()) for diffRefractPix in diffRefractArr]
807  shiftY = [diffRefractPix*np.cos(rotation.asRadians()) for diffRefractPix in diffRefractArr]
808  dcrShift.append(((shiftY[0], shiftX[0]), (shiftY[1], shiftX[1])))
809  else:
810  diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
811  diffRefractPix = diffRefractAmp.asArcseconds()/wcs.getPixelScale().asArcseconds()
812  shiftX = diffRefractPix*np.sin(rotation.asRadians())
813  shiftY = diffRefractPix*np.cos(rotation.asRadians())
814  dcrShift.append((shiftY, shiftX))
815  return dcrShift
816 
817 
818 def calculateImageParallacticAngle(visitInfo, wcs):
819  """Calculate the total sky rotation angle of an exposure.
820 
821  Parameters
822  ----------
823  visitInfo : `lsst.afw.image.VisitInfo`
824  Metadata for the exposure.
825  wcs : `lsst.afw.geom.SkyWcs`
826  Coordinate system definition (wcs) for the exposure.
827 
828  Returns
829  -------
830  `lsst.geom.Angle`
831  The rotation of the image axis, East from North.
832  Equal to the parallactic angle plus any additional rotation of the
833  coordinate system.
834  A rotation angle of 0 degrees is defined with
835  North along the +y axis and East along the +x axis.
836  A rotation angle of 90 degrees is defined with
837  North along the +x axis and East along the -y axis.
838  """
839  parAngle = visitInfo.getBoresightParAngle().asRadians()
840  cd = wcs.getCdMatrix()
841  if wcs.isFlipped:
842  cdAngle = (np.arctan2(-cd[0, 1], cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
843  rotAngle = (cdAngle + parAngle)*geom.radians
844  else:
845  cdAngle = (np.arctan2(cd[0, 1], -cd[0, 0]) + np.arctan2(cd[1, 0], cd[1, 1]))/2.
846  rotAngle = (cdAngle - parAngle)*geom.radians
847  return rotAngle
848 
849 
850 def wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters):
851  """Iterate over the wavelength endpoints of subfilters.
852 
853  Parameters
854  ----------
855  effectiveWavelength : `float`
856  The effective wavelength of the current filter, in nanometers.
857  bandwidth : `float`
858  The bandwidth of the current filter, in nanometers.
859  dcrNumSubfilters : `int`
860  Number of sub-filters used to model chromatic effects within a band.
861 
862  Yields
863  ------
864  `tuple` of two `float`
865  The next set of wavelength endpoints for a subfilter, in nanometers.
866  """
867  lambdaMin = effectiveWavelength - bandwidth/2
868  lambdaMax = effectiveWavelength + bandwidth/2
869  wlStep = bandwidth/dcrNumSubfilters
870  for wl in np.linspace(lambdaMin, lambdaMax, dcrNumSubfilters, endpoint=False):
871  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:389
lsst::afw::image
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
Definition: imageAlgorithm.dox:1
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:366
lsst::ip::diffim.dcrModel.DcrModel.fromQuantum
def fromQuantum(cls, availableCoaddRefs, effectiveWavelength, bandwidth)
Definition: dcrModel.py:170
lsst::ip::diffim.dcrModel.DcrModel.getReferenceImage
def getReferenceImage(self, bbox=None)
Definition: dcrModel.py:349
lsst::ip::diffim.dcrModel.wavelengthGenerator
def wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters)
Definition: dcrModel.py:850
lsst::ip::diffim.dcrModel.applyDcr
def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0., doPrefilter=True, order=3)
Definition: dcrModel.py:696
lsst::ip::diffim.dcrModel.DcrModel.fromDataRef
def fromDataRef(cls, dataRef, effectiveWavelength, bandwidth, datasetType="dcrCoadd", numSubfilters=None, **kwargs)
Definition: dcrModel.py:116
lsst::ip::diffim.dcrModel.DcrModel.variance
def variance(self)
Definition: dcrModel.py:339
lsst::ip::diffim.dcrModel.DcrModel._effectiveWavelength
_effectiveWavelength
Definition: dcrModel.py:56
lsst::ip::diffim.dcrModel.DcrModel.buildMatchedExposure
def buildMatchedExposure(self, exposure=None, visitInfo=None, bbox=None, wcs=None, mask=None)
Definition: dcrModel.py:453
lsst::ip::diffim.dcrModel.DcrModel.regularizeModelFreq
def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor, regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED")
Definition: dcrModel.py:550
lsst::ip::diffim.dcrModel.DcrModel.__len__
def __len__(self)
Definition: dcrModel.py:212
lsst::ip::diffim.dcrModel.DcrModel._filterInfo
_filterInfo
Definition: dcrModel.py:55
lsst::ip::diffim.dcrModel.DcrModel.applyImageThresholds
def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2)
Definition: dcrModel.py:653
lsst::ip::diffim.dcrModel.DcrModel.__setitem__
def __setitem__(self, subfilter, maskedImage)
Definition: dcrModel.py:247
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:59
lsst::ip::diffim.dcrModel.calculateImageParallacticAngle
def calculateImageParallacticAngle(visitInfo, wcs)
Definition: dcrModel.py:818
lsst::ip::diffim.dcrModel.DcrModel.psf
def psf(self)
Definition: dcrModel.py:306
lsst::ip::diffim.dcrModel.DcrModel.bbox
def bbox(self)
Definition: dcrModel.py:317
lsst::ip::diffim.dcrModel.DcrModel.filter
def filter(self)
Definition: dcrModel.py:283
lsst::ip::diffim.dcrModel.DcrModel.mask
def mask(self)
Definition: dcrModel.py:328
lsst::ip::diffim.dcrModel.DcrModel.__getitem__
def __getitem__(self, subfilter)
Definition: dcrModel.py:222
lsst::ip::diffim.dcrModel.DcrModel.effectiveWavelength
def effectiveWavelength(self)
Definition: dcrModel.py:272
lsst::ip::diffim.dcrModel.DcrModel.calculateNoiseCutoff
def calculateNoiseCutoff(self, image, statsCtrl, bufferSize, convergenceMaskPlanes="DETECTED", mask=None, bbox=None)
Definition: dcrModel.py:616
lsst::ip::diffim.dcrModel.DcrModel.photoCalib
photoCalib
Definition: dcrModel.py:61
lsst::ip::diffim.dcrModel.DcrModel.__init__
def __init__(self, modelImages, effectiveWavelength, bandwidth, filterInfo=None, psf=None, mask=None, variance=None, photoCalib=None)
Definition: dcrModel.py:52
lsst::ip::diffim.dcrModel.DcrModel.bandwidth
def bandwidth(self)
Definition: dcrModel.py:295
lsst::ip::diffim.dcrModel.DcrModel.regularizeModelIter
def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor, regularizationWidth=2)
Definition: dcrModel.py:524
lsst::geom
Definition: AffineTransform.h:36
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:504
lsst::ip::diffim.dcrModel.DcrModel.modelImages
modelImages
Definition: dcrModel.py:54
lsst::ip::diffim.dcrModel.DcrModel._variance
_variance
Definition: dcrModel.py:60
lsst::geom::Box2I
An integer coordinate rectangle.
Definition: Box.h:55
lsst::ip::diffim.dcrModel.DcrModel._psf
_psf
Definition: dcrModel.py:58
lsst::ip::diffim.dcrModel.calculateDcr
def calculateDcr(visitInfo, wcs, effectiveWavelength, bandwidth, dcrNumSubfilters, splitSubfilters=False)
Definition: dcrModel.py:762
lsst::ip::diffim.dcrModel.DcrModel.dcrNumSubfilters
dcrNumSubfilters
Definition: dcrModel.py:53
lsst::ip::diffim.dcrModel.DcrModel._bandwidth
_bandwidth
Definition: dcrModel.py:57
lsst::ip::diffim.dcrModel.DcrModel.fromImage
def fromImage(cls, maskedImage, dcrNumSubfilters, effectiveWavelength, bandwidth, filterInfo=None, psf=None, photoCalib=None)
Definition: dcrModel.py:65