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

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.refraction 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, 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
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.
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.
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)
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.
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`
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)
169 @classmethod
170 def fromQuantum(cls, availableCoaddRefs, effectiveWavelength, bandwidth):
171 """Load an existing DcrModel from a Gen 3 repository.
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.
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)
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)
212 def __len__(self):
213 """Return the number of subfilters.
215 Returns
216 -------
217 dcrNumSubfilters : `int`
218 The number of DCR subfilters in the model.
219 """
220 return self.dcrNumSubfilters
222 def __getitem__(self, subfilter):
223 """Iterate over the subfilters of the DCR model.
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``.
232 Returns
233 -------
234 modelImage : `lsst.afw.image.Image`
235 The DCR model for the given ``subfilter``.
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]
247 def __setitem__(self, subfilter, maskedImage):
248 """Update the model image for one subfilter.
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``.
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
271 @property
272 def effectiveWavelength(self):
273 """Return the effective wavelength of the model.
275 Returns
276 -------
277 effectiveWavelength : `float`
278 The effective wavelength of the current filter, in nanometers.
279 """
280 return self._effectiveWavelength
282 @property
283 def filter(self):
284 """Return the filter label for the model.
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
294 @property
295 def bandwidth(self):
296 """Return the bandwidth of the model.
298 Returns
299 -------
300 bandwidth : `float`
301 The bandwidth of the current filter, in nanometers.
302 """
303 return self._bandwidth
305 @property
306 def psf(self):
307 """Return the psf of the model.
309 Returns
310 -------
311 psf : `lsst.afw.detection.Psf`
312 Point spread function (PSF) of the model.
313 """
314 return self._psf
316 @property
317 def bbox(self):
318 """Return the common bounding box of each subfilter image.
320 Returns
321 -------
322 bbox : `lsst.afw.geom.Box2I`
323 Bounding box of the DCR model.
324 """
325 return self[0].getBBox()
327 @property
328 def mask(self):
329 """Return the common mask of each subfilter image.
331 Returns
332 -------
333 mask : `lsst.afw.image.Mask`
334 Mask plane of the DCR model.
335 """
336 return self._mask
338 @property
339 def variance(self):
340 """Return the common variance of each subfilter image.
342 Returns
343 -------
344 variance : `lsst.afw.image.Image`
345 Variance plane of the DCR model.
346 """
347 return self._variance
349 def getReferenceImage(self, bbox=None):
350 """Calculate a reference image from the average of the subfilter
351 images.
353 Parameters
354 ----------
355 bbox : `lsst.afw.geom.Box2I`, optional
356 Sub-region of the coadd. Returns the entire image if `None`.
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)
366 def assign(self, dcrSubModel, bbox=None):
367 """Update a sub-region of the ``DcrModel`` with new values.
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``.
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)
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.
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.
420 Returns
421 -------
422 templateImage : `lsst.afw.image.ImageF`
423 The DCR-matched template
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
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.
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.
472 Returns
473 -------
474 templateExposure : `lsst.afw.image.exposureF`
475 The DCR-matched template
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
504 def conditionDcrModel(self, modelImages, bbox, gain=1.):
505 """Average two iterations' solutions to reduce oscillations.
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
524 def regularizeModelIter(self, subfilter, newModel, bbox, regularizationFactor,
525 regularizationWidth=2):
526 """Restrict large variations in the model between iterations.
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)
550 def regularizeModelFreq(self, modelImages, bbox, statsCtrl, regularizationFactor,
551 regularizationWidth=2, mask=None, convergenceMaskPlanes="DETECTED"):
552 """Restrict large variations in the model between subfilters.
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.
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
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
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.
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.
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)
649 backgroundPixels = mask[bboxShrink].array & (statsCtrl.getAndMask() | convergeMask) == 0
650 noiseCutoff = np.std(image[bboxShrink].array[backgroundPixels])
651 return noiseCutoff
653 def applyImageThresholds(self, image, highThreshold=None, lowThreshold=None, regularizationWidth=2):
654 """Restrict image values to be between upper and lower limits.
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.
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]
696def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0.,
697 doPrefilter=True, order=3):
698 """Shift an image along the X and Y directions.
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.
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
762def calculateDcr(visitInfo, wcs, effectiveWavelength, bandwidth, dcrNumSubfilters, splitSubfilters=False):
763 """Calculate the shift in pixels of an exposure due to DCR.
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
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
818def calculateImageParallacticAngle(visitInfo, wcs):
819 """Calculate the total sky rotation angle of an exposure.
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.
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
850def wavelengthGenerator(effectiveWavelength, bandwidth, dcrNumSubfilters):
851 """Iterate over the wavelength endpoints of subfilters.
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.
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)