Skip to content

Commit 60f2a46

Browse files
committed
Introduce summary value for sky background lumpiness
1 parent 495290a commit 60f2a46

File tree

2 files changed

+77
-12
lines changed

2 files changed

+77
-12
lines changed

python/lsst/pipe/tasks/computeExposureSummaryStats.py

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,22 @@
2222
__all__ = ["ComputeExposureSummaryStatsTask", "ComputeExposureSummaryStatsConfig"]
2323

2424
import warnings
25-
import numpy as np
26-
from scipy.stats import median_abs_deviation as sigmaMad
27-
import astropy.units as units
28-
from astropy.time import Time
29-
from astropy.coordinates import AltAz, SkyCoord, EarthLocation
30-
from lsst.daf.base import DateTime
3125

32-
import lsst.pipe.base as pipeBase
33-
import lsst.pex.config as pexConfig
34-
import lsst.afw.math as afwMath
26+
import astropy.units as units
3527
import lsst.afw.image as afwImage
28+
import lsst.afw.math as afwMath
3629
import lsst.geom as geom
30+
import lsst.ip.isr as ipIsr
31+
import lsst.pex.config as pexConfig
32+
import lsst.pipe.base as pipeBase
33+
import numpy as np
34+
from astropy.coordinates import AltAz, EarthLocation, SkyCoord
35+
from astropy.time import Time
36+
from lsst.daf.base import DateTime
3737
from lsst.meas.algorithms import ScienceSourceSelectorTask
3838
from lsst.utils.timer import timeMethod
39-
import lsst.ip.isr as ipIsr
39+
from scipy.stats import median_abs_deviation as sigmaMad
40+
from skimage.util import view_as_windows
4041

4142

4243
class ComputeExposureSummaryStatsConfig(pexConfig.Config):
@@ -177,6 +178,7 @@ class ComputeExposureSummaryStatsTask(pipeBase.Task):
177178
- zenithDistance
178179
- zeroPoint
179180
- skyBg
181+
- skyLumpiness
180182
- skyNoise
181183
- meanVar
182184
- raCorners
@@ -522,7 +524,12 @@ def update_background_stats(self, summary, background):
522524
Parameters
523525
----------
524526
summary : `lsst.afw.image.ExposureSummaryStats`
525-
Summary object to update in-place.
527+
Summary object to update in-place. This method adds the following
528+
fields:
529+
- `skyBg`: Median sky background value across background models.
530+
- `skyLumpiness`: A measure of sky background lumpiness based on
531+
the spatial variation of the final background image while remaining
532+
robust to outliers, including possible hot pixels.
526533
background : `lsst.afw.math.BackgroundList` or `None`
527534
Background model. If `None`, all fields that depend on the
528535
background will be reset (generally to NaN).
@@ -538,8 +545,15 @@ def update_background_stats(self, summary, background):
538545
bgStats = (bg[0].getStatsImage().getImage().array
539546
for bg in background)
540547
summary.skyBg = float(sum(np.median(bg[np.isfinite(bg)]) for bg in bgStats))
548+
# NOTE: We could ideally use the full background image below to
549+
# compute skyBg, but that slightly changes tests and other results.
550+
# Leaving it as-is for now but this may need to be revisited.
551+
bgarr = background.getImage().array
552+
bgarr[~np.isfinite(bgarr)] = np.nan
553+
summary.skyLumpiness = compute_lumpiness(bgarr)
541554
else:
542555
summary.skyBg = float("nan")
556+
summary.skyLumpiness = float("nan")
543557

544558
def update_masked_image_stats(self, summary, masked_image):
545559
"""Compute summary-statistic fields that depend on the masked image
@@ -899,6 +913,57 @@ def compute_ap_corr_sigma_scaled_delta(
899913
return ap_corr_sigma_scaled_delta
900914

901915

916+
def compute_lumpiness(imarr: np.ndarray, patch_size: int = 16) -> float:
917+
"""Estimate spatial roughness or lumpiness of an image.
918+
919+
It is based on the Median Absolute Deviation (MAD) of local patch means,
920+
normalized by the interquartile range (IQR) of the image values to allow
921+
comparison across images with different background levels. In other words,
922+
it answers: how much patch-to-patch variation exists, relative to the
923+
overall variation in the image (as measured by the IQR). The method is
924+
robust to outliers and works best on moderately sized or larger images.
925+
926+
Parameters
927+
----------
928+
imarr : `numpy.ndarray`
929+
2D input image array.
930+
patch_size : `int`
931+
Size of square patches to divide the image into (default is 16).
932+
Patches are non-overlapping.
933+
934+
Returns
935+
-------
936+
lumpiness : `float`
937+
A scalar lumpiness value. Higher values indicate more spatial
938+
variability across the image and lower values indicate a more uniform
939+
image.
940+
"""
941+
# If the image is smaller than the patch size, return NaN.
942+
if patch_size > min(imarr.shape):
943+
return float("nan")
944+
945+
# Pad image with NaNs to make dimensions divisible by patch_size.
946+
pad_h = (-imarr.shape[0]) % patch_size
947+
pad_w = (-imarr.shape[1]) % patch_size
948+
imarr = np.pad(imarr, ((0, pad_h), (0, pad_w)), mode="constant", constant_values=np.nan)
949+
950+
# Use a memory-efficient view to tile the image into non-overlapping
951+
# patches.
952+
windows = view_as_windows(imarr, (patch_size, patch_size), step=patch_size)
953+
954+
# Compute the mean of each patch (last two axes) to summarize local values.
955+
local_means = np.nanmean(windows, axis=(-2, -1)).ravel()
956+
957+
# Compute the interquartile range (IQR) of the image values.
958+
iqr = np.subtract(*np.nanpercentile(imarr, [75, 25]))
959+
960+
# Estimate lumpiness as the median absolute deviation of local means
961+
# normalized by the interquartile range. MAD is normalized to match
962+
# Gaussian sigma.
963+
lumpiness = float(sigmaMad(local_means, scale="normal", nan_policy="omit")/iqr) if iqr else float("nan")
964+
return lumpiness
965+
966+
902967
def compute_magnitude_limit(
903968
psfArea,
904969
skyBg,

python/lsst/pipe/tasks/postprocess.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1520,7 +1520,7 @@ def run(self, visitSummaryRefs):
15201520
summaryTable = visitSummary.asAstropy()
15211521
selectColumns = ["id", "visit", "physical_filter", "band", "ra", "dec",
15221522
"pixelScale", "zenithDistance",
1523-
"expTime", "zeroPoint", "psfSigma", "skyBg", "skyNoise",
1523+
"expTime", "zeroPoint", "psfSigma", "skyBg", "skyLumpiness", "skyNoise",
15241524
"astromOffsetMean", "astromOffsetStd", "nPsfStar",
15251525
"psfStarDeltaE1Median", "psfStarDeltaE2Median",
15261526
"psfStarDeltaE1Scatter", "psfStarDeltaE2Scatter",

0 commit comments

Comments
 (0)