OPERA RTC-S1 backscatter — download + read + plot¶
A full real-world workflow with the asf backend: discover an OPERA Sentinel-1 Radiometric-Terrain-Corrected scene over a small bbox, download the VH-polarised GeoTIFF (~6 MB), open it with pyramids, and render the backscatter in decibels — the standard SAR visualisation for flood mapping, urban classification, and land-surface monitoring.
Needs the [asf] extra (pip install earthlens[asf]) and NASA Earthdata Login credentials. The download step calls asf_search's authed S3 path; set EARTHDATA_USERNAME + EARTHDATA_PASSWORD (or EARTHDATA_TOKEN, or a ~/.netrc entry for urs.earthdata.nasa.gov) before running. See Authentication.
Setup¶
Imports and the output directory. earthlens provides the unified EarthLens entry point; pyramids reads the GeoTIFF; matplotlib + numpy render it.
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from pyramids.dataset import Dataset
from earthlens.earthlens import EarthLens
OUT_DIR = Path('asf_opera_output')
OUT_DIR.mkdir(exist_ok=True)
1 · Discover an OPERA RTC-S1 scene¶
OPERA RTC scenes are small (~6 MB per polarisation channel), tiled, and analysis-ready. Search over a 0.1° × 0.1° patch of the San Francisco Bay over a 7-day window and take the first scene as the target.
el = EarthLens(
data_source='asf',
variables=['opera-rtc-s1'], # alias 'opera-rtc' also works
start='2024-06-01', end='2024-06-08',
lat_lim=[37.2, 37.3], # SF Bay, ~10 km × 10 km
lon_lim=[-122.3, -122.2],
path=str(OUT_DIR),
max_results=1,
)
[scene] = el.datasource._search()
print('scene :', scene.id)
print('file :', scene.metadata['fileName'])
size_mb = scene.metadata['product'].properties.get('bytes', {}).get(scene.metadata['fileName'], {}).get('bytes', 0) / 1e6
print(f'size : {size_mb:.1f} MB')
2 · Download the scene¶
download() runs the EDL login (lazy — only the first call authenticates) and pulls the VH GeoTIFF to OUT_DIR. A second download() would short-circuit on the existing file (idempotent). The returned list[Path] is what every file-writing earthlens backend returns.
paths = el.download()
print(f'{len(paths)} file(s) on disk:')
for p in paths:
print(f' {p.name} ({p.stat().st_size / 1e6:.1f} MB)')
3 · Open with pyramids¶
OPERA RTC is shipped as a Cloud-Optimized GeoTIFF in linear power units. Read it with pyramids.Dataset.read_file() and inspect the array.
vh_path = paths[0]
ds = Dataset.read_file(str(vh_path))
arr = ds.read_array().astype('float32')
print(f'shape: {arr.shape}, dtype: {arr.dtype}')
print(f'value range: {np.nanmin(arr):.4f} .. {np.nanmax(arr):.4f} (linear power, unitless)')
4 · Convert to decibels and plot¶
Backscatter values in linear units span 5-6 orders of magnitude, so SAR is universally visualised in decibels (10 · log10(σ)). Typical land surfaces sit in the −25 dB to 0 dB range:
- water / smooth surfaces → very low (−25 dB and below; specular reflection)
- bare ground → moderate (−15 to −10 dB)
- vegetation → higher (−10 to −5 dB; volume scattering)
- urban / corner reflectors → highest (0 dB+; double-bounce)
arr = np.where(arr > 0, arr, np.nan) # mask non-positive samples
db = 10 * np.log10(arr)
fig, ax = plt.subplots(figsize=(8, 7))
im = ax.imshow(db, cmap='gray', vmin=-25, vmax=0)
fig.colorbar(im, ax=ax, label='VH backscatter (dB)')
ax.set_title(f'OPERA RTC-S1 VH backscatter\n{vh_path.name}', fontsize=10)
ax.set_axis_off()
plt.tight_layout()
plt.show()
What's in the rest of the bundle?¶
Each OPERA RTC scene actually carries 5 files (the SDK exposes them in product.properties['bytes']): the VH GeoTIFF (downloaded here, ~6 MB), a VV GeoTIFF (also ~6 MB), a mask GeoTIFF flagging shadow / layover / water (~50 KB), an HDF5 metadata file (~100 KB), and an ISO XML sidecar (~160 KB). The backend's _fetch follows the SDK's default — one primary file per product — so a routine call lands the VH. To pull the VV channel as well, set up a second backend instance and walk the bundle URLs from product.properties['bytes'] directly with the authenticated ASFSession.
bundle = scene.metadata['product'].properties.get('bytes', {})
for fname, info in bundle.items():
sz = info.get('bytes', 0)
fmt = info.get('format', '?')
print(f' {fname[:80]:80s} {sz/1e6:>6.2f} MB {fmt}')
Where to go next¶
- Tile a larger AOI by raising
max_results=and looping over the returned paths. - Pair with
opera-dist-alert-s1for an analysis-ready land-surface disturbance flag derived from the RTC time series. - For the InSAR workflow (phase-aware Sentinel-1 SLC + baseline stack), see
sentinel1_insar_stack.ipynb.