lsst.meas.algorithms  19.0.0-6-g5a632bb5+2
subtractBackground.py
Go to the documentation of this file.
1 #
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__ = ("SubtractBackgroundConfig", "SubtractBackgroundTask")
24 
25 import itertools
26 
27 import numpy
28 
29 from lsstDebug import getDebugFrame
30 from lsst.utils import suppress_deprecations
31 
32 import lsst.afw.display as afwDisplay
33 import lsst.afw.image as afwImage
34 import lsst.afw.math as afwMath
35 import lsst.pex.config as pexConfig
36 import lsst.pipe.base as pipeBase
37 from functools import reduce
38 
39 
40 class SubtractBackgroundConfig(pexConfig.Config):
41  """!Config for SubtractBackgroundTask
42 
43  @note Many of these fields match fields in lsst.afw.math.BackgroundControl,
44  the control class for lsst.afw.math.makeBackground
45  """
46  statisticsProperty = pexConfig.ChoiceField(
47  doc="type of statistic to use for grid points",
48  dtype=str, default="MEANCLIP",
49  allowed={
50  "MEANCLIP": "clipped mean",
51  "MEAN": "unclipped mean",
52  "MEDIAN": "median",
53  }
54  )
55  undersampleStyle = pexConfig.ChoiceField(
56  doc="behaviour if there are too few points in grid for requested interpolation style",
57  dtype=str, default="REDUCE_INTERP_ORDER",
58  allowed={
59  "THROW_EXCEPTION": "throw an exception if there are too few points",
60  "REDUCE_INTERP_ORDER": "use an interpolation style with a lower order.",
61  "INCREASE_NXNYSAMPLE": "Increase the number of samples used to make the interpolation grid.",
62  },
63  )
64  binSize = pexConfig.RangeField(
65  doc="how large a region of the sky should be used for each background point",
66  dtype=int, default=128, min=1,
67  )
68  binSizeX = pexConfig.RangeField(
69  doc=("Sky region size to be used for each background point in X direction. "
70  "If 0, the binSize config is used."),
71  dtype=int, default=0, min=0,
72  )
73  binSizeY = pexConfig.RangeField(
74  doc=("Sky region size to be used for each background point in Y direction. "
75  "If 0, the binSize config is used."),
76  dtype=int, default=0, min=0,
77  )
78  algorithm = pexConfig.ChoiceField(
79  doc="how to interpolate the background values. This maps to an enum; see afw::math::Background",
80  dtype=str, default="AKIMA_SPLINE", optional=True,
81  allowed={
82  "CONSTANT": "Use a single constant value",
83  "LINEAR": "Use linear interpolation",
84  "NATURAL_SPLINE": "cubic spline with zero second derivative at endpoints",
85  "AKIMA_SPLINE": "higher-level nonlinear spline that is more robust to outliers",
86  "NONE": "No background estimation is to be attempted",
87  },
88  )
89  ignoredPixelMask = pexConfig.ListField(
90  doc="Names of mask planes to ignore while estimating the background",
91  dtype=str, default=["BAD", "EDGE", "DETECTED", "DETECTED_NEGATIVE", "NO_DATA", ],
92  itemCheck=lambda x: x in afwImage.Mask().getMaskPlaneDict().keys(),
93  )
94  isNanSafe = pexConfig.Field(
95  doc="Ignore NaNs when estimating the background",
96  dtype=bool, default=False,
97  )
98 
99  useApprox = pexConfig.Field(
100  doc="Use Approximate (Chebyshev) to model background.",
101  dtype=bool, default=True,
102  )
103  approxOrderX = pexConfig.Field(
104  doc="Approximation order in X for background Chebyshev (valid only with useApprox=True)",
105  dtype=int, default=6,
106  )
107  # Note: Currently X- and Y-orders must be equal due to a limitation in math::Chebyshev1Function2
108  # The following is being added so that the weighting attribute can also be configurable for the
109  # call to afwMath.ApproximateControl
110  approxOrderY = pexConfig.Field(
111  doc="Approximation order in Y for background Chebyshev (valid only with useApprox=True)",
112  dtype=int, default=-1,
113  )
114  weighting = pexConfig.Field(
115  doc="Use inverse variance weighting in calculation (valid only with useApprox=True)",
116  dtype=bool, default=True,
117  )
118 
119 
120 
126 
127 class SubtractBackgroundTask(pipeBase.Task):
128  r"""!Subtract the background from an exposure
129 
130  @anchor SubtractBackgroundTask_
131 
132  @section meas_algorithms_subtractBackground_Contents Contents
133 
134  - @ref meas_algorithms_subtractBackground_Purpose
135  - @ref meas_algorithms_subtractBackground_Initialize
136  - @ref meas_algorithms_subtractBackground_IO
137  - @ref meas_algorithms_subtractBackground_Config
138  - @ref meas_algorithms_subtractBackground_Metadata
139  - @ref meas_algorithms_subtractBackground_Debug
140  - @ref meas_algorithms_subtractBackground_Example
141 
142  @section meas_algorithms_subtractBackground_Purpose Description
143 
144  Fit a model of the background of an exposure and subtract it.
145 
146  @section meas_algorithms_subtractBackground_Initialize Task initialisation
147 
148  @copydoc \_\_init\_\_
149 
150  @section meas_algorithms_subtractBackground_IO Invoking the Task
151 
152  Call `run` to fit the background and subtract it.
153 
154  Call `fitBackground` to fit the background without subtracting it.
155 
156  @section meas_algorithms_subtractBackground_Config Configuration parameters
157 
158  See @ref SubtractBackgroundConfig
159 
160  @section meas_algorithms_subtractBackground_Metadata Quantities set in exposure Metadata
161 
162  The `run` method will optionally set the following items of exposure metadata;
163  the names may be overridden; the defaults are shown:
164  <dl>
165  <dt>BGMEAN <dd>mean value of background
166  <dt>BGVAR <dd>standard deviation of background
167  </dl>
168 
169  @section meas_algorithms_subtractBackground_Debug Debug variables
170 
171  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a flag
172  `--debug` to import `debug.py` from your `$PYTHONPATH`; see @ref baseDebug for more about `debug.py`.
173 
174  SubtractBackgroundTask has a debug dictionary containing three integer keys:
175  <dl>
176  <dt>unsubtracted
177  <dd>If >0: `fitBackground` displays the unsubtracted masked image overlaid with the grid of cells
178  used to fit the background in the specified frame
179  <dt>subtracted
180  <dd>If >0: `run` displays the background-subtracted exposure is the specified frame
181  <dt>background
182  <dd>If >0: `run` displays the background image in the specified frame
183  </dl>
184 
185  For example, put something like:
186  @code{.py}
187  import lsstDebug
188  def DebugInfo(name):
189  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
190  if name == "lsst.meas.algorithms.subtractBackground":
191  di.display = dict(
192  unsubtracted = 1,
193  subtracted = 2,
194  background = 3,
195  )
196 
197  return di
198 
199  lsstDebug.Info = DebugInfo
200  @endcode
201  into your `debug.py` file and run your task with the `--debug` flag.
202 
203  @section meas_algorithms_subtractBackground_Example A complete example of using SubtractBackgroundTask
204 
205  This code is in @link subtractBackgroundExample.py@endlink in the examples directory, and can be run as:
206  @code
207  python examples/subtractBackgroundExample.py
208  @endcode
209  @dontinclude subtractBackgroundExample.py
210 
211  Import the task (there are some other standard imports; read the file if you're curious)
212  @skipline import SubtractBackgroundTask
213 
214  Create the task, run it, and report mean and variance of background.
215  @skip create the task
216  @until print
217  """
218  ConfigClass = SubtractBackgroundConfig
219  _DefaultName = "subtractBackground"
220 
221  def run(self, exposure, background=None, stats=True, statsKeys=None):
222  """!Fit and subtract the background of an exposure
223 
224  @param[in,out] exposure exposure whose background is to be subtracted
225  @param[in,out] background initial background model already subtracted from exposure
226  (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted.
227  @param[in] stats if True then measure the mean and variance of the full background model
228  and record the results in the exposure's metadata
229  @param[in] statsKeys key names used to store the mean and variance of the background
230  in the exposure's metadata (a pair of strings); if None then use ("BGMEAN", "BGVAR");
231  ignored if stats is false
232 
233  @return an lsst.pipe.base.Struct containing:
234  - background full background model (initial model with changes), an lsst.afw.math.BackgroundList
235  """
236  if background is None:
237  background = afwMath.BackgroundList()
238 
239  maskedImage = exposure.getMaskedImage()
240  fitBg = self.fitBackground(maskedImage)
241  maskedImage -= fitBg.getImageF(self.config.algorithm, self.config.undersampleStyle)
242 
243  actrl = fitBg.getBackgroundControl().getApproximateControl()
244  background.append((fitBg, getattr(afwMath.Interpolate, self.config.algorithm),
245  fitBg.getAsUsedUndersampleStyle(), actrl.getStyle(),
246  actrl.getOrderX(), actrl.getOrderY(), actrl.getWeighting()))
247 
248  if stats:
249  self._addStats(exposure, background, statsKeys=statsKeys)
250 
251  subFrame = getDebugFrame(self._display, "subtracted")
252  if subFrame:
253  subDisp = afwDisplay.getDisplay(frame=subFrame)
254  subDisp.mtv(exposure, title="subtracted")
255 
256  bgFrame = getDebugFrame(self._display, "background")
257  if bgFrame:
258  bgDisp = afwDisplay.getDisplay(frame=bgFrame)
259  bgImage = background.getImage()
260  bgDisp.mtv(bgImage, title="background")
261 
262  return pipeBase.Struct(
263  background=background,
264  )
265 
266  def _addStats(self, exposure, background, statsKeys=None):
267  """Add statistics about the background to the exposure's metadata
268 
269  @param[in,out] exposure exposure whose background was subtracted
270  @param[in,out] background background model (an lsst.afw.math.BackgroundList)
271  @param[in] statsKeys key names used to store the mean and variance of the background
272  in the exposure's metadata (a pair of strings); if None then use ("BGMEAN", "BGVAR");
273  ignored if stats is false
274  """
275  netBgImg = background.getImage()
276  if statsKeys is None:
277  statsKeys = ("BGMEAN", "BGVAR")
278  mnkey, varkey = statsKeys
279  meta = exposure.getMetadata()
280  s = afwMath.makeStatistics(netBgImg, afwMath.MEAN | afwMath.VARIANCE)
281  bgmean = s.getValue(afwMath.MEAN)
282  bgvar = s.getValue(afwMath.VARIANCE)
283  meta.addDouble(mnkey, bgmean)
284  meta.addDouble(varkey, bgvar)
285 
286  def fitBackground(self, maskedImage, nx=0, ny=0, algorithm=None):
287  """!Estimate the background of a masked image
288 
289  @param[in] maskedImage masked image whose background is to be computed
290  @param[in] nx number of x bands; if 0 compute from width and config.binSizeX
291  @param[in] ny number of y bands; if 0 compute from height and config.binSizeY
292  @param[in] algorithm name of interpolation algorithm; if None use self.config.algorithm
293 
294  @return fit background as an lsst.afw.math.Background
295 
296  @throw RuntimeError if lsst.afw.math.makeBackground returns None,
297  which is apparently one way it indicates failure
298  """
299 
300  binSizeX = self.config.binSize if self.config.binSizeX == 0 else self.config.binSizeX
301  binSizeY = self.config.binSize if self.config.binSizeY == 0 else self.config.binSizeY
302 
303  if not nx:
304  nx = maskedImage.getWidth()//binSizeX + 1
305  if not ny:
306  ny = maskedImage.getHeight()//binSizeY + 1
307 
308  unsubFrame = getDebugFrame(self._display, "unsubtracted")
309  if unsubFrame:
310  unsubDisp = afwDisplay.getDisplay(frame=unsubFrame)
311  unsubDisp.mtv(maskedImage, title="unsubtracted")
312  xPosts = numpy.rint(numpy.linspace(0, maskedImage.getWidth() + 1, num=nx, endpoint=True))
313  yPosts = numpy.rint(numpy.linspace(0, maskedImage.getHeight() + 1, num=ny, endpoint=True))
314  with unsubDisp.Buffering():
315  for (xMin, xMax), (yMin, yMax) in itertools.product(zip(xPosts[:-1], xPosts[1:]),
316  zip(yPosts[:-1], yPosts[1:])):
317  unsubDisp.line([(xMin, yMin), (xMin, yMax), (xMax, yMax), (xMax, yMin), (xMin, yMin)])
318 
319  sctrl = afwMath.StatisticsControl()
320  sctrl.setAndMask(reduce(lambda x, y: x | maskedImage.getMask().getPlaneBitMask(y),
321  self.config.ignoredPixelMask, 0x0))
322  sctrl.setNanSafe(self.config.isNanSafe)
323 
324  self.log.debug("Ignoring mask planes: %s" % ", ".join(self.config.ignoredPixelMask))
325 
326  if algorithm is None:
327  algorithm = self.config.algorithm
328 
329  # TODO: DM-22814. This call to a deprecated BackgroundControl constructor
330  # is necessary to support the algorithm parameter; it # should be replaced with
331  #
332  # afwMath.BackgroundControl(nx, ny, sctrl, self.config.statisticsProperty)
333  #
334  # when algorithm has been deprecated and removed.
335  with suppress_deprecations():
336  bctrl = afwMath.BackgroundControl(algorithm, nx, ny,
337  self.config.undersampleStyle, sctrl,
338  self.config.statisticsProperty)
339 
340  # TODO: The following check should really be done within lsst.afw.math.
341  # With the current code structure, it would need to be accounted for in the doGetImage()
342  # function in BackgroundMI.cc (which currently only checks against the interpolation settings,
343  # which is not appropriate when useApprox=True)
344  # and/or the makeApproximate() function in afw/Approximate.cc.
345  # See ticket DM-2920: "Clean up code in afw for Approximate background
346  # estimation" (which includes a note to remove the following and the
347  # similar checks in pipe_tasks/matchBackgrounds.py once implemented)
348  #
349  # Check that config setting of approxOrder/binSize make sense
350  # (i.e. ngrid (= shortDimension/binSize) > approxOrderX) and perform
351  # appropriate undersampleStlye behavior.
352  if self.config.useApprox:
353  if self.config.approxOrderY not in (self.config.approxOrderX, -1):
354  raise ValueError("Error: approxOrderY not in (approxOrderX, -1)")
355  order = self.config.approxOrderX
356  minNumberGridPoints = order + 1
357  if min(nx, ny) <= order:
358  self.log.warn("Too few points in grid to constrain fit: min(nx, ny) < approxOrder) "
359  "[min(%d, %d) < %d]" % (nx, ny, order))
360  if self.config.undersampleStyle == "THROW_EXCEPTION":
361  raise ValueError("Too few points in grid (%d, %d) for order (%d) and binSize (%d, %d)" %
362  (nx, ny, order, binSizeX, binSizeY))
363  elif self.config.undersampleStyle == "REDUCE_INTERP_ORDER":
364  if order < 1:
365  raise ValueError("Cannot reduce approxOrder below 0. "
366  "Try using undersampleStyle = \"INCREASE_NXNYSAMPLE\" instead?")
367  order = min(nx, ny) - 1
368  self.log.warn("Reducing approxOrder to %d" % order)
369  elif self.config.undersampleStyle == "INCREASE_NXNYSAMPLE":
370  # Reduce bin size to the largest acceptable square bins
371  newBinSize = min(maskedImage.getWidth(), maskedImage.getHeight())//(minNumberGridPoints-1)
372  if newBinSize < 1:
373  raise ValueError("Binsize must be greater than 0")
374  newNx = maskedImage.getWidth()//newBinSize + 1
375  newNy = maskedImage.getHeight()//newBinSize + 1
376  bctrl.setNxSample(newNx)
377  bctrl.setNySample(newNy)
378  self.log.warn("Decreasing binSize from (%d, %d) to %d for a grid of (%d, %d)" %
379  (binSizeX, binSizeY, newBinSize, newNx, newNy))
380 
381  actrl = afwMath.ApproximateControl(afwMath.ApproximateControl.CHEBYSHEV, order, order,
382  self.config.weighting)
383  bctrl.setApproximateControl(actrl)
384 
385  bg = afwMath.makeBackground(maskedImage, bctrl)
386  if bg is None:
387  raise RuntimeError("lsst.afw.math.makeBackground failed to fit a background model")
388  return bg
def _addStats(self, exposure, background, statsKeys=None)
def fitBackground(self, maskedImage, nx=0, ny=0, algorithm=None)
Estimate the background of a masked image.
def run(self, exposure, background=None, stats=True, statsKeys=None)
Fit and subtract the background of an exposure.