SAR geometry explained: layover, foreshortening, shadow and look direction

On this page

Problem statement

A radar satellite does not take a picture from above. It looks sideways, at an incidence angle of about 30–46Β° for Sentinel-1, and places each echo by how far away it came from. On flat ground distance and position agree. On a slope facing the sensor the top is nearly as close as the foot, so the slope is squeezed into a few pixels β€” and if the slope is steeper than the incidence angle, the top is closer and lands in front of the foot. A slope facing away and steeper than 90Β° minus that angle is never lit at all.

All of this comes from one fact: a radar measures distance to the sensor, not position on the ground, and it looks sideways. Terrain correction repairs most of the positional distortion and much of the brightness, but not all of it. In Sentinel-1 RTC data over the Sierra Nevada in Spain, steep slopes facing the sensor are still 1.3–2.6 dB brighter than slopes facing away, and which slopes those are flips between ascending and descending passes.

Quick answer

Know which way the sensor was looking, and treat slopes facing it and slopes facing away differently:

import numpy as np, rasterio, pystac_client, planetary_computer
from rasterio.enums import Resampling
from rasterio.vrt import WarpedVRT
from sar_io import STAC, search, passes, grid, read_pass, to_db

bbox = [-3.45, 37.00, -3.25, 37.12]                    # Sierra Nevada, Spain
g = grid(bbox, "EPSG:25830", res=20)
cat = pystac_client.Client.open(STAC, modifier=planetary_computer.sign_inplace)
dem = np.full((g["height"], g["width"]), np.nan, "float32")
for it in cat.search(collections=["cop-dem-glo-30"], bbox=bbox).items():
    with rasterio.open(it.assets["data"].href) as src, WarpedVRT(
            src, crs=g["crs"], transform=g["transform"], width=g["width"],
            height=g["height"], resampling=Resampling.bilinear, nodata=np.nan) as v:
        a = v.read(1)
    dem[np.isnan(dem) & np.isfinite(a)] = a[np.isnan(dem) & np.isfinite(a)]

gy, gx = np.gradient(dem, 20)                          # per row (south) and per column (east)
slope = np.degrees(np.arctan(np.hypot(gx, gy)))
aspect = (np.degrees(np.arctan2(-gx, gy)) + 360) % 360  # compass direction the slope faces
print(f"elevation {np.nanmin(dem):.0f}–{np.nanmax(dem):.0f} m, median slope {np.nanmedian(slope):.1f}Β°")
elevation 1402–3471 m, median slope 22.6Β°

Ascending Sentinel-1 passes fly roughly north and look east; descending passes fly south and look west. A slope that faces west faces an ascending sensor.

Foreshortening, layover and shadow for a side-looking radar over a mountain. foreshorteningbright, narrowfacing slope squeezedlayoversummit nearertop imaged before baseshadowno signalfar side never litThe sensor is up and to the left; every distortion comes from measuring slant range spatialworkflow.io
Three distortions, all caused by measuring range from a sensor that looks sideways.

Look direction, foreshortening, layover and shadow

1. Find the look direction

sat:orbit_state on each item says ascending or descending. Sentinel-1 is right-looking, so ascending passes look roughly east (about 80Β° from north at mid-latitudes) and descending passes roughly west (about 280Β°).

2. Expect foreshortening on slopes facing the sensor

A slope tilted towards the sensor spans less range than its ground length, so it is compressed into fewer pixels and each pixel collects more energy: bright, narrow stripes.

3. Expect layover where those slopes are steeper than the incidence angle

If a slope facing the sensor is steeper than the incidence angle β€” around 30–46Β° for Sentinel-1 β€” its top is closer to the sensor than its base, and it is imaged in reverse, on top of whatever lies in front of it. No correction can separate the mixed signals.

4. Expect shadow behind steep slopes facing away

A slope facing away and steeper than 90Β° minus the incidence angle is not illuminated at all. Shadowed pixels contain only noise.

5. Use terrain-corrected products, and know their limits

RTC products put pixels in their true positions and normalise brightness for the local illuminated area. They cannot recover layover or shadow; some RTC products ship a mask marking them, but the Planetary Computer items used here carry only VV and VH, so the mask has to be computed from a DEM, as in Example 2.

6. Combine passes where geometry matters

Areas in layover on an ascending pass are often clear on a descending one. For mountains, use both and take whichever sees each slope best.

Median VV on steep slopes facing east and west in the Sierra Nevada, from an ascending and a descending pass. median VV on slopes steeper than 25Β°, August 2024east-facingwest-facingascending (looks east)βˆ’8.4 dBβˆ’7.1 dB Β· facing sensordescending (looks west)βˆ’6.0 dB Β· facing sensorβˆ’8.6 dBRTC data from orbits 1 and 81, one week apart; the brighter cell swaps with the pass spatialworkflow.io
Whichever way the sensor looks, the slopes facing it are brighter β€” even after terrain correction.

Slopes seen from both pass directions

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 β€” slopes facing and facing away, from both directions

P = passes(search(bbox, "2024-08-01/2024-08-07"))
for (day, orbit), frames in P.items():
    if orbit not in (1, 81):                   # the two passes that cover the whole box
        continue
    db = to_db(read_pass(frames, "vv", g))
    looks = frames[0].properties["sat:orbit_state"]
    out = []
    for name, lo, hi in [("east-facing", 45, 135), ("west-facing", 225, 315)]:
        m = (slope > 25) & (aspect >= lo) & (aspect < hi) & np.isfinite(db)
        out.append(f"{name} {np.median(db[m]):6.1f} dB")
    print(day, f"{looks:10}", " | ".join(out), f"| coverage {np.isfinite(db).mean():.0%}")
2024-08-01 ascending  east-facing   -8.4 dB | west-facing   -7.1 dB | coverage 100%
2024-08-07 descending east-facing   -6.0 dB | west-facing   -8.6 dB | coverage 100%

Example 2 β€” how much of the scene is in layover or shadow

def distortion(look_azimuth_deg, incidence_deg=39):
    """Share of pixels in layover or shadow for a sensor looking along this azimuth."""
    az = np.radians(look_azimuth_deg)
    north = -gy                                     # rows run south
    rise = gx * np.sin(az) + north * np.cos(az)     # terrain gradient along the look direction
    layover = rise > np.tan(np.radians(incidence_deg))
    shadow = -rise > np.tan(np.radians(90 - incidence_deg))
    ok = np.isfinite(rise)
    return layover[ok].mean(), shadow[ok].mean()

for name, az in [("ascending, looking east", 80), ("descending, looking west", 280)]:
    lay, sha = distortion(az)
    print(f"{name:26} layover {lay:6.2%}  shadow {sha:6.2%}")
ascending, looking east    layover  0.77%  shadow  0.15%
descending, looking west   layover  1.25%  shadow  0.06%

The incidence angle is taken as 39Β°, the middle of Sentinel-1's swath; near range, at 30Β°, layover affects more of the scene, and far range, at 46Β°, less.

Why a sideways look distorts terrain

Why a sideways look creates these distortions

A radar places each echo by the time it takes to return, which is its slant distance from the sensor. On flat ground, slant distance increases steadily across the swath. On a slope facing the sensor, points up the slope are barely farther than points at its foot, so they crowd together; past a critical steepness they are nearer, and the order reverses. Behind a ridge, the pulse never arrives.

Why terrain correction helps but does not finish the job

Geocoding moves each pixel to its true position using a DEM, removing the displacement. Radiometric terrain flattening divides by the illuminated area each pixel represents, removing most of the brightness exaggeration. What remains is a genuine difference in how steep surfaces scatter towards and away from the sensor β€” and layover and shadow, where the information was never recorded separately.

Why ascending and descending disagree

The two passes look from opposite sides. A west-facing slope faces an ascending sensor and turns away from a descending one, so its brightness and its distortions swap between them. Mixing passes in a time series without accounting for that creates artificial changes on every slope.

Classifying a slope pixel as layover, shadow or usable from its steepness along the look direction. is this pixel usable?How steep is the slope along the lookdirection?facing, > ΞΈlayovermixed, unrecoverableaway, > 90Β° βˆ’ ΞΈshadownoise onlyotherwiseusable after RTCresidual 1–3 dBΞΈ is the incidence angle, 30–46Β° across the swath; here 0.8–1.3% of pixels failed spatialworkflow.io
Two thresholds, both set by the incidence angle, decide which pixels carry no usable signal.

Swath incidence, cities and DEM quality

  • Incidence varies across the swath, so the same slope can be fine at far range and in layover at near range.
  • Look azimuth varies with latitude; compute it from orbit metadata for precise work.
  • Cities show layover too: tall buildings lean towards the sensor.
  • DEM quality limits both terrain correction and computed masks.
  • Flat areas are nearly unaffected; these effects matter in mountains and cities.
  • InSAR measures motion along the line of sight, which is why slope orientation matters there too.

FAQ

What is layover in SAR?

Where a slope facing the sensor is steeper than the incidence angle, its top is imaged before its base and overlaps what lies in front.

What is radar shadow?

Areas behind slopes facing away that the pulse never reaches; they contain only noise.

Does terrain correction fix layover?

It fixes positions and most brightness effects, but it cannot separate signals mixed by layover or recover shadow.

Which way does Sentinel-1 look?

To the right of its flight path: roughly east on ascending passes and west on descending ones.

Why do ascending and descending images differ?

They see the terrain from opposite sides, so different slopes face the sensor.

Should I combine ascending and descending?

For mountains, yes β€” but keep them separate in time series.