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