LSST Applications g00d0e8bbd7+edbf708997,g03191d30f7+9ce8016dbd,g1955dfad08+0bd186d245,g199a45376c+5137f08352,g1fd858c14a+a888a50aa2,g262e1987ae+45f9aba685,g29ae962dfc+1c7d47a24f,g2cef7863aa+73c82f25e4,g35bb328faa+edbf708997,g3fd5ace14f+eed17d2c67,g47891489e3+6dc8069a4c,g53246c7159+edbf708997,g64539dfbff+c4107e45b5,g67b6fd64d1+6dc8069a4c,g74acd417e5+f452e9c21a,g786e29fd12+af89c03590,g7ae74a0b1c+a25e60b391,g7aefaa3e3d+2025e9ce17,g7cc15d900a+2d158402f9,g87389fa792+a4172ec7da,g89139ef638+6dc8069a4c,g8d4809ba88+c4107e45b5,g8d7436a09f+e96c132b44,g8ea07a8fe4+db21c37724,g98df359435+aae6d409c1,ga2180abaac+edbf708997,gac66b60396+966efe6077,gb632fb1845+88945a90f8,gbaa8f7a6c5+38b34f4976,gbf99507273+edbf708997,gca7fc764a6+6dc8069a4c,gd7ef33dd92+6dc8069a4c,gda68eeecaf+7d1e613a8d,gdab6d2f7ff+f452e9c21a,gdbb4c4dda9+c4107e45b5,ge410e46f29+6dc8069a4c,ge41e95a9f2+c4107e45b5,geaed405ab2+e194be0d2b,w.2025.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
subtractBackground.py
Go to the documentation of this file.
2# LSST Data Management System
3#
4# Copyright 2008-2017 AURA/LSST.
5#
6# This product includes software developed by the
7# LSST Project (http://www.lsst.org/).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the LSST License Statement and
20# the GNU General Public License along with this program. If not,
21# see <https://www.lsstcorp.org/LegalNotices/>.
22#
23__all__ = (
24 "SubtractBackgroundConfig",
25 "SubtractBackgroundTask",
26 "backgroundFlatContext",
27 "TooManyMaskedPixelsError",
28)
29
30import itertools
31import math
32
33from contextlib import contextmanager
34import numpy
35from scipy.ndimage import median_filter
36
37from lsstDebug import getDebugFrame
38from lsst.utils import suppress_deprecations
39
40import lsst.afw.display as afwDisplay
41import lsst.afw.image as afwImage
42import lsst.afw.math as afwMath
43import lsst.pex.config as pexConfig
44import lsst.pipe.base as pipeBase
45
46
47@contextmanager
48def backgroundFlatContext(maskedImage, doApply, backgroundToPhotometricRatio=None):
49 """Context manager to convert from photometric-flattened to background-
50 flattened image.
51
52 Parameters
53 ----------
54 maskedImage : `lsst.afw.image.MaskedImage`
55 Masked image (image + mask + variance) to convert from a
56 photometrically flat image to an image suitable for background
57 subtraction.
58 doApply : `bool`
59 Apply the conversion? If False, this context manager will not
60 do anything.
61 backgroundToPhotometricRatio : `lsst.afw.image.Image`, optional
62 Image to multiply a photometrically-flattened image by to obtain a
63 background-flattened image.
64 Only used if ``doApply`` is ``True``.
65
66 Yields
67 ------
68 maskedImage : `lsst.afw.image.MaskedImage`
69 Masked image converted into an image suitable for background
70 subtraction.
71
72 Raises
73 ------
74 RuntimeError if doApply is True and no ratio is supplied.
75 ValueError if the ratio is not an `lsst.afw.image.Image`.
76 """
77 if doApply:
78 if backgroundToPhotometricRatio is None:
79 raise RuntimeError("backgroundFlatContext called with doApply=True, "
80 "but without a backgroundToPhotometricRatio")
81 if not isinstance(backgroundToPhotometricRatio, afwImage.Image):
82 raise ValueError("The backgroundToPhotometricRatio must be an lsst.afw.image.Image")
83
84 maskedImage *= backgroundToPhotometricRatio
85
86 try:
87 yield maskedImage
88 finally:
89 if doApply:
90 maskedImage /= backgroundToPhotometricRatio
91
92
93class TooManyMaskedPixelsError(pipeBase.AlgorithmError):
94 """Raised when all pixels in the image are masked and no background
95 can be estimated.
96 """
97
98 @property
99 def metadata(self) -> dict:
100 """There is no metadata associated with this error.
101 """
102 return {}
103
104
105class SubtractBackgroundConfig(pexConfig.Config):
106 """Config for SubtractBackgroundTask
107
108 Many of these fields match fields in `lsst.afw.math.BackgroundControl`,
109 the control class for `lsst.afw.math.makeBackground`
110 """
111 statisticsProperty = pexConfig.ChoiceField(
112 doc="type of statistic to use for grid points",
113 dtype=str, default="MEANCLIP",
114 allowed={
115 "MEANCLIP": "clipped mean",
116 "MEAN": "unclipped mean",
117 "MEDIAN": "median",
118 }
119 )
120 undersampleStyle = pexConfig.ChoiceField(
121 doc="behaviour if there are too few points in grid for requested interpolation style",
122 dtype=str, default="REDUCE_INTERP_ORDER",
123 allowed={
124 "THROW_EXCEPTION": "throw an exception if there are too few points",
125 "REDUCE_INTERP_ORDER": "use an interpolation style with a lower order.",
126 "INCREASE_NXNYSAMPLE": "Increase the number of samples used to make the interpolation grid.",
127 },
128 )
129 binSize = pexConfig.RangeField(
130 doc="how large a region of the sky should be used for each background point",
131 dtype=int, default=128, min=1,
132 )
133 binSizeX = pexConfig.RangeField(
134 doc=("Sky region size to be used for each background point in X direction. "
135 "If 0, the binSize config is used."),
136 dtype=int, default=0, min=0,
137 )
138 binSizeY = pexConfig.RangeField(
139 doc=("Sky region size to be used for each background point in Y direction. "
140 "If 0, the binSize config is used."),
141 dtype=int, default=0, min=0,
142 )
143 algorithm = pexConfig.ChoiceField(
144 doc="how to interpolate the background values. This maps to an enum; see afw::math::Background",
145 dtype=str, default="AKIMA_SPLINE", optional=True,
146 allowed={
147 "CONSTANT": "Use a single constant value",
148 "LINEAR": "Use linear interpolation",
149 "NATURAL_SPLINE": "cubic spline with zero second derivative at endpoints",
150 "AKIMA_SPLINE": "higher-level nonlinear spline that is more robust to outliers",
151 "NONE": "No background estimation is to be attempted",
152 },
153 )
154 ignoredPixelMask = pexConfig.ListField(
155 doc="Names of mask planes to ignore while estimating the background",
156 dtype=str, default=["BAD", "EDGE", "DETECTED", "DETECTED_NEGATIVE", "NO_DATA", ],
157 )
158 isNanSafe = pexConfig.Field(
159 doc="Ignore NaNs when estimating the background",
160 dtype=bool, default=False,
161 )
162
163 useApprox = pexConfig.Field(
164 doc="Use Approximate (Chebyshev) to model background.",
165 dtype=bool, default=True,
166 )
167 approxOrderX = pexConfig.Field(
168 doc="Approximation order in X for background Chebyshev (valid only with useApprox=True)",
169 dtype=int, default=6,
170 )
171 # Note: Currently X- and Y-orders must be equal due to a limitation in math::Chebyshev1Function2
172 # The following is being added so that the weighting attribute can also be configurable for the
173 # call to afwMath.ApproximateControl
174 approxOrderY = pexConfig.Field(
175 doc="Approximation order in Y for background Chebyshev (valid only with useApprox=True)",
176 dtype=int, default=-1,
177 )
178 weighting = pexConfig.Field(
179 doc="Use inverse variance weighting in calculation (valid only with useApprox=True)",
180 dtype=bool, default=True,
181 )
182 doApplyFlatBackgroundRatio = pexConfig.Field(
183 doc="Convert from a photometrically flat image to one suitable to background subtraction? "
184 "If True, then a backgroundToPhotometricRatio must be supplied to the task run method.",
185 dtype=bool,
186 default=False,
187 )
188 doFilterSuperPixels = pexConfig.Field(
189 doc="Remove outliers from the binned image.",
190 dtype=bool,
191 default=False,
192 )
193 superPixelFilterSize = pexConfig.Field(
194 doc="Size of the median filter to use to remove outliers from the binned image.",
195 dtype=int,
196 default=3,
197 )
198
199
200class SubtractBackgroundTask(pipeBase.Task):
201 """Subtract the background from an exposure
202 """
203 ConfigClass = SubtractBackgroundConfig
204 _DefaultName = "subtractBackground"
205
206 def __init__(self, config=None, *, name=None, parentTask=None, log=None):
207 super().__init__(config, name=name, parentTask=parentTask, log=log)
208 self.binSizeX = self.config.binSize if self.config.binSizeX == 0 else self.config.binSizeX
209 self.binSizeY = self.config.binSize if self.config.binSizeY == 0 else self.config.binSizeY
210
211 def run(self, exposure, background=None, stats=True, statsKeys=None, backgroundToPhotometricRatio=None):
212 """Fit and subtract the background of an exposure.
213
214 Parameters
215 ----------
216 exposure : `lsst.afw.image.Exposure`
217 Exposure whose background is to be subtracted.
218 background : `lsst.afw.math.BackgroundList`
219 Initial background model already subtracted. May be None if no background
220 has been subtracted.
221 stats : `bool`
222 If True then measure the mean and variance of the full background model and
223 record the results in the exposure's metadata.
224 statsKeys : `tuple`
225 Key names used to store the mean and variance of the background in the
226 exposure's metadata (another tuple); if None then use ("BGMEAN", "BGVAR");
227 ignored if stats is false.
228 backgroundToPhotometricRatio : `lsst.afw.image.Image`, optional
229 Image to multiply a photometrically-flattened image by to obtain a
230 background-flattened image.
231 Only used if config.doApplyFlatBackgroundRatio = True.
232
233 Returns
234 -------
235 background : `lsst.afw.math.BackgroundList`
236 Full background model (initial model with changes), contained in an
237 `lsst.pipe.base.Struct`.
238 """
239 if background is None:
240 background = afwMath.BackgroundList()
241
242 maskedImage = exposure.maskedImage
243
245 maskedImage,
246 self.config.doApplyFlatBackgroundRatio,
247 backgroundToPhotometricRatio=backgroundToPhotometricRatio,
248 ):
249 fitBg = self.fitBackground(maskedImage)
250 if self.config.doFilterSuperPixels:
251 filterSuperPixels(maskedImage.getBBox(), fitBg,
252 superPixelFilterSize=self.config.superPixelFilterSize)
253
254 maskedImage -= fitBg.getImageF(self.config.algorithm, self.config.undersampleStyle)
255
256 actrl = fitBg.getBackgroundControl().getApproximateControl()
257 background.append((fitBg, getattr(afwMath.Interpolate, self.config.algorithm),
258 fitBg.getAsUsedUndersampleStyle(), actrl.getStyle(),
259 actrl.getOrderX(), actrl.getOrderY(), actrl.getWeighting()))
260
261 if stats:
262 self._addStats(exposure, background, statsKeys=statsKeys)
263
264 subFrame = getDebugFrame(self._display, "subtracted")
265 if subFrame:
266 subDisp = afwDisplay.getDisplay(frame=subFrame)
267 subDisp.mtv(exposure, title="subtracted")
268
269 bgFrame = getDebugFrame(self._display, "background")
270 if bgFrame:
271 bgDisp = afwDisplay.getDisplay(frame=bgFrame)
272 bgImage = background.getImage()
273 bgDisp.mtv(bgImage, title="background")
274
275 return pipeBase.Struct(
276 background=background,
277 )
278
279 def _addStats(self, exposure, background, statsKeys=None):
280 """Add statistics about the background to the exposure's metadata
281
282 Parameters
283 ----------
284 exposure : `lsst.afw.image.Exposure`
285 Exposure whose background was subtracted.
286 background : `lsst.afw.math.BackgroundList`
287 Background model
288 statsKeys : `tuple`
289 Key names used to store the mean and variance of the background in
290 the exposure's metadata (a tuple); if None then use
291 ("BGMEAN", "BGVAR"); ignored if stats is false.
292 """
293 netBgImg = background.getImage()
294 if statsKeys is None:
295 statsKeys = ("BGMEAN", "BGVAR")
296 mnkey, varkey = statsKeys
297 meta = exposure.getMetadata()
298 s = afwMath.makeStatistics(netBgImg, afwMath.MEAN | afwMath.VARIANCE)
299 bgmean = s.getValue(afwMath.MEAN)
300 bgvar = s.getValue(afwMath.VARIANCE)
301 meta.addDouble(mnkey, bgmean)
302 meta.addDouble(varkey, bgvar)
303
304 def fitBackground(self, maskedImage, nx=0, ny=0, algorithm=None):
305 """Estimate the background of a masked image
306
307 Parameters
308 ----------
309 maskedImage : `lsst.afw.image.maskedImage`
310 Masked image whose background is to be computed
311 nx : 'int`
312 Number of x bands; if 0 compute from width and `self.config.binSizeX`
313 ny : `int`
314 Number of y bands; if 0 compute from height and `self.config.binSizeY`
315 algorithm : `str`
316 Name of interpolation algorithm; if None use `self.config.algorithm`
317
318 Returns
319 -------
320 bg : `lsst.afw.math.Background`
321 A fit background
322
323 Raises
324 ------
325 RuntimeError
326 Raised if lsst.afw.math.makeBackground returns None, an indicator
327 of failure.
328 """
329
330 if not nx:
331 nx = math.ceil(maskedImage.getWidth() / self.binSizeX)
332 if not ny:
333 ny = math.ceil(maskedImage.getHeight() / self.binSizeY)
334
335 unsubFrame = getDebugFrame(self._display, "unsubtracted")
336 if unsubFrame:
337 unsubDisp = afwDisplay.getDisplay(frame=unsubFrame)
338 unsubDisp.mtv(maskedImage, title="unsubtracted")
339 xPosts = numpy.rint(numpy.linspace(0, maskedImage.getWidth() + 1, num=nx, endpoint=True))
340 yPosts = numpy.rint(numpy.linspace(0, maskedImage.getHeight() + 1, num=ny, endpoint=True))
341 with unsubDisp.Buffering():
342 for (xMin, xMax), (yMin, yMax) in itertools.product(zip(xPosts[:-1], xPosts[1:]),
343 zip(yPosts[:-1], yPosts[1:])):
344 unsubDisp.line([(xMin, yMin), (xMin, yMax), (xMax, yMax), (xMax, yMin), (xMin, yMin)])
345
347 badMask = maskedImage.mask.getPlaneBitMask(self.config.ignoredPixelMask)
348
349 sctrl.setAndMask(badMask)
350 sctrl.setNanSafe(self.config.isNanSafe)
351
352 self.log.debug("Ignoring mask planes: %s", ", ".join(self.config.ignoredPixelMask))
353 if (maskedImage.mask.getArray() & badMask).all():
354 raise TooManyMaskedPixelsError("All pixels masked. Cannot estimate background.")
355
356 if algorithm is None:
357 algorithm = self.config.algorithm
358
359 # TODO: DM-22814. This call to a deprecated BackgroundControl constructor
360 # is necessary to support the algorithm parameter; it # should be replaced with
361 #
362 # afwMath.BackgroundControl(nx, ny, sctrl, self.config.statisticsProperty)
363 #
364 # when algorithm has been deprecated and removed.
365 with suppress_deprecations():
366 bctrl = afwMath.BackgroundControl(algorithm, nx, ny,
367 self.config.undersampleStyle, sctrl,
368 self.config.statisticsProperty)
369
370 # TODO: The following check should really be done within lsst.afw.math.
371 # With the current code structure, it would need to be accounted for in the doGetImage()
372 # function in BackgroundMI.cc (which currently only checks against the interpolation settings,
373 # which is not appropriate when useApprox=True)
374 # and/or the makeApproximate() function in afw/Approximate.cc.
375 # See ticket DM-2920: "Clean up code in afw for Approximate background
376 # estimation" (which includes a note to remove the following and the
377 # similar checks in pipe_tasks/matchBackgrounds.py once implemented)
378 #
379 # Check that config setting of approxOrder/binSize make sense
380 # (i.e. ngrid (= shortDimension/binSize) > approxOrderX) and perform
381 # appropriate undersampleStlye behavior.
382 if self.config.useApprox:
383 if self.config.approxOrderY not in (self.config.approxOrderX, -1):
384 raise ValueError("Error: approxOrderY not in (approxOrderX, -1)")
385 order = self.config.approxOrderX
386 minNumberGridPoints = order + 1
387 if min(nx, ny) <= order:
388 self.log.warning("Too few points in grid to constrain fit: min(nx, ny) < approxOrder) "
389 "[min(%d, %d) < %d]", nx, ny, order)
390 if self.config.undersampleStyle == "THROW_EXCEPTION":
391 raise ValueError("Too few points in grid (%d, %d) for order (%d) and binSize (%d, %d)" %
392 (nx, ny, order, self.binSizeX, self.binSizeY))
393 elif self.config.undersampleStyle == "REDUCE_INTERP_ORDER":
394 if order < 1:
395 raise ValueError("Cannot reduce approxOrder below 0. "
396 "Try using undersampleStyle = \"INCREASE_NXNYSAMPLE\" instead?")
397 order = min(nx, ny) - 1
398 self.log.warning("Reducing approxOrder to %d", order)
399 elif self.config.undersampleStyle == "INCREASE_NXNYSAMPLE":
400 # Reduce bin size to the largest acceptable square bins
401 newBinSize = min(maskedImage.getWidth(), maskedImage.getHeight())//(minNumberGridPoints-1)
402 if newBinSize < 1:
403 raise ValueError("Binsize must be greater than 0")
404 newNx = maskedImage.getWidth()//newBinSize + 1
405 newNy = maskedImage.getHeight()//newBinSize + 1
406 bctrl.setNxSample(newNx)
407 bctrl.setNySample(newNy)
408 self.log.warning("Decreasing binSize from (%d, %d) to %d for a grid of (%d, %d)",
409 self.binSizeX, self.binSizeY, newBinSize, newNx, newNy)
410
411 actrl = afwMath.ApproximateControl(afwMath.ApproximateControl.CHEBYSHEV, order, order,
412 self.config.weighting)
413 bctrl.setApproximateControl(actrl)
414
415 bg = afwMath.makeBackground(maskedImage, bctrl)
416 if bg is None:
417 raise RuntimeError("lsst.afw.math.makeBackground failed to fit a background model")
418 return bg
419
420
421def filterSuperPixels(bbox, background, superPixelFilterSize=3):
422 """Remove outliers from the binned background model.
423
424 Parameters
425 ----------
426 bbox : `lsst.geom.Box2I`
427 Bounding box of the original image.
428 background : `lsst.afw.math.BackgroundMI`
429 Fit and binned background image, which will be modified in place.
430 superPixelFilterSize : `int`, optional
431 Size of the median filter to use, in pixels.
432 """
433 statsImg = background.getStatsImage()
434 # scipy's median_filter can't handle NaN values
435 bad = numpy.isnan(statsImg.image.array)
436 if numpy.count_nonzero(bad) > 0:
437 statsImg.image.array[bad] = numpy.nanmedian(statsImg.image.array)
438 statsImg.image.array = median_filter(statsImg.image.array, mode="reflect", size=superPixelFilterSize)
439 background = afwMath.BackgroundMI(bbox, statsImg)
A class to represent a 2-dimensional array of pixels.
Definition Image.h:51
Control how to make an approximation.
Definition Approximate.h:48
Pass parameters to a Background object.
Definition Background.h:57
A class to evaluate image background levels.
Definition Background.h:435
Pass parameters to a Statistics object.
Definition Statistics.h:83
run(self, exposure, background=None, stats=True, statsKeys=None, backgroundToPhotometricRatio=None)
fitBackground(self, maskedImage, nx=0, ny=0, algorithm=None)
__init__(self, config=None, *, name=None, parentTask=None, log=None)
std::shared_ptr< Background > makeBackground(ImageT const &img, BackgroundControl const &bgCtrl)
A convenience function that uses function overloading to make the correct type of Background.
Definition Background.h:533
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361
filterSuperPixels(bbox, background, superPixelFilterSize=3)
backgroundFlatContext(maskedImage, doApply, backgroundToPhotometricRatio=None)