1 from __future__
import absolute_import, division, print_function
11 from lsst.pex.config import Config, Field, ListField, ChoiceField, ConfigField, RangeField
16 """Measure a robust mean of an array 20 array : `numpy.ndarray` 21 Array for which to measure the mean. 23 k-sigma rejection threshold. 28 Robust mean of `array`. 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()
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",
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",
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" 52 "NONE":
"No background estimation is to be attempted",
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")
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",
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"])
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")
81 """Task for creating, persisting and using sky frames 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. 89 ConfigClass = SkyMeasurementConfig
92 """Retrieve sky frame from the butler 96 butler : `lsst.daf.persistence.Butler` 99 Data identifier for calib 103 sky : `lsst.afw.math.BackgroundList` 106 exp = butler.get(
"sky", calibId)
111 """Convert an exposure to background model 113 Calibs need to be persisted as an Exposure, so we need to convert 114 the persisted Exposure to a background model. 118 bgExp : `lsst.afw.image.Exposure` 119 Background model in Exposure format. 123 bg : `lsst.afw.math.BackgroundList` 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,
141 """Convert a background model to an exposure 143 Calibs need to be persisted as an Exposure, so we need to convert 144 the background model to an Exposure. 148 statsImage : `lsst.afw.image.MaskedImageF` 149 Background model's statistics image. 150 bbox : `lsst.afw.geom.Box2I` 151 Bounding box for image. 155 exp : `lsst.afw.image.Exposure` 156 Background model in Exposure format. 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)
168 """Measure a background model for image 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`). 174 The `BackgroundMI` is wrapped in a `BackgroundList` so it can be 175 pickled and persisted. 179 image : `lsst.afw.image.MaskedImage` 180 Image for which to measure background. 184 bgModel : `lsst.afw.math.BackgroundList` 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",
196 self.config.background.statistic
199 bg = afwMath.makeBackground(image, ctrl)
201 return afwMath.BackgroundList((
203 afwMath.stringToInterpStyle(self.config.background.algorithm),
204 afwMath.stringToUndersampleStyle(
"REDUCE_INTERP_ORDER"),
205 afwMath.ApproximateControl.UNKNOWN,
210 """Average multiple background models 212 The input background models should be a `BackgroundList` consisting 213 of a single `BackgroundMI`. 217 bgList : `list` of `lsst.afw.math.BackgroundList` 218 Background models to average. 222 bgExp : `lsst.afw.image.Exposure` 223 Background model in Exposure format. 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" 233 maskVal = afwImage.Mask.getPlaneBitMask(
"BAD")
235 bad = numpy.isnan(img.getImage().getArray())
236 img.getMask().getArray()[bad] = maskVal
238 stats = afwMath.StatisticsControl()
239 stats.setAndMask(maskVal)
240 stats.setNanSafe(
True)
241 combined = afwMath.statisticsStack(images, afwMath.MEANCLIP, stats)
247 array = combined.getImage().getArray()
248 bad = numpy.isnan(array)
249 median = numpy.median(array[~bad])
256 """Measure scale of background model in image 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. 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. 273 imageSamples : `numpy.ndarray` 274 Sample measurements on image. 275 skySamples : `numpy.ndarray` 276 Sample measurements on sky frame. 278 if isinstance(image, afwImage.Exposure):
279 image = image.getMaskedImage()
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)
291 for xIndex, yIndex
in itertools.product(range(xNumSamples), range(yNumSamples)):
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
303 """Solve multiple scales for a single scale factor 305 Having measured samples from the image and sky frame, we 306 fit for the scaling factor. 310 scales : `list` of a `tuple` of two `numpy.ndarray` arrays 311 A `list` of the results from `measureScale` method. 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)
328 return afwMath.LeastSquares.fromDesignMatrix(skySamples[mask].reshape(mask.sum(), 1),
330 afwMath.LeastSquares.DIRECT_SVD).getSolution()
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)
338 with numpy.errstate(invalid=
"ignore"):
339 bad = numpy.abs(residuals) > self.config.skyRej*stdev
345 """Subtract sky frame from science image 349 image : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage` 351 skyBackground : `lsst.afw.math.BackgroundList` 352 Sky background model. 354 Scale to apply to background model. 355 bgList : `lsst.afw.math.BackgroundList` 356 List of backgrounds applied to image 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:
365 bgData = list(skyBackground[0])
367 statsImage = bg.getStatsImage().clone()
369 newBg = afwMath.BackgroundMI(bg.getImageBBox(), statsImage)
370 newBgData = [newBg] + bgData[1:]
371 bgList.append(newBgData)
375 """Interpolate in one dimension 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. 383 method : `lsst.afw.math.Interpolate.Style` 384 Interpolation method to use. 385 xSample : `numpy.ndarray` 387 ySample : `numpy.ndarray` 388 Vector of coordinates. 389 xInterp : `numpy.ndarray` 390 Vector of ordinates to which to interpolate. 394 yInterp : `numpy.ndarray` 395 Vector of interpolated coordinates. 398 if len(xSample) == 0:
399 return numpy.ones_like(xInterp)*numpy.nan
401 return afwMath.makeInterpolate(xSample.astype(float), ySample.astype(float),
402 method).interpolate(xInterp.astype(float))
404 if method == afwMath.Interpolate.CONSTANT:
406 return numpy.ones_like(xInterp)*numpy.nan
407 newMethod = afwMath.lookupMaxInterpStyle(len(xSample))
408 if newMethod == method:
409 newMethod = afwMath.Interpolate.CONSTANT
414 """Interpolate bad pixels in an image array 416 The bad pixels are modified in the array. 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, 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)
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]],
441 isBad = numpy.isnan(array)
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]])
450 """Configuration for FocalPlaneBackground 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. 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,
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",
474 binning = Field(dtype=int, default=64, doc=
"Binning to use for CCD background model (pixels)")
478 """Background model for a focal plane camera 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. 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. 488 The constructor you probably want to use is the `fromCamera` classmethod. 490 There are two use patterns for building a background model: 492 * Serial: create a `FocalPlaneBackground`, then `addCcd` for each of the 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. 499 Once you've built the background model, you can apply it to individual 500 CCDs with the `toCcdBackground` method. 504 """Construct from a camera object 508 config : `FocalPlaneBackgroundConfig` 509 Configuration for measuring backgrounds. 510 camera : `lsst.afw.cameraGeom.Camera` 511 Camera for which to measure backgrounds. 513 cameraBox = afwGeom.Box2D()
515 for point
in ccd.getCorners(afwCameraGeom.FOCAL_PLANE):
516 cameraBox.include(point)
518 width, height = cameraBox.getDimensions()
520 offset = afwGeom.Extent2D(cameraBox.getMin())*-1
522 dims = afwGeom.Extent2I(int(numpy.ceil(width/config.xSize)) + 2,
523 int(numpy.ceil(height/config.ySize)) + 2)
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))
529 return cls(config, dims, afwGeom.makeTransform(transform))
531 def __init__(self, config, dims, transform, values=None, numbers=None):
534 Developers should note that changes to the signature of this method 535 require coordinated changes to the `__reduce__` and `clone` methods. 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. 555 values = afwImage.ImageF(self.
dims)
558 values = values.clone()
559 assert(values.getDimensions() == self.
dims)
562 numbers = afwImage.ImageF(self.
dims)
565 numbers = numbers.clone()
566 assert(numbers.getDimensions() == self.
dims)
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. 587 exposure : `lsst.afw.image.Exposure` 588 CCD exposure to measure 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)
597 toSample = transform.then(self.
transform)
599 warped = afwImage.ImageF(self.
_values.getBBox())
600 warpedCounts = afwImage.ImageF(self.
_numbers.getBBox())
601 width, height = warped.getDimensions()
603 stats = afwMath.StatisticsControl()
604 stats.setAndMask(maskVal)
605 stats.setNanSafe(
True)
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))
611 bbox = afwGeom.Box2I(afwGeom.Point2I(llc), afwGeom.Point2I(urc))
612 bbox.clip(image.getBBox())
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):
621 warped[xx, yy, afwImage.LOCAL] = mean*num
622 warpedCounts[xx, yy, afwImage.LOCAL] = num
628 """Produce a background model for a CCD 630 The superpixel background model is warped back to the 631 CCD frame, for application to the individual CCD. 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. 642 bg : `lsst.afw.math.BackgroundList` 643 Background model for CCD. 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)))
651 toSample = afwGeom.makeTransform(binTransform).then(transform).then(self.
transform)
654 fpNorm = afwImage.ImageF(focalPlane.getBBox())
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)
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()
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,
678 """Merge with another FocalPlaneBackground 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. 686 other : `FocalPlaneBackground` 687 Another background model to merge. 691 self : `FocalPlaneBackground` 692 The merged background model. 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))
704 """Merge with another FocalPlaneBackground 708 other : `FocalPlaneBackground` 709 Another background model to merge. 713 self : `FocalPlaneBackground` 714 The merged background model. 716 return self.
merge(other)
719 """Return the background model data 721 This is the measurement of the background for each of the superpixels. 726 isBad = self.
_numbers.getArray() < thresh
def fromCamera(cls, config, camera)
def robustMean(array, rej=3.0)
def interpolateBadPixels(array, isBad, interpolationStyle)
def addCcd(self, exposure)
def subtractSkyFrame(self, image, skyBackground, scale, bgList=None)
def toCcdBackground(self, detector, bbox)
def interpolate1D(method, xSample, ySample, xInterp)
def measureBackground(self, image)
def averageBackgrounds(self, bgList)
def getSkyData(self, butler, calibId)
def exposureToBackground(bgExp)
def solveScales(self, scales)
def __init__(self, config, dims, transform, values=None, numbers=None)
def measureScale(self, image, skyBackground)
def __iadd__(self, other)
def backgroundToExposure(self, statsImage, bbox)