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