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