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