lsst.pipe.drivers  16.0-1-g98efed3+2
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  with numpy.errstate(invalid="ignore"): # suppress NAN warnings
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[xx, yy, afwImage.LOCAL] = mean*num
615  warpedCounts[xx, yy,afwImage.LOCAL] = 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.getInverse(), ctrl)
654  afwMath.warpImage(norm, fpNorm, toSample.getInverse(), 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:254
def backgroundToExposure(self, statsImage, bbox)
Definition: background.py:139