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