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")
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. 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` 188 stats.setAndMask(image.getMask().getPlaneBitMask(self.
config.background.mask))
189 stats.setNanSafe(
True)
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
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
239 stats.setAndMask(maskVal)
240 stats.setNanSafe(
True)
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. 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)
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
296 subImage = image.Factory(image, box)
297 subSky = sky.Factory(sky, box)
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 359 image = image.getMaskedImage()
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()
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
404 if method == afwMath.Interpolate.CONSTANT:
406 return numpy.ones_like(xInterp)*numpy.nan
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)
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"])
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. 515 for point
in ccd.getCorners(afwCameraGeom.FOCAL_PLANE):
516 cameraBox.include(point)
518 width, height = cameraBox.getDimensions()
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))
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()
604 stats.setAndMask(maskVal)
605 stats.setNanSafe(
True)
607 pixels = itertools.product(range(width), range(height))
608 for xx, yy
in pixels:
612 bbox.clip(image.getBBox())
615 subImage = image.Factory(image, bbox)
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)*
654 fpNorm = afwImage.ImageF(focalPlane.getBBox())
657 image = afwImage.ImageF(bbox.getDimensions()//self.
config.binning)
658 norm = afwImage.ImageF(image.getBBox())
665 isBad = numpy.isnan(image.getArray())
666 mask.getArray()[isBad] = mask.getPlaneBitMask(
"BAD")
667 image.getArray()[isBad] = image.getArray()[~isBad].mean()
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
UndersampleStyle stringToUndersampleStyle(std::string const &style)
Conversion function to switch a string to an UndersampleStyle.
Interpolate::Style stringToInterpStyle(std::string const &style)
Conversion function to switch a string to an Interpolate::Style.
def fromCamera(cls, config, camera)
A floating-point coordinate rectangle geometry.
def robustMean(array, rej=3.0)
int warpImage(DestImageT &destImage, SrcImageT const &srcImage, geom::TransformPoint2ToPoint2 const &srcToDest, WarpingControl const &control, typename DestImageT::SinglePixel padValue=lsst::afw::math::edgePixel< DestImageT >(typename lsst::afw::image::detail::image_traits< DestImageT >::image_category()))
A variant of warpImage that uses a Transform<Point2Endpoint, Point2Endpoint> instead of a pair of WCS...
A class to contain the data, WCS, and other information needed to describe an image of the sky...
def interpolateBadPixels(array, isBad, interpolationStyle)
def addCcd(self, exposure)
def subtractSkyFrame(self, image, skyBackground, scale, bgList=None)
std::shared_ptr< Background > makeBackground(ImageT const &img, BackgroundControl const &bgCtrl)
A convenience function that uses function overloading to make the correct type of Background...
Parameters to control convolution.
daf::base::PropertySet * set
def toCcdBackground(self, detector, bbox)
Pass parameters to a Background object.
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT >> image, typename std::shared_ptr< Mask< MaskPixelT >> mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT >> variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
bool all(CoordinateExpr< N > const &expr) noexcept
Return true if all elements are true.
Pass parameters to a Statistics object.
def interpolate1D(method, xSample, ySample, xInterp)
def measureBackground(self, image)
Represent a 2-dimensional array of bitmask pixels.
def averageBackgrounds(self, bgList)
def getSkyData(self, butler, calibId)
std::shared_ptr< Interpolate > makeInterpolate(ndarray::Array< double const, 1 > const &x, ndarray::Array< double const, 1 > const &y, Interpolate::Style const style=Interpolate::AKIMA_SPLINE)
A class to manipulate images, masks, and variance as a single object.
Interpolate::Style lookupMaxInterpStyle(int const n)
Get the highest order Interpolation::Style available for 'n' points.
def exposureToBackground(bgExp)
std::shared_ptr< image::MaskedImage< PixelT > > statisticsStack(image::MaskedImage< PixelT > const &image, Property flags, char dimension, StatisticsControl const &sctrl)
A function to compute statistics on the rows or columns of an image.
def solveScales(self, scales)
def __init__(self, config, dims, transform, values=None, numbers=None)
A class to evaluate image background levels.
def measureScale(self, image, skyBackground)
def __iadd__(self, other)
An integer coordinate rectangle.
daf::base::PropertyList * list
std::shared_ptr< TransformPoint2ToPoint2 > makeTransform(lsst::geom::AffineTransform const &affine)
Wrap an lsst::geom::AffineTransform as a Transform.
def backgroundToExposure(self, statsImage, bbox)