SAR Data INspection and Exploration
Browser-native visualization and export for NISAR HDF5 and Cloud Optimized GeoTIFFs
▲ Click the screenshot and your browser starts streaming a full NISAR frame at native 20 m resolution — a ~1 GB Cloud Optimized GeoTIFF pair — rendered as a dual-pol RGB composite (HH / HV / HH÷HV) over the Ucayali floodplain in Pacaya-Samiria, Peru. No login, no server, no download: HTTP Range reads fetch only the tiles you look at (a few MB), so it opens in seconds — then pan and zoom anywhere in the 240×270 km frame down to full detail. To stream the same region live from the NASA archive as HDF5 (CMR granule search + chunked range reads, free Earthdata token required), use this link.
This project is under active development. Some features are experimental or incomplete. Bug reports welcome via GitHub Issues.
Live demo — nicksteiner.github.io/sardine
The hosted build runs entirely in your browser. Three ways to load data:
| What you do | What you need | |
|---|---|---|
| Drag-and-drop | Drop a .tif, NISAR .h5, NITF/SICD, or .geojson onto the page |
A local file |
| Public URL | Paste a public COG URL into "Direct URL" — Capella Open Data, Umbra Open Data, Sentinel-2 cogs, etc. | A CORS-friendly URL |
| Earthdata streaming | Open the Earthdata Login panel, paste a token from urs.earthdata.nasa.gov/profile → Generate Token, and stream NISAR / Sentinel-1 / OPERA directly from NASA DAACs | An EDL token (free) |
NASA DAACs don't send CORS headers, so the hosted build relays data requests through a small Cloudflare Worker (sardine-edl-proxy/, ~200 lines) that adds CORS and forwards your token to NASA. Straight answers about that:
- Your token stays in your browser's localStorage and is sent only to the Worker and NASA servers. It travels in a request header (not the URL). The Worker hard-allowlists NASA hostnames, drops the auth header on any redirect leaving the allowlist, and doesn't log or store tokens — but SARdine is a research project, and you shouldn't have to take our word for it:
- Run your own relay in ~2 minutes. The Worker source is right here in the repo.
cd sardine-edl-proxy && npx wrangler deployputs an identical copy on your free Cloudflare account; paste your*.workers.devURL into SARdine's Earthdata panel (Proxy URL field) and your token never touches ours. - The stakes are low by design. EDL tokens are read-only data-access credentials for (mostly public) NASA data. They expire, and you can revoke them anytime. Worst case is someone downloads free satellite data under your account.
- Running locally?
npm run devuses a local Vite proxy — no Worker involved at all.
SARdine is a browser-native exploration and visualization GUI for NISAR HDF5 data. Drop in a file, explore the data, and export a figure or a georeferenced dataset. Everything runs in the browser; there is nothing to install.
It reads NISAR L2 GCOV HDF5 (.h5) directly using a custom JavaScript HDF5 reader (h5chunk). Rendering runs on the GPU via WebGL2 shaders: dB scaling, colormaps, contrast stretching, and RGB composites stay interactive while panning and zooming. GCOV/GUNW files are 2–8 GB but only viewport-intersecting chunks are ever read.
Explore full-resolution SAR data Drop in a large NISAR file and pan/zoom through it at full resolution. Switch polarizations (HH, HV, VV), adjust contrast with the live histogram, and apply stretch modes (linear, sqrt, gamma, sigmoid).
Make publication-ready figures Enable the dual-pol RGB composite to map polarization channels to color. Export a georeferenced figure PNG with scale bar, corner coordinates, and colorbar. Export scatter plots and histograms as publication SVGs.
Classify and map land cover Draw an ROI, open the 2D feature space scatter (e.g. HH dB vs HV dB), define class regions by drawing rectangles, and see the classification overlay on the map in real time. Filter by incidence angle range (NISAR HDF5 only).
Subset and export data Draw an ROI and export a subregion as a raw Float32 GeoTIFF (linear power, with CRS and tiepoints) or a rendered RGBA GeoTIFF. Works for both single-pol and RGB composites.
Time series and change detection Load multiple dates for ROI time series plotting, or compose a multi-date RGB composite for change detection visualization.
Stream from S3 Paste a presigned URL and stream directly from a bucket — same workflow, no download required. Chunk decompression runs in a Web Worker pool, and a persistent IndexedDB cache speeds up repeat visits to the same scene.
Share exactly what you see
Copy Link captures the data source, render settings, and active ROI as a URL. A link with just a bbox resolves its own granule via NASA CMR (newest, best-coverage, full-frame first) and loads only the region — coordinates outlive granule IDs across reprocessing campaigns. See docs/DEEP_LINKS.md.
Save your markup, keep your provenance
Annotations, ROIs, transects, and classifier regions save/load as GeoJSON with a versioned schema (observer, method, timestamps, per-ROI statistics). Every GeoTIFF export writes a {name}.tif.json sidecar carrying the full product identification, render state, and derived_from lineage.
SARdine requires Node.js (v18 or later) and npm.
git clone https://github.com/nicksteiner/sardine.git
cd sardine
npm install
npm run devOpen http://localhost:5173 in Chrome, Edge, or Firefox (WebGL2 required).
macOS: brew install node or download from https://nodejs.org/
Windows: Download the installer from https://nodejs.org/ (LTS recommended).
Linux (Debian/Ubuntu):
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejsPick a mode from the File Type dropdown in the left panel.
- File Type → NISAR GCOV HDF5 (Local File)
- Choose File → pick your
.h5 - Pick Frequency (
frequencyA= high res,frequencyB= low res) - Pick Polarization (
HHHH,HVHV,VVVV, etc.) - Optionally switch to RGB Composite and pick a preset
- Load Dataset
Nothing is uploaded. Chunks are read directly from disk via the browser File API.
- File Type → Remote Bucket / S3
- Paste the presigned URL into Direct URL
- Load from URL — metadata arrives via Range requests (~8 MB)
- Pick frequency and polarization → Load Remote Dataset
File type is detected from the path: .h5/.hdf5/.he5 → NISAR, .tif/.tiff → COG.
Every load is addressable. Examples (full parameter reference in docs/DEEP_LINKS.md):
?url=<granule .h5/.tif URL>&min=-16&max=2&db=1&cmap=viridis # explicit source + render state
?url=<granule>&bbox=-77.48,38.90,-77.26,39.01 # + region: view, ROI, and chunk
# fetching scoped to the bbox
?bbox=-74.26,40.49,-73.70,40.92&db=1 # region-first: resolves the best
# NISAR granule via NASA CMR
?bbox=…&t=2025-12-01/2025-12-31 # same place, bounded in time
Links carrying a region auto-load (the fetch is bounded); plain granule links stay click-to-load. A verified end-to-end walkthrough against live ASF DAAC data is in docs/DEMO.md.
- File Type → Cloud Optimized GeoTIFF (URL)
- Paste URL (presigned or public)
- Load COG
RGB mode assigns polarization channels to color, which makes land cover patterns easier to see. The most common case is a dual-pol file (HH + HV), which gives you two independent channels — SARdine fills the third using a ratio.
You need: A NISAR GCOV file with at least two polarizations (e.g. HHHH and HVHV).
- Load your file (File Type → NISAR GCOV HDF5 (Local File), click Choose File)
- In the Mode selector, switch from Single Polarization to RGB Composite
- Set Preset to dual-pol-h (HH+HV) or dual-pol-v (VV+VH) depending on your file
- Click Load Dataset — SARdine loads both bands and renders the composite
- Press
Rto auto-stretch contrast across all channels - Open the histogram (
H) — you'll see three overlapping distributions, one per channel - Drag per-channel contrast handles to balance the composite if one color dominates
- Press
Ctrl+Sto export a figure PNG with the RGB triangle colorbar
Tips:
- Water appears dark in all channels — usually a deep blue-black
- Vegetation is bright in cross-pol (HV) — shows as green
- Urban double-bounce is bright in co-pol (HH) — shows as red/orange
- Switch Stretch to
sqrtorgammato bring out low-backscatter features
The feature space classifier lets you define land cover classes by drawing regions on a 2D scatter plot of two polarimetric channels. Classification updates on the map in real time.
You need: A NISAR GCOV file with at least two polarizations (e.g. HHHH and HVHV).
- Load a single-pol dataset first (e.g.
HHHH) so the basemap is visible - Draw an ROI — click and drag on the map to define your analysis region. Keep it focused on your area of interest; large ROIs are slower to scatter.
- Press
Cto open the Feature Space panel - Set the X axis to
HH dBand Y axis toHV dBusing the axis dropdowns - The scatter plot populates with one point per pixel in your ROI
- Click + Add Class and give it a name (e.g. "Water")
- Draw a rectangle on the scatter plot by clicking and dragging — pixels within that dB range light up on the map in the class color
- Repeat for additional classes (e.g. "Vegetation", "Urban")
- To filter by incidence angle (NISAR only): expand Incidence Angle and set a min/max range
- Export options:
- SVG button → publication-quality scatter plot with class regions
- Map button → classification raster as a georeferenced PNG
- GeoTIFF → rendered classification as an RGBA GeoTIFF
Tips:
- Zoom in on the scatter plot to draw precise class boundaries
- Water typically clusters at very low HH and HV (bottom-left corner)
- Urban double-bounce is high HH, low HV (right side, low on Y axis)
- Vegetation is high HV relative to HH (upper-left to center)
SARdine supports two time series workflows: plotting backscatter over time for a fixed ROI, and compositing multiple dates into a single RGB image for change detection.
You need: Two or more NISAR GCOV files from the same track, different acquisition dates.
- Load your first date as usual (Load Dataset)
- Draw an ROI over your target area (e.g. a field, wetland, or urban block)
- In the Time Series panel, click Add Date and load a second
.h5file - Repeat for all dates you want to include
- SARdine extracts the mean backscatter (dB) within the ROI for each date and plots it as a time series
- Click Export SVG to export the time series chart as a vector figure
Tips:
- Use a small, homogeneous ROI for cleaner signal — a 1 km² patch of a single land cover type
- Multi-look averaging is applied before extraction, matching on-screen rendering
- Add multiple ROIs to compare two land cover types on the same chart
This workflow assigns each of three acquisition dates to a color channel (R, G, B). Pixels that appear white have not changed; colored pixels changed between dates. Magenta or cyan patches often indicate flooding or vegetation change.
- In Mode, select RGB Composite → Preset: Multi-Date
- In the Red channel slot, load your earliest date
- In the Green channel slot, load your middle date
- In the Blue channel slot, load your latest date
- Click Load Dataset — SARdine reads the same polarization from all three files and composites them
- Press
Rto auto-stretch each channel independently - Zoom into areas of interest — colored patches indicate change, white/grey indicates stable backscatter
- Export via
Ctrl+Sfor a figure PNG, or use GeoTIFF → RGB Composite for a 3-band georeferenced export
Tips:
- Use the same polarization (e.g.
HHHH) across all dates for a fair comparison - Flood detection: flooded areas go dark in SAR — they appear as a single-channel dropout (pure R, G, or B color)
- For subtle change, try adjusting stretch to
sigmoidto compress the dynamic range
| Control | Description |
|---|---|
| Colormap | Grayscale, viridis, inferno, plasma, phase, sardine, flood, diverging, polarimetric |
| Contrast | Min/max dB range — drag sliders or use Auto for percentile-based stretch |
| Stretch | Linear, sqrt, gamma, sigmoid transfer function |
| Multi-look | Data reduction (box-filter averaging in linear power) |
| Histogram | Floating viewport histogram — auto-updates on pan/zoom, SVG export |
| Classifier | 2D/1D feature space scatter with class region drawing and incidence angle filter |
| Overture | Overlay boundaries, roads, or places from Overture Maps |
| Key | Action |
|---|---|
H |
Toggle histogram overlay |
C |
Toggle feature space classifier |
F |
Fit view to data bounds |
R |
Reset contrast to auto |
G |
Toggle coordinate grid |
M |
Toggle overview map |
Ctrl+S |
Save figure (PNG) |
- Draw an ROI on the map (click and drag)
- Press
Cto open the Feature Space scatter plot - Click + Add Class to define a land cover class
- Draw a rectangle on the scatter plot to assign pixels in that dB range
- Pixels within the class region are colored on the map in real time
- Export: SVG for the scatter plot, Map for the classification raster
- Raw Float32 — linear power values with CRS and tiepoints
- Rendered RGBA — what you see on screen (dB, colormap, contrast) as a 4-band GeoTIFF
- RGB Composite — 3-band GeoTIFF when in composite mode
Draw an ROI to export a subregion, or export the full extent.
Canvas capture with overlays: scale bar, corner coordinates, colorbar (or RGB triangle for composites), and classification overlay.
Vector graphics:
- Scatter plot — density heatmap with class regions, open L-axes, outward ticks, Helvetica
- Histogram — filled distribution with contrast limit markers and legend
- Classification map — embedded raster with vector legend and pixel counts
File/URL → h5chunk → Chunks → GPU Texture → GLSL Shader → Screen
↓
dB scale → stretch → colormap → contrast
h5chunk is a pure JavaScript HDF5 chunk reader. It parses the HDF5 superblock, object headers, and B-tree to build a chunk index, then fetches only the chunks intersecting the current viewport via File.slice() (local) or HTTP Range (remote). Chunks are decompressed (deflate + shuffle) into Float32Arrays and uploaded directly as WebGL2 textures. The fragment shader handles the rest.
GCOV and GUNW products are supported. GUNW (interferometric phase and coherence) is developmental.
| Dependency | Role |
|---|---|
| React 18 | UI framework |
| deck.gl 8.9 | WebGL tile/bitmap rendering |
| geotiff.js | COG loading via HTTP Range |
| h5chunk (built-in) | Cloud-optimized HDF5 streaming (pure JS) |
| h5wasm | HDF5 attribute/metadata parsing (WASM) |
| MapLibre GL | Basemap rendering |
| Vite | Dev server and build tool |
cd ~/sardine
npm install --legacy-peer-deps && npm run build
node server/launch.cjs --data-dir /home/jovyanAccess via JupyterLab proxy: https://<hub-host>/user/<username>/proxy/8050/
npm install # Install dependencies
npm run dev # Dev server at localhost:5173
npm run build # Production build → dist/
npm test # Full suite: structural + pipeline regressions + unit tests
npm run test:unit # Behavioral unit tests (test/unit/*.test.mjs, auto-discovered)
npm run test:quick # Fast smoke tests
npm run benchmark # GPU vs CPU performance comparison| Issue | Status |
|---|---|
| Georeferencing errors on coarse overviews | Low priority |
| Slow loading RGB frequency A | Low priority — frequency A has no HDF5 pyramids; zoomed-in views are fast, full-frame overviews are not. Use a bbox deep link or frequency B for browse. |
AGPL-3.0. Previously MIT (through 3/5/26). Commercial licensing available — contact nick.steiner@gmail.com.
h5wasm · geotiff.js · deck.gl · NISAR cloud-optimization: NSIDC + JPL
CCNY Earth & Atmospheric Sciences
