LSST Applications g04e9c324dd+8c5ae1fdc5,g134cb467dc+1b3060144d,g18429d2f64+f642bf4753,g199a45376c+0ba108daf9,g1fd858c14a+2dcf163641,g262e1987ae+7b8c96d2ca,g29ae962dfc+3bd6ecb08a,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+53e1a9e7c5,g4595892280+fef73a337f,g47891489e3+2efcf17695,g4d44eb3520+642b70b07e,g53246c7159+8c5ae1fdc5,g67b6fd64d1+2efcf17695,g67fd3c3899+b70e05ef52,g74acd417e5+317eb4c7d4,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g89139ef638+2efcf17695,g8d7436a09f+3be3c13596,g8ea07a8fe4+9f5ccc88ac,g90f42f885a+a4e7b16d9b,g97be763408+ad77d7208f,g9dd6db0277+b70e05ef52,ga681d05dcb+a3f46e7fff,gabf8522325+735880ea63,gac2eed3f23+2efcf17695,gb89ab40317+2efcf17695,gbf99507273+8c5ae1fdc5,gd8ff7fe66e+b70e05ef52,gdab6d2f7ff+317eb4c7d4,gdc713202bf+b70e05ef52,gdfd2d52018+b10e285e0f,ge365c994fd+310e8507c4,ge410e46f29+2efcf17695,geaed405ab2+562b3308c0,gffca2db377+8c5ae1fdc5,w.2025.35
LSST Data Management Base Package
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 PipelineTask, PipelineTaskConfig, PipelineTaskConnections, Struct
35from lsst.pipe.base.connectionTypes import Input, Output, PrerequisiteInput
37 FocalPlaneBackground,
38 FocalPlaneBackgroundConfig,
39 MaskObjectsTask,
40 SkyMeasurementTask,
41)
42from lsst.pipe.tasks.visualizeVisit import VisualizeMosaicExpConfig, VisualizeMosaicExpTask
43
44
45def _skyFrameLookup(datasetType, registry, quantumDataId, collections):
46 """Lookup function to identify sky frames.
47
48 Parameters
49 ----------
50 datasetType : `lsst.daf.butler.DatasetType`
51 Dataset to lookup.
52 registry : `lsst.daf.butler.Registry`
53 Butler registry to query.
54 quantumDataId : `lsst.daf.butler.DataCoordinate`
55 Data id to transform to find sky frames.
56 The ``detector`` entry will be stripped.
57 collections : `lsst.daf.butler.CollectionSearch`
58 Collections to search through.
59
60 Returns
61 -------
62 results : `list` [`lsst.daf.butler.DatasetRef`]
63 List of datasets that will be used as sky calibration frames.
64 """
65 newDataId = quantumDataId.subset(registry.dimensions.conform(["instrument", "visit"]))
66 skyFrames = []
67 for dataId in registry.queryDataIds(["visit", "detector"], dataId=newDataId).expanded():
68 skyFrame = registry.findDataset(
69 datasetType, dataId, collections=collections, timespan=dataId.timespan
70 )
71 skyFrames.append(skyFrame)
72 return skyFrames
73
74
75def _reorderAndPadList(inputList, inputKeys, outputKeys, padWith=None):
76 """Match the order of one list to another, padding if necessary.
77
78 Parameters
79 ----------
80 inputList : `list`
81 List to be reordered and padded. Elements can be any type.
82 inputKeys : iterable
83 Iterable of values to be compared with outputKeys.
84 Length must match `inputList`.
85 outputKeys : iterable
86 Iterable of values to be compared with inputKeys.
87 padWith :
88 Any value to be inserted where one of inputKeys is not in outputKeys.
89
90 Returns
91 -------
92 outputList : `list`
93 Copy of inputList reordered per outputKeys and padded with `padWith`
94 so that the length matches length of outputKeys.
95 """
96 outputList = []
97 for outputKey in outputKeys:
98 if outputKey in inputKeys:
99 outputList.append(inputList[inputKeys.index(outputKey)])
100 else:
101 outputList.append(padWith)
102 return outputList
103
104
105class SkyCorrectionConnections(PipelineTaskConnections, dimensions=("instrument", "visit")):
106 rawLinker = Input(
107 doc="Raw data to provide exp-visit linkage to connect calExp inputs to camera/sky calibs.",
108 name="raw",
109 multiple=True,
110 deferLoad=True,
111 storageClass="Exposure",
112 dimensions=["instrument", "exposure", "detector"],
113 )
114 calExps = Input(
115 doc="Background-subtracted calibrated exposures.",
116 name="calexp",
117 multiple=True,
118 deferLoad=True,
119 storageClass="ExposureF",
120 dimensions=["instrument", "visit", "detector"],
121 )
122 calBkgs = Input(
123 doc="Subtracted backgrounds for input calibrated exposures.",
124 name="calexpBackground",
125 multiple=True,
126 storageClass="Background",
127 dimensions=["instrument", "visit", "detector"],
128 )
129 backgroundToPhotometricRatioHandles = Input(
130 doc="Ratio of a background-flattened image to a photometric-flattened image. "
131 "Only used if doApplyFlatBackgroundRatio is True.",
132 multiple=True,
133 name="background_to_photometric_ratio",
134 storageClass="Image",
135 dimensions=["instrument", "visit", "detector"],
136 deferLoad=True,
137 )
138 skyFrames = PrerequisiteInput(
139 doc="Calibration sky frames.",
140 name="sky",
141 multiple=True,
142 deferLoad=True,
143 storageClass="ExposureF",
144 dimensions=["instrument", "physical_filter", "detector"],
145 isCalibration=True,
146 lookupFunction=_skyFrameLookup,
147 )
148 camera = PrerequisiteInput(
149 doc="Input camera.",
150 name="camera",
151 storageClass="Camera",
152 dimensions=["instrument"],
153 isCalibration=True,
154 )
155 skyCorr = Output(
156 doc="Sky correction data, to be subtracted from the calibrated exposures.",
157 name="skyCorr",
158 multiple=True,
159 storageClass="Background",
160 dimensions=["instrument", "visit", "detector"],
161 )
162 calExpMosaic = Output(
163 doc="Full focal plane mosaicked image of the sky corrected calibrated exposures.",
164 name="calexp_skyCorr_visit_mosaic",
165 storageClass="ImageF",
166 dimensions=["instrument", "visit"],
167 )
168 calBkgMosaic = Output(
169 doc="Full focal plane mosaicked image of the sky corrected calibrated exposure backgrounds.",
170 name="calexpBackground_skyCorr_visit_mosaic",
171 storageClass="ImageF",
172 dimensions=["instrument", "visit"],
173 )
174
175 def __init__(self, *, config: "SkyCorrectionConfig | None" = None):
176 super().__init__(config=config)
177 assert config is not None
178 if not config.doSky:
179 del self.skyFrames
180 if not config.doApplyFlatBackgroundRatio:
182
183
184class SkyCorrectionConfig(PipelineTaskConfig, pipelineConnections=SkyCorrectionConnections):
185 doApplyFlatBackgroundRatio = Field(
186 dtype=bool,
187 default=False,
188 doc="This should be True if the input image was processed with an illumination correction.",
189 )
190 maskObjects = ConfigurableField(
191 target=MaskObjectsTask,
192 doc="Mask Objects",
193 )
194 doMaskObjects = Field(
195 dtype=bool,
196 default=True,
197 doc="Iteratively mask objects to find good sky?",
198 )
199 bgModel1 = ConfigField(
200 dtype=FocalPlaneBackgroundConfig,
201 doc="Initial background model, prior to sky frame subtraction",
202 )
203 undoBgModel1 = Field(
204 dtype=bool,
205 default=False,
206 doc="If True, adds back initial background model after sky and removes bgModel1 from the list",
207 )
209 target=SkyMeasurementTask,
210 doc="Sky measurement",
211 )
212 doSky = Field(
213 dtype=bool,
214 default=True,
215 doc="Do sky frame subtraction?",
216 )
217 bgModel2 = ConfigField(
218 dtype=FocalPlaneBackgroundConfig,
219 doc="Final (cleanup) background model, after sky frame subtraction",
220 )
221 doBgModel2 = Field(
222 dtype=bool,
223 default=True,
224 doc="Do final (cleanup) background model subtraction, after sky frame subtraction?",
225 )
226 binning = Field(
227 dtype=int,
228 default=8,
229 doc="Binning factor for constructing full focal plane '*_camera' output datasets",
230 )
231
232 def setDefaults(self):
233 Config.setDefaults(self)
234 self.bgModel2.doSmooth = True
235 self.bgModel2.minFrac = 0.3
236 self.bgModel2.xSize = 256
237 self.bgModel2.ySize = 256
238 self.bgModel2.smoothScale = 1.0
239
240 def validate(self):
241 super().validate()
242 if self.undoBgModel1 and not self.doSky and not self.doBgModel2:
243 raise ValueError("If undoBgModel1 is True, task requires at least one of doSky or doBgModel2.")
244
245
246class SkyCorrectionTask(PipelineTask):
247 """Perform a full focal plane sky correction."""
248
249 ConfigClass = SkyCorrectionConfig
250 _DefaultName = "skyCorr"
251
252 def __init__(self, *args, **kwargs):
253 super().__init__(**kwargs)
254 self.makeSubtask("sky")
255 self.makeSubtask("maskObjects")
256
257 def runQuantum(self, butlerQC, inputRefs, outputRefs):
258 # Sort input/output connections by detector ID, padding where
259 # necessary, to ensure that all detectors are processed consistently.
260 # Detector IDs are defined from the intersection of calExps, calBkgs,
261 # and optionally skyFrames and backgroundToPhotometricRatioHandles.
262 # This resolves potential missing data issues when processing a visit
263 # that contains only partial inputs.
264 calExpOrder = {ref.dataId["detector"] for ref in inputRefs.calExps}
265 calBkgOrder = {ref.dataId["detector"] for ref in inputRefs.calBkgs}
266 detectorOrder = calExpOrder & calBkgOrder
267 if self.config.doApplyFlatBackgroundRatio:
268 ratioOrder = {ref.dataId["detector"] for ref in inputRefs.backgroundToPhotometricRatioHandles}
269 detectorOrder &= ratioOrder
270 if self.config.doSky:
271 skyFrameOrder = {ref.dataId["detector"] for ref in inputRefs.skyFrames}
272 detectorOrder &= skyFrameOrder
273 detectorOrder = sorted(detectorOrder)
274 inputRefs.calExps = _reorderAndPadList(
275 inputRefs.calExps, [ref.dataId["detector"] for ref in inputRefs.calExps], detectorOrder
276 )
277 inputRefs.calBkgs = _reorderAndPadList(
278 inputRefs.calBkgs, [ref.dataId["detector"] for ref in inputRefs.calBkgs], detectorOrder
279 )
280 # Only attempt to fetch sky frames if they are going to be applied.
281 if self.config.doSky:
282 inputRefs.skyFrames = _reorderAndPadList(
283 inputRefs.skyFrames, [ref.dataId["detector"] for ref in inputRefs.skyFrames], detectorOrder
284 )
285 else:
286 inputRefs.skyFrames = []
287 # Only attempt to fetch flat ratios if they are going to be applied.
288 if self.config.doApplyFlatBackgroundRatio:
289 inputRefs.backgroundToPhotometricRatioHandles = _reorderAndPadList(
290 inputRefs.backgroundToPhotometricRatioHandles,
291 [ref.dataId["detector"] for ref in inputRefs.backgroundToPhotometricRatioHandles],
292 detectorOrder,
293 )
294 else:
295 inputRefs.backgroundToPhotometricRatioHandles = []
296 outputRefs.skyCorr = _reorderAndPadList(
297 outputRefs.skyCorr, [ref.dataId["detector"] for ref in outputRefs.skyCorr], detectorOrder
298 )
299 inputs = butlerQC.get(inputRefs)
300 inputs.pop("rawLinker", None)
301 outputs = self.run(**inputs)
302 butlerQC.put(outputs, outputRefs)
303
304 def run(self, calExps, calBkgs, skyFrames, camera, backgroundToPhotometricRatioHandles=[]):
305 """Perform sky correction on a visit.
306
307 The original visit-level background is first restored to the calibrated
308 exposure and the existing background model is inverted in-place. If
309 doMaskObjects is True, the mask map associated with this exposure will
310 be iteratively updated (over nIter loops) by re-estimating the
311 background each iteration and redetecting footprints.
312
313 An initial full focal plane sky subtraction (bgModel1) will take place
314 prior to scaling and subtracting the sky frame.
315
316 If doSky is True, the sky frame will be scaled to the flux in the input
317 visit.
318
319 If doBgModel2 is True, a final full focal plane sky subtraction will
320 take place after the sky frame has been subtracted.
321
322 The first N elements of the returned skyCorr will consist of inverted
323 elements of the calexpBackground model (i.e., subtractive). All
324 subsequent elements appended to skyCorr thereafter will be additive
325 such that, when skyCorr is subtracted from a calexp, the net result
326 will be to undo the initial per-detector background solution and then
327 apply the skyCorr model thereafter. Adding skyCorr to a
328 calexpBackground will effectively negate the calexpBackground,
329 returning only the additive background components of the skyCorr
330 background model.
331
332 Parameters
333 ----------
334 calExps : `list` [`lsst.afw.image.ExposureF`]
335 Detector calibrated exposure images for the visit.
336 calBkgs : `list` [`lsst.afw.math.BackgroundList`]
337 Detector background lists matching the calibrated exposures.
338 skyFrames : `list` [`lsst.afw.image.ExposureF`]
339 Sky frame calibration data for the input detectors.
340 camera : `lsst.afw.cameraGeom.Camera`
341 Camera matching the input data to process.
342 backgroundToPhotometricRatioHandles :
343 `list` [`lsst.daf.butler.DeferredDatasetHandle`], optional
344 Deferred dataset handles pointing to the Background to photometric
345 ratio images for the input detectors.
346
347 Returns
348 -------
349 results : `Struct` containing:
350 skyFrameScale : `float`
351 Scale factor applied to the sky frame.
352 skyCorr : `list` [`lsst.afw.math.BackgroundList`]
353 Detector-level sky correction background lists.
354 calExpMosaic : `lsst.afw.image.ExposureF`
355 Visit-level mosaic of the sky corrected data, binned.
356 Analogous to `calexp - skyCorr`.
357 calBkgMosaic : `lsst.afw.image.ExposureF`
358 Visit-level mosaic of the sky correction background, binned.
359 Analogous to `calexpBackground + skyCorr`.
360 """
361 # Process each detector separately and merge into bgModel1.
362 # This avoids storing every full-res image in-memory at once.
363 bgModel1 = FocalPlaneBackground.fromCamera(self.config.bgModel1, camera)
364 detectors = []
365 masks = []
366 skyCorrs = []
367 bgModel1Indices = []
368 if not self.config.doApplyFlatBackgroundRatio:
369 backgroundToPhotometricRatioHandles = [None] * len(calExps)
370 for calExpHandle, calBkg, backgroundToPhotometricRatioHandle in zip(
371 calExps, calBkgs, backgroundToPhotometricRatioHandles
372 ):
373 calExp = self._getCalExp(
374 calExpHandle, backgroundToPhotometricRatioHandle=backgroundToPhotometricRatioHandle
375 )
376 detectors.append(calExp.getDetector())
377
378 # Restore original background in-place; optionally refine mask maps
379 _ = self._restoreOriginalBackgroundRefineMask(calExp, calBkg)
380 masks.append(calExp.mask)
381 skyCorrs.append(calBkg) # Contains only the inverted original background elements at this stage
382 bgModel1Indices.append(len(calBkg)) # Index of the original background element
383
384 # Make a background model for the image, using bgModel1 configs
385 bgModel1Detector = FocalPlaneBackground.fromCamera(self.config.bgModel1, camera)
386 bgModel1Detector.addCcd(calExp)
387 bgModel1.merge(bgModel1Detector)
388 self.log.info(
389 "Detector %d: Merged %d unmasked pixels (%.1f%s of detector area) into initial BG model",
390 calExp.getDetector().getId(),
391 bgModel1Detector._numbers.getArray().sum(),
392 100 * bgModel1Detector._numbers.getArray().sum() / calExp.getBBox().getArea(),
393 "%",
394 )
395
396 # Validate bgModel1
397 self._validateBgModel("bgModel1", bgModel1, self.config.bgModel1)
398
399 # Update skyCorrs with new bgModel1 background elements
400 for detector, skyCorr in zip(detectors, skyCorrs):
401 with warnings.catch_warnings():
402 warnings.filterwarnings("ignore", "invalid value encountered")
403 calBkgElement = bgModel1.toCcdBackground(detector, detector.getBBox())
404 skyCorr.append(calBkgElement[0])
405
406 # Fit a scaled sky frame to all input exposures
407 skyFrameScale = None
408 if self.config.doSky:
409 skyFrameScale = self._fitSkyFrame(
410 calExps, masks, skyCorrs, skyFrames, backgroundToPhotometricRatioHandles
411 )
412
413 # Remove the initial background model (bgModel1) from every skyCorr
414 if self.config.undoBgModel1:
415 for skyCorr, bgModel1Index in zip(skyCorrs, bgModel1Indices):
416 skyCorr._backgrounds.pop(bgModel1Index)
417 self.log.info(
418 "Initial background models (bgModel1s) have been removed from all skyCorr background lists",
419 )
420
421 # Bin exposures, generate full-fp bg, map to CCDs and subtract in-place
422 if self.config.doBgModel2:
423 bgModel2 = FocalPlaneBackground.fromCamera(self.config.bgModel2, camera)
424 for calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle in zip(
425 calExps, masks, skyCorrs, backgroundToPhotometricRatioHandles
426 ):
427 calExp = self._getCalExp(calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle)
428
429 # Make a background model for the image, using bgModel2 configs
430 bgModel2Detector = FocalPlaneBackground.fromCamera(self.config.bgModel2, camera)
431 bgModel2Detector.addCcd(calExp)
432 bgModel2.merge(bgModel2Detector)
433 self.log.info(
434 "Detector %d: Merged %d unmasked pixels (%.1f%s of detector area) into final BG model",
435 calExp.getDetector().getId(),
436 bgModel2Detector._numbers.getArray().sum(),
437 100 * bgModel2Detector._numbers.getArray().sum() / calExp.getBBox().getArea(),
438 "%",
439 )
440
441 # Validate bgModel2
442 self._validateBgModel("bgModel2", bgModel2, self.config.bgModel2)
443
444 # Update skyCorrs with new bgModel2 background elements
445 for detector, skyCorr in zip(detectors, skyCorrs):
446 with warnings.catch_warnings():
447 warnings.filterwarnings("ignore", "invalid value encountered")
448 calBkgElement = bgModel2.toCcdBackground(detector, detector.getBBox())
449 skyCorr.append(calBkgElement[0])
450
451 # Make camera-level mosaics of bg subtracted calexps and subtracted bgs
452 calExpsBinned = []
453 calBkgsBinned = []
454 for calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle, bgModel1Index in zip(
455 calExps, masks, skyCorrs, backgroundToPhotometricRatioHandles, bgModel1Indices
456 ):
457 calExp = self._getCalExp(calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle)
458
459 skyCorrExtra = skyCorr.clone() # new skyCorr elements created in this task
460 skyCorrExtra._backgrounds = skyCorrExtra._backgrounds[bgModel1Index:]
461 skyCorrExtraMI = makeMaskedImage(skyCorrExtra.getImage())
462 skyCorrExtraMI.setMask(calExp.getMask())
463
464 calExpsBinned.append(binImage(calExp.getMaskedImage(), self.config.binning))
465 calBkgsBinned.append(binImage(skyCorrExtraMI, self.config.binning))
466
467 mosConfig = VisualizeMosaicExpConfig()
468 mosConfig.binning = self.config.binning
469 mosTask = VisualizeMosaicExpTask(config=mosConfig)
470 detectorIds = [detector.getId() for detector in detectors]
471 calExpMosaic = mosTask.run(calExpsBinned, camera, inputIds=detectorIds).outputData
472 calBkgMosaic = mosTask.run(calBkgsBinned, camera, inputIds=detectorIds).outputData
473
474 return Struct(
475 skyFrameScale=skyFrameScale,
476 skyCorr=skyCorrs,
477 calExpMosaic=calExpMosaic,
478 calBkgMosaic=calBkgMosaic,
479 )
480
481 def _getCalExp(self, calExpHandle, mask=None, skyCorr=None, backgroundToPhotometricRatioHandle=None):
482 """Get a calexp from a DeferredDatasetHandle, and optionally apply an
483 updated mask and skyCorr.
484
485 Parameters
486 ----------
487 calExpHandle : `~lsst.afw.image.ExposureF`
488 | `lsst.daf.butler.DeferredDatasetHandle`
489 Either the image exposure data or a handle to the calexp dataset.
490 mask : `lsst.afw.image.Mask`, optional
491 Mask to apply to the calexp.
492 skyCorr : `lsst.afw.math.BackgroundList`, optional
493 Background list to subtract from the calexp.
494
495 Returns
496 -------
497 calExp : `lsst.afw.image.ExposureF`
498 The calexp with the mask and skyCorr applied.
499 """
500 if isinstance(calExpHandle, DeferredDatasetHandle):
501 calExp: ExposureF = calExpHandle.get()
502 else:
503 # Here we clone the imaging data to avoid modifying data which is
504 # used in downstream processing.
505 calExp: ExposureF = calExpHandle.clone()
506
507 # Convert from background-flattened to photometric-flattened images
508 # Note: remember to convert back to background-flattened images
509 # if science images are to be output by this task.
510 if self.config.doApplyFlatBackgroundRatio:
511 if not backgroundToPhotometricRatioHandle:
512 raise ValueError(
513 "A list of backgroundToPhotometricRatioHandles must be supplied if "
514 "config.doApplyFlatBackgroundRatio=True.",
515 )
516 ratioImage = backgroundToPhotometricRatioHandle.get()
517 calExp.maskedImage *= ratioImage
518 self.log.info(
519 "Detector %d: Converted background-flattened image to a photometric-flattened image",
520 calExp.getDetector().getId(),
521 )
522
523 if mask is not None:
524 calExp.setMask(mask)
525 if skyCorr is not None:
526 image = calExp.getMaskedImage()
527 image -= skyCorr.getImage()
528
529 return calExp
530
531 def _getSkyFrame(self, skyFrameHandle):
532 """Get a calexp from a DeferredDatasetHandle, and optionally apply an
533 updated mask and skyCorr.
534
535 Parameters
536 ----------
537 skyFrameHandle : `lsst.daf.butler.DeferredDatasetHandle`
538 Either the sky frame data or a handle to the sky frame dataset.
539
540 Returns
541 -------
542 skyFrame : `lsst.afw.image.ExposureF`
543 The calexp with the mask and skyCorr applied.
544 """
545 if isinstance(skyFrameHandle, DeferredDatasetHandle):
546 skyFrame: ExposureF = skyFrameHandle.get()
547 else:
548 skyFrame: ExposureF = skyFrameHandle
549 return skyFrame
550
551 def _restoreOriginalBackgroundRefineMask(self, calExp, calBkg):
552 """Restore original background to a calexp and invert the related
553 background model; optionally refine the mask plane.
554
555 The original visit-level background is restored to the calibrated
556 exposure and the existing background model is inverted in-place. If
557 doMaskObjects is True, the mask map associated with the exposure will
558 be iteratively updated (over nIter loops) by re-estimating the
559 background each iteration and redetecting footprints.
560
561 The background model modified in-place in this method will comprise the
562 first N elements of the skyCorr dataset type, i.e., these N elements
563 are the inverse of the calexpBackground model. All subsequent elements
564 appended to skyCorr will be additive such that, when skyCorr is
565 subtracted from a calexp, the net result will be to undo the initial
566 per-detector background solution and then apply the skyCorr model
567 thereafter. Adding skyCorr to a calexpBackground will effectively
568 negate the calexpBackground, returning only the additive background
569 components of the skyCorr background model.
570
571 Parameters
572 ----------
573 calExp : `lsst.afw.image.ExposureF`
574 Detector level calexp image.
575 calBkg : `lsst.afw.math.BackgroundList`
576 Detector level background lists associated with the calexp.
577
578 Returns
579 -------
580 calExp : `lsst.afw.image.ExposureF`
581 The calexp with the originally subtracted background restored.
582 skyCorrBase : `lsst.afw.math.BackgroundList`
583 The inverted original background models; the genesis for skyCorr.
584 """
585 image = calExp.getMaskedImage()
586
587 # Invert all elements of the existing bg model; restore in calexp
588 for calBkgElement in calBkg:
589 statsImage = calBkgElement[0].getStatsImage()
590 statsImage *= -1
591 skyCorrBase = calBkg.getImage()
592 image -= skyCorrBase
593
594 stats = np.nanpercentile(skyCorrBase.array, [50, 75, 25])
595 self.log.info(
596 "Detector %d: Original background restored (BG median = %.1f counts, BG IQR = %.1f counts)",
597 calExp.getDetector().getId(),
598 -stats[0],
599 np.subtract(*stats[1:]),
600 )
601
602 # Iteratively subtract bg, re-detect sources, and add bg back on
603 if self.config.doMaskObjects:
604 maskFrac0 = 1 - np.sum(calExp.mask.array == 0) / calExp.mask.array.size
605 self.maskObjects.findObjects(calExp)
606 maskFrac1 = 1 - np.sum(calExp.mask.array == 0) / calExp.mask.array.size
607
608 self.log.info(
609 "Detector %d: Iterative source detection and mask growth has increased masked area by %.1f%%",
610 calExp.getDetector().getId(),
611 (100 * (maskFrac1 - maskFrac0)),
612 )
613
614 return calExp, skyCorrBase
615
616 def _validateBgModel(self, bgModelID, bgModel, config):
617 """Check that the background model contains enough valid superpixels,
618 and raise a useful error if not.
619
620 Parameters
621 ----------
622 bgModelID : `str`
623 Identifier for the background model.
624 bgModel : `~lsst.pipe.tasks.background.FocalPlaneBackground`
625 Background model to check.
626 config : `~lsst.pipe.tasks.background.FocalPlaneBackgroundConfig`
627 Configuration used to create the background model.
628 """
629 bgModelArray = bgModel._numbers.getArray()
630 spArea = (config.xSize / config.pixelSize) * (config.ySize / config.pixelSize)
631 self.log.info(
632 "%s: FP background model constructed using %.2f x %.2f mm (%d x %d pixel) superpixels",
633 bgModelID,
634 config.xSize,
635 config.ySize,
636 int(config.xSize / config.pixelSize),
637 int(config.ySize / config.pixelSize),
638 )
639 self.log.info(
640 "%s: Pixel data exists in %d of %d superpixels; the most populated superpixel is %.1f%% filled",
641 bgModelID,
642 np.sum(bgModelArray > 0),
643 bgModelArray.size,
644 100 * np.max(bgModelArray) / spArea,
645 )
646
647 thresh = config.minFrac * spArea
648 if np.all(bgModelArray < thresh):
649 raise RuntimeError(
650 f"No background model superpixels are more than {100*config.minFrac}% filled. "
651 "Try decreasing the minFrac configuration parameter, optimizing the subset of detectors "
652 "being processed, or increasing the number of detectors being processed."
653 )
654
655 with warnings.catch_warnings():
656 warnings.filterwarnings("ignore", r"invalid value encountered")
657 stats = np.nanpercentile(bgModel.getStatsImage().array, [50, 75, 25])
658 self.log.info(
659 "%s: FP BG median = %.1f counts, FP BG IQR = %.1f counts",
660 bgModelID,
661 stats[0],
662 np.subtract(*stats[1:]),
663 )
664
665 def _fitSkyFrame(self, calExps, masks, skyCorrs, skyFrames, backgroundToPhotometricRatioHandles):
666 """Determine the full focal plane sky frame scale factor relative to
667 an input list of calibrated exposures.
668
669 This method measures the sky frame scale on all inputs, resulting in
670 values equal to the background method solveScales().
671
672 Input skyCorrs are updated in-place.
673
674 Parameters
675 ----------
676 calExps : `list` [`lsst.afw.image.ExposureF`]
677 Calibrated exposures to be background subtracted.
678 masks : `list` [`lsst.afw.image.Mask`]
679 Masks associated with the input calibrated exposures.
680 skyCorrs : `list` [`lsst.afw.math.BackgroundList`]
681 Background lists associated with the input calibrated exposures.
682 skyFrames : `list` [`lsst.afw.image.ExposureF`]
683 Sky frame calibration data for the input detectors.
684
685 Returns
686 -------
687 scale : `float`
688 Fitted scale factor applied to the sky frame.
689 """
690 skyBkgs = []
691 scales = []
692 for calExpHandle, mask, skyCorr, skyFrameHandle, backgroundToPhotometricRatioHandle in zip(
693 calExps, masks, skyCorrs, skyFrames, backgroundToPhotometricRatioHandles
694 ):
695 calExp = self._getCalExp(calExpHandle, mask, skyCorr, backgroundToPhotometricRatioHandle)
696 skyFrame = self._getSkyFrame(skyFrameHandle)
697 skyBkg = self.sky.exposureToBackground(skyFrame)
698 del skyFrame # Free up memory
699 skyBkgs.append(skyBkg)
700 # Return a tuple of gridded image and sky frame clipped means
701 samples = self.sky.measureScale(calExp.getMaskedImage(), skyBkg)
702 scales.append(samples)
703 scale = self.sky.solveScales(scales)
704 for skyCorr, skyBkg in zip(skyCorrs, skyBkgs):
705 bgData = list(skyBkg[0])
706 bg = bgData[0]
707 statsImage = bg.getStatsImage().clone()
708 statsImage *= scale
709 newBg = BackgroundMI(bg.getImageBBox(), statsImage)
710 newBgData = [newBg] + bgData[1:]
711 skyCorr.append(newBgData)
712 self.log.info("Sky frame subtracted with a scale factor of %.5f", scale)
713 return scale
A class to evaluate image background levels.
Definition Background.h:435
__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)
_skyFrameLookup(datasetType, registry, quantumDataId, collections)