LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
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 
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 
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"
51  " to outliers",
52  "NONE": "No background estimation is to be attempted",
53  })
54  mask = ListField(dtype=str, default=["SAT", "BAD", "EDGE", "DETECTED", "DETECTED_NEGATIVE", "NO_DATA"],
55  doc="Names of mask planes to ignore while estimating the background")
56 
57 
59  """Parameters controlling the measurement of sky statistics"""
60  statistic = ChoiceField(dtype=str, default="MEANCLIP", doc="type of statistic to use for grid points",
61  allowed={"MEANCLIP": "clipped mean",
62  "MEAN": "unclipped mean",
63  "MEDIAN": "median"})
64  clip = Field(doc="Clipping threshold for background", dtype=float, default=3.0)
65  nIter = Field(doc="Clipping iterations for background", dtype=int, default=3)
66  mask = ListField(doc="Mask planes to reject", dtype=str,
67  default=["SAT", "DETECTED", "DETECTED_NEGATIVE", "BAD", "NO_DATA"])
68 
69 
71  """Configuration for SkyMeasurementTask"""
72  skyIter = Field(dtype=int, default=3, doc="k-sigma rejection iterations for sky scale")
73  skyRej = Field(dtype=float, default=3.0, doc="k-sigma rejection threshold for sky 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 
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)
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  # Ensure more samples than pixels
281  xNumSamples = min(self.config.xNumSamples, image.getWidth())
282  yNumSamples = min(self.config.yNumSamples, image.getHeight())
283  xLimits = numpy.linspace(0, image.getWidth(), xNumSamples + 1, dtype=int)
284  yLimits = numpy.linspace(0, image.getHeight(), yNumSamples + 1, dtype=int)
285  sky = skyBackground.getImage()
286  maskVal = image.getMask().getPlaneBitMask(self.config.stats.mask)
287  ctrl = afwMath.StatisticsControl(self.config.stats.clip, self.config.stats.nIter, maskVal)
288  statistic = afwMath.stringToStatisticsProperty(self.config.stats.statistic)
289  imageSamples = []
290  skySamples = []
291  for xIndex, yIndex in itertools.product(range(xNumSamples), range(yNumSamples)):
292  # -1 on the stop because Box2I is inclusive of the end point and we don't want to overlap boxes
293  xStart, xStop = xLimits[xIndex], xLimits[xIndex + 1] - 1
294  yStart, yStop = yLimits[yIndex], yLimits[yIndex + 1] - 1
295  box = afwGeom.Box2I(afwGeom.Point2I(xStart, yStart), afwGeom.Point2I(xStop, yStop))
296  subImage = image.Factory(image, box)
297  subSky = sky.Factory(sky, box)
298  imageSamples.append(afwMath.makeStatistics(subImage, statistic, ctrl).getValue())
299  skySamples.append(afwMath.makeStatistics(subSky, statistic, ctrl).getValue())
300  return imageSamples, skySamples
301 
302  def solveScales(self, scales):
303  """Solve multiple scales for a single scale factor
304 
305  Having measured samples from the image and sky frame, we
306  fit for the scaling factor.
307 
308  Parameters
309  ----------
310  scales : `list` of a `tuple` of two `numpy.ndarray` arrays
311  A `list` of the results from `measureScale` method.
312 
313  Returns
314  -------
315  scale : `float`
316  Scale factor.
317  """
318  imageSamples = []
319  skySamples = []
320  for ii, ss in scales:
321  imageSamples.extend(ii)
322  skySamples.extend(ss)
323  assert len(imageSamples) == len(skySamples)
324  imageSamples = numpy.array(imageSamples)
325  skySamples = numpy.array(skySamples)
326 
327  def solve(mask):
328  return afwMath.LeastSquares.fromDesignMatrix(skySamples[mask].reshape(mask.sum(), 1),
329  imageSamples[mask],
330  afwMath.LeastSquares.DIRECT_SVD).getSolution()
331 
332  mask = numpy.isfinite(imageSamples) & numpy.isfinite(skySamples)
333  for ii in range(self.config.skyIter):
334  solution = solve(mask)
335  residuals = imageSamples - solution*skySamples
336  lq, uq = numpy.percentile(residuals[mask], [25, 75])
337  stdev = 0.741*(uq - lq) # Robust stdev from IQR
338  with numpy.errstate(invalid="ignore"): # suppress NAN warnings
339  bad = numpy.abs(residuals) > self.config.skyRej*stdev
340  mask[bad] = False
341 
342  return solve(mask)
343 
344  def subtractSkyFrame(self, image, skyBackground, scale, bgList=None):
345  """Subtract sky frame from science image
346 
347  Parameters
348  ----------
349  image : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage`
350  Science image.
351  skyBackground : `lsst.afw.math.BackgroundList`
352  Sky background model.
353  scale : `float`
354  Scale to apply to background model.
355  bgList : `lsst.afw.math.BackgroundList`
356  List of backgrounds applied to image
357  """
358  if isinstance(image, afwImage.Exposure):
359  image = image.getMaskedImage()
360  if isinstance(image, afwImage.MaskedImage):
361  image = image.getImage()
362  image.scaledMinus(scale, skyBackground.getImage())
363  if bgList is not None:
364  # Append the sky frame to the list of applied background models
365  bgData = list(skyBackground[0])
366  bg = bgData[0]
367  statsImage = bg.getStatsImage().clone()
368  statsImage *= scale
369  newBg = afwMath.BackgroundMI(bg.getImageBBox(), statsImage)
370  newBgData = [newBg] + bgData[1:]
371  bgList.append(newBgData)
372 
373 
374 def interpolate1D(method, xSample, ySample, xInterp):
375  """Interpolate in one dimension
376 
377  Interpolates the curve provided by `xSample` and `ySample` at
378  the positions of `xInterp`. Automatically backs off the
379  interpolation method to achieve successful interpolation.
380 
381  Parameters
382  ----------
383  method : `lsst.afw.math.Interpolate.Style`
384  Interpolation method to use.
385  xSample : `numpy.ndarray`
386  Vector of ordinates.
387  ySample : `numpy.ndarray`
388  Vector of coordinates.
389  xInterp : `numpy.ndarray`
390  Vector of ordinates to which to interpolate.
391 
392  Returns
393  -------
394  yInterp : `numpy.ndarray`
395  Vector of interpolated coordinates.
396 
397  """
398  if len(xSample) == 0:
399  return numpy.ones_like(xInterp)*numpy.nan
400  try:
401  return afwMath.makeInterpolate(xSample.astype(float), ySample.astype(float),
402  method).interpolate(xInterp.astype(float))
403  except Exception:
404  if method == afwMath.Interpolate.CONSTANT:
405  # We've already tried the most basic interpolation and it failed
406  return numpy.ones_like(xInterp)*numpy.nan
407  newMethod = afwMath.lookupMaxInterpStyle(len(xSample))
408  if newMethod == method:
409  newMethod = afwMath.Interpolate.CONSTANT
410  return interpolate1D(newMethod, xSample, ySample, xInterp)
411 
412 
413 def interpolateBadPixels(array, isBad, interpolationStyle):
414  """Interpolate bad pixels in an image array
415 
416  The bad pixels are modified in the array.
417 
418  Parameters
419  ----------
420  array : `numpy.ndarray`
421  Image array with bad pixels.
422  isBad : `numpy.ndarray` of type `bool`
423  Boolean array indicating which pixels are bad.
424  interpolationStyle : `str`
425  Style for interpolation (see `lsst.afw.math.Background`);
426  supported values are CONSTANT, LINEAR, NATURAL_SPLINE,
427  AKIMA_SPLINE.
428  """
429  if numpy.all(isBad):
430  raise RuntimeError("No good pixels in image array")
431  height, width = array.shape
432  xIndices = numpy.arange(width, dtype=float)
433  yIndices = numpy.arange(height, dtype=float)
434  method = afwMath.stringToInterpStyle(interpolationStyle)
435  isGood = ~isBad
436  for y in range(height):
437  if numpy.any(isBad[y, :]) and numpy.any(isGood[y, :]):
438  array[y][isBad[y]] = interpolate1D(method, xIndices[isGood[y]], array[y][isGood[y]],
439  xIndices[isBad[y]])
440 
441  isBad = numpy.isnan(array)
442  isGood = ~isBad
443  for x in range(width):
444  if numpy.any(isBad[:, x]) and numpy.any(isGood[:, x]):
445  array[:, x][isBad[:, x]] = interpolate1D(method, yIndices[isGood[:, x]],
446  array[:, x][isGood[:, x]], yIndices[isBad[:, x]])
447 
448 
450  """Configuration for FocalPlaneBackground
451 
452  Note that `xSize` and `ySize` are floating-point values, as
453  the focal plane frame is usually defined in units of microns
454  or millimetres rather than pixels. As such, their values will
455  need to be revised according to each particular camera. For
456  this reason, no defaults are set for those.
457  """
458  xSize = Field(dtype=float, doc="Bin size in x")
459  ySize = Field(dtype=float, doc="Bin size in y")
460  minFrac = Field(dtype=float, default=0.1, doc="Minimum fraction of bin size for good measurement")
461  mask = ListField(dtype=str, doc="Mask planes to treat as bad",
462  default=["BAD", "SAT", "INTRP", "DETECTED", "DETECTED_NEGATIVE", "EDGE", "NO_DATA"])
463  interpolation = ChoiceField(
464  doc="how to interpolate the background values. This maps to an enum; see afw::math::Background",
465  dtype=str, default="AKIMA_SPLINE", optional=True,
466  allowed={
467  "CONSTANT": "Use a single constant value",
468  "LINEAR": "Use linear interpolation",
469  "NATURAL_SPLINE": "cubic spline with zero second derivative at endpoints",
470  "AKIMA_SPLINE": "higher-level nonlinear spline that is more robust to outliers",
471  "NONE": "No background estimation is to be attempted",
472  },
473  )
474  binning = Field(dtype=int, default=64, doc="Binning to use for CCD background model (pixels)")
475 
476 
478  """Background model for a focal plane camera
479 
480  We model the background empirically with the "superpixel" method: we
481  measure the background in each superpixel and interpolate between
482  superpixels to yield the model.
483 
484  The principal difference between this and `lsst.afw.math.BackgroundMI`
485  is that here the superpixels are defined in the frame of the focal
486  plane of the camera which removes discontinuities across detectors.
487 
488  The constructor you probably want to use is the `fromCamera` classmethod.
489 
490  There are two use patterns for building a background model:
491 
492  * Serial: create a `FocalPlaneBackground`, then `addCcd` for each of the
493  CCDs in an exposure.
494 
495  * Parallel: create a `FocalPlaneBackground`, then `clone` it for each
496  of the CCDs in an exposure and use those to `addCcd` their respective
497  CCD image. Finally, `merge` all the clones into the original.
498 
499  Once you've built the background model, you can apply it to individual
500  CCDs with the `toCcdBackground` method.
501  """
502  @classmethod
503  def fromCamera(cls, config, camera):
504  """Construct from a camera object
505 
506  Parameters
507  ----------
508  config : `FocalPlaneBackgroundConfig`
509  Configuration for measuring backgrounds.
510  camera : `lsst.afw.cameraGeom.Camera`
511  Camera for which to measure backgrounds.
512  """
513  cameraBox = afwGeom.Box2D()
514  for ccd in camera:
515  for point in ccd.getCorners(afwCameraGeom.FOCAL_PLANE):
516  cameraBox.include(point)
517 
518  width, height = cameraBox.getDimensions()
519  # Offset so that we run from zero
520  offset = afwGeom.Extent2D(cameraBox.getMin())*-1
521  # Add an extra pixel buffer on either side
522  dims = afwGeom.Extent2I(int(numpy.ceil(width/config.xSize)) + 2,
523  int(numpy.ceil(height/config.ySize)) + 2)
524  # Transform takes us from focal plane coordinates --> sample coordinates
525  transform = (afwGeom.AffineTransform.makeTranslation(afwGeom.Extent2D(1, 1))*
526  afwGeom.AffineTransform.makeScaling(1.0/config.xSize, 1.0/config.ySize)*
527  afwGeom.AffineTransform.makeTranslation(offset))
528 
529  return cls(config, dims, afwGeom.makeTransform(transform))
530 
531  def __init__(self, config, dims, transform, values=None, numbers=None):
532  """Constructor
533 
534  Developers should note that changes to the signature of this method
535  require coordinated changes to the `__reduce__` and `clone` methods.
536 
537  Parameters
538  ----------
539  config : `FocalPlaneBackgroundConfig`
540  Configuration for measuring backgrounds.
541  dims : `lsst.afw.geom.Extent2I`
542  Dimensions for background samples.
543  transform : `lsst.afw.geom.TransformPoint2ToPoint2`
544  Transformation from focal plane coordinates to sample coordinates.
545  values : `lsst.afw.image.ImageF`
546  Measured background values.
547  numbers : `lsst.afw.image.ImageF`
548  Number of pixels in each background measurement.
549  """
550  self.config = config
551  self.dims = dims
552  self.transform = transform
553 
554  if values is None:
555  values = afwImage.ImageF(self.dims)
556  values.set(0.0)
557  else:
558  values = values.clone()
559  assert(values.getDimensions() == self.dims)
560  self._values = values
561  if numbers is None:
562  numbers = afwImage.ImageF(self.dims) # float for dynamic range and convenience
563  numbers.set(0.0)
564  else:
565  numbers = numbers.clone()
566  assert(numbers.getDimensions() == self.dims)
567  self._numbers = numbers
568 
569  def __reduce__(self):
570  return self.__class__, (self.config, self.dims, self.transform, self._values, self._numbers)
571 
572  def clone(self):
573  return self.__class__(self.config, self.dims, self.transform, self._values, self._numbers)
574 
575  def addCcd(self, exposure):
576  """Add CCD to model
577 
578  We measure the background on the CCD (clipped mean), and record
579  the results in the model. For simplicity, measurements are made
580  in a box on the CCD corresponding to the warped coordinates of the
581  superpixel rather than accounting for little rotations, etc.
582  We also record the number of pixels used in the measurement so we
583  can have a measure of confidence in each bin's value.
584 
585  Parameters
586  ----------
587  exposure : `lsst.afw.image.Exposure`
588  CCD exposure to measure
589  """
590  detector = exposure.getDetector()
591  transform = detector.getTransformMap().getTransform(detector.makeCameraSys(afwCameraGeom.PIXELS),
592  detector.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
593  image = exposure.getMaskedImage()
594  maskVal = image.getMask().getPlaneBitMask(self.config.mask)
595 
596  # Warp the binned image to the focal plane
597  toSample = transform.then(self.transform) # CCD pixels --> focal plane --> sample
598 
599  warped = afwImage.ImageF(self._values.getBBox())
600  warpedCounts = afwImage.ImageF(self._numbers.getBBox())
601  width, height = warped.getDimensions()
602 
603  stats = afwMath.StatisticsControl()
604  stats.setAndMask(maskVal)
605  stats.setNanSafe(True)
606  # Iterating over individual pixels in python is usually bad because it's slow, but there aren't many.
607  pixels = itertools.product(range(width), range(height))
608  for xx, yy in pixels:
609  llc = toSample.applyInverse(afwGeom.Point2D(xx - 0.5, yy - 0.5))
610  urc = toSample.applyInverse(afwGeom.Point2D(xx + 0.5, yy + 0.5))
612  bbox.clip(image.getBBox())
613  if bbox.isEmpty():
614  continue
615  subImage = image.Factory(image, bbox)
616  result = afwMath.makeStatistics(subImage, afwMath.MEANCLIP | afwMath.NPOINT, stats)
617  mean = result.getValue(afwMath.MEANCLIP)
618  num = result.getValue(afwMath.NPOINT)
619  if not numpy.isfinite(mean) or not numpy.isfinite(num):
620  continue
621  warped[xx, yy, afwImage.LOCAL] = mean*num
622  warpedCounts[xx, yy, afwImage.LOCAL] = num
623 
624  self._values += warped
625  self._numbers += warpedCounts
626 
627  def toCcdBackground(self, detector, bbox):
628  """Produce a background model for a CCD
629 
630  The superpixel background model is warped back to the
631  CCD frame, for application to the individual CCD.
632 
633  Parameters
634  ----------
635  detector : `lsst.afw.cameraGeom.Detector`
636  CCD for which to produce background model.
637  bbox : `lsst.afw.geom.Box2I`
638  Bounding box of CCD exposure.
639 
640  Returns
641  -------
642  bg : `lsst.afw.math.BackgroundList`
643  Background model for CCD.
644  """
645  transform = detector.getTransformMap().getTransform(detector.makeCameraSys(afwCameraGeom.PIXELS),
646  detector.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
647  binTransform = (afwGeom.AffineTransform.makeScaling(self.config.binning)*
648  afwGeom.AffineTransform.makeTranslation(afwGeom.Extent2D(0.5, 0.5)))
649 
650  # Binned image on CCD --> unbinned image on CCD --> focal plane --> binned focal plane
651  toSample = afwGeom.makeTransform(binTransform).then(transform).then(self.transform)
652 
653  focalPlane = self.getStatsImage()
654  fpNorm = afwImage.ImageF(focalPlane.getBBox())
655  fpNorm.set(1.0)
656 
657  image = afwImage.ImageF(bbox.getDimensions()//self.config.binning)
658  norm = afwImage.ImageF(image.getBBox())
659  ctrl = afwMath.WarpingControl("bilinear")
660  afwMath.warpImage(image, focalPlane, toSample.inverted(), ctrl)
661  afwMath.warpImage(norm, fpNorm, toSample.inverted(), ctrl)
662  image /= norm
663 
664  mask = afwImage.Mask(image.getBBox())
665  isBad = numpy.isnan(image.getArray())
666  mask.getArray()[isBad] = mask.getPlaneBitMask("BAD")
667  image.getArray()[isBad] = image.getArray()[~isBad].mean()
668 
669  return afwMath.BackgroundList(
670  (afwMath.BackgroundMI(bbox, afwImage.makeMaskedImage(image, mask)),
671  afwMath.stringToInterpStyle(self.config.interpolation),
672  afwMath.stringToUndersampleStyle("REDUCE_INTERP_ORDER"),
673  afwMath.ApproximateControl.UNKNOWN,
674  0, 0, False)
675  )
676 
677  def merge(self, other):
678  """Merge with another FocalPlaneBackground
679 
680  This allows multiple background models to be constructed from
681  different CCDs, and then merged to form a single consistent
682  background model for the entire focal plane.
683 
684  Parameters
685  ----------
686  other : `FocalPlaneBackground`
687  Another background model to merge.
688 
689  Returns
690  -------
691  self : `FocalPlaneBackground`
692  The merged background model.
693  """
694  if (self.config.xSize, self.config.ySize) != (other.config.xSize, other.config.ySize):
695  raise RuntimeError("Size mismatch: %s vs %s" % ((self.config.xSize, self.config.ySize),
696  (other.config.xSize, other.config.ySize)))
697  if self.dims != other.dims:
698  raise RuntimeError("Dimensions mismatch: %s vs %s" % (self.dims, other.dims))
699  self._values += other._values
700  self._numbers += other._numbers
701  return self
702 
703  def __iadd__(self, other):
704  """Merge with another FocalPlaneBackground
705 
706  Parameters
707  ----------
708  other : `FocalPlaneBackground`
709  Another background model to merge.
710 
711  Returns
712  -------
713  self : `FocalPlaneBackground`
714  The merged background model.
715  """
716  return self.merge(other)
717 
718  def getStatsImage(self):
719  """Return the background model data
720 
721  This is the measurement of the background for each of the superpixels.
722  """
723  values = self._values.clone()
724  values /= self._numbers
725  thresh = self.config.minFrac*self.config.xSize*self.config.ySize
726  isBad = self._numbers.getArray() < thresh
727  interpolateBadPixels(values.getArray(), isBad, self.config.interpolation)
728  return values
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
A floating-point coordinate rectangle geometry.
Definition: Box.h:294
def robustMean(array, rej=3.0)
Definition: background.py:15
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:413
def subtractSkyFrame(self, image, skyBackground, scale, bgList=None)
Definition: background.py:344
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:566
Parameters to control convolution.
Definition: warpExposure.h:250
daf::base::PropertySet * set
Definition: fits.cc:832
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:446
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:374
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:531
A class to evaluate image background levels.
Definition: Background.h:444
def measureScale(self, image, skyBackground)
Definition: background.py:255
An integer coordinate rectangle.
Definition: Box.h:54
daf::base::PropertyList * list
Definition: fits.cc:833
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:140