SAR values come out negative, huge or averaged wrongly

On this page

Problem statement

A Sentinel-1 array should hold small positive numbers, and yet the minimum is −32768, the maximum is in the thousands, a histogram is a single spike at zero, or a zonal mean comes out several decibels away from what the same zone looks like on screen. These are four faces of one problem: backscatter arrives in linear power with a nodata convention, and every one of those symptoms comes from treating it as something else.

On a Sentinel-1 RTC frame over Valencia, a window at the frame edge is 32.7% nodata; left in, it drags the window's mean to −10,715. Fifty-one of its valid pixels sit above 0 dB, which is a corner reflector and not an error. And averaging the open-water class of the same scene in decibels rather than in power moves its mean by 8.6 dB.

Quick answer

Mask nodata, keep linear power for arithmetic, and convert to decibels only for display and thresholds:

import numpy as np
import rasterio
from sar_io import search

item = search([-0.46, 39.25, -0.33, 39.45], "2024-10-20/2024-10-20", orbit=110)[0]
with rasterio.open(item.assets["vv"].href) as src:
    raw = src.read(1, window=((192, 592), (4800, 5200)))    # straddles the frame edge
    nodata = src.nodata

print(f"raw: min {raw.min():.0f}, max {raw.max():.1f}, nodata {nodata}, "
      f"pixels at nodata {np.mean(raw == nodata):.1%}, at zero {np.mean(raw == 0):.1%}")

vv = np.where(raw > 0, raw, np.nan)                 # nodata and zeros out
db = 10 * np.log10(vv)
print(f"valid: {np.nanmin(vv):.5f} to {np.nanmax(vv):.1f} linear "
      f"= {np.nanmin(db):.1f} to {np.nanmax(db):.1f} dB")
raw: min -32768, max 2.9, nodata -32768.0, pixels at nodata 32.7%, at zero 0.0%
valid: 0.00734 to 2.9 linear = -21.3 to 4.7 dB

Positive decibels are not an error. A corner formed by two flat surfaces can return more power than the diffuse reference the coefficient is defined against.

Symptoms of SAR values read in the wrong units, and the fix for each. what the numbers are telling yousymptomcause and fixminimum is −32768nodata left in — mask beforearithmeticmean is −10,715same cause, now invisible in anumbervalues above 1 (positive dB)corner reflectors — legitimate,keep themzonal mean several dB offaveraged decibels — average powerinsteadOnly the third row is not a mistake spatialworkflow.io
Four symptoms, three causes: nodata left in, units confused, arithmetic in the wrong scale.

Read nodata, drop zeros, compute in linear power

1. Read the nodata value from the file

Do not hard-code it. src.nodata is −32768.0 for these products; other providers differ.

2. Drop zeros as well

Frame padding and some processing steps leave exact zeros. They are not measurements, and their logarithm is minus infinity.

3. Expect a wide range

Valid linear values span roughly 0.0001 to several thousand. Water sits near 0.005 (−23 dB), fields near 0.1 (−10 dB), and corner reflectors above 1 (positive dB).

4. Do arithmetic in linear power

Means, composites, resampling and filters operate on power. Convert the result once.

5. Check the scale convention

Power values use 10·log₁₀; amplitude values use 20·log₁₀. If a provider's decibels are twice what you expect, the product holds amplitude.

6. Check for pre-scaled integers

Some archives store decibels as scaled integers with a scale_factor. Reading them as power gives values of thousands that are not corner reflectors.

The value ranges in a Sentinel-1 RTC array, from nodata through water and fields to corner reflectors. what lives in a Sentinel-1 RTC array−32768nodata: mask it0.005 – 0.5water to fields: −23 to −3 dB> 1corner reflectors: 0 dB and upwater-23 dBfields-10 dBtowns-4 dBships, roofs5 dBA window 32.7% nodata averaged −10,715 with the flag left in, and 0.116 without it spatialworkflow.io
One array, three kinds of number: nodata, real backscatter and legitimate bright targets.

What surviving nodata does

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 — what nodata does if it survives

bad_mean = raw.mean()
good_mean = np.nanmean(vv)
print(f"mean with nodata included: {bad_mean:,.1f} (in linear units!)   "
      f"mean of valid pixels: {good_mean:.4f} = {10 * np.log10(good_mean):.2f} dB")
with np.errstate(divide="ignore", invalid="ignore"):
    print("log of nodata:", np.log10(np.array([-32768.0, 0.0])))
mean with nodata included: -10,714.9 (in linear units!)   mean of valid pixels: 0.1164 = -9.34 dB
log of nodata: [ nan -inf]

Example 2 — the bright pixels are real

bright = vv[vv > 1]
print(f"{bright.size} pixels above 1 (0 dB) out of {np.isfinite(vv).sum():,}: "
      f"up to {np.nanmax(vv):.0f} linear = {10 * np.log10(np.nanmax(vv)):.1f} dB")
51 pixels above 1 (0 dB) out of 107,681: up to 3 linear = 4.7 dB

A handful of pixels far above the rest is what a metal roof, a ship or a pylon looks like. Clipping them changes every mean that includes them.

Example 3 — the decibel mean, once more

zone = vv[np.isfinite(vv)]
print(f"mean of power -> {10 * np.log10(zone.mean()):.2f} dB")
print(f"mean of dB    -> {np.mean(10 * np.log10(zone)):.2f} dB")
print(f"median (same either way) -> {10 * np.log10(np.median(zone)):.2f} dB")
mean of power -> -9.34 dB
mean of dB    -> -9.94 dB
median (same either way) -> -9.92 dB

Why nodata is a number and bright values are real

Why nodata is −32768 and not NaN

The value comes from the integer conventions of earlier products and has been kept for compatibility. It is far outside the valid range on purpose, so it is obvious when it survives — as long as someone looks. In a mean it is not obvious at all: it drags the result towards a large negative number that still prints as a number.

Why values above one are legitimate

Backscatter coefficients compare the power returned by a surface with that of an ideal diffuse reflector of the same area. A corner reflector directs almost all the energy back to the sensor rather than scattering it, so the ratio exceeds one. Positive decibels over cities, ships and metal structures are expected; over a field they would not be.

Why the median is safe in either scale

The logarithm is a monotonic transformation, so it preserves order and therefore preserves the median and every percentile. Only operations that add values — means, sums, convolutions — care which scale they are in. That makes percentile statistics a convenient way to report backscatter without worrying about it.

Which operations on SAR values are safe in linear power, in decibels, or in both. safe in which scalelinear powerdecibelsmean, sum✓correct✕biased lowfilter, resample✓correct✕biased lowmedian, percentile✓same✓sameratio✓divide✓subtractthreshold, display!unreadable✓what to useAnything that adds values cares about the scale; anything order-preserving does not spatialworkflow.io
Order-preserving operations are safe in either scale; anything that adds is not.

Provider conventions and amplitude products

  • Different providers, different nodata: 0, −9999 and NaN all appear; always read it from the file.
  • Float32 precision is ample for backscatter; the range is not the problem, the conventions are.
  • Clipping for display is fine; clipping before statistics is not.
  • Negative decibels are normal — most of the land is between −25 and 0 dB.
  • Sigma nought and gamma nought differ; mixing them shifts values by the incidence angle's cosine.
  • Amplitude products need 20·log₁₀ and have a different noise floor.

FAQ

What does -32768 mean in Sentinel-1 data?

It is the nodata value. Mask it before any arithmetic.

Why are some SAR values greater than one?

Corner reflectors — buildings, ships, metal — return more power than the diffuse reference. Positive decibels are legitimate.

Why is my zonal mean wrong?

Almost always because it was computed on decibels. Average power and convert once.

Should I clip bright pixels?

For display, yes. For statistics, no — they are real measurements.

Is 10·log10 or 20·log10 right?

10·log₁₀ for power, 20·log₁₀ for amplitude. Sentinel-1 RTC holds power.

Are medians affected by the scale?

No. Percentiles are preserved by the logarithm.