🌾 Crop Classification

May 31, 2026 · View on GitHub

Crop Classification

🌾 Crop Classification

Multi-Temporal Satellite Imagery · Deep Learning · RapidEye · USDA CDL

Python TensorFlow License Tests

Classify crop types from sequences of satellite images using temporal deep learning.
Rather than a single snapshot, the model learns each crop's unique growth curve across an entire season.


📖 Why temporal?

A field of grapes and a field of almonds can look nearly identical in early spring. Their spectral signatures diverge only as the season progresses — grapes leaf out later, almonds fruit earlier. Single-image classifiers miss this entirely.

This project stacks 10 multi-temporal RapidEye scenes across a full 2017 growing season and trains models that see the complete phenological arc of each crop type, achieving ~95% accuracy with a Bidirectional LSTM.


🏗️ Architecture

Three models are provided, all trained on the same pipeline:

ModelInputHow it works
MLP(T × F,) flat vectorFully-connected baseline; fast to train
1-D CNN(T, F) sequenceDetects local temporal patterns via convolution
BiLSTM(T, F) sequenceReads the sequence forward and backward; best captures crop phenology

T = 10 timestamps · F = 6 features per step (5 spectral bands + NDVI)

All models share:

  • StandardScaler — fitted on training data only, persisted for inference
  • ✅ Balanced class weights — handles the 13× pixel-count imbalance between classes
  • EarlyStopping + ReduceLROnPlateau — no fixed epoch count
  • ✅ Stratified train / val / test splits — 70 / 15 / 15
  • ✅ Per-class precision / recall / F1 + confusion matrix at evaluation

📡 Dataset

The dataset is assembled from two publicly accessible sources and is not redistributed here. You can recreate it yourself:

Source 1 — Satellite imagery (Planet RapidEye)

10 scenes over a tile in California's Central Valley, 5 m resolution, 5 bands each:
Blue · Green · Red · Red-Edge · NIR

View acquisition dates
DateGrowing season stage
2017-03-06Early spring
2017-04-10Green-up
2017-06-01Early summer
2017-06-15Peak growth begins
2017-07-08Summer peak
2017-08-07Late summer
2017-09-05Senescence
2017-09-23Late senescence
2017-10-15Post-harvest
2017-12-07Winter dormancy

🔗 Get the imagery: Apply for Planet's free Education & Research Program, then download scenes via Planet Explorer or the Planet Python SDK.

Source 2 — Ground-truth labels (USDA CDL 2017)

Pixel-level crop labels from the USDA NASS Cropland Data Layer, a free annually-updated land-cover raster at 30 m resolution.

🔗 Get the labels: USDA NASS CropScape → select year 2017 → draw your bounding box → export GeoTIFF.

5 target classes (top classes by pixel count in the study area):

CDL CodeCropPixel Count
36Alfalfa1,543,068
69Grapes8,311,968
75Almonds4,729,104
121Developed / Open Space766,044
225Dbl Crop WinWht/Corn617,040

Expected file layout

Dataset/
├── cdl2017.tiff        ← USDA CDL ground-truth labels
├── 20170306.tiff
├── 20170410.tiff
├── 20170601.tiff
├── 20170615.tiff
├── 20170708.tiff
├── 20170807.tiff
├── 20170905.tiff
├── 20170923.tiff
├── 20171015.tiff
└── 20171207.tiff

Both rasters must share the same spatial extent and CRS.
Use gdalwarp to reproject/clip if needed.


⚙️ Installation

Option A — Conda (recommended, handles GDAL automatically)

conda env create -f environment.yml
conda activate crop-cls

Option B — pip (install GDAL via your system first)

pip install -r requirements.txt

See GDAL_INSTALLATION.md for platform-specific GDAL setup.


🚀 Quick Start

# 1. Build the pixel dataset from raw GeoTIFFs  (~500 MB output)
python scripts/preprocess.py

# 2. Train the recommended model (Bidirectional LSTM)
python scripts/train.py --model lstm

# 3. Run tests
pytest tests/ -v

All training options

python scripts/train.py \
  --model     lstm   \   # mlp | cnn | lstm
  --epochs    50     \   # max epochs (EarlyStopping applies)
  --batch-size 256   \
  --test-size  0.15  \   # fraction held out for final evaluation
  --val-size   0.15  \   # fraction used during training validation
  --dropout    0.3

Output files

After training, results are written to models_saved/ and results/:

FileDescription
models_saved/{model}_best.kerasBest checkpoint by validation loss
models_saved/{model}_final.kerasFinal model after all epochs
models_saved/scaler.joblibFitted StandardScaler (required for inference)
models_saved/label_encoder.joblibFitted LabelEncoder
results/{model}_history.csvPer-epoch loss and accuracy
results/{model}_report.txtPer-class precision / recall / F1
results/{model}_confusion_matrix.pngConfusion matrix heatmap
results/{model}_training_curve.pngLoss and accuracy curves

🗂️ Project Structure

Crop-Classification/
├── src/
│   ├── config.py        # paths, class labels, hyperparameter constants
│   ├── preprocess.py    # GeoTIFF → pixel CSV (with pixel-correspondence fix)
│   ├── features.py      # NDVI and spectral index computation
│   ├── models.py        # MLP, 1-D CNN, Bidirectional LSTM definitions
│   ├── train.py         # training pipeline
│   └── evaluate.py      # metrics, confusion matrix, training curves
├── scripts/
│   ├── preprocess.py    # CLI: build dataset
│   └── train.py         # CLI: train & evaluate
├── tests/
│   ├── test_features.py    # 7 unit tests
│   └── test_preprocess.py  # 4 unit tests (GDAL mocked)
├── Dataset/
│   └── download.txt     # data source instructions
├── environment.yml
├── requirements.txt
└── README.md

🐛 Key Fixes vs. Original Code

The original notebooks had several bugs that invalidated the results entirely:

#IssueOriginalFixed
1Pixel correspondence 🔴Each timestamp sampled pixels independently — temporal sequences were spatially incoherentIndices sampled once per class; identical pixel locations used across all timestamps
2Dropout rate 🔴Dropout(5) — out of [0,1] range, undefined behaviourDropout(0.3)
3EvaluationOverall accuracy onlyPer-class precision / recall / F1 + confusion matrix
4Class imbalanceNot handledBalanced class weights via compute_class_weight
5Scaler persistenceFitted then discardedSaved to models_saved/scaler.joblib
6TrainingFixed 10 epochsEarlyStopping + ModelCheckpoint + ReduceLROnPlateau
7Model savingModel lost after trainingSaved as .keras checkpoint
8FrameworkStandalone Keras 2 / TF 1.x (EOL)TensorFlow 2.x / Keras 3
9NDVI formula(Blue − NIR) / (NIR + Blue) — incorrectStandard (NIR − Red) / (NIR + Red)
10Column namingGeneric col_0 … col_50Semantic t{t}_b{b} and t{t}_ndvi

📚 Reference

Rose M. Rustowicz et al., Semantic Segmentation of Satellite Images Using Deep Learning
Stanford CS229, 2017 — Paper PDF