LSSTApplications  17.0+11,17.0+34,17.0+56,17.0+57,17.0+59,17.0+7,17.0-1-g377950a+33,17.0.1-1-g114240f+2,17.0.1-1-g4d4fbc4+28,17.0.1-1-g55520dc+49,17.0.1-1-g5f4ed7e+52,17.0.1-1-g6dd7d69+17,17.0.1-1-g8de6c91+11,17.0.1-1-gb9095d2+7,17.0.1-1-ge9fec5e+5,17.0.1-1-gf4e0155+55,17.0.1-1-gfc65f5f+50,17.0.1-1-gfc6fb1f+20,17.0.1-10-g87f9f3f+1,17.0.1-11-ge9de802+16,17.0.1-16-ga14f7d5c+4,17.0.1-17-gc79d625+1,17.0.1-17-gdae4c4a+8,17.0.1-2-g26618f5+29,17.0.1-2-g54f2ebc+9,17.0.1-2-gf403422+1,17.0.1-20-g2ca2f74+6,17.0.1-23-gf3eadeb7+1,17.0.1-3-g7e86b59+39,17.0.1-3-gb5ca14a,17.0.1-3-gd08d533+40,17.0.1-30-g596af8797,17.0.1-4-g59d126d+4,17.0.1-4-gc69c472+5,17.0.1-6-g5afd9b9+4,17.0.1-7-g35889ee+1,17.0.1-7-gc7c8782+18,17.0.1-9-gc4bbfb2+3,w.2019.22
LSSTDataManagementBasePackage
background.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 
3 import numpy
4 import itertools
5 from scipy.ndimage import gaussian_filter
6 
7 import lsst.afw.math as afwMath
8 import lsst.afw.image as afwImage
9 import lsst.afw.geom as afwGeom
10 import lsst.afw.cameraGeom as afwCameraGeom
11 import lsst.meas.algorithms as measAlg
12 import lsst.afw.table as afwTable
13 
14 from lsst.pex.config import Config, Field, ListField, ChoiceField, ConfigField, RangeField, ConfigurableField
15 from lsst.pipe.base import Task
16 
17 
18 def robustMean(array, rej=3.0):
19  """Measure a robust mean of an array
20 
21  Parameters
22  ----------
23  array : `numpy.ndarray`
24  Array for which to measure the mean.
25  rej : `float`
26  k-sigma rejection threshold.
27 
28  Returns
29  -------
30  mean : `array.dtype`
31  Robust mean of `array`.
32  """
33  q1, median, q3 = numpy.percentile(array, [25.0, 50.0, 100.0])
34  good = numpy.abs(array - median) < rej*0.74*(q3 - q1)
35  return array[good].mean()
36 
37 
39  """Configuration for background measurement"""
40  statistic = ChoiceField(dtype=str, default="MEANCLIP", doc="type of statistic to use for grid points",
41  allowed={"MEANCLIP": "clipped mean",
42  "MEAN": "unclipped mean",
43  "MEDIAN": "median"})
44  xBinSize = RangeField(dtype=int, default=32, min=1, doc="Superpixel size in x")
45  yBinSize = RangeField(dtype=int, default=32, min=1, doc="Superpixel size in y")
46  algorithm = ChoiceField(dtype=str, default="NATURAL_SPLINE", optional=True,
47  doc="How to interpolate the background values. "
48  "This maps to an enum; see afw::math::Background",
49  allowed={
50  "CONSTANT": "Use a single constant value",
51  "LINEAR": "Use linear interpolation",
52  "NATURAL_SPLINE": "cubic spline with zero second derivative at endpoints",
53  "AKIMA_SPLINE": "higher-level nonlinear spline that is more robust"
54  " to outliers",
55  "NONE": "No background estimation is to be attempted",
56  })
57  mask = ListField(dtype=str, default=["SAT", "BAD", "EDGE", "DETECTED", "DETECTED_NEGATIVE", "NO_DATA"],
58  doc="Names of mask planes to ignore while estimating the background")
59 
60 
62  """Parameters controlling the measurement of sky statistics"""
63  statistic = ChoiceField(dtype=str, default="MEANCLIP", doc="type of statistic to use for grid points",
64  allowed={"MEANCLIP": "clipped mean",
65  "MEAN": "unclipped mean",
66  "MEDIAN": "median"})
67  clip = Field(doc="Clipping threshold for background", dtype=float, default=3.0)
68  nIter = Field(doc="Clipping iterations for background", dtype=int, default=3)
69  mask = ListField(doc="Mask planes to reject", dtype=str,
70  default=["SAT", "DETECTED", "DETECTED_NEGATIVE", "BAD", "NO_DATA"])
71 
72 
74  """Configuration for SkyMeasurementTask"""
75  skyIter = Field(dtype=int, default=3, doc="k-sigma rejection iterations for sky scale")
76  skyRej = Field(dtype=float, default=3.0, doc="k-sigma rejection threshold for sky scale")
77  background = ConfigField(dtype=BackgroundConfig, doc="Background measurement")
78  xNumSamples = Field(dtype=int, default=4, doc="Number of samples in x for scaling sky frame")
79  yNumSamples = Field(dtype=int, default=4, doc="Number of samples in y for scaling sky frame")
80  stats = ConfigField(dtype=SkyStatsConfig, doc="Measurement of sky statistics in the samples")
81 
82 
84  """Task for creating, persisting and using sky frames
85 
86  A sky frame is like a fringe frame (the sum of many exposures of the night sky,
87  combined with rejection to remove astrophysical objects) except the structure
88  is on larger scales, and hence we bin the images and represent them as a
89  background model (a `lsst.afw.math.BackgroundMI`). The sky frame represents
90  the dominant response of the camera to the sky background.
91  """
92  ConfigClass = SkyMeasurementConfig
93 
94  def getSkyData(self, butler, calibId):
95  """Retrieve sky frame from the butler
96 
97  Parameters
98  ----------
99  butler : `lsst.daf.persistence.Butler`
100  Data butler
101  calibId : `dict`
102  Data identifier for calib
103 
104  Returns
105  -------
106  sky : `lsst.afw.math.BackgroundList`
107  Sky frame
108  """
109  exp = butler.get("sky", calibId)
110  return self.exposureToBackground(exp)
111 
112  @staticmethod
114  """Convert an exposure to background model
115 
116  Calibs need to be persisted as an Exposure, so we need to convert
117  the persisted Exposure to a background model.
118 
119  Parameters
120  ----------
121  bgExp : `lsst.afw.image.Exposure`
122  Background model in Exposure format.
123 
124  Returns
125  -------
126  bg : `lsst.afw.math.BackgroundList`
127  Background model
128  """
129  header = bgExp.getMetadata()
130  xMin = header.getScalar("BOX.MINX")
131  yMin = header.getScalar("BOX.MINY")
132  xMax = header.getScalar("BOX.MAXX")
133  yMax = header.getScalar("BOX.MAXY")
134  algorithm = header.getScalar("ALGORITHM")
135  bbox = afwGeom.Box2I(afwGeom.Point2I(xMin, yMin), afwGeom.Point2I(xMax, yMax))
136  return afwMath.BackgroundList(
137  (afwMath.BackgroundMI(bbox, bgExp.getMaskedImage()),
138  afwMath.stringToInterpStyle(algorithm),
139  afwMath.stringToUndersampleStyle("REDUCE_INTERP_ORDER"),
140  afwMath.ApproximateControl.UNKNOWN,
141  0, 0, False))
142 
143  def backgroundToExposure(self, statsImage, bbox):
144  """Convert a background model to an exposure
145 
146  Calibs need to be persisted as an Exposure, so we need to convert
147  the background model to an Exposure.
148 
149  Parameters
150  ----------
151  statsImage : `lsst.afw.image.MaskedImageF`
152  Background model's statistics image.
153  bbox : `lsst.afw.geom.Box2I`
154  Bounding box for image.
155 
156  Returns
157  -------
158  exp : `lsst.afw.image.Exposure`
159  Background model in Exposure format.
160  """
161  exp = afwImage.makeExposure(statsImage)
162  header = exp.getMetadata()
163  header.set("BOX.MINX", bbox.getMinX())
164  header.set("BOX.MINY", bbox.getMinY())
165  header.set("BOX.MAXX", bbox.getMaxX())
166  header.set("BOX.MAXY", bbox.getMaxY())
167  header.set("ALGORITHM", self.config.background.algorithm)
168  return exp
169 
170  def measureBackground(self, image):
171  """Measure a background model for image
172 
173  This doesn't use a full-featured background model (e.g., no Chebyshev
174  approximation) because we just want the binning behaviour. This will
175  allow us to average the bins later (`averageBackgrounds`).
176 
177  The `BackgroundMI` is wrapped in a `BackgroundList` so it can be
178  pickled and persisted.
179 
180  Parameters
181  ----------
182  image : `lsst.afw.image.MaskedImage`
183  Image for which to measure background.
184 
185  Returns
186  -------
187  bgModel : `lsst.afw.math.BackgroundList`
188  Background model.
189  """
190  stats = afwMath.StatisticsControl()
191  stats.setAndMask(image.getMask().getPlaneBitMask(self.config.background.mask))
192  stats.setNanSafe(True)
194  self.config.background.algorithm,
195  max(int(image.getWidth()/self.config.background.xBinSize + 0.5), 1),
196  max(int(image.getHeight()/self.config.background.yBinSize + 0.5), 1),
197  "REDUCE_INTERP_ORDER",
198  stats,
199  self.config.background.statistic
200  )
201 
202  bg = afwMath.makeBackground(image, ctrl)
203 
204  return afwMath.BackgroundList((
205  bg,
206  afwMath.stringToInterpStyle(self.config.background.algorithm),
207  afwMath.stringToUndersampleStyle("REDUCE_INTERP_ORDER"),
208  afwMath.ApproximateControl.UNKNOWN,
209  0, 0, False
210  ))
211 
212  def averageBackgrounds(self, bgList):
213  """Average multiple background models
214 
215  The input background models should be a `BackgroundList` consisting
216  of a single `BackgroundMI`.
217 
218  Parameters
219  ----------
220  bgList : `list` of `lsst.afw.math.BackgroundList`
221  Background models to average.
222 
223  Returns
224  -------
225  bgExp : `lsst.afw.image.Exposure`
226  Background model in Exposure format.
227  """
228  assert all(len(bg) == 1 for bg in bgList), "Mixed bgList: %s" % ([len(bg) for bg in bgList],)
229  images = [bg[0][0].getStatsImage() for bg in bgList]
230  boxes = [bg[0][0].getImageBBox() for bg in bgList]
231  assert len(set((box.getMinX(), box.getMinY(), box.getMaxX(), box.getMaxY()) for box in boxes)) == 1, \
232  "Bounding boxes not all equal"
233  bbox = boxes.pop(0)
234 
235  # Ensure bad pixels are masked
236  maskVal = afwImage.Mask.getPlaneBitMask("BAD")
237  for img in images:
238  bad = numpy.isnan(img.getImage().getArray())
239  img.getMask().getArray()[bad] = maskVal
240 
241  stats = afwMath.StatisticsControl()
242  stats.setAndMask(maskVal)
243  stats.setNanSafe(True)
244  combined = afwMath.statisticsStack(images, afwMath.MEANCLIP, stats)
245 
246  # Set bad pixels to the median
247  # Specifically NOT going to attempt to interpolate the bad values because we're only working on a
248  # single CCD here and can't use the entire field-of-view to do the interpolation (which is what we
249  # would need to avoid introducing problems at the edge of CCDs).
250  array = combined.getImage().getArray()
251  bad = numpy.isnan(array)
252  median = numpy.median(array[~bad])
253  array[bad] = median
254 
255  # Put it into an exposure, which is required for calibs
256  return self.backgroundToExposure(combined, bbox)
257 
258  def measureScale(self, image, skyBackground):
259  """Measure scale of background model in image
260 
261  We treat the sky frame much as we would a fringe frame
262  (except the length scale of the variations is different):
263  we measure samples on the input image and the sky frame,
264  which we will use to determine the scaling factor in the
265  'solveScales` method.
266 
267  Parameters
268  ----------
269  image : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage`
270  Science image for which to measure scale.
271  skyBackground : `lsst.afw.math.BackgroundList`
272  Sky background model.
273 
274  Returns
275  -------
276  imageSamples : `numpy.ndarray`
277  Sample measurements on image.
278  skySamples : `numpy.ndarray`
279  Sample measurements on sky frame.
280  """
281  if isinstance(image, afwImage.Exposure):
282  image = image.getMaskedImage()
283  # Ensure more samples than pixels
284  xNumSamples = min(self.config.xNumSamples, image.getWidth())
285  yNumSamples = min(self.config.yNumSamples, image.getHeight())
286  xLimits = numpy.linspace(0, image.getWidth(), xNumSamples + 1, dtype=int)
287  yLimits = numpy.linspace(0, image.getHeight(), yNumSamples + 1, dtype=int)
288  sky = skyBackground.getImage()
289  maskVal = image.getMask().getPlaneBitMask(self.config.stats.mask)
290  ctrl = afwMath.StatisticsControl(self.config.stats.clip, self.config.stats.nIter, maskVal)
291  statistic = afwMath.stringToStatisticsProperty(self.config.stats.statistic)
292  imageSamples = []
293  skySamples = []
294  for xIndex, yIndex in itertools.product(range(xNumSamples), range(yNumSamples)):
295  # -1 on the stop because Box2I is inclusive of the end point and we don't want to overlap boxes
296  xStart, xStop = xLimits[xIndex], xLimits[xIndex + 1] - 1
297  yStart, yStop = yLimits[yIndex], yLimits[yIndex + 1] - 1
298  box = afwGeom.Box2I(afwGeom.Point2I(xStart, yStart), afwGeom.Point2I(xStop, yStop))
299  subImage = image.Factory(image, box)
300  subSky = sky.Factory(sky, box)
301  imageSamples.append(afwMath.makeStatistics(subImage, statistic, ctrl).getValue())
302  skySamples.append(afwMath.makeStatistics(subSky, statistic, ctrl).getValue())
303  return imageSamples, skySamples
304 
305  def solveScales(self, scales):
306  """Solve multiple scales for a single scale factor
307 
308  Having measured samples from the image and sky frame, we
309  fit for the scaling factor.
310 
311  Parameters
312  ----------
313  scales : `list` of a `tuple` of two `numpy.ndarray` arrays
314  A `list` of the results from `measureScale` method.
315 
316  Returns
317  -------
318  scale : `float`
319  Scale factor.
320  """
321  imageSamples = []
322  skySamples = []
323  for ii, ss in scales:
324  imageSamples.extend(ii)
325  skySamples.extend(ss)
326  assert len(imageSamples) == len(skySamples)
327  imageSamples = numpy.array(imageSamples)
328  skySamples = numpy.array(skySamples)
329 
330  def solve(mask):
331  return afwMath.LeastSquares.fromDesignMatrix(skySamples[mask].reshape(mask.sum(), 1),
332  imageSamples[mask],
333  afwMath.LeastSquares.DIRECT_SVD).getSolution()
334 
335  mask = numpy.isfinite(imageSamples) & numpy.isfinite(skySamples)
336  for ii in range(self.config.skyIter):
337  solution = solve(mask)
338  residuals = imageSamples - solution*skySamples
339  lq, uq = numpy.percentile(residuals[mask], [25, 75])
340  stdev = 0.741*(uq - lq) # Robust stdev from IQR
341  with numpy.errstate(invalid="ignore"): # suppress NAN warnings
342  bad = numpy.abs(residuals) > self.config.skyRej*stdev
343  mask[bad] = False
344 
345  return solve(mask)
346 
347  def subtractSkyFrame(self, image, skyBackground, scale, bgList=None):
348  """Subtract sky frame from science image
349 
350  Parameters
351  ----------
352  image : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage`
353  Science image.
354  skyBackground : `lsst.afw.math.BackgroundList`
355  Sky background model.
356  scale : `float`
357  Scale to apply to background model.
358  bgList : `lsst.afw.math.BackgroundList`
359  List of backgrounds applied to image
360  """
361  if isinstance(image, afwImage.Exposure):
362  image = image.getMaskedImage()
363  if isinstance(image, afwImage.MaskedImage):
364  image = image.getImage()
365  image.scaledMinus(scale, skyBackground.getImage())
366  if bgList is not None:
367  # Append the sky frame to the list of applied background models
368  bgData = list(skyBackground[0])
369  bg = bgData[0]
370  statsImage = bg.getStatsImage().clone()
371  statsImage *= scale
372  newBg = afwMath.BackgroundMI(bg.getImageBBox(), statsImage)
373  newBgData = [newBg] + bgData[1:]
374  bgList.append(newBgData)
375 
376 
377 def interpolate1D(method, xSample, ySample, xInterp):
378  """Interpolate in one dimension
379 
380  Interpolates the curve provided by `xSample` and `ySample` at
381  the positions of `xInterp`. Automatically backs off the
382  interpolation method to achieve successful interpolation.
383 
384  Parameters
385  ----------
386  method : `lsst.afw.math.Interpolate.Style`
387  Interpolation method to use.
388  xSample : `numpy.ndarray`
389  Vector of ordinates.
390  ySample : `numpy.ndarray`
391  Vector of coordinates.
392  xInterp : `numpy.ndarray`
393  Vector of ordinates to which to interpolate.
394 
395  Returns
396  -------
397  yInterp : `numpy.ndarray`
398  Vector of interpolated coordinates.
399 
400  """
401  if len(xSample) == 0:
402  return numpy.ones_like(xInterp)*numpy.nan
403  try:
404  return afwMath.makeInterpolate(xSample.astype(float), ySample.astype(float),
405  method).interpolate(xInterp.astype(float))
406  except Exception:
407  if method == afwMath.Interpolate.CONSTANT:
408  # We've already tried the most basic interpolation and it failed
409  return numpy.ones_like(xInterp)*numpy.nan
410  newMethod = afwMath.lookupMaxInterpStyle(len(xSample))
411  if newMethod == method:
412  newMethod = afwMath.Interpolate.CONSTANT
413  return interpolate1D(newMethod, xSample, ySample, xInterp)
414 
415 
416 def interpolateBadPixels(array, isBad, interpolationStyle):
417  """Interpolate bad pixels in an image array
418 
419  The bad pixels are modified in the array.
420 
421  Parameters
422  ----------
423  array : `numpy.ndarray`
424  Image array with bad pixels.
425  isBad : `numpy.ndarray` of type `bool`
426  Boolean array indicating which pixels are bad.
427  interpolationStyle : `str`
428  Style for interpolation (see `lsst.afw.math.Background`);
429  supported values are CONSTANT, LINEAR, NATURAL_SPLINE,
430  AKIMA_SPLINE.
431  """
432  if numpy.all(isBad):
433  raise RuntimeError("No good pixels in image array")
434  height, width = array.shape
435  xIndices = numpy.arange(width, dtype=float)
436  yIndices = numpy.arange(height, dtype=float)
437  method = afwMath.stringToInterpStyle(interpolationStyle)
438  isGood = ~isBad
439  for y in range(height):
440  if numpy.any(isBad[y, :]) and numpy.any(isGood[y, :]):
441  array[y][isBad[y]] = interpolate1D(method, xIndices[isGood[y]], array[y][isGood[y]],
442  xIndices[isBad[y]])
443 
444  isBad = numpy.isnan(array)
445  isGood = ~isBad
446  for x in range(width):
447  if numpy.any(isBad[:, x]) and numpy.any(isGood[:, x]):
448  array[:, x][isBad[:, x]] = interpolate1D(method, yIndices[isGood[:, x]],
449  array[:, x][isGood[:, x]], yIndices[isBad[:, x]])
450 
451 
453  """Configuration for FocalPlaneBackground
454 
455  Note that `xSize` and `ySize` are floating-point values, as
456  the focal plane frame is usually defined in units of microns
457  or millimetres rather than pixels. As such, their values will
458  need to be revised according to each particular camera. For
459  this reason, no defaults are set for those.
460  """
461  xSize = Field(dtype=float, doc="Bin size in x")
462  ySize = Field(dtype=float, doc="Bin size in y")
463  minFrac = Field(dtype=float, default=0.1, doc="Minimum fraction of bin size for good measurement")
464  mask = ListField(dtype=str, doc="Mask planes to treat as bad",
465  default=["BAD", "SAT", "INTRP", "DETECTED", "DETECTED_NEGATIVE", "EDGE", "NO_DATA"])
466  interpolation = ChoiceField(
467  doc="how to interpolate the background values. This maps to an enum; see afw::math::Background",
468  dtype=str, default="AKIMA_SPLINE", optional=True,
469  allowed={
470  "CONSTANT": "Use a single constant value",
471  "LINEAR": "Use linear interpolation",
472  "NATURAL_SPLINE": "cubic spline with zero second derivative at endpoints",
473  "AKIMA_SPLINE": "higher-level nonlinear spline that is more robust to outliers",
474  "NONE": "No background estimation is to be attempted",
475  },
476  )
477  doSmooth = Field(dtype=bool, default=False, doc="Do smoothing?")
478  smoothScale = Field(dtype=float, default=2.0, doc="Smoothing scale, as a multiple of the bin size")
479  binning = Field(dtype=int, default=64, doc="Binning to use for CCD background model (pixels)")
480 
481 
483  """Background model for a focal plane camera
484 
485  We model the background empirically with the "superpixel" method: we
486  measure the background in each superpixel and interpolate between
487  superpixels to yield the model.
488 
489  The principal difference between this and `lsst.afw.math.BackgroundMI`
490  is that here the superpixels are defined in the frame of the focal
491  plane of the camera which removes discontinuities across detectors.
492 
493  The constructor you probably want to use is the `fromCamera` classmethod.
494 
495  There are two use patterns for building a background model:
496 
497  * Serial: create a `FocalPlaneBackground`, then `addCcd` for each of the
498  CCDs in an exposure.
499 
500  * Parallel: create a `FocalPlaneBackground`, then `clone` it for each
501  of the CCDs in an exposure and use those to `addCcd` their respective
502  CCD image. Finally, `merge` all the clones into the original.
503 
504  Once you've built the background model, you can apply it to individual
505  CCDs with the `toCcdBackground` method.
506  """
507  @classmethod
508  def fromCamera(cls, config, camera):
509  """Construct from a camera object
510 
511  Parameters
512  ----------
513  config : `FocalPlaneBackgroundConfig`
514  Configuration for measuring backgrounds.
515  camera : `lsst.afw.cameraGeom.Camera`
516  Camera for which to measure backgrounds.
517  """
518  cameraBox = afwGeom.Box2D()
519  for ccd in camera:
520  for point in ccd.getCorners(afwCameraGeom.FOCAL_PLANE):
521  cameraBox.include(point)
522 
523  width, height = cameraBox.getDimensions()
524  # Offset so that we run from zero
525  offset = afwGeom.Extent2D(cameraBox.getMin())*-1
526  # Add an extra pixel buffer on either side
527  dims = afwGeom.Extent2I(int(numpy.ceil(width/config.xSize)) + 2,
528  int(numpy.ceil(height/config.ySize)) + 2)
529  # Transform takes us from focal plane coordinates --> sample coordinates
530  transform = (afwGeom.AffineTransform.makeTranslation(afwGeom.Extent2D(1, 1))*
531  afwGeom.AffineTransform.makeScaling(1.0/config.xSize, 1.0/config.ySize)*
532  afwGeom.AffineTransform.makeTranslation(offset))
533 
534  return cls(config, dims, afwGeom.makeTransform(transform))
535 
536  def __init__(self, config, dims, transform, values=None, numbers=None):
537  """Constructor
538 
539  Developers should note that changes to the signature of this method
540  require coordinated changes to the `__reduce__` and `clone` methods.
541 
542  Parameters
543  ----------
544  config : `FocalPlaneBackgroundConfig`
545  Configuration for measuring backgrounds.
546  dims : `lsst.afw.geom.Extent2I`
547  Dimensions for background samples.
548  transform : `lsst.afw.geom.TransformPoint2ToPoint2`
549  Transformation from focal plane coordinates to sample coordinates.
550  values : `lsst.afw.image.ImageF`
551  Measured background values.
552  numbers : `lsst.afw.image.ImageF`
553  Number of pixels in each background measurement.
554  """
555  self.config = config
556  self.dims = dims
557  self.transform = transform
558 
559  if values is None:
560  values = afwImage.ImageF(self.dims)
561  values.set(0.0)
562  else:
563  values = values.clone()
564  assert(values.getDimensions() == self.dims)
565  self._values = values
566  if numbers is None:
567  numbers = afwImage.ImageF(self.dims) # float for dynamic range and convenience
568  numbers.set(0.0)
569  else:
570  numbers = numbers.clone()
571  assert(numbers.getDimensions() == self.dims)
572  self._numbers = numbers
573 
574  def __reduce__(self):
575  return self.__class__, (self.config, self.dims, self.transform, self._values, self._numbers)
576 
577  def clone(self):
578  return self.__class__(self.config, self.dims, self.transform, self._values, self._numbers)
579 
580  def addCcd(self, exposure):
581  """Add CCD to model
582 
583  We measure the background on the CCD (clipped mean), and record
584  the results in the model. For simplicity, measurements are made
585  in a box on the CCD corresponding to the warped coordinates of the
586  superpixel rather than accounting for little rotations, etc.
587  We also record the number of pixels used in the measurement so we
588  can have a measure of confidence in each bin's value.
589 
590  Parameters
591  ----------
592  exposure : `lsst.afw.image.Exposure`
593  CCD exposure to measure
594  """
595  detector = exposure.getDetector()
596  transform = detector.getTransformMap().getTransform(detector.makeCameraSys(afwCameraGeom.PIXELS),
597  detector.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
598  image = exposure.getMaskedImage()
599  maskVal = image.getMask().getPlaneBitMask(self.config.mask)
600 
601  # Warp the binned image to the focal plane
602  toSample = transform.then(self.transform) # CCD pixels --> focal plane --> sample
603 
604  warped = afwImage.ImageF(self._values.getBBox())
605  warpedCounts = afwImage.ImageF(self._numbers.getBBox())
606  width, height = warped.getDimensions()
607 
608  stats = afwMath.StatisticsControl()
609  stats.setAndMask(maskVal)
610  stats.setNanSafe(True)
611  # Iterating over individual pixels in python is usually bad because it's slow, but there aren't many.
612  pixels = itertools.product(range(width), range(height))
613  for xx, yy in pixels:
614  llc = toSample.applyInverse(afwGeom.Point2D(xx - 0.5, yy - 0.5))
615  urc = toSample.applyInverse(afwGeom.Point2D(xx + 0.5, yy + 0.5))
617  bbox.clip(image.getBBox())
618  if bbox.isEmpty():
619  continue
620  subImage = image.Factory(image, bbox)
621  result = afwMath.makeStatistics(subImage, afwMath.MEANCLIP | afwMath.NPOINT, stats)
622  mean = result.getValue(afwMath.MEANCLIP)
623  num = result.getValue(afwMath.NPOINT)
624  if not numpy.isfinite(mean) or not numpy.isfinite(num):
625  continue
626  warped[xx, yy, afwImage.LOCAL] = mean*num
627  warpedCounts[xx, yy, afwImage.LOCAL] = num
628 
629  self._values += warped
630  self._numbers += warpedCounts
631 
632  def toCcdBackground(self, detector, bbox):
633  """Produce a background model for a CCD
634 
635  The superpixel background model is warped back to the
636  CCD frame, for application to the individual CCD.
637 
638  Parameters
639  ----------
640  detector : `lsst.afw.cameraGeom.Detector`
641  CCD for which to produce background model.
642  bbox : `lsst.afw.geom.Box2I`
643  Bounding box of CCD exposure.
644 
645  Returns
646  -------
647  bg : `lsst.afw.math.BackgroundList`
648  Background model for CCD.
649  """
650  transform = detector.getTransformMap().getTransform(detector.makeCameraSys(afwCameraGeom.PIXELS),
651  detector.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
652  binTransform = (afwGeom.AffineTransform.makeScaling(self.config.binning)*
653  afwGeom.AffineTransform.makeTranslation(afwGeom.Extent2D(0.5, 0.5)))
654 
655  # Binned image on CCD --> unbinned image on CCD --> focal plane --> binned focal plane
656  toSample = afwGeom.makeTransform(binTransform).then(transform).then(self.transform)
657 
658  focalPlane = self.getStatsImage()
659  fpNorm = afwImage.ImageF(focalPlane.getBBox())
660  fpNorm.set(1.0)
661 
662  image = afwImage.ImageF(bbox.getDimensions()//self.config.binning)
663  norm = afwImage.ImageF(image.getBBox())
664  ctrl = afwMath.WarpingControl("bilinear")
665  afwMath.warpImage(image, focalPlane, toSample.inverted(), ctrl)
666  afwMath.warpImage(norm, fpNorm, toSample.inverted(), ctrl)
667  image /= norm
668 
669  mask = afwImage.Mask(image.getBBox())
670  isBad = numpy.isnan(image.getArray())
671  mask.getArray()[isBad] = mask.getPlaneBitMask("BAD")
672  image.getArray()[isBad] = image.getArray()[~isBad].mean()
673 
674  return afwMath.BackgroundList(
675  (afwMath.BackgroundMI(bbox, afwImage.makeMaskedImage(image, mask)),
676  afwMath.stringToInterpStyle(self.config.interpolation),
677  afwMath.stringToUndersampleStyle("REDUCE_INTERP_ORDER"),
678  afwMath.ApproximateControl.UNKNOWN,
679  0, 0, False)
680  )
681 
682  def merge(self, other):
683  """Merge with another FocalPlaneBackground
684 
685  This allows multiple background models to be constructed from
686  different CCDs, and then merged to form a single consistent
687  background model for the entire focal plane.
688 
689  Parameters
690  ----------
691  other : `FocalPlaneBackground`
692  Another background model to merge.
693 
694  Returns
695  -------
696  self : `FocalPlaneBackground`
697  The merged background model.
698  """
699  if (self.config.xSize, self.config.ySize) != (other.config.xSize, other.config.ySize):
700  raise RuntimeError("Size mismatch: %s vs %s" % ((self.config.xSize, self.config.ySize),
701  (other.config.xSize, other.config.ySize)))
702  if self.dims != other.dims:
703  raise RuntimeError("Dimensions mismatch: %s vs %s" % (self.dims, other.dims))
704  self._values += other._values
705  self._numbers += other._numbers
706  return self
707 
708  def __iadd__(self, other):
709  """Merge with another FocalPlaneBackground
710 
711  Parameters
712  ----------
713  other : `FocalPlaneBackground`
714  Another background model to merge.
715 
716  Returns
717  -------
718  self : `FocalPlaneBackground`
719  The merged background model.
720  """
721  return self.merge(other)
722 
723  def getStatsImage(self):
724  """Return the background model data
725 
726  This is the measurement of the background for each of the superpixels.
727  """
728  values = self._values.clone()
729  values /= self._numbers
730  thresh = self.config.minFrac*self.config.xSize*self.config.ySize
731  isBad = self._numbers.getArray() < thresh
732  if self.config.doSmooth:
733  array = values.getArray()
734  array[:] = smoothArray(array, isBad, self.config.smoothScale)
735  isBad = numpy.isnan(values.array)
736  if numpy.any(isBad):
737  interpolateBadPixels(values.getArray(), isBad, self.config.interpolation)
738  return values
739 
740 
742  """Configuration for MaskObjectsTask"""
743  nIter = Field(dtype=int, default=3, doc="Number of iterations")
744  subtractBackground = ConfigurableField(target=measAlg.SubtractBackgroundTask,
745  doc="Background subtraction")
746  detection = ConfigurableField(target=measAlg.SourceDetectionTask, doc="Source detection")
747  detectSigma = Field(dtype=float, default=5.0, doc="Detection threshold (standard deviations)")
748  doInterpolate = Field(dtype=bool, default=True, doc="Interpolate when removing objects?")
749  interpolate = ConfigurableField(target=measAlg.SubtractBackgroundTask, doc="Interpolation")
750 
751  def setDefaults(self):
752  self.detection.reEstimateBackground = False
753  self.detection.doTempLocalBackground = False
754  self.detection.doTempWideBackground = False
755  self.detection.thresholdValue = 2.5
756  self.subtractBackground.binSize = 1024
757  self.subtractBackground.useApprox = False
758  self.interpolate.binSize = 256
759  self.interpolate.useApprox = False
760 
761  def validate(self):
762  if (self.detection.reEstimateBackground or
763  self.detection.doTempLocalBackground or
764  self.detection.doTempWideBackground):
765  raise RuntimeError("Incorrect settings for object masking: reEstimateBackground, "
766  "doTempLocalBackground and doTempWideBackground must be False")
767 
768 
770  """Iterative masking of objects on an Exposure
771 
772  This task makes more exhaustive object mask by iteratively doing detection
773  and background-subtraction. The purpose of this task is to get true
774  background removing faint tails of large objects. This is useful to get a
775  clean sky estimate from relatively small number of visits.
776 
777  We deliberately use the specified ``detectSigma`` instead of the PSF,
778  in order to better pick up the faint wings of objects.
779  """
780  ConfigClass = MaskObjectsConfig
781 
782  def __init__(self, *args, **kwargs):
783  super().__init__(*args, **kwargs)
784  # Disposable schema suppresses warning from SourceDetectionTask.__init__
785  self.makeSubtask("detection", schema=afwTable.Schema())
786  self.makeSubtask("interpolate")
787  self.makeSubtask("subtractBackground")
788 
789  def run(self, exposure, maskPlanes=None):
790  """Mask objects on Exposure
791 
792  Objects are found and removed.
793 
794  Parameters
795  ----------
796  exposure : `lsst.afw.image.Exposure`
797  Exposure on which to mask objects.
798  maskPlanes : iterable of `str`, optional
799  List of mask planes to remove.
800  """
801  self.findObjects(exposure)
802  self.removeObjects(exposure, maskPlanes)
803 
804  def findObjects(self, exposure):
805  """Iteratively find objects on an exposure
806 
807  Objects are masked with the ``DETECTED`` mask plane.
808 
809  Parameters
810  ----------
811  exposure : `lsst.afw.image.Exposure`
812  Exposure on which to mask objects.
813  """
814  for _ in range(self.config.nIter):
815  bg = self.subtractBackground.run(exposure).background
816  self.detection.detectFootprints(exposure, sigma=self.config.detectSigma, clearMask=True)
817  exposure.maskedImage += bg.getImage()
818 
819  def removeObjects(self, exposure, maskPlanes=None):
820  """Remove objects from exposure
821 
822  We interpolate over using a background model if ``doInterpolate`` is
823  set; otherwise we simply replace everything with the median.
824 
825  Parameters
826  ----------
827  exposure : `lsst.afw.image.Exposure`
828  Exposure on which to mask objects.
829  maskPlanes : iterable of `str`, optional
830  List of mask planes to remove. ``DETECTED`` will be added as well.
831  """
832  image = exposure.image
833  mask = exposure.mask
834  maskVal = mask.getPlaneBitMask("DETECTED")
835  if maskPlanes is not None:
836  maskVal |= mask.getPlaneBitMask(maskPlanes)
837  isBad = mask.array & maskVal > 0
838 
839  if self.config.doInterpolate:
840  smooth = self.interpolate.fitBackground(exposure.maskedImage)
841  replace = smooth.getImageF().array[isBad]
842  mask.array &= ~mask.getPlaneBitMask(["DETECTED"])
843  else:
844  replace = numpy.median(image.array[~isBad])
845  image.array[isBad] = replace
846 
847 
848 def smoothArray(array, bad, sigma):
849  """Gaussian-smooth an array while ignoring bad pixels
850 
851  It's not sufficient to set the bad pixels to zero, as then they're treated
852  as if they are zero, rather than being ignored altogether. We need to apply
853  a correction to that image that removes the effect of the bad pixels.
854 
855  Parameters
856  ----------
857  array : `numpy.ndarray` of floating-point
858  Array to smooth.
859  bad : `numpy.ndarray` of `bool`
860  Flag array indicating bad pixels.
861  sigma : `float`
862  Gaussian sigma.
863 
864  Returns
865  -------
866  convolved : `numpy.ndarray`
867  Smoothed image.
868  """
869  convolved = gaussian_filter(numpy.where(bad, 0.0, array), sigma, mode="constant", cval=0.0)
870  denominator = gaussian_filter(numpy.where(bad, 0.0, 1.0), sigma, mode="constant", cval=0.0)
871  return convolved/denominator
Defines the fields and offsets for a table.
Definition: Schema.h:50
UndersampleStyle stringToUndersampleStyle(std::string const &style)
Conversion function to switch a string to an UndersampleStyle.
Definition: Background.cc:119
Interpolate::Style stringToInterpStyle(std::string const &style)
Conversion function to switch a string to an Interpolate::Style.
Definition: Interpolate.cc:257
def removeObjects(self, exposure, maskPlanes=None)
Definition: background.py:819
def makeSubtask(self, name, keyArgs)
Definition: task.py:275
A floating-point coordinate rectangle geometry.
Definition: Box.h:305
def robustMean(array, rej=3.0)
Definition: background.py:18
int warpImage(DestImageT &destImage, SrcImageT const &srcImage, geom::TransformPoint2ToPoint2 const &srcToDest, WarpingControl const &control, typename DestImageT::SinglePixel padValue=lsst::afw::math::edgePixel< DestImageT >(typename lsst::afw::image::detail::image_traits< DestImageT >::image_category()))
A variant of warpImage that uses a Transform<Point2Endpoint, Point2Endpoint> instead of a pair of WCS...
A class to contain the data, WCS, and other information needed to describe an image of the sky...
Definition: Exposure.h:72
def interpolateBadPixels(array, isBad, interpolationStyle)
Definition: background.py:416
def subtractSkyFrame(self, image, skyBackground, scale, bgList=None)
Definition: background.py:347
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:562
def run(self, exposure, maskPlanes=None)
Definition: background.py:789
Parameters to control convolution.
Definition: warpExposure.h:250
def smoothArray(array, bad, sigma)
Definition: background.py:848
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
daf::base::PropertySet * set
Definition: fits.cc:884
Pass parameters to a Background object.
Definition: Background.h:57
int min
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT >> image, typename std::shared_ptr< Mask< MaskPixelT >> mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT >> variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
Definition: MaskedImage.h:1280
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.
Definition: Exposure.h:457
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
Definition: Statistics.cc:747
bool all(CoordinateExpr< N > const &expr) noexcept
Return true if all elements are true.
Pass parameters to a Statistics object.
Definition: Statistics.h:93
def interpolate1D(method, xSample, ySample, xInterp)
Definition: background.py:377
Represent a 2-dimensional array of bitmask pixels.
Definition: Mask.h:78
std::shared_ptr< Interpolate > makeInterpolate(ndarray::Array< double const, 1 > const &x, ndarray::Array< double const, 1 > const &y, Interpolate::Style const style=Interpolate::AKIMA_SPLINE)
Definition: Interpolate.cc:353
A class to manipulate images, masks, and variance as a single object.
Definition: MaskedImage.h:74
int max
Interpolate::Style lookupMaxInterpStyle(int const n)
Get the highest order Interpolation::Style available for &#39;n&#39; points.
Definition: Interpolate.cc:275
std::shared_ptr< image::MaskedImage< PixelT > > statisticsStack(image::MaskedImage< PixelT > const &image, Property flags, char dimension, StatisticsControl const &sctrl)
A function to compute statistics on the rows or columns of an image.
Definition: Stack.cc:478
def __init__(self, config, dims, transform, values=None, numbers=None)
Definition: background.py:536
A class to evaluate image background levels.
Definition: Background.h:444
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
def measureScale(self, image, skyBackground)
Definition: background.py:258
An integer coordinate rectangle.
Definition: Box.h:54
daf::base::PropertyList * list
Definition: fits.cc:885
std::shared_ptr< TransformPoint2ToPoint2 > makeTransform(lsst::geom::AffineTransform const &affine)
Wrap an lsst::geom::AffineTransform as a Transform.
def backgroundToExposure(self, statsImage, bbox)
Definition: background.py:143