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 to outliers",
51 "NONE":
"No background estimation is to be attempted",
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")
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",
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",])
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")
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()
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)
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
297 """Solve multiple scales for a single scale factor 299 Having measured samples from the image and sky frame, we 300 fit for the scaling factor. 304 scales : `list` of a `tuple` of two `numpy.ndarray` arrays 305 A `list` of the results from `measureScale` method. 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)
322 return afwMath.LeastSquares.fromDesignMatrix(skySamples[mask].reshape(mask.sum(), 1),
324 afwMath.LeastSquares.DIRECT_SVD).getSolution()
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)
332 bad = numpy.abs(residuals) > self.config.skyRej*stdev
338 """Subtract sky frame from science image 342 image : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage` 344 skyBackground : `lsst.afw.math.BackgroundList` 345 Sky background model. 347 Scale to apply to background model. 348 bgList : `lsst.afw.math.BackgroundList` 349 List of backgrounds applied to image 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:
358 bgData = list(skyBackground[0])
360 statsImage = bg.getStatsImage().clone()
362 newBg = afwMath.BackgroundMI(bg.getImageBBox(), statsImage)
363 newBgData = [newBg] + bgData[1:]
364 bgList.append(newBgData)
368 """Interpolate in one dimension 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. 376 method : `lsst.afw.math.Interpolate.Style` 377 Interpolation method to use. 378 xSample : `numpy.ndarray` 380 ySample : `numpy.ndarray` 381 Vector of coordinates. 382 xInterp : `numpy.ndarray` 383 Vector of ordinates to which to interpolate. 387 yInterp : `numpy.ndarray` 388 Vector of interpolated coordinates. 391 if len(xSample) == 0:
392 return numpy.ones_like(xInterp)*numpy.nan
394 return afwMath.makeInterpolate(xSample.astype(float), ySample.astype(float),
395 method).interpolate(xInterp.astype(float))
397 if method == afwMath.Interpolate.CONSTANT:
399 return numpy.ones_like(xInterp)*numpy.nan
400 newMethod = afwMath.lookupMaxInterpStyle(len(xSample))
401 if newMethod == method:
402 newMethod = afwMath.Interpolate.CONSTANT
407 """Interpolate bad pixels in an image array 409 The bad pixels are modified in the array. 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, 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)
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]],
434 isBad = numpy.isnan(array)
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]])
443 """Configuration for FocalPlaneBackground 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. 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,
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",
467 binning = Field(dtype=int, default=64, doc=
"Binning to use for CCD background model (pixels)")
471 """Background model for a focal plane camera 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. 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. 481 The constructor you probably want to use is the `fromCamera` classmethod. 483 There are two use patterns for building a background model: 485 * Serial: create a `FocalPlaneBackground`, then `addCcd` for each of the 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. 492 Once you've built the background model, you can apply it to individual 493 CCDs with the `toCcdBackground` method. 497 """Construct from a camera object 501 config : `FocalPlaneBackgroundConfig` 502 Configuration for measuring backgrounds. 503 camera : `lsst.afw.cameraGeom.Camera` 504 Camera for which to measure backgrounds. 506 cameraBox = afwGeom.Box2D()
508 for point
in ccd.getCorners(afwCameraGeom.FOCAL_PLANE):
509 cameraBox.include(point)
511 width, height = cameraBox.getDimensions()
513 offset = afwGeom.Extent2D(cameraBox.getMin())*-1
515 dims = afwGeom.Extent2I(int(numpy.ceil(width/config.xSize)) + 2,
516 int(numpy.ceil(height/config.ySize)) + 2)
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))
522 return cls(config, dims, afwGeom.makeTransform(transform))
524 def __init__(self, config, dims, transform, values=None, numbers=None):
527 Developers should note that changes to the signature of this method 528 require coordinated changes to the `__reduce__` and `clone` methods. 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. 548 values = afwImage.ImageF(self.
dims)
551 values = values.clone()
552 assert(values.getDimensions() == self.
dims)
555 numbers = afwImage.ImageF(self.
dims)
558 numbers = numbers.clone()
559 assert(numbers.getDimensions() == self.
dims)
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. 580 exposure : `lsst.afw.image.Exposure` 581 CCD exposure to measure 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)
590 toSample = transform.then(self.
transform)
592 warped = afwImage.ImageF(self.
_values.getBBox())
593 warpedCounts = afwImage.ImageF(self.
_numbers.getBBox())
594 width, height = warped.getDimensions()
596 stats = afwMath.StatisticsControl()
597 stats.setAndMask(maskVal)
598 stats.setNanSafe(
True)
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())
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):
614 warped.set(xx, yy, mean*num)
615 warpedCounts.set(xx, yy, num)
621 """Produce a background model for a CCD 623 The superpixel background model is warped back to the 624 CCD frame, for application to the individual CCD. 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. 635 bg : `lsst.afw.math.BackgroundList` 636 Background model for CCD. 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)))
644 toSample = afwGeom.makeTransform(binTransform).then(transform).then(self.
transform)
647 fpNorm = afwImage.ImageF(focalPlane.getBBox())
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)
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()
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,
671 """Merge with another FocalPlaneBackground 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. 679 other : `FocalPlaneBackground` 680 Another background model to merge. 684 self : `FocalPlaneBackground` 685 The merged background model. 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))
697 """Merge with another FocalPlaneBackground 701 other : `FocalPlaneBackground` 702 Another background model to merge. 706 self : `FocalPlaneBackground` 707 The merged background model. 709 return self.
merge(other)
712 """Return the background model data 714 This is the measurement of the background for each of the superpixels. 719 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)