lsst.pipe.tasks g253578fa50+0eeb8841d4
Loading...
Searching...
No Matches
skyCorrection.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22from __future__ import annotations
23
24__all__ = ["SkyCorrectionTask", "SkyCorrectionConfig"]
25
26import warnings
27
28import numpy as np
29
30from lsst.afw.image import ExposureF, makeMaskedImage
31from lsst.afw.math import BackgroundMI, binImage
32from lsst.daf.butler import DeferredDatasetHandle
33from lsst.pex.config import Config, ConfigField, ConfigurableField, Field
34from lsst.pipe.base import (
35 AlgorithmError,
36 AnnotatedPartialOutputsError,
37 PipelineTask,
38 PipelineTaskConfig,
39 PipelineTaskConnections,
40 Struct,
41)
42from lsst.pipe.base.connectionTypes import Input, Output, PrerequisiteInput
44 FocalPlaneBackground,
45 FocalPlaneBackgroundConfig,
46 MaskObjectsTask,
47 SkyMeasurementTask,
48)
49from lsst.pipe.tasks.visualizeVisit import VisualizeMosaicExpConfig, VisualizeMosaicExpTask
50
51
52def _reorderAndPadList(inputList, inputKeys, outputKeys, padWith=None):
53 """Match the order of one list to another, padding if necessary.
54
55 Parameters
56 ----------
57 inputList : `list`
58 List to be reordered and padded. Elements can be any type.
59 inputKeys : iterable
60 Iterable of values to be compared with outputKeys.
61 Length must match `inputList`.
62 outputKeys : iterable
63 Iterable of values to be compared with inputKeys.
64 padWith :
65 Any value to be inserted where one of inputKeys is not in outputKeys.
66
67 Returns
68 -------
69 outputList : `list`
70 Copy of inputList reordered per outputKeys and padded with `padWith`
71 so that the length matches length of outputKeys.
72 """
73 outputList = []
74 for outputKey in outputKeys:
75 if outputKey in inputKeys:
76 outputList.append(inputList[inputKeys.index(outputKey)])
77 else:
78 outputList.append(padWith)
79 return outputList
80
81
82class SkyCorrectionConnections(PipelineTaskConnections, dimensions=("instrument", "visit")):
83 calExps = Input(
84 doc="Background-subtracted calibrated exposures.",
85 name="calexp",
86 multiple=True,
87 deferLoad=True,
88 storageClass="ExposureF",
89 dimensions=["instrument", "visit", "detector"],
90 )
91 calBkgs = Input(
92 doc="Subtracted backgrounds for input calibrated exposures.",
93 name="calexpBackground",
94 multiple=True,
95 storageClass="Background",
96 dimensions=["instrument", "visit", "detector"],
97 )
98 backgroundToPhotometricRatioHandles = Input(
99 doc="Ratio of a background-flattened image to a photometric-flattened image. "
100 "Only used if doApplyFlatBackgroundRatio is True.",
101 multiple=True,
102 name="background_to_photometric_ratio",
103 storageClass="Image",
104 dimensions=["instrument", "visit", "detector"],
105 deferLoad=True,
106 )
107 skyFrames = PrerequisiteInput(
108 doc="Calibration sky frames.",
109 name="sky",
110 multiple=True,
111 deferLoad=True,
112 storageClass="ExposureF",
113 dimensions=["instrument", "physical_filter", "detector"],
114 isCalibration=True,
115 )
116 camera = PrerequisiteInput(
117 doc="Input camera.",
118 name="camera",
119 storageClass="Camera",
120 dimensions=["instrument"],
121 isCalibration=True,
122 )
123 skyCorr = Output(
124 doc="Sky correction data, to be subtracted from the calibrated exposures.",
125 name="skyCorr",
126 multiple=True,
127 storageClass="Background",
128 dimensions=["instrument", "visit", "detector"],
129 )
130 calExpMosaic = Output(
131 doc="Full focal plane mosaicked image of the sky corrected calibrated exposures.",
132 name="calexp_skyCorr_visit_mosaic",
133 storageClass="ImageF",
134 dimensions=["instrument", "visit"],
135 )
136 calBkgMosaic = Output(
137 doc="Full focal plane mosaicked image of the sky corrected calibrated exposure backgrounds.",
138 name="calexpBackground_skyCorr_visit_mosaic",
139 storageClass="ImageF",
140 dimensions=["instrument", "visit"],
141 )
142
143 def __init__(self, *, config: "SkyCorrectionConfig | None" = None):
144 super().__init__(config=config)
145 assert config is not None
146 if not config.doSky:
147 del self.skyFrames
148 if not config.doApplyFlatBackgroundRatio:
150
151
152class SkyCorrectionConfig(PipelineTaskConfig, pipelineConnections=SkyCorrectionConnections):
153 doApplyFlatBackgroundRatio = Field(
154 dtype=bool,
155 default=False,
156 doc="This should be True if the input image was processed with an illumination correction.",
157 )
158 maskObjects = ConfigurableField(
159 target=MaskObjectsTask,
160 doc="Mask Objects",
161 )
162 doMaskObjects = Field(
163 dtype=bool,
164 default=True,
165 doc="Iteratively mask objects to find good sky?",
166 )
167 bgModel1 = ConfigField(
168 dtype=FocalPlaneBackgroundConfig,
169 doc="Initial background model, prior to sky frame subtraction",
170 )
171 undoBgModel1 = Field(
172 dtype=bool,
173 default=False,
174 doc="If True, adds back initial background model after sky and removes bgModel1 from the list",
175 )
176 sky = ConfigurableField(
177 target=SkyMeasurementTask,
178 doc="Sky measurement",
179 )
180 doSky = Field(
181 dtype=bool,
182 default=True,
183 doc="Do sky frame subtraction?",
184 )
185 bgModel2 = ConfigField(
186 dtype=FocalPlaneBackgroundConfig,
187 doc="Final (cleanup) background model, after sky frame subtraction",
188 )
189 doBgModel2 = Field(
190 dtype=bool,
191 default=True,
192 doc="Do final (cleanup) background model subtraction, after sky frame subtraction?",
193 )
194 binning = Field(
195 dtype=int,
196 default=8,
197 doc="Binning factor for constructing full focal plane '*_camera' output datasets",
198 )
199
200 def setDefaults(self):
201 Config.setDefaults(self)
202 self.bgModel2.doSmooth = True
203 self.bgModel2.minFrac = 0.3
204 self.bgModel2.xSize = 256
205 self.bgModel2.ySize = 256
206 self.bgModel2.smoothScale = 1.0
207
208 def validate(self):
209 super().validate()
210 if self.undoBgModel1 and not self.doSky and not self.doBgModel2:
211 raise ValueError("If undoBgModel1 is True, task requires at least one of doSky or doBgModel2.")
212
213
214class SkyCorrectionTask(PipelineTask):
215 """Perform a full focal plane sky correction."""
216
217 ConfigClass = SkyCorrectionConfig
218 _DefaultName = "skyCorr"
219
220 def __init__(self, *args, **kwargs):
221 super().__init__(**kwargs)
222 self.makeSubtask("sky")
223 self.makeSubtask("maskObjects")
224
225 def runQuantum(self, butlerQC, inputRefs, outputRefs):
226 # Sort input/output connections by detector ID, padding where
227 # necessary, to ensure that all detectors are processed consistently.
228 # Detector IDs are defined from the intersection of calExps, calBkgs,
229 # and optionally skyFrames and backgroundToPhotometricRatioHandles.
230 # This resolves potential missing data issues when processing a visit
231 # that contains only partial inputs.
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)
242 inputRefs.calExps = _reorderAndPadList(
243 inputRefs.calExps, [ref.dataId["detector"] for ref in inputRefs.calExps], detectorOrder
244 )
245 inputRefs.calBkgs = _reorderAndPadList(
246 inputRefs.calBkgs, [ref.dataId["detector"] for ref in inputRefs.calBkgs], detectorOrder
247 )
248 # Only attempt to fetch sky frames if they are going to be applied.
249 if self.config.doSky:
250 inputRefs.skyFrames = _reorderAndPadList(
251 inputRefs.skyFrames, [ref.dataId["detector"] for ref in inputRefs.skyFrames], detectorOrder
252 )
253 else:
254 inputRefs.skyFrames = []
255 # Only attempt to fetch flat ratios if they are going to be applied.
256 if self.config.doApplyFlatBackgroundRatio:
257 inputRefs.backgroundToPhotometricRatioHandles = _reorderAndPadList(
258 inputRefs.backgroundToPhotometricRatioHandles,
259 [ref.dataId["detector"] for ref in inputRefs.backgroundToPhotometricRatioHandles],
260 detectorOrder,
261 )
262 else:
263 inputRefs.backgroundToPhotometricRatioHandles = []
264 outputRefs.skyCorr = _reorderAndPadList(
265 outputRefs.skyCorr, [ref.dataId["detector"] for ref in outputRefs.skyCorr], detectorOrder
266 )
267 inputs = butlerQC.get(inputRefs)
268 try:
269 outputs = self.run(**inputs)
270 except AlgorithmError as e:
271 error = AnnotatedPartialOutputsError.annotate(
272 e,
273 self, # type: ignore
274 log=self.log,
275 )
276 raise error from e
277 butlerQC.put(outputs, outputRefs)
278
279 def run(self, calExps, calBkgs, skyFrames, camera, backgroundToPhotometricRatioHandles=[]):
280 """Perform sky correction on a visit.
281
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.
287
288 An initial full focal plane sky subtraction (bgModel1) will take place
289 prior to scaling and subtracting the sky frame.
290
291 If doSky is True, the sky frame will be scaled to the flux in the input
292 visit.
293
294 If doBgModel2 is True, a final full focal plane sky subtraction will
295 take place after the sky frame has been subtracted.
296
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
305 background model.
306
307 Parameters
308 ----------
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.
321
322 Returns
323 -------
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`.
335 """
336 # Process each detector separately and merge into bgModel1.
337 # This avoids storing every full-res image in-memory at once.
338 bgModel1 = FocalPlaneBackground.fromCamera(self.config.bgModel1, camera)
339 detectors = []
340 masks = []
341 skyCorrs = []
342 bgModel1Indices = []
343 if not self.config.doApplyFlatBackgroundRatio:
344 backgroundToPhotometricRatioHandles = [None] * len(calExps)
345 for calExpHandle, calBkg, backgroundToPhotometricRatioHandle in zip(
346 calExps, calBkgs, backgroundToPhotometricRatioHandles
347 ):
348 calExp = self._getCalExp(
349 calExpHandle, backgroundToPhotometricRatioHandle=backgroundToPhotometricRatioHandle
350 )
351 detectors.append(calExp.getDetector())
352
353 # Restore original background in-place; optionally refine mask maps
354 _ = self._restoreOriginalBackgroundRefineMask(calExp, calBkg)
355 masks.append(calExp.mask)
356 skyCorrs.append(calBkg) # Contains only the inverted original background elements at this stage
357 bgModel1Indices.append(len(calBkg)) # Index of the original background element
358
359 # Make a background model for the image, using bgModel1 configs
360 bgModel1Detector = FocalPlaneBackground.fromCamera(self.config.bgModel1, camera)
361 bgModel1Detector.addCcd(calExp)
362 bgModel1.merge(bgModel1Detector)
363 self.log.info(
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(),
368 "%",
369 )
370
371 # Validate bgModel1
372 self._validateBgModel("bgModel1", bgModel1, self.config.bgModel1)
373
374 # Update skyCorrs with new bgModel1 background elements
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])
380
381 # Fit a scaled sky frame to all input exposures
382 skyFrameScale = None
383 if self.config.doSky:
384 skyFrameScale = self._fitSkyFrame(
385 calExps, masks, skyCorrs, skyFrames, backgroundToPhotometricRatioHandles
386 )
387
388 # Remove the initial background model (bgModel1) from every skyCorr
389 if self.config.undoBgModel1:
390 for skyCorr, bgModel1Index in zip(skyCorrs, bgModel1Indices):
391 skyCorr._backgrounds.pop(bgModel1Index)
392 self.log.info(
393 "Initial background models (bgModel1s) have been removed from all skyCorr background lists",
394 )
395
396 # Bin exposures, generate full-fp bg, map to CCDs and subtract in-place
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
401 ):
402 calExp = self._getCalExp(calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle)
403
404 # Make a background model for the image, using bgModel2 configs
405 bgModel2Detector = FocalPlaneBackground.fromCamera(self.config.bgModel2, camera)
406 bgModel2Detector.addCcd(calExp)
407 bgModel2.merge(bgModel2Detector)
408 self.log.info(
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(),
413 "%",
414 )
415
416 # Validate bgModel2
417 self._validateBgModel("bgModel2", bgModel2, self.config.bgModel2)
418
419 # Update skyCorrs with new bgModel2 background elements
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])
425
426 # Make camera-level mosaics of bg subtracted calexps and subtracted bgs
427 calExpsBinned = []
428 calBkgsBinned = []
429 for calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle, bgModel1Index in zip(
430 calExps, masks, skyCorrs, backgroundToPhotometricRatioHandles, bgModel1Indices
431 ):
432 calExp = self._getCalExp(calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle)
433
434 skyCorrExtra = skyCorr.clone() # new skyCorr elements created in this task
435 skyCorrExtra._backgrounds = skyCorrExtra._backgrounds[bgModel1Index:]
436 skyCorrExtraMI = makeMaskedImage(skyCorrExtra.getImage())
437 skyCorrExtraMI.setMask(calExp.getMask())
438
439 calExpsBinned.append(binImage(calExp.getMaskedImage(), self.config.binning))
440 calBkgsBinned.append(binImage(skyCorrExtraMI, self.config.binning))
441
442 mosConfig = VisualizeMosaicExpConfig()
443 mosConfig.binning = self.config.binning
444 mosTask = VisualizeMosaicExpTask(config=mosConfig)
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
448
449 return Struct(
450 skyFrameScale=skyFrameScale,
451 skyCorr=skyCorrs,
452 calExpMosaic=calExpMosaic,
453 calBkgMosaic=calBkgMosaic,
454 )
455
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.
459
460 Parameters
461 ----------
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.
469
470 Returns
471 -------
472 calExp : `lsst.afw.image.ExposureF`
473 The calexp with the mask and skyCorr applied.
474 """
475 if isinstance(calExpHandle, DeferredDatasetHandle):
476 calExp: ExposureF = calExpHandle.get()
477 else:
478 # Here we clone the imaging data to avoid modifying data which is
479 # used in downstream processing.
480 calExp: ExposureF = calExpHandle.clone()
481
482 # Convert from background-flattened to photometric-flattened images
483 # Note: remember to convert back to background-flattened images
484 # if science images are to be output by this task.
485 if self.config.doApplyFlatBackgroundRatio:
486 if not backgroundToPhotometricRatioHandle:
487 raise ValueError(
488 "A list of backgroundToPhotometricRatioHandles must be supplied if "
489 "config.doApplyFlatBackgroundRatio=True.",
490 )
491 ratioImage = backgroundToPhotometricRatioHandle.get()
492 calExp.maskedImage *= ratioImage
493 self.log.info(
494 "Detector %d: Converted background-flattened image to a photometric-flattened image",
495 calExp.getDetector().getId(),
496 )
497
498 if mask is not None:
499 calExp.setMask(mask)
500 if skyCorr is not None:
501 image = calExp.getMaskedImage()
502 image -= skyCorr.getImage()
503
504 return calExp
505
506 def _getSkyFrame(self, skyFrameHandle):
507 """Get a calexp from a DeferredDatasetHandle, and optionally apply an
508 updated mask and skyCorr.
509
510 Parameters
511 ----------
512 skyFrameHandle : `lsst.daf.butler.DeferredDatasetHandle`
513 Either the sky frame data or a handle to the sky frame dataset.
514
515 Returns
516 -------
517 skyFrame : `lsst.afw.image.ExposureF`
518 The calexp with the mask and skyCorr applied.
519 """
520 if isinstance(skyFrameHandle, DeferredDatasetHandle):
521 skyFrame: ExposureF = skyFrameHandle.get()
522 else:
523 skyFrame: ExposureF = skyFrameHandle
524 return skyFrame
525
526 def _restoreOriginalBackgroundRefineMask(self, calExp, calBkg):
527 """Restore original background to a calexp and invert the related
528 background model; optionally refine the mask plane.
529
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.
535
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.
545
546 Parameters
547 ----------
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.
552
553 Returns
554 -------
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.
559 """
560 image = calExp.getMaskedImage()
561
562 # Invert all elements of the existing bg model; restore in calexp
563 for calBkgElement in calBkg:
564 statsImage = calBkgElement[0].getStatsImage()
565 statsImage *= -1
566 skyCorrBase = calBkg.getImage()
567 image -= skyCorrBase
568
569 stats = np.nanpercentile(skyCorrBase.array, [50, 75, 25])
570 self.log.info(
571 "Detector %d: Original background restored (BG median = %.1f counts, BG IQR = %.1f counts)",
572 calExp.getDetector().getId(),
573 -stats[0],
574 np.subtract(*stats[1:]),
575 )
576
577 # Iteratively subtract bg, re-detect sources, and add bg back on
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
582
583 self.log.info(
584 "Detector %d: Iterative source detection and mask growth has increased masked area by %.1f%%",
585 calExp.getDetector().getId(),
586 (100 * (maskFrac1 - maskFrac0)),
587 )
588
589 return calExp, skyCorrBase
590
591 def _validateBgModel(self, bgModelID, bgModel, config):
592 """Check that the background model contains enough valid superpixels,
593 and raise a useful error if not.
594
595 Parameters
596 ----------
597 bgModelID : `str`
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.
603 """
604 bgModelArray = bgModel._numbers.getArray()
605 spArea = (config.xSize / config.pixelSize) * (config.ySize / config.pixelSize)
606 self.log.info(
607 "%s: FP background model constructed using %.2f x %.2f mm (%d x %d pixel) superpixels",
608 bgModelID,
609 config.xSize,
610 config.ySize,
611 int(config.xSize / config.pixelSize),
612 int(config.ySize / config.pixelSize),
613 )
614 self.log.info(
615 "%s: Pixel data exists in %d of %d superpixels; the most populated superpixel is %.1f%% filled",
616 bgModelID,
617 np.sum(bgModelArray > 0),
618 bgModelArray.size,
619 100 * np.max(bgModelArray) / spArea,
620 )
621
622 thresh = config.minFrac * spArea
623 if np.all(bgModelArray < thresh):
624 raise NoUsableBackgroundSuperpixelsError(bgModelID, config.minFrac)
625
626 with warnings.catch_warnings():
627 warnings.filterwarnings("ignore", r"invalid value encountered")
628 stats = np.nanpercentile(bgModel.getStatsImage().array, [50, 75, 25])
629 self.log.info(
630 "%s: FP BG median = %.1f counts, FP BG IQR = %.1f counts",
631 bgModelID,
632 stats[0],
633 np.subtract(*stats[1:]),
634 )
635
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.
639
640 This method measures the sky frame scale on all inputs, resulting in
641 values equal to the background method solveScales().
642
643 Input skyCorrs are updated in-place.
644
645 Parameters
646 ----------
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.
655
656 Returns
657 -------
658 scale : `float`
659 Fitted scale factor applied to the sky frame.
660 """
661 skyBkgs = []
662 scales = []
663 for calExpHandle, mask, skyCorr, skyFrameHandle, backgroundToPhotometricRatioHandle in zip(
664 calExps, masks, skyCorrs, skyFrames, backgroundToPhotometricRatioHandles
665 ):
666 calExp = self._getCalExp(calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle)
667 skyFrame = self._getSkyFrame(skyFrameHandle)
668 skyBkg = self.sky.exposureToBackground(skyFrame)
669 del skyFrame # Free up memory
670 skyBkgs.append(skyBkg)
671 # Return a tuple of gridded image and sky frame clipped means
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])
677 bg = bgData[0]
678 statsImage = bg.getStatsImage().clone()
679 statsImage *= scale
680 newBg = BackgroundMI(bg.getImageBBox(), statsImage)
681 newBgData = [newBg] + bgData[1:]
682 skyCorr.append(newBgData)
683 self.log.info("Sky frame subtracted with a scale factor of %.5f", scale)
684 return scale
685
686
688 def __init__(self, bgModelID: str, minFrac: float):
689 self.bgModelID = bgModelID
690 self.minFrac = float(minFrac)
691 message = (
692 f"No background model superpixels in {bgModelID} are more than {100 * self.minFrac:.1f}% filled. "
693 "Try decreasing the minFrac configuration parameter, optimizing the subset of detectors "
694 "being processed, or increasing the number of detectors being processed."
695 )
696 super().__init__(message)
697
698 @property
699 def metadata(self) -> dict:
700 return {
701 "bgModelID": self.bgModelID,
702 }
__init__(self, *, "SkyCorrectionConfig | None" config=None)
_validateBgModel(self, bgModelID, bgModel, config)
run(self, calExps, calBkgs, skyFrames, camera, backgroundToPhotometricRatioHandles=[])
_fitSkyFrame(self, calExps, masks, skyCorrs, skyFrames, backgroundToPhotometricRatioHandles)
runQuantum(self, butlerQC, inputRefs, outputRefs)
_getCalExp(self, calExpHandle, mask=None, skyCorr=None, backgroundToPhotometricRatioHandle=None)
_reorderAndPadList(inputList, inputKeys, outputKeys, padWith=None)