232 calExpOrder = {ref.dataId[
"detector"]
for ref
in inputRefs.calExps}
233 calBkgOrder = {ref.dataId[
"detector"]
for ref
in inputRefs.calBkgs}
234 detectorOrder = calExpOrder & calBkgOrder
235 if self.config.doApplyFlatBackgroundRatio:
236 ratioOrder = {ref.dataId[
"detector"]
for ref
in inputRefs.backgroundToPhotometricRatioHandles}
237 detectorOrder &= ratioOrder
238 if self.config.doSky:
239 skyFrameOrder = {ref.dataId[
"detector"]
for ref
in inputRefs.skyFrames}
240 detectorOrder &= skyFrameOrder
241 detectorOrder = sorted(detectorOrder)
243 inputRefs.calExps, [ref.dataId[
"detector"]
for ref
in inputRefs.calExps], detectorOrder
246 inputRefs.calBkgs, [ref.dataId[
"detector"]
for ref
in inputRefs.calBkgs], detectorOrder
249 if self.config.doSky:
251 inputRefs.skyFrames, [ref.dataId[
"detector"]
for ref
in inputRefs.skyFrames], detectorOrder
254 inputRefs.skyFrames = []
256 if self.config.doApplyFlatBackgroundRatio:
258 inputRefs.backgroundToPhotometricRatioHandles,
259 [ref.dataId[
"detector"]
for ref
in inputRefs.backgroundToPhotometricRatioHandles],
263 inputRefs.backgroundToPhotometricRatioHandles = []
265 outputRefs.skyCorr, [ref.dataId[
"detector"]
for ref
in outputRefs.skyCorr], detectorOrder
267 inputs = butlerQC.get(inputRefs)
269 outputs = self.
run(**inputs)
270 except AlgorithmError
as e:
271 error = AnnotatedPartialOutputsError.annotate(
277 butlerQC.put(outputs, outputRefs)
279 def run(self, calExps, calBkgs, skyFrames, camera, backgroundToPhotometricRatioHandles=[]):
280 """Perform sky correction on a visit.
282 The original visit-level background is first restored to the calibrated
283 exposure and the existing background model is inverted in-place. If
284 doMaskObjects is True, the mask map associated with this exposure will
285 be iteratively updated (over nIter loops) by re-estimating the
286 background each iteration and redetecting footprints.
288 An initial full focal plane sky subtraction (bgModel1) will take place
289 prior to scaling and subtracting the sky frame.
291 If doSky is True, the sky frame will be scaled to the flux in the input
294 If doBgModel2 is True, a final full focal plane sky subtraction will
295 take place after the sky frame has been subtracted.
297 The first N elements of the returned skyCorr will consist of inverted
298 elements of the calexpBackground model (i.e., subtractive). All
299 subsequent elements appended to skyCorr thereafter will be additive
300 such that, when skyCorr is subtracted from a calexp, the net result
301 will be to undo the initial per-detector background solution and then
302 apply the skyCorr model thereafter. Adding skyCorr to a
303 calexpBackground will effectively negate the calexpBackground,
304 returning only the additive background components of the skyCorr
309 calExps : `list` [`lsst.afw.image.ExposureF`]
310 Detector calibrated exposure images for the visit.
311 calBkgs : `list` [`lsst.afw.math.BackgroundList`]
312 Detector background lists matching the calibrated exposures.
313 skyFrames : `list` [`lsst.afw.image.ExposureF`]
314 Sky frame calibration data for the input detectors.
315 camera : `lsst.afw.cameraGeom.Camera`
316 Camera matching the input data to process.
317 backgroundToPhotometricRatioHandles :
318 `list` [`lsst.daf.butler.DeferredDatasetHandle`], optional
319 Deferred dataset handles pointing to the Background to photometric
320 ratio images for the input detectors.
324 results : `Struct` containing:
325 skyFrameScale : `float`
326 Scale factor applied to the sky frame.
327 skyCorr : `list` [`lsst.afw.math.BackgroundList`]
328 Detector-level sky correction background lists.
329 calExpMosaic : `lsst.afw.image.ExposureF`
330 Visit-level mosaic of the sky corrected data, binned.
331 Analogous to `calexp - skyCorr`.
332 calBkgMosaic : `lsst.afw.image.ExposureF`
333 Visit-level mosaic of the sky correction background, binned.
334 Analogous to `calexpBackground + skyCorr`.
338 bgModel1 = FocalPlaneBackground.fromCamera(self.config.bgModel1, camera)
343 if not self.config.doApplyFlatBackgroundRatio:
344 backgroundToPhotometricRatioHandles = [
None] * len(calExps)
345 for calExpHandle, calBkg, backgroundToPhotometricRatioHandle
in zip(
346 calExps, calBkgs, backgroundToPhotometricRatioHandles
349 calExpHandle, backgroundToPhotometricRatioHandle=backgroundToPhotometricRatioHandle
351 detectors.append(calExp.getDetector())
355 masks.append(calExp.mask)
356 skyCorrs.append(calBkg)
357 bgModel1Indices.append(len(calBkg))
360 bgModel1Detector = FocalPlaneBackground.fromCamera(self.config.bgModel1, camera)
361 bgModel1Detector.addCcd(calExp)
362 bgModel1.merge(bgModel1Detector)
364 "Detector %d: Merged %d unmasked pixels (%.1f%s of detector area) into initial BG model",
365 calExp.getDetector().getId(),
366 bgModel1Detector._numbers.getArray().sum(),
367 100 * bgModel1Detector._numbers.getArray().sum() / calExp.getBBox().getArea(),
375 for detector, skyCorr
in zip(detectors, skyCorrs):
376 with warnings.catch_warnings():
377 warnings.filterwarnings(
"ignore",
"invalid value encountered")
378 calBkgElement = bgModel1.toCcdBackground(detector, detector.getBBox())
379 skyCorr.append(calBkgElement[0])
383 if self.config.doSky:
385 calExps, masks, skyCorrs, skyFrames, backgroundToPhotometricRatioHandles
389 if self.config.undoBgModel1:
390 for skyCorr, bgModel1Index
in zip(skyCorrs, bgModel1Indices):
391 skyCorr._backgrounds.pop(bgModel1Index)
393 "Initial background models (bgModel1s) have been removed from all skyCorr background lists",
397 if self.config.doBgModel2:
398 bgModel2 = FocalPlaneBackground.fromCamera(self.config.bgModel2, camera)
399 for calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle
in zip(
400 calExps, masks, skyCorrs, backgroundToPhotometricRatioHandles
402 calExp = self.
_getCalExp(calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle)
405 bgModel2Detector = FocalPlaneBackground.fromCamera(self.config.bgModel2, camera)
406 bgModel2Detector.addCcd(calExp)
407 bgModel2.merge(bgModel2Detector)
409 "Detector %d: Merged %d unmasked pixels (%.1f%s of detector area) into final BG model",
410 calExp.getDetector().getId(),
411 bgModel2Detector._numbers.getArray().sum(),
412 100 * bgModel2Detector._numbers.getArray().sum() / calExp.getBBox().getArea(),
420 for detector, skyCorr
in zip(detectors, skyCorrs):
421 with warnings.catch_warnings():
422 warnings.filterwarnings(
"ignore",
"invalid value encountered")
423 calBkgElement = bgModel2.toCcdBackground(detector, detector.getBBox())
424 skyCorr.append(calBkgElement[0])
429 for calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle, bgModel1Index
in zip(
430 calExps, masks, skyCorrs, backgroundToPhotometricRatioHandles, bgModel1Indices
432 calExp = self.
_getCalExp(calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle)
434 skyCorrExtra = skyCorr.clone()
435 skyCorrExtra._backgrounds = skyCorrExtra._backgrounds[bgModel1Index:]
436 skyCorrExtraMI = makeMaskedImage(skyCorrExtra.getImage())
437 skyCorrExtraMI.setMask(calExp.getMask())
439 calExpsBinned.append(binImage(calExp.getMaskedImage(), self.config.binning))
440 calBkgsBinned.append(binImage(skyCorrExtraMI, self.config.binning))
443 mosConfig.binning = self.config.binning
445 detectorIds = [detector.getId()
for detector
in detectors]
446 calExpMosaic = mosTask.run(calExpsBinned, camera, inputIds=detectorIds).outputData
447 calBkgMosaic = mosTask.run(calBkgsBinned, camera, inputIds=detectorIds).outputData
450 skyFrameScale=skyFrameScale,
452 calExpMosaic=calExpMosaic,
453 calBkgMosaic=calBkgMosaic,
456 def _getCalExp(self, calExpHandle, mask=None, skyCorr=None, backgroundToPhotometricRatioHandle=None):
457 """Get a calexp from a DeferredDatasetHandle, and optionally apply an
458 updated mask and skyCorr.
462 calExpHandle : `~lsst.afw.image.ExposureF`
463 | `lsst.daf.butler.DeferredDatasetHandle`
464 Either the image exposure data or a handle to the calexp dataset.
465 mask : `lsst.afw.image.Mask`, optional
466 Mask to apply to the calexp.
467 skyCorr : `lsst.afw.math.BackgroundList`, optional
468 Background list to subtract from the calexp.
472 calExp : `lsst.afw.image.ExposureF`
473 The calexp with the mask and skyCorr applied.
475 if isinstance(calExpHandle, DeferredDatasetHandle):
476 calExp: ExposureF = calExpHandle.get()
480 calExp: ExposureF = calExpHandle.clone()
485 if self.config.doApplyFlatBackgroundRatio:
486 if not backgroundToPhotometricRatioHandle:
488 "A list of backgroundToPhotometricRatioHandles must be supplied if "
489 "config.doApplyFlatBackgroundRatio=True.",
491 ratioImage = backgroundToPhotometricRatioHandle.get()
492 calExp.maskedImage *= ratioImage
494 "Detector %d: Converted background-flattened image to a photometric-flattened image",
495 calExp.getDetector().getId(),
500 if skyCorr
is not None:
501 image = calExp.getMaskedImage()
502 image -= skyCorr.getImage()
527 """Restore original background to a calexp and invert the related
528 background model; optionally refine the mask plane.
530 The original visit-level background is restored to the calibrated
531 exposure and the existing background model is inverted in-place. If
532 doMaskObjects is True, the mask map associated with the exposure will
533 be iteratively updated (over nIter loops) by re-estimating the
534 background each iteration and redetecting footprints.
536 The background model modified in-place in this method will comprise the
537 first N elements of the skyCorr dataset type, i.e., these N elements
538 are the inverse of the calexpBackground model. All subsequent elements
539 appended to skyCorr will be additive such that, when skyCorr is
540 subtracted from a calexp, the net result will be to undo the initial
541 per-detector background solution and then apply the skyCorr model
542 thereafter. Adding skyCorr to a calexpBackground will effectively
543 negate the calexpBackground, returning only the additive background
544 components of the skyCorr background model.
548 calExp : `lsst.afw.image.ExposureF`
549 Detector level calexp image.
550 calBkg : `lsst.afw.math.BackgroundList`
551 Detector level background lists associated with the calexp.
555 calExp : `lsst.afw.image.ExposureF`
556 The calexp with the originally subtracted background restored.
557 skyCorrBase : `lsst.afw.math.BackgroundList`
558 The inverted original background models; the genesis for skyCorr.
560 image = calExp.getMaskedImage()
563 for calBkgElement
in calBkg:
564 statsImage = calBkgElement[0].getStatsImage()
566 skyCorrBase = calBkg.getImage()
569 stats = np.nanpercentile(skyCorrBase.array, [50, 75, 25])
571 "Detector %d: Original background restored (BG median = %.1f counts, BG IQR = %.1f counts)",
572 calExp.getDetector().getId(),
574 np.subtract(*stats[1:]),
578 if self.config.doMaskObjects:
579 maskFrac0 = 1 - np.sum(calExp.mask.array == 0) / calExp.mask.array.size
580 self.maskObjects.findObjects(calExp)
581 maskFrac1 = 1 - np.sum(calExp.mask.array == 0) / calExp.mask.array.size
584 "Detector %d: Iterative source detection and mask growth has increased masked area by %.1f%%",
585 calExp.getDetector().getId(),
586 (100 * (maskFrac1 - maskFrac0)),
589 return calExp, skyCorrBase
592 """Check that the background model contains enough valid superpixels,
593 and raise a useful error if not.
598 Identifier for the background model.
599 bgModel : `~lsst.pipe.tasks.background.FocalPlaneBackground`
600 Background model to check.
601 config : `~lsst.pipe.tasks.background.FocalPlaneBackgroundConfig`
602 Configuration used to create the background model.
604 bgModelArray = bgModel._numbers.getArray()
605 spArea = (config.xSize / config.pixelSize) * (config.ySize / config.pixelSize)
607 "%s: FP background model constructed using %.2f x %.2f mm (%d x %d pixel) superpixels",
611 int(config.xSize / config.pixelSize),
612 int(config.ySize / config.pixelSize),
615 "%s: Pixel data exists in %d of %d superpixels; the most populated superpixel is %.1f%% filled",
617 np.sum(bgModelArray > 0),
619 100 * np.max(bgModelArray) / spArea,
622 thresh = config.minFrac * spArea
623 if np.all(bgModelArray < thresh):
626 with warnings.catch_warnings():
627 warnings.filterwarnings(
"ignore",
r"invalid value encountered")
628 stats = np.nanpercentile(bgModel.getStatsImage().array, [50, 75, 25])
630 "%s: FP BG median = %.1f counts, FP BG IQR = %.1f counts",
633 np.subtract(*stats[1:]),
636 def _fitSkyFrame(self, calExps, masks, skyCorrs, skyFrames, backgroundToPhotometricRatioHandles):
637 """Determine the full focal plane sky frame scale factor relative to
638 an input list of calibrated exposures.
640 This method measures the sky frame scale on all inputs, resulting in
641 values equal to the background method solveScales().
643 Input skyCorrs are updated in-place.
647 calExps : `list` [`lsst.afw.image.ExposureF`]
648 Calibrated exposures to be background subtracted.
649 masks : `list` [`lsst.afw.image.Mask`]
650 Masks associated with the input calibrated exposures.
651 skyCorrs : `list` [`lsst.afw.math.BackgroundList`]
652 Background lists associated with the input calibrated exposures.
653 skyFrames : `list` [`lsst.afw.image.ExposureF`]
654 Sky frame calibration data for the input detectors.
659 Fitted scale factor applied to the sky frame.
663 for calExpHandle, mask, skyCorr, skyFrameHandle, backgroundToPhotometricRatioHandle
in zip(
664 calExps, masks, skyCorrs, skyFrames, backgroundToPhotometricRatioHandles
666 calExp = self.
_getCalExp(calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle)
668 skyBkg = self.sky.exposureToBackground(skyFrame)
670 skyBkgs.append(skyBkg)
672 samples = self.sky.measureScale(calExp.getMaskedImage(), skyBkg)
673 scales.append(samples)
674 scale = self.sky.solveScales(scales)
675 for skyCorr, skyBkg
in zip(skyCorrs, skyBkgs):
676 bgData = list(skyBkg[0])
678 statsImage = bg.getStatsImage().clone()
681 newBgData = [newBg] + bgData[1:]
682 skyCorr.append(newBgData)
683 self.log.info(
"Sky frame subtracted with a scale factor of %.5f", scale)