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