Coverage for python/lsst/ip/diffim/dcrModel.py : 12%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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#
23import numpy as np
24from scipy import ndimage
25from lsst.afw.coord import differentialRefraction
26import lsst.afw.image as afwImage
27import lsst.geom as geom
29__all__ = ["DcrModel", "applyDcr", "calculateDcr", "calculateImageParallacticAngle"]
32class DcrModel:
33 """A model of the true sky after correcting chromatic effects.
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
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 """
52 def __init__(self, modelImages, effectiveWavelength, bandwidth, filterLabel=None, psf=None,
53 mask=None, variance=None, photoCalib=None):
54 self.dcrNumSubfilters = len(modelImages)
55 self.modelImages = modelImages
56 self._filterLabel = filterLabel
57 self._effectiveWavelength = effectiveWavelength
58 self._bandwidth = bandwidth
59 self._psf = psf
60 self._mask = mask
61 self._variance = variance
62 self.photoCalib = photoCalib
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.
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.
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)
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.
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`
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)
168 @classmethod
169 def fromQuantum(cls, availableCoaddRefs, effectiveWavelength, bandwidth):
170 """Load an existing DcrModel from a Gen 3 repository.
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.
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)
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)
211 def __len__(self):
212 """Return the number of subfilters.
214 Returns
215 -------
216 dcrNumSubfilters : `int`
217 The number of DCR subfilters in the model.
218 """
219 return self.dcrNumSubfilters
221 def __getitem__(self, subfilter):
222 """Iterate over the subfilters of the DCR model.
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``.
231 Returns
232 -------
233 modelImage : `lsst.afw.image.Image`
234 The DCR model for the given ``subfilter``.
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.modelImages[subfilter]
246 def __setitem__(self, subfilter, maskedImage):
247 """Update the model image for one subfilter.
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``.
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.bbox:
267 raise ValueError("The bounding box of a subfilter must not change.")
268 self.modelImages[subfilter] = maskedImage
270 @property
271 def effectiveWavelength(self):
272 """Return the effective wavelength of the model.
274 Returns
275 -------
276 effectiveWavelength : `float`
277 The effective wavelength of the current filter, in nanometers.
278 """
279 return self._effectiveWavelength
281 @property
282 def filter(self):
283 """Return the filter label for the model.
285 Returns
286 -------
287 filterLabel : `lsst.afw.image.FilterLabel`
288 The filter used for the input observations.
289 """
290 return self._filterLabel
292 @property
293 def bandwidth(self):
294 """Return the bandwidth of the model.
296 Returns
297 -------
298 bandwidth : `float`
299 The bandwidth of the current filter, in nanometers.
300 """
301 return self._bandwidth
303 @property
304 def psf(self):
305 """Return the psf of the model.
307 Returns
308 -------
309 psf : `lsst.afw.detection.Psf`
310 Point spread function (PSF) of the model.
311 """
312 return self._psf
314 @property
315 def bbox(self):
316 """Return the common bounding box of each subfilter image.
318 Returns
319 -------
320 bbox : `lsst.afw.geom.Box2I`
321 Bounding box of the DCR model.
322 """
323 return self[0].getBBox()
325 @property
326 def mask(self):
327 """Return the common mask of each subfilter image.
329 Returns
330 -------
331 mask : `lsst.afw.image.Mask`
332 Mask plane of the DCR model.
333 """
334 return self._mask
336 @property
337 def variance(self):
338 """Return the common variance of each subfilter image.
340 Returns
341 -------
342 variance : `lsst.afw.image.Image`
343 Variance plane of the DCR model.
344 """
345 return self._variance
347 def getReferenceImage(self, bbox=None):
348 """Calculate a reference image from the average of the subfilter
349 images.
351 Parameters
352 ----------
353 bbox : `lsst.afw.geom.Box2I`, optional
354 Sub-region of the coadd. Returns the entire image if `None`.
356 Returns
357 -------
358 refImage : `numpy.ndarray`
359 The reference image with no chromatic effects applied.
360 """
361 bbox = bbox or self.bbox
362 return np.mean([model[bbox].array for model in self], axis=0)
364 def assign(self, dcrSubModel, bbox=None):
365 """Update a sub-region of the ``DcrModel`` with new values.
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``.
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.bbox
384 for model, subModel in zip(self, dcrSubModel):
385 model.assign(subModel[bbox], bbox)
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.
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.
418 Returns
419 -------
420 templateImage : `lsst.afw.image.ImageF`
421 The DCR-matched template
423 Raises
424 ------
425 ValueError
426 If neither ``exposure`` or all of ``visitInfo``, ``bbox``, and
427 ``wcs`` are set.
428 """
429 if self.effectiveWavelength is None or self.bandwidth 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.effectiveWavelength, self.bandwidth, len(self),
439 splitSubfilters=splitSubfilters)
440 templateImage = afwImage.ImageF(bbox)
441 refModel = self.getReferenceImage(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
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.
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.
470 Returns
471 -------
472 templateExposure : `lsst.afw.image.exposureF`
473 The DCR-matched template
475 Raises
476 ------
477 RuntimeError
478 If no `photcCalib` is set.
479 """
480 if bbox is None:
481 bbox = exposure.getBBox()
482 templateImage = self.buildMatchedTemplate(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.mask[bbox]
487 maskedImage.variance = self.variance[bbox]
488 # The variance of the stacked image will be `dcrNumSubfilters`
489 # times the variance of the individual subfilters.
490 maskedImage.variance *= self.dcrNumSubfilters
491 templateExposure = afwImage.ExposureF(bbox, wcs)
492 templateExposure.setMaskedImage(maskedImage[bbox])
493 templateExposure.setPsf(self.psf)
494 templateExposure.setFilterLabel(self.filterLabel)
495 if self.photoCalib 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.photoCalib)
500 return templateExposure
502 def conditionDcrModel(self, modelImages, bbox, gain=1.):
503 """Average two iterations' solutions to reduce oscillations.
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
522 def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor,
523 regularizationWidth=2):
524 """Restrict large variations in the model between iterations.
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.applyImageThresholds(newImage, highThreshold=highThreshold, lowThreshold=lowThreshold,
546 regularizationWidth=regularizationWidth)
548 def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor,
549 regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
550 """Restrict large variations in the model between subfilters.
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.
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.calculateNoiseCutoff(modelImages[0], statsCtrl, bufferSize=5, mask=mask, bbox=bbox)
581 referenceImage = self.getReferenceImage(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
598 lowThreshold = smoothRef/maxDiff
599 highThreshold = smoothRef*maxDiff
600 for model in modelImages:
601 self.applyImageThresholds(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
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.
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.
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.bbox
641 if mask is None:
642 mask = self.mask[bbox]
643 bboxShrink = geom.Box2I(bbox)
644 bboxShrink.grow(-bufferSize)
645 convergeMask = mask.getPlaneBitMask(convergenceMaskPlanes)
647 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
648 noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
649 return noiseCutoff
651 def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2):
652 """Restrict image values to be between upper and lower limits.
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.
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]
694def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0.,
695 doPrefilter=True, order=3):
696 """Shift an image along the X and Y directions.
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.
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
760def calculateDcr(visitInfo, wcs, effectiveWavelength, bandwidth, dcrNumSubfilters, splitSubfilters=False):
761 """Calculate the shift in pixels of an exposure due to DCR.
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
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
816def calculateImageParallacticAngle(visitInfo, wcs):
817 """Calculate the total sky rotation angle of an exposure.
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.
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
848def wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters):
849 """Iterate over the wavelength endpoints of subfilters.
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.
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)