lsst.pipe.drivers  6.0b0-hsc-15-g8ba6355+3
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 
6 import lsst.afw.math as afwMath
7 import lsst.afw.image as afwImage
8 import lsst.afw.geom as afwGeom
9 import lsst.afw.cameraGeom as afwCameraGeom
10 
11 from lsst.pex.config import Config, Field, ListField, ChoiceField, ConfigField, RangeField
12 from lsst.pipe.base import Task
13 
14 
15 def robustMean(array, rej=3.0):
16  """Measure a robust mean of an array
17 
18  Parameters
19  ----------
20  array : `numpy.ndarray`
21  Array for which to measure the mean.
22  rej : `float`
23  k-sigma rejection threshold.
24 
25  Returns
26  -------
27  mean : `array.dtype`
28  Robust mean of `array`.
29  """
30  q1, median, q3 = numpy.percentile(array, [25.0, 50.0, 100.0])
31  good = numpy.abs(array - median) < rej*0.74*(q3 - q1)
32  return array[good].mean()
33 
34 
35 class BackgroundConfig(Config):
36  """Configuration for background measurement"""
37  statistic = ChoiceField(dtype=str, default="MEANCLIP", doc="type of statistic to use for grid points",
38  allowed={"MEANCLIP": "clipped mean",
39  "MEAN": "unclipped mean",
40  "MEDIAN": "median",})
41  xBinSize = RangeField(dtype=int, default=32, min=1, doc="Superpixel size in x")
42  yBinSize = RangeField(dtype=int, default=32, min=1, doc="Superpixel size in y")
43  algorithm = ChoiceField(dtype=str, default="NATURAL_SPLINE", optional=True,
44  doc="How to interpolate the background values. "
45  "This maps to an enum; see afw::math::Background",
46  allowed={
47  "CONSTANT": "Use a single constant value",
48  "LINEAR": "Use linear interpolation",
49  "NATURAL_SPLINE": "cubic spline with zero second derivative at endpoints",
50  "AKIMA_SPLINE": "higher-level nonlinear spline that is more robust to outliers",
51  "NONE": "No background estimation is to be attempted",
52  })
53  mask = ListField(dtype=str, default=["SAT", "BAD", "EDGE", "DETECTED", "DETECTED_NEGATIVE", "NO_DATA",],
54  doc="Names of mask planes to ignore while estimating the background")
55 
56 
57 class SkyStatsConfig(Config):
58  """Parameters controlling the measurement of sky statistics"""
59  statistic = ChoiceField(dtype=str, default="MEANCLIP", doc="type of statistic to use for grid points",
60  allowed={"MEANCLIP": "clipped mean",
61  "MEAN": "unclipped mean",
62  "MEDIAN": "median",})
63  clip = Field(doc="Clipping threshold for background", dtype=float, default=3.0)
64  nIter = Field(doc="Clipping iterations for background", dtype=int, default=3)
65  mask = ListField(doc="Mask planes to reject", dtype=str,
66  default=["SAT", "DETECTED", "DETECTED_NEGATIVE", "BAD", "NO_DATA",])
67 
68 
69 class SkyMeasurementConfig(Config):
70  """Configuration for SkyMeasurementTask"""
71  skyIter = Field(dtype=int, default=3, doc="k-sigma rejection iterations for sky scale")
72  skyRej = Field(dtype=float, default=3.0, doc="k-sigma rejection threshold for sky scale")
73  background = ConfigField(dtype=BackgroundConfig, doc="Background measurement")
74  xNumSamples = Field(dtype=int, default=4, doc="Number of samples in x for scaling sky frame")
75  yNumSamples = Field(dtype=int, default=4, doc="Number of samples in y for scaling sky frame")
76  stats = ConfigField(dtype=SkyStatsConfig, doc="Measurement of sky statistics in the samples")
77 
78 
79 class SkyMeasurementTask(Task):
80  """Task for creating, persisting and using sky frames
81 
82  A sky frame is like a fringe frame (the sum of many exposures of the night sky,
83  combined with rejection to remove astrophysical objects) except the structure
84  is on larger scales, and hence we bin the images and represent them as a
85  background model (a `lsst.afw.math.BackgroundMI`). The sky frame represents
86  the dominant response of the camera to the sky background.
87  """
88  ConfigClass = SkyMeasurementConfig
89 
90  def getSkyData(self, butler, calibId):
91  """Retrieve sky frame from the butler
92 
93  Parameters
94  ----------
95  butler : `lsst.daf.persistence.Butler`
96  Data butler
97  calibId : `dict`
98  Data identifier for calib
99 
100  Returns
101  -------
102  sky : `lsst.afw.math.BackgroundList`
103  Sky frame
104  """
105  exp = butler.get("sky", calibId)
106  return self.exposureToBackground(exp)
107 
108  @staticmethod
110  """Convert an exposure to background model
111 
112  Calibs need to be persisted as an Exposure, so we need to convert
113  the persisted Exposure to a background model.
114 
115  Parameters
116  ----------
117  bgExp : `lsst.afw.image.Exposure`
118  Background model in Exposure format.
119 
120  Returns
121  -------
122  bg : `lsst.afw.math.BackgroundList`
123  Background model
124  """
125  header = bgExp.getMetadata()
126  xMin = header.get("BOX.MINX")
127  yMin = header.get("BOX.MINY")
128  xMax = header.get("BOX.MAXX")
129  yMax = header.get("BOX.MAXY")
130  algorithm = header.get("ALGORITHM")
131  bbox = afwGeom.Box2I(afwGeom.Point2I(xMin, yMin), afwGeom.Point2I(xMax, yMax))
132  return afwMath.BackgroundList(
133  (afwMath.BackgroundMI(bbox, bgExp.getMaskedImage()),
134  afwMath.stringToInterpStyle(algorithm),
135  afwMath.stringToUndersampleStyle("REDUCE_INTERP_ORDER"),
136  afwMath.ApproximateControl.UNKNOWN,
137  0, 0, False))
138 
139  def backgroundToExposure(self, statsImage, bbox):
140  """Convert a background model to an exposure
141 
142  Calibs need to be persisted as an Exposure, so we need to convert
143  the background model to an Exposure.
144 
145  Parameters
146  ----------
147  statsImage : `lsst.afw.image.MaskedImageF`
148  Background model's statistics image.
149  bbox : `lsst.afw.geom.Box2I`
150  Bounding box for image.
151 
152  Returns
153  -------
154  exp : `lsst.afw.image.Exposure`
155  Background model in Exposure format.
156  """
157  exp = afwImage.makeExposure(statsImage)
158  header = exp.getMetadata()
159  header.set("BOX.MINX", bbox.getMinX())
160  header.set("BOX.MINY", bbox.getMinY())
161  header.set("BOX.MAXX", bbox.getMaxX())
162  header.set("BOX.MAXY", bbox.getMaxY())
163  header.set("ALGORITHM", self.config.background.algorithm)
164  return exp
165 
166  def measureBackground(self, image):
167  """Measure a background model for image
168 
169  This doesn't use a full-featured background model (e.g., no Chebyshev
170  approximation) because we just want the binning behaviour. This will
171  allow us to average the bins later (`averageBackgrounds`).
172 
173  The `BackgroundMI` is wrapped in a `BackgroundList` so it can be
174  pickled and persisted.
175 
176  Parameters
177  ----------
178  image : `lsst.afw.image.MaskedImage`
179  Image for which to measure background.
180 
181  Returns
182  -------
183  bgModel : `lsst.afw.math.BackgroundList`
184  Background model.
185  """
186  stats = afwMath.StatisticsControl()
187  stats.setAndMask(image.getMask().getPlaneBitMask(self.config.background.mask))
188  stats.setNanSafe(True)
189  ctrl = afwMath.BackgroundControl(
190  self.config.background.algorithm,
191  max(int(image.getWidth()/self.config.background.xBinSize + 0.5), 1),
192  max(int(image.getHeight()/self.config.background.yBinSize + 0.5), 1),
193  "REDUCE_INTERP_ORDER",
194  stats,
195  self.config.background.statistic
196  )
197 
198  bg = afwMath.makeBackground(image, ctrl)
199 
200  return afwMath.BackgroundList((
201  bg,
202  afwMath.stringToInterpStyle(self.config.background.algorithm),
203  afwMath.stringToUndersampleStyle("REDUCE_INTERP_ORDER"),
204  afwMath.ApproximateControl.UNKNOWN,
205  0, 0, False
206  ))
207 
208  def averageBackgrounds(self, bgList):
209  """Average multiple background models
210 
211  The input background models should be a `BackgroundList` consisting
212  of a single `BackgroundMI`.
213 
214  Parameters
215  ----------
216  bgList : `list` of `lsst.afw.math.BackgroundList`
217  Background models to average.
218 
219  Returns
220  -------
221  bgExp : `lsst.afw.image.Exposure`
222  Background model in Exposure format.
223  """
224  assert all(len(bg) == 1 for bg in bgList), "Mixed bgList: %s" % ([len(bg) for bg in bgList],)
225  images = [bg[0][0].getStatsImage() for bg in bgList]
226  boxes = [bg[0][0].getImageBBox() for bg in bgList]
227  assert len(set((box.getMinX(), box.getMinY(), box.getMaxX(), box.getMaxY()) for box in boxes)) == 1, \
228  "Bounding boxes not all equal"
229  bbox = boxes.pop(0)
230 
231  # Ensure bad pixels are masked
232  maskVal = afwImage.Mask.getPlaneBitMask("BAD")
233  for img in images:
234  bad = numpy.isnan(img.getImage().getArray())
235  img.getMask().getArray()[bad] = maskVal
236 
237  stats = afwMath.StatisticsControl()
238  stats.setAndMask(maskVal)
239  stats.setNanSafe(True)
240  combined = afwMath.statisticsStack(images, afwMath.MEANCLIP, stats)
241 
242  # Set bad pixels to the median
243  # Specifically NOT going to attempt to interpolate the bad values because we're only working on a
244  # single CCD here and can't use the entire field-of-view to do the interpolation (which is what we
245  # would need to avoid introducing problems at the edge of CCDs).
246  array = combined.getImage().getArray()
247  bad = numpy.isnan(array)
248  median = numpy.median(array[~bad])
249  array[bad] = median
250 
251  # Put it into an exposure, which is required for calibs
252  return self.backgroundToExposure(combined, bbox)
253 
254  def measureScale(self, image, skyBackground):
255  """Measure scale of background model in image
256 
257  We treat the sky frame much as we would a fringe frame
258  (except the length scale of the variations is different):
259  we measure samples on the input image and the sky frame,
260  which we will use to determine the scaling factor in the
261  'solveScales` method.
262 
263  Parameters
264  ----------
265  image : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage`
266  Science image for which to measure scale.
267  skyBackground : `lsst.afw.math.BackgroundList`
268  Sky background model.
269 
270  Returns
271  -------
272  imageSamples : `numpy.ndarray`
273  Sample measurements on image.
274  skySamples : `numpy.ndarray`
275  Sample measurements on sky frame.
276  """
277  if isinstance(image, afwImage.Exposure):
278  image = image.getMaskedImage()
279  xLimits = numpy.linspace(0, image.getWidth() - 1, self.config.xNumSamples + 1, dtype=int)
280  yLimits = numpy.linspace(0, image.getHeight() - 1, self.config.yNumSamples + 1, dtype=int)
281  sky = skyBackground.getImage()
282  maskVal = image.getMask().getPlaneBitMask(self.config.stats.mask)
283  ctrl = afwMath.StatisticsControl(self.config.stats.clip, self.config.stats.nIter, maskVal)
284  statistic = afwMath.stringToStatisticsProperty(self.config.stats.statistic)
285  imageSamples = []
286  skySamples = []
287  for xStart, yStart, xStop, yStop in zip(xLimits[:-1], yLimits[:-1], xLimits[1:], yLimits[1:]):
288  box = afwGeom.Box2I(afwGeom.Point2I(xStart, yStart), afwGeom.Point2I(xStop, yStop))
289  subImage = image.Factory(image, box)
290  subSky = sky.Factory(sky, box)
291  imageSamples.append(afwMath.makeStatistics(subImage, statistic, ctrl).getValue())
292  skySamples.append(afwMath.makeStatistics(subSky, statistic, ctrl).getValue())
293  return imageSamples, skySamples
294 
295  def solveScales(self, scales):
296  """Solve multiple scales for a single scale factor
297 
298  Having measured samples from the image and sky frame, we
299  fit for the scaling factor.
300 
301  Parameters
302  ----------
303  scales : `list` of a `tuple` of two `numpy.ndarray` arrays
304  A `list` of the results from `measureScale` method.
305 
306  Returns
307  -------
308  scale : `float`
309  Scale factor.
310  """
311  imageSamples = []
312  skySamples = []
313  for ii, ss in scales:
314  imageSamples.extend(ii)
315  skySamples.extend(ss)
316  assert len(imageSamples) == len(skySamples)
317  imageSamples = numpy.array(imageSamples)
318  skySamples = numpy.array(skySamples)
319 
320  def solve(mask):
321  return afwMath.LeastSquares.fromDesignMatrix(skySamples[mask].reshape(mask.sum(), 1),
322  imageSamples[mask],
323  afwMath.LeastSquares.DIRECT_SVD).getSolution()
324 
325  mask = numpy.isfinite(imageSamples) & numpy.isfinite(skySamples)
326  for ii in range(self.config.skyIter):
327  solution = solve(mask)
328  residuals = imageSamples - solution*skySamples
329  lq, uq = numpy.percentile(residuals[mask], [25, 75])
330  stdev = 0.741*(uq - lq) # Robust stdev from IQR
331  bad = numpy.abs(residuals) > self.config.skyRej*stdev
332  mask[bad] = False
333 
334  return solve(mask)
335 
336  def subtractSkyFrame(self, image, skyBackground, scale, bgList=None):
337  """Subtract sky frame from science image
338 
339  Parameters
340  ----------
341  image : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage`
342  Science image.
343  skyBackground : `lsst.afw.math.BackgroundList`
344  Sky background model.
345  scale : `float`
346  Scale to apply to background model.
347  bgList : `lsst.afw.math.BackgroundList`
348  List of backgrounds applied to image
349  """
350  if isinstance(image, afwImage.Exposure):
351  image = image.getMaskedImage()
352  if isinstance(image, afwImage.MaskedImage):
353  image = image.getImage()
354  image.scaledMinus(scale, skyBackground.getImage())
355  if bgList is not None:
356  # Append the sky frame to the list of applied background models
357  bgData = list(skyBackground[0])
358  bg = bgData[0]
359  statsImage = bg.getStatsImage().clone()
360  statsImage *= scale
361  newBg = afwMath.BackgroundMI(bg.getImageBBox(), statsImage)
362  newBgData = [newBg] + bgData[1:]
363  bgList.append(newBgData)
364 
365 
366 def interpolate1D(method, xSample, ySample, xInterp):
367  """Interpolate in one dimension
368 
369  Interpolates the curve provided by `xSample` and `ySample` at
370  the positions of `xInterp`. Automatically backs off the
371  interpolation method to achieve successful interpolation.
372 
373  Parameters
374  ----------
375  method : `lsst.afw.math.Interpolate.Style`
376  Interpolation method to use.
377  xSample : `numpy.ndarray`
378  Vector of ordinates.
379  ySample : `numpy.ndarray`
380  Vector of coordinates.
381  xInterp : `numpy.ndarray`
382  Vector of ordinates to which to interpolate.
383 
384  Returns
385  -------
386  yInterp : `numpy.ndarray`
387  Vector of interpolated coordinates.
388 
389  """
390  if len(xSample) == 0:
391  return numpy.ones_like(xInterp)*numpy.nan
392  try:
393  return afwMath.makeInterpolate(xSample.astype(float), ySample.astype(float),
394  method).interpolate(xInterp.astype(float))
395  except:
396  if method == afwMath.Interpolate.CONSTANT:
397  # We've already tried the most basic interpolation and it failed
398  return numpy.ones_like(xInterp)*numpy.nan
399  newMethod = afwMath.lookupMaxInterpStyle(len(xSample))
400  if newMethod == method:
401  newMethod = afwMath.Interpolate.CONSTANT
402  return interpolate1D(newMethod, xSample, ySample, xInterp)
403 
404 
405 def interpolateBadPixels(array, isBad, interpolationStyle):
406  """Interpolate bad pixels in an image array
407 
408  The bad pixels are modified in the array.
409 
410  Parameters
411  ----------
412  array : `numpy.ndarray`
413  Image array with bad pixels.
414  isBad : `numpy.ndarray` of type `bool`
415  Boolean array indicating which pixels are bad.
416  interpolationStyle : `str`
417  Style for interpolation (see `lsst.afw.math.Background`);
418  supported values are CONSTANT, LINEAR, NATURAL_SPLINE,
419  AKIMA_SPLINE.
420  """
421  if numpy.all(isBad):
422  raise RuntimeError("No good pixels in image array")
423  height, width = array.shape
424  xIndices = numpy.arange(width, dtype=float)
425  yIndices = numpy.arange(height, dtype=float)
426  method = afwMath.stringToInterpStyle(interpolationStyle)
427  isGood = ~isBad
428  for y in range(height):
429  if numpy.any(isBad[y, :]) and numpy.any(isGood[y, :]):
430  array[y][isBad[y]] = interpolate1D(method, xIndices[isGood[y]], array[y][isGood[y]],
431  xIndices[isBad[y]])
432 
433  isBad = numpy.isnan(array)
434  isGood = ~isBad
435  for x in range(width):
436  if numpy.any(isBad[:, x]) and numpy.any(isGood[:, x]):
437  array[:, x][isBad[:, x]] = interpolate1D(method, yIndices[isGood[:, x]],
438  array[:, x][isGood[:, x]], yIndices[isBad[:, x]])
439 
440 
442  """Configuration for FocalPlaneBackground
443 
444  Note that `xSize` and `ySize` are floating-point values, as
445  the focal plane frame is usually defined in units of microns
446  or millimetres rather than pixels. As such, their values will
447  need to be revised according to each particular camera. For
448  this reason, no defaults are set for those.
449  """
450  xSize = Field(dtype=float, doc="Bin size in x")
451  ySize = Field(dtype=float, doc="Bin size in y")
452  minFrac = Field(dtype=float, default=0.1, doc="Minimum fraction of bin size for good measurement")
453  mask = ListField(dtype=str, doc="Mask planes to treat as bad",
454  default=["BAD", "SAT", "INTRP", "DETECTED", "DETECTED_NEGATIVE", "EDGE", "NO_DATA"])
455  interpolation = ChoiceField(
456  doc="how to interpolate the background values. This maps to an enum; see afw::math::Background",
457  dtype=str, default="AKIMA_SPLINE", optional=True,
458  allowed={
459  "CONSTANT": "Use a single constant value",
460  "LINEAR": "Use linear interpolation",
461  "NATURAL_SPLINE": "cubic spline with zero second derivative at endpoints",
462  "AKIMA_SPLINE": "higher-level nonlinear spline that is more robust to outliers",
463  "NONE": "No background estimation is to be attempted",
464  },
465  )
466  binning = Field(dtype=int, default=64, doc="Binning to use for CCD background model (pixels)")
467 
468 
469 class FocalPlaneBackground(object):
470  """Background model for a focal plane camera
471 
472  We model the background empirically with the "superpixel" method: we
473  measure the background in each superpixel and interpolate between
474  superpixels to yield the model.
475 
476  The principal difference between this and `lsst.afw.math.BackgroundMI`
477  is that here the superpixels are defined in the frame of the focal
478  plane of the camera which removes discontinuities across detectors.
479 
480  The constructor you probably want to use is the `fromCamera` classmethod.
481 
482  There are two use patterns for building a background model:
483 
484  * Serial: create a `FocalPlaneBackground`, then `addCcd` for each of the
485  CCDs in an exposure.
486 
487  * Parallel: create a `FocalPlaneBackground`, then `clone` it for each
488  of the CCDs in an exposure and use those to `addCcd` their respective
489  CCD image. Finally, `merge` all the clones into the original.
490 
491  Once you've built the background model, you can apply it to individual
492  CCDs with the `toCcdBackground` method.
493  """
494  @classmethod
495  def fromCamera(cls, config, camera):
496  """Construct from a camera object
497 
498  Parameters
499  ----------
500  config : `FocalPlaneBackgroundConfig`
501  Configuration for measuring backgrounds.
502  camera : `lsst.afw.cameraGeom.Camera`
503  Camera for which to measure backgrounds.
504  """
505  cameraBox = afwGeom.Box2D()
506  for ccd in camera:
507  for point in ccd.getCorners(afwCameraGeom.FOCAL_PLANE):
508  cameraBox.include(point)
509 
510  width, height = cameraBox.getDimensions()
511  # Offset so that we run from zero
512  offset = afwGeom.Extent2D(cameraBox.getMin())*-1
513  # Add an extra pixel buffer on either side
514  dims = afwGeom.Extent2I(int(numpy.ceil(width/config.xSize)) + 2,
515  int(numpy.ceil(height/config.ySize)) + 2)
516  # Transform takes us from focal plane coordinates --> sample coordinates
517  transform = (afwGeom.AffineTransform.makeTranslation(afwGeom.Extent2D(1, 1))*
518  afwGeom.AffineTransform.makeScaling(1.0/config.xSize, 1.0/config.ySize)*
519  afwGeom.AffineTransform.makeTranslation(offset))
520 
521  return cls(config, dims, afwGeom.makeTransform(transform))
522 
523  def __init__(self, config, dims, transform, values=None, numbers=None):
524  """Constructor
525 
526  Developers should note that changes to the signature of this method
527  require coordinated changes to the `__reduce__` and `clone` methods.
528 
529  Parameters
530  ----------
531  config : `FocalPlaneBackgroundConfig`
532  Configuration for measuring backgrounds.
533  dims : `lsst.afw.geom.Extent2I`
534  Dimensions for background samples.
535  transform : `lsst.afw.geom.TransformPoint2ToPoint2`
536  Transformation from focal plane coordinates to sample coordinates.
537  values : `lsst.afw.image.ImageF`
538  Measured background values.
539  numbers : `lsst.afw.image.ImageF`
540  Number of pixels in each background measurement.
541  """
542  self.config = config
543  self.dims = dims
544  self.transform = transform
545 
546  if values is None:
547  values = afwImage.ImageF(self.dims)
548  values.set(0.0)
549  else:
550  values = values.clone()
551  assert(values.getDimensions() == self.dims)
552  self._values = values
553  if numbers is None:
554  numbers = afwImage.ImageF(self.dims) # float for dynamic range and convenience
555  numbers.set(0.0)
556  else:
557  numbers = numbers.clone()
558  assert(numbers.getDimensions() == self.dims)
559  self._numbers = numbers
560 
561  def __reduce__(self):
562  return self.__class__, (self.config, self.dims, self.transform, self._values, self._numbers)
563 
564  def clone(self):
565  return self.__class__(self.config, self.dims, self.transform, self._values, self._numbers)
566 
567  def addCcd(self, exposure):
568  """Add CCD to model
569 
570  We measure the background on the CCD (clipped mean), and record
571  the results in the model. For simplicity, measurements are made
572  in a box on the CCD corresponding to the warped coordinates of the
573  superpixel rather than accounting for little rotations, etc.
574  We also record the number of pixels used in the measurement so we
575  can have a measure of confidence in each bin's value.
576 
577  Parameters
578  ----------
579  exposure : `lsst.afw.image.Exposure`
580  CCD exposure to measure
581  """
582  detector = exposure.getDetector()
583  transform = detector.getTransformMap().getTransform(detector.makeCameraSys(afwCameraGeom.PIXELS),
584  detector.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
585  image = exposure.getMaskedImage()
586  maskVal = image.getMask().getPlaneBitMask(self.config.mask)
587 
588  # Warp the binned image to the focal plane
589  toSample = transform.then(self.transform) # CCD pixels --> focal plane --> sample
590 
591  warped = afwImage.ImageF(self._values.getBBox())
592  warpedCounts = afwImage.ImageF(self._numbers.getBBox())
593  width, height = warped.getDimensions()
594 
595  stats = afwMath.StatisticsControl()
596  stats.setAndMask(maskVal)
597  stats.setNanSafe(True)
598  # Iterating over individual pixels in python is usually bad because it's slow, but there aren't many.
599  pixels = itertools.product(range(width), range(height))
600  for xx, yy in pixels:
601  llc = toSample.applyInverse(afwGeom.Point2D(xx - 0.5, yy - 0.5))
602  urc = toSample.applyInverse(afwGeom.Point2D(xx + 0.5, yy + 0.5))
603  bbox = afwGeom.Box2I(afwGeom.Point2I(llc), afwGeom.Point2I(urc))
604  bbox.clip(image.getBBox())
605  if bbox.isEmpty():
606  continue
607  subImage = image.Factory(image, bbox)
608  result = afwMath.makeStatistics(subImage, afwMath.MEANCLIP | afwMath.NPOINT, stats)
609  mean = result.getValue(afwMath.MEANCLIP)
610  num = result.getValue(afwMath.NPOINT)
611  if not numpy.isfinite(mean) or not numpy.isfinite(num):
612  continue
613  warped.set(xx, yy, mean*num)
614  warpedCounts.set(xx, yy, num)
615 
616  self._values += warped
617  self._numbers += warpedCounts
618 
619  def toCcdBackground(self, detector, bbox):
620  """Produce a background model for a CCD
621 
622  The superpixel background model is warped back to the
623  CCD frame, for application to the individual CCD.
624 
625  Parameters
626  ----------
627  detector : `lsst.afw.cameraGeom.Detector`
628  CCD for which to produce background model.
629  bbox : `lsst.afw.geom.Box2I`
630  Bounding box of CCD exposure.
631 
632  Returns
633  -------
634  bg : `lsst.afw.math.BackgroundList`
635  Background model for CCD.
636  """
637  transform = detector.getTransformMap().getTransform(detector.makeCameraSys(afwCameraGeom.PIXELS),
638  detector.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
639  binTransform = (afwGeom.AffineTransform.makeScaling(self.config.binning)*
640  afwGeom.AffineTransform.makeTranslation(afwGeom.Extent2D(0.5, 0.5)))
641 
642  # Binned image on CCD --> unbinned image on CCD --> focal plane --> binned focal plane
643  toSample = afwGeom.makeTransform(binTransform).then(transform).then(self.transform)
644 
645  focalPlane = self.getStatsImage()
646  fpNorm = afwImage.ImageF(focalPlane.getBBox())
647  fpNorm.set(1.0)
648 
649  image = afwImage.ImageF(bbox.getDimensions()//self.config.binning)
650  norm = afwImage.ImageF(image.getBBox())
651  ctrl = afwMath.WarpingControl("bilinear")
652  afwMath.warpImage(image, focalPlane, toSample, ctrl)
653  afwMath.warpImage(norm, fpNorm, toSample, ctrl)
654  image /= norm
655 
656  mask = afwImage.Mask(image.getBBox())
657  isBad = numpy.isnan(image.getArray())
658  mask.getArray()[isBad] = mask.getPlaneBitMask("BAD")
659  image.getArray()[isBad] = image.getArray()[~isBad].mean()
660 
661  return afwMath.BackgroundList(
662  (afwMath.BackgroundMI(bbox, afwImage.makeMaskedImage(image, mask)),
663  afwMath.stringToInterpStyle(self.config.interpolation),
664  afwMath.stringToUndersampleStyle("REDUCE_INTERP_ORDER"),
665  afwMath.ApproximateControl.UNKNOWN,
666  0, 0, False)
667  )
668 
669  def merge(self, other):
670  """Merge with another FocalPlaneBackground
671 
672  This allows multiple background models to be constructed from
673  different CCDs, and then merged to form a single consistent
674  background model for the entire focal plane.
675 
676  Parameters
677  ----------
678  other : `FocalPlaneBackground`
679  Another background model to merge.
680 
681  Returns
682  -------
683  self : `FocalPlaneBackground`
684  The merged background model.
685  """
686  if (self.config.xSize, self.config.ySize) != (other.config.xSize, other.config.ySize):
687  raise RuntimeError("Size mismatch: %s vs %s" % ((self.config.xSize, self.config.ySize),
688  (other.config.xSize, other.config.ySize)))
689  if self.dims != other.dims:
690  raise RuntimeError("Dimensions mismatch: %s vs %s" % (self.dims, other.dims))
691  self._values += other._values
692  self._numbers += other._numbers
693  return self
694 
695  def __iadd__(self, other):
696  """Merge with another FocalPlaneBackground
697 
698  Parameters
699  ----------
700  other : `FocalPlaneBackground`
701  Another background model to merge.
702 
703  Returns
704  -------
705  self : `FocalPlaneBackground`
706  The merged background model.
707  """
708  return self.merge(other)
709 
710  def getStatsImage(self):
711  """Return the background model data
712 
713  This is the measurement of the background for each of the superpixels.
714  """
715  values = self._values.clone()
716  values /= self._numbers
717  thresh = self.config.minFrac*self.config.xSize*self.config.ySize
718  isBad = self._numbers.getArray() < thresh
719  interpolateBadPixels(values.getArray(), isBad, self.config.interpolation)
720  return values
721 
722 
def robustMean(array, rej=3.0)
Definition: background.py:15
def interpolateBadPixels(array, isBad, interpolationStyle)
Definition: background.py:405
def subtractSkyFrame(self, image, skyBackground, scale, bgList=None)
Definition: background.py:336
def interpolate1D(method, xSample, ySample, xInterp)
Definition: background.py:366
def __init__(self, config, dims, transform, values=None, numbers=None)
Definition: background.py:523
def measureScale(self, image, skyBackground)
Definition: background.py:254
def backgroundToExposure(self, statsImage, bbox)
Definition: background.py:139