A Sentinel-1 / Sentinel-2 fusion pipeline that screens a real Indonesian oil-palm concession for forest loss after the EU Deforestation Regulation (EUDR) cutoff of 31 December 2020, validated against three independent satellite reference systems (RADD, Hansen GFC, JRC TMF).
This is a capability demonstration, not a legal-grade compliance product. Validation is satellite-vs-satellite, not field ground-truth. Do not use this for real EUDR due diligence. See Limitations.
Given a concession boundary, the pipeline determines whether forest standing at the 2020-12-31 cutoff was cleared afterward, dates the loss, and emits a per-plot deforestation-free status in the shape of an EUDR due-diligence statement (GeoJSON), plus an interactive web-map.
The core method is unsupervised change-point detection on a dense Sentinel-1 SAR backscatter time-series, confirmed by Sentinel-2 optical drops. SAR is the load-bearing choice: Indonesian tropical forest is near-permanently cloud-covered, so optical-only detection is frequently blind. Radar penetrates cloud.
A supervised XGBoost model is included as a documented comparison, not as the primary detector — and the comparison's honest result is part of the contribution (see Findings).
Perkebunan Boven Digoel Sejahtera (PBDS) — formerly Manunggal Sukses Mandiri, Digoel Agri group. Boven Digoel, Papua Selatan, Indonesia. ~38,956 ha, oil palm. Selected by an objective screen of all 11 Boven Digoel oil-palm concessions.
- Working CRS: EPSG:32754 (UTM 54S).
- Forest standing at cutoff: ~38,800 ha (Hansen GFC 2025 v1.13 and JRC TMF agree within 0.9%, spatial IoU 98.8%).
- Valid forest analysis mask (baseline ∩ S1-data ∩ S2-data): 38,825.4 ha.
- Forest baseline at cutoff. Hansen GFC (tree-cover ≥30% minus pre-2021 loss) as the primary precautionary mask; JRC TMF (undisturbed + degraded classes) as an independent cross-check.
- Data acquisition (Google Earth Engine). Sentinel-1 GRD monthly composites (VV + VH, dB; orbit ASCENDING relative-155; 2019-01 → 2026-06) and Sentinel-2 annual composites (NDVI + NBR; Cloud Score+ masked). Both exported clipped to AOI, grid-aligned to a common 10 m grid.
- Change detection (primary). Per-pixel z-departure from a fixed pre-cutoff
reference (2019–2020) with first-crossing logic — not argmax, which is
selection-biased. A loss pixel requires VH and VV joint confirmation
(
z ≥ k,k = 3.0) plus a ≥3-month persistence guard. Sentinel-2 NDVI/NBR drops provide an asymmetric confirmation flag and a two-tier confidence ordering. - Classification (comparison). XGBoost with spatial block cross-validation (k=5, 1 km blocks), trained on RADD labels — evaluated as in-AOI generalization, not as a validation claim against RADD (that would be circular).
- Validation. Detected loss cross-checked against RADD (SAR), Hansen GFC (Landsat optical), and JRC TMF (Landsat optical), reported separately. Precision, recall, and confusion matrices at pixel and patch level, dual-domain (all-inclusive and with the dominant clearing excluded).
- Outputs. Per-plot binary status + GeoJSON statements + loss date/area + web-map.
The k = 3.0 threshold was frozen a-priori from held-out specificity in the
detection phase, then ratified (not re-selected) during validation. Thresholds and
operating points are never fit to the validation references.
1. High, cross-corroborated precision (headline). Change-point detection at k=3.0 achieves precision ≈ 0.81–0.85 (strict-pixel) and ≈ 0.87–0.90 (patch-area) across all three independent references. Mutual agreement across a SAR and two Landsat-optical references is the strongest evidence in the study.
2. Low recall, by design and by structure (not a bug). Strict-pixel recall ≈ 0.10. This follows directly from the conservative high-precision design (k=3.0, joint VH∧VV, persistence ≥3) and from a SAR-subset structural ceiling: even at k=2.0 the detector flags less area than the broad optical references, because SAR keys on structural/biomass change and misses some canopy-only loss regardless of threshold.
3. The AOI is a single-clearing structure. One mega-patch (~331.6 ha) accounts for ~94% of all reference loss. This dominates every metric and is the central structural constraint of the study. Reported "insensitivity to k" in area-weighted recall is a restatement of this degeneracy, not a general robustness property — it would not transfer to a multi-clearing AOI.
4. When supervised learning does not help. The XGBoost comparison does not demonstrate strong independent loss detection here. Three converging, predicted reasons: the degenerate positive structure, partial circularity between the top S1 features and the S1-based RADD labels, and a weak independent optical signal. This is reported as a result, not hidden as a failure — "when supervised doesn't help" is itself publishable, and it is why the unsupervised change-point detector is the primary product.
All in outputs/:
| Artifact | Path | Contents |
|---|---|---|
| Web-map | outputs/webmap/pbds_webmap.html |
Self-contained Leaflet/Folium map (satellite + light basemaps; plot status, detection by size class, RADD reference; legend; popups). Open directly in a browser — no server. |
| Plot status (DDS) | outputs/geojson/pbds_status.geojson |
Single plot, EUDR-statement attributes (status, detected/reference loss area, earliest loss, per-year breakdown, method, validated_against). |
| Detection patches | outputs/geojson/pbds_loss_detected.geojson |
819 change-point patches with area, size class, first-loss month, S2-confirmation fraction. |
| RADD reference | outputs/geojson/pbds_loss_reference.geojson |
72 RADD reference patches with area and size class. |
| Metrics | outputs/phase05_core_metrics.json, outputs/phase05_pr_vs_k.json |
Validation metrics (core + k-sensitivity sweep). |
| Figures | outputs/figures/ |
Per-phase diagnostic and result figures. |
Headline status: PBDS = non-compliant (post-cutoff loss > 0 from two independent sources). Detected loss 42.52 ha (change-point, conservative); reference loss 351.0 ha (RADD); earliest loss 2021-01; ~90% of loss in 2021.
# from repo root — just open the file
xdg-open outputs/webmap/pbds_webmap.html # Linux
# or double-click it in a file manager / drag into a browser tabDefault view: Esri satellite basemap, PBDS boundary, change-point detection coloured by patch size. Toggle the RADD reference layer to see the ~331 ha dominant clearing that the conservative detector only partially flags — a direct visual of the by-design low recall.
eudr-deforestation-demo/
├── README.md # this file
├── environment.yml # pinned conda env (exact == pins)
├── config/
│ ├── aoi.geojson # PBDS boundary
│ └── aoi.yaml # AOI + cutoff + S1/S2 acquisition lock
├── gee/ # Earth Engine export scripts (cloud-side)
│ ├── aoi.py # importable AOI loader
│ ├── 01_baseline.py # Hansen + TMF baselines
│ ├── 02a/02b/02c_*.py # S1 orbit diag, S1 monthly, S2 annual exports
│ ├── 04a_radd_label_export.py
│ ├── 05a/05b/05c_*.py # Hansen loss, TMF defor, TMF degrad exports
│ └── scoping/ # concession screen + AOI build (reference)
├── src/ # local processing (consumes GEE exports)
│ ├── 03a..03e_*.py # detection: baseline resample, valid mask,
│ │ # S1 change-point, held-out null, S2 drop,
│ │ # fusion, feature stack
│ ├── 04b..04e_*.py # XGBoost: label+patch, spatial blocks, CV, refit
│ ├── 05d..05i_*.py # validation: ref stack, k-sweep, mega footprint,
│ │ # core metrics, PR-vs-k, figures
│ ├── 06a..06d_*.py # outputs: vectorize, status, breakdown, DoD gate
│ └── 07a_build_webmap.py # web-map
├── outputs/ # geojson, figures, webmap, metrics (tracked)
├── docs/
│ └── REPRODUCIBILITY.md # run order, env, determinism, honest scope
└── phases/ # per-phase decision/handoff notes (the trace)
data/ is gitignored (raw + interim rasters, ~6.4 GB, on external storage).
This is not a one-command rebuild. Full reproduction requires a Google Earth
Engine account, re-running the GEE exports, and ~hours of local compute to
regenerate ~6.4 GB of intermediate rasters. The committed outputs/ (GeoJSON,
metrics, figures, web-map) let a reviewer inspect every result without re-running.
The full run order, environment setup, and determinism guarantees (fixed seed,
frozen k, grid asserts, per-phase DoD gates) are documented in
docs/REPRODUCIBILITY.md.
Quick environment setup:
conda env create -f environment.yml
conda activate eudr-deforestation
earthengine authenticate # one-time, requires a GEE account- Demonstration, not legal-grade. Real EUDR due diligence carries legal liability. This is a portfolio/research artifact and is not authoritative.
- Satellite-vs-satellite validation. Precision/recall are measured against other satellite alert systems (RADD, Hansen, TMF), not field survey. No claim of absolute ground-truth accuracy.
- Single AOI. Findings — especially the k-robustness and the supervised-learning result — are shaped by this concession's single-clearing structure and may not generalize to multi-clearing landscapes.
- End-period reference gap. The optical references (Hansen, TMF) end in 2024; RADD and the change-point detector run to 2026-06. Late-period detections scored against optical references are reference-undercounts, not necessarily false positives.
- RADD date semantics. RADD
Dateis the confirmed-detection date, not the clearing date; a confirmation lag makes early-2021 labels slightly permissive near the cutoff.
The pipeline is built to be a portfolio piece, a credibility proof (validated against established systems), a service sample, and a seed for a future compliance-screening tool. The skills and pipeline transfer to broader deforestation/ESG demand (RSPO, bank financing conditions, buyer requirements) independent of EUDR's regulatory timeline. One AOI, deep and rigorous — depth over breadth.