Speckle explained: why radar images look grainy
On this page
Problem statement
A radar pixel is not a reading of one surface. It is the sum of the echoes from every scatterer inside the cell — stalks, ripples, stones — each arriving with its own phase, and because the radar is coherent those echoes reinforce or cancel according to path differences of a fraction of a wavelength. Neighbouring pixels of the same lake or field therefore return different intensities, scattered around the true value with known statistics. That scatter is speckle: not noise added by the instrument, but a property of how a coherent image is formed, and reduced only by averaging independent looks.
Knowing those statistics is what makes speckle manageable. On a patch of the Albufera lagoon south of Valencia, single Sentinel-1 pixels scatter with a standard deviation of 1.6 dB around the patch mean, and averaging a handful of pixels or dates shrinks that predictably.
Quick answer
Measure speckle on a homogeneous patch with the equivalent number of looks, ENL = mean² / variance, computed on linear intensity:
import numpy as np
from sar_io import search, passes, grid, read_pass, to_db
bbox = [-0.46, 39.25, -0.33, 39.45]
g = grid(bbox, "EPSG:25830")
frames = next(iter(passes(search(bbox, "2024-10-20/2024-10-20", orbit=110)).values()))
vv = read_pass(frames, "vv", g)
def patch(img, lon, lat, size=40):
from pyproj import Transformer
x, y = Transformer.from_crs(4326, g["crs"], always_xy=True).transform(lon, lat)
col, row = ~g["transform"] * (x, y)
r, c = int(row), int(col)
return img[r - size // 2:r + size // 2, c - size // 2:c + size // 2]
lake = patch(vv, -0.352, 39.333) # open water in the lagoon
enl = lake.mean() ** 2 / lake.var()
print(f"ENL {enl:.1f}; spread of single pixels {to_db(lake).std():.2f} dB")
ENL 4.4; spread of single pixels 1.60 dB
A higher ENL means less speckle. Single-look complex data has ENL 1; the GRD products Sentinel-1 RTC is made from are multi-looked to about 4–5.
Recognise it, measure it, average it in linear units
1. Recognise it
Speckle is a granular texture at the scale of single pixels, strongest in bright areas because it is multiplicative: its size scales with the signal.
2. Measure it on something uniform
Choose a patch that should be homogeneous — calm water, a large bare field — and compute ENL on linear values. Do not compute it on decibels; the formula assumes intensity.
3. Reduce it by averaging
Averaging N independent samples raises ENL N-fold. The samples can be neighbouring pixels (spatial filtering), different dates (temporal averaging), or both — but neighbouring pixels in a terrain-corrected product are not independent. On the lagoon a 3×3 mean, nine pixels, raised ENL only from 4.4 to 6.5, and a 7×7 mean, forty-nine pixels, to 11.6.
4. Choose spatial or temporal
Spatial filters cost resolution and blur edges. Temporal averaging keeps resolution but assumes the surface did not change. A flood map needs spatial filtering of one date; a land-cover map can average a season.
5. Filter in linear units
Average intensities, not decibels. The mean of decibels is the log of the geometric mean, which is biased low — by 0.3 dB on the lagoon patch below, and by more on single-look data.
6. Do not expect it to vanish
Filters trade speckle for blur. After filtering, the remaining variation is a mix of real texture and residual speckle, and no filter separates them perfectly.
Spatial against temporal averaging
The data used on this page
Sentinel-1 radiometrically terrain-corrected backscatter (sentinel-1-rtc) from the Microsoft Planetary Computer, read through sar_io.py — the small module from the reading guide that searches, groups frames into passes and mosaics each pass onto a fixed grid.
%%writefile sar_io.py
"""Find, read and mosaic Sentinel-1 RTC scenes from Microsoft Planetary Computer."""
from collections import defaultdict
import numpy as np
import planetary_computer
import pystac_client
import rasterio
from rasterio.enums import Resampling
from rasterio.transform import from_origin
from rasterio.vrt import WarpedVRT
from rasterio.warp import transform_bounds
STAC = "https://planetarycomputer.microsoft.com/api/stac/v1"
def search(bbox, dates, orbit=None, direction=None, collection="sentinel-1-rtc"):
"""Items over bbox (lon/lat) in a date range, filtered by relative orbit and pass direction."""
cat = pystac_client.Client.open(STAC, modifier=planetary_computer.sign_inplace)
items = cat.search(collections=[collection], bbox=bbox, datetime=dates).item_collection()
keep = [i for i in items
if (orbit is None or i.properties["sat:relative_orbit"] == orbit)
and (direction is None or i.properties["sat:orbit_state"] == direction)]
return sorted(keep, key=lambda i: i.datetime)
def passes(items):
"""Group frames into passes: same day, same relative orbit."""
groups = defaultdict(list)
for i in items:
groups[(i.datetime.date(), i.properties["sat:relative_orbit"])].append(i)
return dict(sorted(groups.items()))
def grid(bbox, crs, res=10):
"""A fixed target grid: every pass is read onto this, so arrays line up."""
left, bottom, right, top = transform_bounds(4326, crs, *bbox)
width, height = int((right - left) // res), int((top - bottom) // res)
return {"crs": crs, "transform": from_origin(left, top, res, res),
"width": width, "height": height}
def read_pass(frames, band, g):
"""Mosaic one pass's frames for one band onto grid g. Linear power; NaN where no data."""
out = np.full((g["height"], g["width"]), np.nan, dtype="float32")
for item in frames:
with rasterio.open(item.assets[band].href) as src, WarpedVRT(
src, crs=g["crs"], transform=g["transform"], width=g["width"],
height=g["height"], resampling=Resampling.bilinear, nodata=np.nan) as vrt:
a = vrt.read(1)
a[a <= 0] = np.nan # -32768 nodata and zero-padding
fill = np.isnan(out) & np.isfinite(a)
out[fill] = a[fill]
return out
def to_db(linear):
return 10 * np.log10(linear)
Overwriting sar_io.py
Example 1 — spatial averaging
from scipy.ndimage import uniform_filter
def nan_mean_filter(img, k):
"""Box mean that ignores NaN: one missing pixel would otherwise blank k² neighbours."""
ok = np.isfinite(img)
total = uniform_filter(np.where(ok, img, 0), size=k)
count = uniform_filter(ok.astype("float32"), size=k)
return np.where(count > 0, total / np.maximum(count, 1e-9), np.nan)
print("missing pixels in the scene:", int(np.isnan(vv).sum()))
for k in (1, 3, 7):
avg = nan_mean_filter(vv, k) if k > 1 else vv
p = patch(avg, -0.352, 39.333)
print(f"{k}x{k} mean: ENL {p.mean() ** 2 / p.var():6.1f}, spread {to_db(p).std():.2f} dB")
missing pixels in the scene: 49
1x1 mean: ENL 4.4, spread 1.60 dB
3x3 mean: ENL 6.5, spread 1.24 dB
7x7 mean: ENL 11.6, spread 0.91 dB
Example 2 — temporal averaging over nine dates
series = passes(search(bbox, "2024-07-01/2024-10-20", orbit=110))
stack = np.stack([read_pass(f, "vv", g) for f in list(series.values())[-9:]])
mean9 = np.nanmean(stack, axis=0)
p = patch(mean9, -0.352, 39.333)
print(f"{len(stack)} dates: ENL {p.mean() ** 2 / p.var():.1f}, spread {to_db(p).std():.2f} dB")
9 dates: ENL 18.9, spread 0.84 dB
Nine dates give an ENL of 18.9, not the 40 that nine independent looks of 4.4 would. The lagoon is not the same surface on every date — wind roughens it — so temporal averaging mixes real change with speckle, which is exactly the assumption step 4 warns about.
Example 3 — the bias of averaging in decibels
lin_mean = to_db(lake.mean())
db_mean = to_db(lake).mean()
print(f"mean of linear, in dB: {lin_mean:.2f} mean of dB: {db_mean:.2f} "
f"bias {db_mean - lin_mean:.2f} dB")
mean of linear, in dB: -19.54 mean of dB: -19.87 bias -0.32 dB
Why coherent imaging produces speckle
Why coherent imaging produces speckle
Each pixel contains many small scatterers at slightly different distances from the sensor. Their echoes arrive with random relative phases and add as vectors. Sometimes they reinforce and the pixel is bright; sometimes they cancel and it is dark. Two pixels of the same surface have different random arrangements and so different sums, even though nothing about the surface differs.
Why intensity follows a known distribution
For a fully developed speckle field, single-look intensity is exponentially distributed: its standard deviation equals its mean, so ENL is 1. Averaging L independent looks gives a gamma distribution with ENL L. That is why ENL is the natural measure, and why the spread in decibels falls predictably as looks are added.
Why decibel averages are biased
The logarithm is concave, so the average of logs is less than the log of the average. For gamma-distributed intensity the gap depends only on the number of looks and is largest for single pixels. Averaging in decibels therefore darkens every speckled surface, and darkens it by different amounts depending on how much filtering has already been applied.
Wind on water, point targets and resampling
- Wind on water makes lakes non-homogeneous; pick calm patches for ENL.
- Point targets such as ships and pylons are not speckled; they dominate any average they fall in.
- Resampling during terrain correction correlates neighbouring pixels, so spatial averages gain fewer looks than their pixel count.
- Coherence between dates is lost over vegetation, so temporal speckle is independent; over cities it is not.
- Deep-learning despeckling exists; validate it on known homogeneous areas.
- Report ENL with any filtered product so readers know how much averaging was done.
Internal links
- SAR imagery explained — what the pixels measure
- How to filter speckle in Sentinel-1 images — Lee and other filters
- A SAR image looks noisy after processing — when speckle is the problem
- SAR values come out negative, huge or averaged wrongly — dB and linear
- How to make a Sentinel-1 composite over a period — temporal averaging in practice
- How to convert SAR backscatter to decibels — the units
FAQ
What is speckle in SAR?
A granular texture caused by random interference between the echoes of many scatterers within each pixel.
Is speckle noise?
Not in the usual sense. It is deterministic for a given arrangement of scatterers and follows known statistics.
How do I measure speckle?
With the equivalent number of looks, mean² / variance of linear intensity over a homogeneous area.
How do I reduce it?
Average: spatially with a filter, or over several dates.
Should I filter in dB or linear?
Linear. Averaging decibels is biased low.
Why is speckle worse in bright areas?
It is multiplicative, so its absolute size scales with the signal.