Interactive Course

Mapping Rice Fields
from Space

How satellite radar, time-series analysis, and a neural network combine to map millions of hectares of paddy fields across Java, Indonesia — even through clouds.

🛰
Sentinel-1 SAR
📊
29 Features
🧠
Neural Network
🌾
Paddy Map

The pipeline: from raw radar signal to a 3-million-hectare paddy map

Scroll down or use arrow keys to begin ↓

01

Why Map Rice Fields with Radar?

The problem this system solves — and why optical satellites aren't enough

The Challenge: Clouds Over the Tropics

Java Island, Indonesia, produces a huge share of the country's rice. Knowing exactly where and when rice is growing is critical for food security planning. But there's a problem: Java is in the tropics, and clouds block satellite cameras for most of the growing season.

Optical Satellites

Use visible light — just like a camera. Clouds completely block the view. Useless during rainy season when rice is actually growing.

📡

Radar Satellites (SAR)

Send out radio waves and listen for the echo. Radar penetrates clouds, works day and night. Perfect for tropical monitoring.

Sentinel-1: Our Radar Eye in the Sky

The European Space Agency's Sentinel-1 satellite orbits Earth, revisiting Java every 12 days. It sends SAR (radar) pulses toward the ground and measures the backscatter — the energy that bounces back.

1
Satellite sends radar pulse

A microwave beam hits the ground. Unlike light, microwaves go right through clouds and rain.

2
Surface bounces energy back

Different surfaces return different amounts of energy. Flooded paddy fields (smooth water) reflect almost everything away. Dense vegetation scatters energy back.

3
Satellite records the echo

The returned signal strength (backscatter) is stored as a number for each pixel. Low number = smooth surface. High number = rough/vegetated surface.

VH Polarization: The Rice Detector

Sentinel-1 records radar in two polarizations: VH (vertical-send, horizontal-receive) and VV (vertical-send, vertical-receive). This system primarily uses VH because rice paddies create a very distinctive VH signature as they grow.

💡
Why VH is special for rice

When rice is young and the field is flooded, radar bounces off the water surface like a mirror — almost no signal returns via the VH channel. As rice grows taller, the stems and leaves scramble the signal, and VH backscatter increases dramatically. This "dip then rise" pattern is unique to paddy fields and is how the system identifies them.

Check Your Understanding

A farmer floods their paddy field, then rice grows over the next 3 months. What happens to the VH radar signal?

Why is radar preferred over optical satellites for monitoring rice in tropical Java?

02

Meet the Cast of Characters

The five main components that turn radar signals into a paddy map

The Assembly Line

Think of this system like an assembly line in a factory. Raw materials (radar data) come in one end, and a finished product (a map showing where rice grows) comes out the other. Each station on the line has a specialized job.

🛰
SAR Preprocessor

Cleans and calibrates raw satellite radar data. Removes noise, corrects for terrain, converts to usable units.

🧬
Feature Engine

Transforms raw backscatter values into 29 meaningful numbers per pixel — temporal patterns, differences, ratios, and phenology indicators.

SMOTE Balancer

Fixes the training data imbalance by creating synthetic "non-paddy" samples, so the model learns to say "no" as well as "yes."

🧠
Neural Network (MLP)

A 3-layer brain that takes 29 features in and outputs a single probability: "how likely is this pixel to be paddy?"

🗺
Multi-Year Compositor

Combines predictions from multiple time periods and years. Only marks a pixel as paddy if the evidence is consistent and strong.

How the Files Map to Characters

s1-land-cover-classification/
s1_preprocess_snap.py 🛰 SAR Preprocessor — cleans raw radar data using ESA SNAP
utils_paddy_vh.py 🧬 Feature Engine — extracts 29 features per pixel
train_paddy_vh_smote.py ⚖ SMOTE Balancer + 🧠 Neural Network training
predict_paddy_vh_multiyear.py 🧠 Neural Network inference — applies model to full island
create_multiyear_composite_paddy.py 🗺 Compositor — combines years into final high-confidence map
config_paddy_vh.py Settings and thresholds for all components

Check Your Understanding

You want to add a new feature that measures how rapidly the radar signal is changing. Which component would you modify?

03

How Data Flows Through the System

Tracing a single pixel from raw radar echo to "paddy" or "not paddy"

The Journey of a Pixel

Imagine one 50-meter square on the ground in Java. Every 12 days, Sentinel-1 flies overhead and records a radar echo from that spot. Let's trace what happens to that data as it moves through the system.

🛰
Raw Radar
🔧
SNAP
🧬
Features
🧠
Model
🗺
Map
Click "Next Step" to trace a pixel through the pipeline
Step 0 / 7

Behind the Scenes: Components Talking

Here's what it might sound like if the system's components could talk to each other during a prediction run:

Pipeline Group Chat
0 / 6

Check Your Understanding

How much time history does the system consider for each prediction?

04

Feature Engineering: The Secret Sauce

How 7 raw radar numbers become 29 meaningful features that describe rice growth

From Raw Signal to Meaning

A feature is a single number that captures something meaningful about a pixel. The feature engineering step is where domain knowledge meets code: we encode what agricultural scientists know about rice growth into numbers a computer can use.

📈

7 Temporal Values

The raw radar signal at each of the 7 time steps. Like 7 snapshots of how "rough" the surface is.

6 Differences

How much the signal changed between consecutive time steps. Captures the "speed" of growth.

÷

6 Ratios

Relative change between time steps. A ratio of 1.5 means the signal grew by 50%, regardless of the absolute level.

🌾

10 Phenology Indicators

Rice growth stage detectors: flooding, early growth, late growth, reproductive, ripening, post-harvest — plus min/max timing.

Inside the Feature Engine

Here's how the code builds 29 features from 7 time-series values. This is from utils_paddy_vh.py:

CODE
# Temporal differences: rate of change
pol_diff = pol_values[:, :-1] - pol_values[:, 1:]

# Temporal ratios: relative change
denominator = np.abs(pol_values[:, 1:])
denominator = np.where(denominator < 1e-10, 1e-10, denominator)
pol_ratio = pol_values[:, :-1] / denominator
PLAIN ENGLISH

Calculate how much the signal changed between each pair of consecutive time steps. Positive = signal increased. Negative = signal decreased.

Now calculate the relative change — divide current by previous. First, make sure we never divide by zero (replace any near-zero values with a tiny number).

A ratio of 1.2 means "20% increase." A ratio of 0.8 means "20% decrease." This captures proportional change regardless of absolute signal strength.

Reading the Rice Growth Stages

The phenology indicators are the most clever features. They encode agricultural science directly into the model: "given this radar value, which growth stage is the rice most likely in?"

CODE
# Paddy-optimized VH thresholds (backscatter x100)
flooding_thresh = -2200
early_veg_high = -1800
late_veg_high = -1500
reproductive_high = -1200

# 1. Flooding: very low backscatter
flooding_detected = (current_value < flooding_thresh)

# 6. Post-harvest: low current but high past max
post_harvest = ((current_value < harvest_thresh) &
               (max_values > late_veg_high) &
               (range_values > 400))
PLAIN ENGLISH

Define thresholds for each growth stage. These numbers come from agricultural research on how rice affects radar signals. Values are in decibels x100 (so -2200 means -22 dB).

Flooding detection: If the current signal is very low (below -22 dB), the field is likely flooded — water acts like a mirror, reflecting radar away.

Post-harvest detection: The signal is low NOW, but it was high recently (plants were there), and the range is large (big swing from crop to bare). Three conditions must ALL be true — this prevents false positives.

Simulate: SAR Signal Over Time

Watch how the radar signal changes across different land cover types. Notice the distinctive "V-shaped dip" for paddy fields during flooding.

Check Your Understanding

Why does the system calculate both temporal differences AND temporal ratios?

05

The Training Data Trap (and SMOTE to the Rescue)

Why 92% paddy data created a broken model — and how synthetic samples fixed it

The Imbalance Problem

Imagine you're studying for a test where 92 out of 100 questions have the answer "A." You'd learn to just guess "A" every time and still get 92% right — but you'd be useless at actually telling answers apart. That's exactly what happened with the original training data.

92% / 8%
Before SMOTE

9,757 paddy samples vs 838 non-paddy. Model learned to say "paddy" to everything. Detected only 2 hectares.

50% / 50%
After SMOTE

9,757 real paddy + 9,757 synthetic non-paddy. Model learned the actual difference. Detected 196,335 hectares.

The 93,894x improvement

The SMOTE-balanced model detected 93,894 times more paddy area than the imbalanced model. The imbalanced model had "high accuracy" (92%) but was completely useless — it just predicted everything as paddy. This is one of the most common traps in machine learning.

How SMOTE Creates Synthetic Samples

SMOTE works like an art forger who studies real paintings to create convincing new ones. It looks at the existing non-paddy samples, finds ones that are similar to each other, and creates new synthetic samples along the line between them.

1
Pick a minority sample

Choose one real non-paddy sample from the 838 available.

2
Find its 5 nearest neighbors

Look at the 5 most similar non-paddy samples (measured across all 29 features).

3
Interpolate along the line

Pick a random point on the line between the original and one neighbor. This new point is realistic because it's "between" two real examples.

4
Repeat until balanced

Keep generating synthetic samples until you have equal numbers of paddy and non-paddy (9,757 each = 19,514 total).

The SMOTE Code

Here's the actual SMOTE application from train_paddy_vh_smote.py:

CODE
k_neighbors = min(5, n_non_paddy - 1)

smote = SMOTE(random_state=42, k_neighbors=k_neighbors)
X_resampled, y_resampled = smote.fit_resample(X, y)

scaler = RobustScaler()
X_scaled = scaler.fit_transform(X_resampled)
PLAIN ENGLISH

Use 5 neighbors (or fewer if we don't have enough samples) when generating synthetic data. This controls how "diverse" the fake samples are.

Create the SMOTE generator (random_state=42 means we get the same result every time — reproducibility). Then balance the dataset: X gets the features, y gets the labels (paddy/non-paddy).

Scale all features using RobustScaler, which handles outliers better than standard scaling. This ensures no single feature dominates just because its numbers are bigger.

Check Your Understanding

Your model has 95% accuracy but only detects 2 hectares of paddy in an area where 200,000 hectares should exist. What's most likely wrong?

06

The Neural Network Brain

How a 3-layer network turns 29 features into a paddy/non-paddy decision

A Network Like a Funnel

Think of the MLP like a funnel: 29 numbers go in at the wide end, get progressively compressed through 128, 64, and 32 neurons, and a single probability comes out at the narrow end.

Input 29 features Layer 1 128 BatchNorm LeakyReLU Dropout 30% Layer 2 64 BatchNorm LeakyReLU Dropout 30% Layer 3 32 LeakyReLU Dropout 20% Output 0-1 Probability

The Model Architecture in Code

From utils_paddy_vh.py — each layer serves a specific purpose:

CODE
model = tf.keras.Sequential([
  tf.keras.layers.Input(shape=(29,)),
  tf.keras.layers.Dense(128, kernel_regularizer=l2(0.001)),
  tf.keras.layers.BatchNormalization(),
  tf.keras.layers.LeakyReLU(negative_slope=0.1),
  tf.keras.layers.Dropout(0.3),
  # ... layers 2 and 3 similar ...
  tf.keras.layers.Dense(1),
  TemperatureLayer(temperature=0.5),
  tf.keras.layers.Activation('sigmoid')
])
PLAIN ENGLISH

Create a sequential (layer-by-layer) model that accepts 29 features.

Dense(128): 128 neurons, each connecting to all 29 inputs. L2 regularization penalizes extreme weights — like telling the model "don't rely too heavily on any single feature."

BatchNorm: Normalize values between layers. SAR data has huge dynamic range; this keeps training stable.

LeakyReLU: The "activation function" — decides which signals pass through. "Leaky" means even weak signals get through (at 10% strength), preventing dead neurons.

Dropout(0.3): Randomly silence 30% of neurons during training. Forces the network to be redundant — no single neuron becomes a point of failure.

Final output: Compress to 1 number, divide by 0.5 (temperature scaling makes predictions more confident), then squeeze through sigmoid to get a 0-1 probability.

💡
Why MLP and not a fancier model?

The input data is a table of 29 numbers per pixel — not an image. CNNs are designed for images where spatial neighbors matter. Here, each pixel is independent — its 29 features already capture all the temporal context needed. An MLP is simpler, faster to train, and works just as well for tabular data.

Check Your Understanding

The model uses Dropout(0.3) — randomly silencing 30% of neurons during training. What's the purpose?

07

Building the Final Map

How multi-year consensus filtering produces a high-confidence 3-million-hectare paddy map

The "Jury Deliberation" Approach

Think of each prediction period as a juror. A single juror might be wrong, but if 5 out of 14 jurors across 2 separate trials all agree — you can be pretty confident. That's the multi-year consensus approach.

min_detections: 5 Pixel must be detected as paddy in at least 5 time periods per year
min_confidence: 0.7 Each detection must have at least 70% model confidence
min_years: 2 Must be detected as paddy in BOTH 2023 and 2024
min_mean_confidence: 0.6 Average confidence across all detections must exceed 60%

The Compositing Algorithm

From create_multiyear_composite_paddy.py:

CODE
# Only count high-confidence detections
high_conf = (paddy_stack == 1) & (conf_stack >= min_confidence)

# How many periods detected paddy?
frequency = np.sum(high_conf, axis=0)

# Paddy if enough detections
year_paddy = (frequency >= min_detections)

# Multi-year consensus
num_years = np.sum(year_stack == 1, axis=0)
consensus = (num_years >= min_years)
PLAIN ENGLISH

For each pixel and each time period: only count it as "paddy detected" if BOTH the prediction says paddy AND the model is at least 70% confident.

Count how many time periods (out of 14 for 2023 and 11 for 2024) had high-confidence paddy detections.

For each year: mark the pixel as "paddy this year" only if it was detected in 5 or more periods. This filters out one-off false positives.

Final check: count how many years agree. Only mark as paddy on the final map if BOTH years say "yes." This catches fields that are consistently rice, not just one-time detections.

The Results

3.02M ha
Final paddy area after all masking
25
Total prediction periods used (14 + 11)
80%
Model accuracy (cross-validated)
💡
Layered filtering is a universal pattern

This "multiple independent checks must agree" pattern appears everywhere in software: spam filters use it (multiple signals must all say "spam"), fraud detection uses it (multiple rules must trigger), and even self-driving cars use it (multiple sensors must agree on an obstacle). It's called consensus filtering — and it's one of the most powerful ways to reduce false positives.

Check Your Understanding

A government agency says your map is too conservative — they think there's more paddy than 3M hectares. Which parameter would you adjust first?

08

The Big Picture

Connecting all the pieces — and where this system could go next

The Full Architecture at a Glance

1
Sentinel-1 acquires radar data every 12 days

54 bands across 2023-2024, stored as a multi-year GeoTIFF stack at 50m resolution

2
ESA SNAP preprocesses raw scenes

Orbit correction, noise removal, terrain correction, speckle filtering, dB conversion

3
Feature Engine extracts 29 features per pixel

Temporal values + differences + ratios + paddy phenology stages. Vectorized: 1.2M pixels at once

4
SMOTE balances the training data

92%/8% imbalance fixed to 50%/50% using synthetic minority samples

5
MLP neural network classifies each pixel

128→64→32→1 architecture with temperature-scaled sigmoid output. 80% accuracy, 89.6% AUC

6
Multi-year compositor builds final consensus map

25 periods across 2 years, confidence filtering, terrain + water masking. Final: 3.02M hectares

Key Takeaways

💡

Domain Knowledge > Data

The phenology features — encoding what scientists know about rice growth — are more powerful than adding more raw data.

Balance Your Data

A 92/8 split created a useless model. SMOTE turned it into one that finds 3 million hectares. Always check class distribution first.

🔍

Consensus > Single Shot

One prediction can be wrong. Multiple independent predictions that agree are much more reliable. Use consensus filtering for critical decisions.

Vectorize Everything

Processing 1.2 million pixels one-by-one would take hours. NumPy vectorization does it in seconds. Always batch your computations.

Final Challenge

You discover the model is misclassifying highland tea plantations near Bandung as paddy fields. What's the best approach to fix this?

Java Island has millions of pixels. How does the system avoid running out of memory during prediction?

Course Complete!

You now understand how satellite radar, feature engineering, SMOTE balancing, neural networks, and consensus filtering combine to map 3 million hectares of rice fields across Java, Indonesia.

Built from the s1-land-cover-classification codebase