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 with numpy.errstate(invalid=
"ignore"):
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[xx, yy, afwImage.LOCAL] = mean*num
615 warpedCounts[xx, yy,afwImage.LOCAL] = 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.getInverse(), ctrl)
654 afwMath.warpImage(norm, fpNorm, toSample.getInverse(), 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)