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 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")
80 """Task for creating, persisting and using sky frames 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. 88 ConfigClass = SkyMeasurementConfig
91 """Retrieve sky frame from the butler 95 butler : `lsst.daf.persistence.Butler` 98 Data identifier for calib 102 sky : `lsst.afw.math.BackgroundList` 105 exp = butler.get(
"sky", calibId)
110 """Convert an exposure to background model 112 Calibs need to be persisted as an Exposure, so we need to convert 113 the persisted Exposure to a background model. 117 bgExp : `lsst.afw.image.Exposure` 118 Background model in Exposure format. 122 bg : `lsst.afw.math.BackgroundList` 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,
140 """Convert a background model to an exposure 142 Calibs need to be persisted as an Exposure, so we need to convert 143 the background model to an Exposure. 147 statsImage : `lsst.afw.image.MaskedImageF` 148 Background model's statistics image. 149 bbox : `lsst.afw.geom.Box2I` 150 Bounding box for image. 154 exp : `lsst.afw.image.Exposure` 155 Background model in Exposure format. 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)
167 """Measure a background model for image 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`). 173 The `BackgroundMI` is wrapped in a `BackgroundList` so it can be 174 pickled and persisted. 178 image : `lsst.afw.image.MaskedImage` 179 Image for which to measure background. 183 bgModel : `lsst.afw.math.BackgroundList` 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",
195 self.config.background.statistic
198 bg = afwMath.makeBackground(image, ctrl)
200 return afwMath.BackgroundList((
202 afwMath.stringToInterpStyle(self.config.background.algorithm),
203 afwMath.stringToUndersampleStyle(
"REDUCE_INTERP_ORDER"),
204 afwMath.ApproximateControl.UNKNOWN,
209 """Average multiple background models 211 The input background models should be a `BackgroundList` consisting 212 of a single `BackgroundMI`. 216 bgList : `list` of `lsst.afw.math.BackgroundList` 217 Background models to average. 221 bgExp : `lsst.afw.image.Exposure` 222 Background model in Exposure format. 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" 232 maskVal = afwImage.Mask.getPlaneBitMask(
"BAD")
234 bad = numpy.isnan(img.getImage().getArray())
235 img.getMask().getArray()[bad] = maskVal
237 stats = afwMath.StatisticsControl()
238 stats.setAndMask(maskVal)
239 stats.setNanSafe(
True)
240 combined = afwMath.statisticsStack(images, afwMath.MEANCLIP, stats)
246 array = combined.getImage().getArray()
247 bad = numpy.isnan(array)
248 median = numpy.median(array[~bad])
255 """Measure scale of background model in image 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. 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. 272 imageSamples : `numpy.ndarray` 273 Sample measurements on image. 274 skySamples : `numpy.ndarray` 275 Sample measurements on sky frame. 277 if isinstance(image, afwImage.Exposure):
278 image = image.getMaskedImage()
280 xNumSamples = min(self.config.xNumSamples, image.getWidth())
281 yNumSamples = min(self.config.yNumSamples, image.getHeight())
282 xLimits = numpy.linspace(0, image.getWidth(), xNumSamples + 1, dtype=int)
283 yLimits = numpy.linspace(0, image.getHeight(), yNumSamples + 1, dtype=int)
284 sky = skyBackground.getImage()
285 maskVal = image.getMask().getPlaneBitMask(self.config.stats.mask)
286 ctrl = afwMath.StatisticsControl(self.config.stats.clip, self.config.stats.nIter, maskVal)
287 statistic = afwMath.stringToStatisticsProperty(self.config.stats.statistic)
290 for xIndex, yIndex
in itertools.product(range(xNumSamples), range(yNumSamples)):
292 xStart, xStop = xLimits[xIndex], xLimits[xIndex + 1] - 1
293 yStart, yStop = yLimits[yIndex], yLimits[yIndex + 1] - 1
294 box = afwGeom.Box2I(afwGeom.Point2I(xStart, yStart), afwGeom.Point2I(xStop, yStop))
295 subImage = image.Factory(image, box)
296 subSky = sky.Factory(sky, box)
297 imageSamples.append(afwMath.makeStatistics(subImage, statistic, ctrl).getValue())
298 skySamples.append(afwMath.makeStatistics(subSky, statistic, ctrl).getValue())
299 return imageSamples, skySamples
302 """Solve multiple scales for a single scale factor 304 Having measured samples from the image and sky frame, we 305 fit for the scaling factor. 309 scales : `list` of a `tuple` of two `numpy.ndarray` arrays 310 A `list` of the results from `measureScale` method. 319 for ii, ss
in scales:
320 imageSamples.extend(ii)
321 skySamples.extend(ss)
322 assert len(imageSamples) == len(skySamples)
323 imageSamples = numpy.array(imageSamples)
324 skySamples = numpy.array(skySamples)
327 return afwMath.LeastSquares.fromDesignMatrix(skySamples[mask].reshape(mask.sum(), 1),
329 afwMath.LeastSquares.DIRECT_SVD).getSolution()
331 mask = numpy.isfinite(imageSamples) & numpy.isfinite(skySamples)
332 for ii
in range(self.config.skyIter):
333 solution = solve(mask)
334 residuals = imageSamples - solution*skySamples
335 lq, uq = numpy.percentile(residuals[mask], [25, 75])
336 stdev = 0.741*(uq - lq)
337 with numpy.errstate(invalid=
"ignore"):
338 bad = numpy.abs(residuals) > self.config.skyRej*stdev
344 """Subtract sky frame from science image 348 image : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage` 350 skyBackground : `lsst.afw.math.BackgroundList` 351 Sky background model. 353 Scale to apply to background model. 354 bgList : `lsst.afw.math.BackgroundList` 355 List of backgrounds applied to image 357 if isinstance(image, afwImage.Exposure):
358 image = image.getMaskedImage()
359 if isinstance(image, afwImage.MaskedImage):
360 image = image.getImage()
361 image.scaledMinus(scale, skyBackground.getImage())
362 if bgList
is not None:
364 bgData = list(skyBackground[0])
366 statsImage = bg.getStatsImage().clone()
368 newBg = afwMath.BackgroundMI(bg.getImageBBox(), statsImage)
369 newBgData = [newBg] + bgData[1:]
370 bgList.append(newBgData)
374 """Interpolate in one dimension 376 Interpolates the curve provided by `xSample` and `ySample` at 377 the positions of `xInterp`. Automatically backs off the 378 interpolation method to achieve successful interpolation. 382 method : `lsst.afw.math.Interpolate.Style` 383 Interpolation method to use. 384 xSample : `numpy.ndarray` 386 ySample : `numpy.ndarray` 387 Vector of coordinates. 388 xInterp : `numpy.ndarray` 389 Vector of ordinates to which to interpolate. 393 yInterp : `numpy.ndarray` 394 Vector of interpolated coordinates. 397 if len(xSample) == 0:
398 return numpy.ones_like(xInterp)*numpy.nan
400 return afwMath.makeInterpolate(xSample.astype(float), ySample.astype(float),
401 method).interpolate(xInterp.astype(float))
403 if method == afwMath.Interpolate.CONSTANT:
405 return numpy.ones_like(xInterp)*numpy.nan
406 newMethod = afwMath.lookupMaxInterpStyle(len(xSample))
407 if newMethod == method:
408 newMethod = afwMath.Interpolate.CONSTANT
413 """Interpolate bad pixels in an image array 415 The bad pixels are modified in the array. 419 array : `numpy.ndarray` 420 Image array with bad pixels. 421 isBad : `numpy.ndarray` of type `bool` 422 Boolean array indicating which pixels are bad. 423 interpolationStyle : `str` 424 Style for interpolation (see `lsst.afw.math.Background`); 425 supported values are CONSTANT, LINEAR, NATURAL_SPLINE, 429 raise RuntimeError(
"No good pixels in image array")
430 height, width = array.shape
431 xIndices = numpy.arange(width, dtype=float)
432 yIndices = numpy.arange(height, dtype=float)
433 method = afwMath.stringToInterpStyle(interpolationStyle)
435 for y
in range(height):
436 if numpy.any(isBad[y, :])
and numpy.any(isGood[y, :]):
437 array[y][isBad[y]] =
interpolate1D(method, xIndices[isGood[y]], array[y][isGood[y]],
440 isBad = numpy.isnan(array)
442 for x
in range(width):
443 if numpy.any(isBad[:, x])
and numpy.any(isGood[:, x]):
444 array[:, x][isBad[:, x]] =
interpolate1D(method, yIndices[isGood[:, x]],
445 array[:, x][isGood[:, x]], yIndices[isBad[:, x]])
449 """Configuration for FocalPlaneBackground 451 Note that `xSize` and `ySize` are floating-point values, as 452 the focal plane frame is usually defined in units of microns 453 or millimetres rather than pixels. As such, their values will 454 need to be revised according to each particular camera. For 455 this reason, no defaults are set for those. 457 xSize = Field(dtype=float, doc=
"Bin size in x")
458 ySize = Field(dtype=float, doc=
"Bin size in y")
459 minFrac = Field(dtype=float, default=0.1, doc=
"Minimum fraction of bin size for good measurement")
460 mask = ListField(dtype=str, doc=
"Mask planes to treat as bad",
461 default=[
"BAD",
"SAT",
"INTRP",
"DETECTED",
"DETECTED_NEGATIVE",
"EDGE",
"NO_DATA"])
462 interpolation = ChoiceField(
463 doc=
"how to interpolate the background values. This maps to an enum; see afw::math::Background",
464 dtype=str, default=
"AKIMA_SPLINE", optional=
True,
466 "CONSTANT":
"Use a single constant value",
467 "LINEAR":
"Use linear interpolation",
468 "NATURAL_SPLINE":
"cubic spline with zero second derivative at endpoints",
469 "AKIMA_SPLINE":
"higher-level nonlinear spline that is more robust to outliers",
470 "NONE":
"No background estimation is to be attempted",
473 binning = Field(dtype=int, default=64, doc=
"Binning to use for CCD background model (pixels)")
477 """Background model for a focal plane camera 479 We model the background empirically with the "superpixel" method: we 480 measure the background in each superpixel and interpolate between 481 superpixels to yield the model. 483 The principal difference between this and `lsst.afw.math.BackgroundMI` 484 is that here the superpixels are defined in the frame of the focal 485 plane of the camera which removes discontinuities across detectors. 487 The constructor you probably want to use is the `fromCamera` classmethod. 489 There are two use patterns for building a background model: 491 * Serial: create a `FocalPlaneBackground`, then `addCcd` for each of the 494 * Parallel: create a `FocalPlaneBackground`, then `clone` it for each 495 of the CCDs in an exposure and use those to `addCcd` their respective 496 CCD image. Finally, `merge` all the clones into the original. 498 Once you've built the background model, you can apply it to individual 499 CCDs with the `toCcdBackground` method. 503 """Construct from a camera object 507 config : `FocalPlaneBackgroundConfig` 508 Configuration for measuring backgrounds. 509 camera : `lsst.afw.cameraGeom.Camera` 510 Camera for which to measure backgrounds. 512 cameraBox = afwGeom.Box2D()
514 for point
in ccd.getCorners(afwCameraGeom.FOCAL_PLANE):
515 cameraBox.include(point)
517 width, height = cameraBox.getDimensions()
519 offset = afwGeom.Extent2D(cameraBox.getMin())*-1
521 dims = afwGeom.Extent2I(int(numpy.ceil(width/config.xSize)) + 2,
522 int(numpy.ceil(height/config.ySize)) + 2)
524 transform = (afwGeom.AffineTransform.makeTranslation(afwGeom.Extent2D(1, 1))*
525 afwGeom.AffineTransform.makeScaling(1.0/config.xSize, 1.0/config.ySize)*
526 afwGeom.AffineTransform.makeTranslation(offset))
528 return cls(config, dims, afwGeom.makeTransform(transform))
530 def __init__(self, config, dims, transform, values=None, numbers=None):
533 Developers should note that changes to the signature of this method 534 require coordinated changes to the `__reduce__` and `clone` methods. 538 config : `FocalPlaneBackgroundConfig` 539 Configuration for measuring backgrounds. 540 dims : `lsst.afw.geom.Extent2I` 541 Dimensions for background samples. 542 transform : `lsst.afw.geom.TransformPoint2ToPoint2` 543 Transformation from focal plane coordinates to sample coordinates. 544 values : `lsst.afw.image.ImageF` 545 Measured background values. 546 numbers : `lsst.afw.image.ImageF` 547 Number of pixels in each background measurement. 554 values = afwImage.ImageF(self.
dims)
557 values = values.clone()
558 assert(values.getDimensions() == self.
dims)
561 numbers = afwImage.ImageF(self.
dims)
564 numbers = numbers.clone()
565 assert(numbers.getDimensions() == self.
dims)
577 We measure the background on the CCD (clipped mean), and record 578 the results in the model. For simplicity, measurements are made 579 in a box on the CCD corresponding to the warped coordinates of the 580 superpixel rather than accounting for little rotations, etc. 581 We also record the number of pixels used in the measurement so we 582 can have a measure of confidence in each bin's value. 586 exposure : `lsst.afw.image.Exposure` 587 CCD exposure to measure 589 detector = exposure.getDetector()
590 transform = detector.getTransformMap().getTransform(detector.makeCameraSys(afwCameraGeom.PIXELS),
591 detector.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
592 image = exposure.getMaskedImage()
593 maskVal = image.getMask().getPlaneBitMask(self.
config.mask)
596 toSample = transform.then(self.
transform)
598 warped = afwImage.ImageF(self.
_values.getBBox())
599 warpedCounts = afwImage.ImageF(self.
_numbers.getBBox())
600 width, height = warped.getDimensions()
602 stats = afwMath.StatisticsControl()
603 stats.setAndMask(maskVal)
604 stats.setNanSafe(
True)
606 pixels = itertools.product(range(width), range(height))
607 for xx, yy
in pixels:
608 llc = toSample.applyInverse(afwGeom.Point2D(xx - 0.5, yy - 0.5))
609 urc = toSample.applyInverse(afwGeom.Point2D(xx + 0.5, yy + 0.5))
610 bbox = afwGeom.Box2I(afwGeom.Point2I(llc), afwGeom.Point2I(urc))
611 bbox.clip(image.getBBox())
614 subImage = image.Factory(image, bbox)
615 result = afwMath.makeStatistics(subImage, afwMath.MEANCLIP | afwMath.NPOINT, stats)
616 mean = result.getValue(afwMath.MEANCLIP)
617 num = result.getValue(afwMath.NPOINT)
618 if not numpy.isfinite(mean)
or not numpy.isfinite(num):
620 warped[xx, yy, afwImage.LOCAL] = mean*num
621 warpedCounts[xx, yy,afwImage.LOCAL] = num
627 """Produce a background model for a CCD 629 The superpixel background model is warped back to the 630 CCD frame, for application to the individual CCD. 634 detector : `lsst.afw.cameraGeom.Detector` 635 CCD for which to produce background model. 636 bbox : `lsst.afw.geom.Box2I` 637 Bounding box of CCD exposure. 641 bg : `lsst.afw.math.BackgroundList` 642 Background model for CCD. 644 transform = detector.getTransformMap().getTransform(detector.makeCameraSys(afwCameraGeom.PIXELS),
645 detector.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
646 binTransform = (afwGeom.AffineTransform.makeScaling(self.
config.binning)*
647 afwGeom.AffineTransform.makeTranslation(afwGeom.Extent2D(0.5, 0.5)))
650 toSample = afwGeom.makeTransform(binTransform).then(transform).then(self.
transform)
653 fpNorm = afwImage.ImageF(focalPlane.getBBox())
656 image = afwImage.ImageF(bbox.getDimensions()//self.
config.binning)
657 norm = afwImage.ImageF(image.getBBox())
658 ctrl = afwMath.WarpingControl(
"bilinear")
659 afwMath.warpImage(image, focalPlane, toSample.inverted(), ctrl)
660 afwMath.warpImage(norm, fpNorm, toSample.inverted(), ctrl)
663 mask = afwImage.Mask(image.getBBox())
664 isBad = numpy.isnan(image.getArray())
665 mask.getArray()[isBad] = mask.getPlaneBitMask(
"BAD")
666 image.getArray()[isBad] = image.getArray()[~isBad].mean()
668 return afwMath.BackgroundList(
669 (afwMath.BackgroundMI(bbox, afwImage.makeMaskedImage(image, mask)),
670 afwMath.stringToInterpStyle(self.
config.interpolation),
671 afwMath.stringToUndersampleStyle(
"REDUCE_INTERP_ORDER"),
672 afwMath.ApproximateControl.UNKNOWN,
677 """Merge with another FocalPlaneBackground 679 This allows multiple background models to be constructed from 680 different CCDs, and then merged to form a single consistent 681 background model for the entire focal plane. 685 other : `FocalPlaneBackground` 686 Another background model to merge. 690 self : `FocalPlaneBackground` 691 The merged background model. 693 if (self.
config.xSize, self.
config.ySize) != (other.config.xSize, other.config.ySize):
694 raise RuntimeError(
"Size mismatch: %s vs %s" % ((self.
config.xSize, self.
config.ySize),
695 (other.config.xSize, other.config.ySize)))
696 if self.
dims != other.dims:
697 raise RuntimeError(
"Dimensions mismatch: %s vs %s" % (self.
dims, other.dims))
703 """Merge with another FocalPlaneBackground 707 other : `FocalPlaneBackground` 708 Another background model to merge. 712 self : `FocalPlaneBackground` 713 The merged background model. 715 return self.
merge(other)
718 """Return the background model data 720 This is the measurement of the background for each of the superpixels. 725 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)