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()
279 xLimits = numpy.linspace(0, image.getWidth() - 1, self.config.xNumSamples + 1, dtype=int)
280 yLimits = numpy.linspace(0, image.getHeight() - 1, self.config.yNumSamples + 1, dtype=int)
281 sky = skyBackground.getImage()
282 maskVal = image.getMask().getPlaneBitMask(self.config.stats.mask)
283 ctrl = afwMath.StatisticsControl(self.config.stats.clip, self.config.stats.nIter, maskVal)
284 statistic = afwMath.stringToStatisticsProperty(self.config.stats.statistic)
287 for xStart, yStart, xStop, yStop
in zip(xLimits[:-1], yLimits[:-1], xLimits[1:], yLimits[1:]):
288 box = afwGeom.Box2I(afwGeom.Point2I(xStart, yStart), afwGeom.Point2I(xStop, yStop))
289 subImage = image.Factory(image, box)
290 subSky = sky.Factory(sky, box)
291 imageSamples.append(afwMath.makeStatistics(subImage, statistic, ctrl).getValue())
292 skySamples.append(afwMath.makeStatistics(subSky, statistic, ctrl).getValue())
293 return imageSamples, skySamples
296 """Solve multiple scales for a single scale factor 298 Having measured samples from the image and sky frame, we 299 fit for the scaling factor. 303 scales : `list` of a `tuple` of two `numpy.ndarray` arrays 304 A `list` of the results from `measureScale` method. 313 for ii, ss
in scales:
314 imageSamples.extend(ii)
315 skySamples.extend(ss)
316 assert len(imageSamples) == len(skySamples)
317 imageSamples = numpy.array(imageSamples)
318 skySamples = numpy.array(skySamples)
321 return afwMath.LeastSquares.fromDesignMatrix(skySamples[mask].reshape(mask.sum(), 1),
323 afwMath.LeastSquares.DIRECT_SVD).getSolution()
325 mask = numpy.isfinite(imageSamples) & numpy.isfinite(skySamples)
326 for ii
in range(self.config.skyIter):
327 solution = solve(mask)
328 residuals = imageSamples - solution*skySamples
329 lq, uq = numpy.percentile(residuals[mask], [25, 75])
330 stdev = 0.741*(uq - lq)
331 bad = numpy.abs(residuals) > self.config.skyRej*stdev
337 """Subtract sky frame from science image 341 image : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage` 343 skyBackground : `lsst.afw.math.BackgroundList` 344 Sky background model. 346 Scale to apply to background model. 347 bgList : `lsst.afw.math.BackgroundList` 348 List of backgrounds applied to image 350 if isinstance(image, afwImage.Exposure):
351 image = image.getMaskedImage()
352 if isinstance(image, afwImage.MaskedImage):
353 image = image.getImage()
354 image.scaledMinus(scale, skyBackground.getImage())
355 if bgList
is not None:
357 bgData = list(skyBackground[0])
359 statsImage = bg.getStatsImage().clone()
361 newBg = afwMath.BackgroundMI(bg.getImageBBox(), statsImage)
362 newBgData = [newBg] + bgData[1:]
363 bgList.append(newBgData)
367 """Interpolate in one dimension 369 Interpolates the curve provided by `xSample` and `ySample` at 370 the positions of `xInterp`. Automatically backs off the 371 interpolation method to achieve successful interpolation. 375 method : `lsst.afw.math.Interpolate.Style` 376 Interpolation method to use. 377 xSample : `numpy.ndarray` 379 ySample : `numpy.ndarray` 380 Vector of coordinates. 381 xInterp : `numpy.ndarray` 382 Vector of ordinates to which to interpolate. 386 yInterp : `numpy.ndarray` 387 Vector of interpolated coordinates. 390 if len(xSample) == 0:
391 return numpy.ones_like(xInterp)*numpy.nan
393 return afwMath.makeInterpolate(xSample.astype(float), ySample.astype(float),
394 method).interpolate(xInterp.astype(float))
396 if method == afwMath.Interpolate.CONSTANT:
398 return numpy.ones_like(xInterp)*numpy.nan
399 newMethod = afwMath.lookupMaxInterpStyle(len(xSample))
400 if newMethod == method:
401 newMethod = afwMath.Interpolate.CONSTANT
406 """Interpolate bad pixels in an image array 408 The bad pixels are modified in the array. 412 array : `numpy.ndarray` 413 Image array with bad pixels. 414 isBad : `numpy.ndarray` of type `bool` 415 Boolean array indicating which pixels are bad. 416 interpolationStyle : `str` 417 Style for interpolation (see `lsst.afw.math.Background`); 418 supported values are CONSTANT, LINEAR, NATURAL_SPLINE, 422 raise RuntimeError(
"No good pixels in image array")
423 height, width = array.shape
424 xIndices = numpy.arange(width, dtype=float)
425 yIndices = numpy.arange(height, dtype=float)
426 method = afwMath.stringToInterpStyle(interpolationStyle)
428 for y
in range(height):
429 if numpy.any(isBad[y, :])
and numpy.any(isGood[y, :]):
430 array[y][isBad[y]] =
interpolate1D(method, xIndices[isGood[y]], array[y][isGood[y]],
433 isBad = numpy.isnan(array)
435 for x
in range(width):
436 if numpy.any(isBad[:, x])
and numpy.any(isGood[:, x]):
437 array[:, x][isBad[:, x]] =
interpolate1D(method, yIndices[isGood[:, x]],
438 array[:, x][isGood[:, x]], yIndices[isBad[:, x]])
442 """Configuration for FocalPlaneBackground 444 Note that `xSize` and `ySize` are floating-point values, as 445 the focal plane frame is usually defined in units of microns 446 or millimetres rather than pixels. As such, their values will 447 need to be revised according to each particular camera. For 448 this reason, no defaults are set for those. 450 xSize = Field(dtype=float, doc=
"Bin size in x")
451 ySize = Field(dtype=float, doc=
"Bin size in y")
452 minFrac = Field(dtype=float, default=0.1, doc=
"Minimum fraction of bin size for good measurement")
453 mask = ListField(dtype=str, doc=
"Mask planes to treat as bad",
454 default=[
"BAD",
"SAT",
"INTRP",
"DETECTED",
"DETECTED_NEGATIVE",
"EDGE",
"NO_DATA"])
455 interpolation = ChoiceField(
456 doc=
"how to interpolate the background values. This maps to an enum; see afw::math::Background",
457 dtype=str, default=
"AKIMA_SPLINE", optional=
True,
459 "CONSTANT":
"Use a single constant value",
460 "LINEAR":
"Use linear interpolation",
461 "NATURAL_SPLINE":
"cubic spline with zero second derivative at endpoints",
462 "AKIMA_SPLINE":
"higher-level nonlinear spline that is more robust to outliers",
463 "NONE":
"No background estimation is to be attempted",
466 binning = Field(dtype=int, default=64, doc=
"Binning to use for CCD background model (pixels)")
470 """Background model for a focal plane camera 472 We model the background empirically with the "superpixel" method: we 473 measure the background in each superpixel and interpolate between 474 superpixels to yield the model. 476 The principal difference between this and `lsst.afw.math.BackgroundMI` 477 is that here the superpixels are defined in the frame of the focal 478 plane of the camera which removes discontinuities across detectors. 480 The constructor you probably want to use is the `fromCamera` classmethod. 482 There are two use patterns for building a background model: 484 * Serial: create a `FocalPlaneBackground`, then `addCcd` for each of the 487 * Parallel: create a `FocalPlaneBackground`, then `clone` it for each 488 of the CCDs in an exposure and use those to `addCcd` their respective 489 CCD image. Finally, `merge` all the clones into the original. 491 Once you've built the background model, you can apply it to individual 492 CCDs with the `toCcdBackground` method. 496 """Construct from a camera object 500 config : `FocalPlaneBackgroundConfig` 501 Configuration for measuring backgrounds. 502 camera : `lsst.afw.cameraGeom.Camera` 503 Camera for which to measure backgrounds. 505 cameraBox = afwGeom.Box2D()
507 for point
in ccd.getCorners(afwCameraGeom.FOCAL_PLANE):
508 cameraBox.include(point)
510 width, height = cameraBox.getDimensions()
512 offset = afwGeom.Extent2D(cameraBox.getMin())*-1
514 dims = afwGeom.Extent2I(int(numpy.ceil(width/config.xSize)) + 2,
515 int(numpy.ceil(height/config.ySize)) + 2)
517 transform = (afwGeom.AffineTransform.makeTranslation(afwGeom.Extent2D(1, 1))*
518 afwGeom.AffineTransform.makeScaling(1.0/config.xSize, 1.0/config.ySize)*
519 afwGeom.AffineTransform.makeTranslation(offset))
521 return cls(config, dims, afwGeom.makeTransform(transform))
523 def __init__(self, config, dims, transform, values=None, numbers=None):
526 Developers should note that changes to the signature of this method 527 require coordinated changes to the `__reduce__` and `clone` methods. 531 config : `FocalPlaneBackgroundConfig` 532 Configuration for measuring backgrounds. 533 dims : `lsst.afw.geom.Extent2I` 534 Dimensions for background samples. 535 transform : `lsst.afw.geom.TransformPoint2ToPoint2` 536 Transformation from focal plane coordinates to sample coordinates. 537 values : `lsst.afw.image.ImageF` 538 Measured background values. 539 numbers : `lsst.afw.image.ImageF` 540 Number of pixels in each background measurement. 547 values = afwImage.ImageF(self.
dims)
550 values = values.clone()
551 assert(values.getDimensions() == self.
dims)
554 numbers = afwImage.ImageF(self.
dims)
557 numbers = numbers.clone()
558 assert(numbers.getDimensions() == self.
dims)
570 We measure the background on the CCD (clipped mean), and record 571 the results in the model. For simplicity, measurements are made 572 in a box on the CCD corresponding to the warped coordinates of the 573 superpixel rather than accounting for little rotations, etc. 574 We also record the number of pixels used in the measurement so we 575 can have a measure of confidence in each bin's value. 579 exposure : `lsst.afw.image.Exposure` 580 CCD exposure to measure 582 detector = exposure.getDetector()
583 transform = detector.getTransformMap().getTransform(detector.makeCameraSys(afwCameraGeom.PIXELS),
584 detector.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
585 image = exposure.getMaskedImage()
586 maskVal = image.getMask().getPlaneBitMask(self.
config.mask)
589 toSample = transform.then(self.
transform)
591 warped = afwImage.ImageF(self.
_values.getBBox())
592 warpedCounts = afwImage.ImageF(self.
_numbers.getBBox())
593 width, height = warped.getDimensions()
595 stats = afwMath.StatisticsControl()
596 stats.setAndMask(maskVal)
597 stats.setNanSafe(
True)
599 pixels = itertools.product(range(width), range(height))
600 for xx, yy
in pixels:
601 llc = toSample.applyInverse(afwGeom.Point2D(xx - 0.5, yy - 0.5))
602 urc = toSample.applyInverse(afwGeom.Point2D(xx + 0.5, yy + 0.5))
603 bbox = afwGeom.Box2I(afwGeom.Point2I(llc), afwGeom.Point2I(urc))
604 bbox.clip(image.getBBox())
607 subImage = image.Factory(image, bbox)
608 result = afwMath.makeStatistics(subImage, afwMath.MEANCLIP | afwMath.NPOINT, stats)
609 mean = result.getValue(afwMath.MEANCLIP)
610 num = result.getValue(afwMath.NPOINT)
611 if not numpy.isfinite(mean)
or not numpy.isfinite(num):
613 warped.set(xx, yy, mean*num)
614 warpedCounts.set(xx, yy, num)
620 """Produce a background model for a CCD 622 The superpixel background model is warped back to the 623 CCD frame, for application to the individual CCD. 627 detector : `lsst.afw.cameraGeom.Detector` 628 CCD for which to produce background model. 629 bbox : `lsst.afw.geom.Box2I` 630 Bounding box of CCD exposure. 634 bg : `lsst.afw.math.BackgroundList` 635 Background model for CCD. 637 transform = detector.getTransformMap().getTransform(detector.makeCameraSys(afwCameraGeom.PIXELS),
638 detector.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
639 binTransform = (afwGeom.AffineTransform.makeScaling(self.
config.binning)*
640 afwGeom.AffineTransform.makeTranslation(afwGeom.Extent2D(0.5, 0.5)))
643 toSample = afwGeom.makeTransform(binTransform).then(transform).then(self.
transform)
646 fpNorm = afwImage.ImageF(focalPlane.getBBox())
649 image = afwImage.ImageF(bbox.getDimensions()//self.
config.binning)
650 norm = afwImage.ImageF(image.getBBox())
651 ctrl = afwMath.WarpingControl(
"bilinear")
652 afwMath.warpImage(image, focalPlane, toSample.getInverse(), ctrl)
653 afwMath.warpImage(norm, fpNorm, toSample.getInverse(), ctrl)
656 mask = afwImage.Mask(image.getBBox())
657 isBad = numpy.isnan(image.getArray())
658 mask.getArray()[isBad] = mask.getPlaneBitMask(
"BAD")
659 image.getArray()[isBad] = image.getArray()[~isBad].mean()
661 return afwMath.BackgroundList(
662 (afwMath.BackgroundMI(bbox, afwImage.makeMaskedImage(image, mask)),
663 afwMath.stringToInterpStyle(self.
config.interpolation),
664 afwMath.stringToUndersampleStyle(
"REDUCE_INTERP_ORDER"),
665 afwMath.ApproximateControl.UNKNOWN,
670 """Merge with another FocalPlaneBackground 672 This allows multiple background models to be constructed from 673 different CCDs, and then merged to form a single consistent 674 background model for the entire focal plane. 678 other : `FocalPlaneBackground` 679 Another background model to merge. 683 self : `FocalPlaneBackground` 684 The merged background model. 686 if (self.
config.xSize, self.
config.ySize) != (other.config.xSize, other.config.ySize):
687 raise RuntimeError(
"Size mismatch: %s vs %s" % ((self.
config.xSize, self.
config.ySize),
688 (other.config.xSize, other.config.ySize)))
689 if self.
dims != other.dims:
690 raise RuntimeError(
"Dimensions mismatch: %s vs %s" % (self.
dims, other.dims))
696 """Merge with another FocalPlaneBackground 700 other : `FocalPlaneBackground` 701 Another background model to merge. 705 self : `FocalPlaneBackground` 706 The merged background model. 708 return self.
merge(other)
711 """Return the background model data 713 This is the measurement of the background for each of the superpixels. 718 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)