Introduction to EuroSAT
The EuroSAT dataset is a benchmark dataset for land use and land cover (LULC) classification based on Sentinel-2 satellite imagery. It was published by the German Research Center for Artificial Intelligence (DFKI) and contains 27,000 georeferenced image patches across 10 land use classes.
The 10 Land Use Classes
| Class | Samples | Description |
|---|---|---|
| AnnualCrop | 3,000 | Seasonal cultivated farmland |
| Forest | 3,000 | Dense tree cover areas |
| HerbaceousVegetation | 3,000 | Grasslands and meadows |
| Highway | 2,500 | Major road infrastructure |
| Industrial | 2,500 | Factories, warehouses, industrial zones |
| Pasture | 2,000 | Livestock grazing areas |
| PermanentCrop | 2,500 | Orchards, vineyards, permanent cultivation |
| Residential | 3,000 | Urban housing areas |
| River | 2,500 | Waterways and river corridors |
| SeaLake | 3,000 | Large water bodies |
Sentinel-2 is a European Space Agency (ESA) mission providing high-resolution optical imagery at 10m spatial resolution. Each EuroSAT patch covers approximately 640m x 640m on the ground, capturing scenes from over 34 European countries.
Setting Up Google Colab
Google Colab provides free access to GPUs (T4, sometimes V100) which dramatically speeds up deep learning training. Here is how to set up your environment.
Step 1: Enable GPU Runtime
In Colab, go to Runtime > Change runtime type and select T4 GPU as the hardware accelerator.
Step 2: Verify GPU Access
# Verify GPU is available import tensorflow as tf print("TensorFlow version:", tf.__version__) print("GPU available:", tf.config.list_physical_devices('GPU')) print("Num GPUs:", len(tf.config.list_physical_devices('GPU')))
Step 3: Install Dependencies
# All libraries are pre-installed in Colab # Just import what we need import numpy as np import matplotlib.pyplot as plt import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers, models import tensorflow_datasets as tfds from sklearn.metrics import ( classification_report, confusion_matrix, ConfusionMatrixDisplay ) import warnings warnings.filterwarnings('ignore') print("All imports successful!")
Loading the EuroSAT Dataset
TensorFlow Datasets (TFDS) hosts the EuroSAT RGB version. We load it directly with a train/validation/test split.
# Load EuroSAT dataset from TensorFlow Datasets (ds_train, ds_val, ds_test), ds_info = tfds.load( 'eurosat/rgb', split=['train[:70%]', 'train[70%:85%]', 'train[85%:]'], with_info=True, as_supervised=True, ) # Dataset info num_classes = ds_info.features['label'].num_classes class_names = ds_info.features['label'].names print(f"Number of classes: {num_classes}") print(f"Class names: {class_names}") print(f"Train samples: {tf.data.experimental.cardinality(ds_train).numpy()}") print(f"Val samples: {tf.data.experimental.cardinality(ds_val).numpy()}") print(f"Test samples: {tf.data.experimental.cardinality(ds_test).numpy()}")
Visualize Sample Images
# Display a grid of sample satellite images fig, axes = plt.subplots(2, 5, figsize=(16, 7)) for i, (image, label) in enumerate(ds_train.take(10)): ax = axes[i // 5][i % 5] ax.imshow(image.numpy()) ax.set_title(class_names[label.numpy()], fontsize=10, fontweight='bold') ax.axis('off') plt.suptitle('EuroSAT Sentinel-2 Satellite Patches', fontsize=14, fontweight='bold') plt.tight_layout() plt.show()
Data Preprocessing and Augmentation
We normalize pixel values to [0, 1], resize images, batch the dataset, and add data augmentation to improve generalization.
IMG_SIZE = 64 BATCH_SIZE = 64 AUTOTUNE = tf.data.AUTOTUNE def preprocess(image, label): """Normalize pixel values to [0, 1].""" image = tf.cast(image, tf.float32) / 255.0 return image, label # Data augmentation layer (applied only during training) data_augmentation = keras.Sequential([ layers.RandomFlip("horizontal_and_vertical"), layers.RandomRotation(0.2), layers.RandomZoom(0.1), layers.RandomContrast(0.1), ], name="data_augmentation") # Prepare datasets ds_train_prepared = ( ds_train .map(preprocess, num_parallel_calls=AUTOTUNE) .cache() .shuffle(5000) .batch(BATCH_SIZE) .map(lambda x, y: (data_augmentation(x, training=True), y), num_parallel_calls=AUTOTUNE) .prefetch(AUTOTUNE) ) ds_val_prepared = ( ds_val .map(preprocess, num_parallel_calls=AUTOTUNE) .cache() .batch(BATCH_SIZE) .prefetch(AUTOTUNE) ) ds_test_prepared = ( ds_test .map(preprocess, num_parallel_calls=AUTOTUNE) .batch(BATCH_SIZE) .prefetch(AUTOTUNE) ) print("Datasets prepared with augmentation!")
Preview Augmented Samples
# Show augmentation effect on a single image sample_img, sample_label = next(iter(ds_train.take(1))) sample_img = tf.cast(sample_img, tf.float32) / 255.0 sample_batch = tf.expand_dims(sample_img, 0) sample_batch = tf.repeat(sample_batch, 8, axis=0) augmented = data_augmentation(sample_batch, training=True) fig, axes = plt.subplots(1, 8, figsize=(18, 3)) for i in range(8): axes[i].imshow(augmented[i].numpy()) axes[i].axis('off') axes[i].set_title(f"Aug {i+1}", fontsize=9) plt.suptitle(f"Augmentations of: {class_names[sample_label.numpy()]}", fontsize=12, fontweight='bold') plt.tight_layout() plt.show()
Building a CNN Model
We start with a custom Convolutional Neural Network (CNN) built from scratch. This model uses three convolutional blocks followed by dense layers for classification.
def build_cnn(input_shape=(64, 64, 3), num_classes=10): """Build a custom CNN for satellite image classification.""" model = keras.Sequential([ # Block 1 layers.Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=input_shape), layers.BatchNormalization(), layers.Conv2D(32, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # Block 2 layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # Block 3 layers.Conv2D(128, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.Conv2D(128, (3, 3), activation='relu', padding='same'), layers.GlobalAveragePooling2D(), layers.Dropout(0.4), # Classification head layers.Dense(256, activation='relu'), layers.BatchNormalization(), layers.Dropout(0.3), layers.Dense(num_classes, activation='softmax'), ], name="EuroSAT_CNN") return model model = build_cnn() model.summary()
Compile the Model
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-3),
loss='sparse_categorical_crossentropy',
metrics=['accuracy'],
)
print("Model compiled. Ready to train!")Training the Model
We train for 25 epochs with early stopping and learning rate reduction callbacks to prevent overfitting.
# Define callbacks callbacks = [ keras.callbacks.EarlyStopping( monitor='val_loss', patience=5, restore_best_weights=True, verbose=1 ), keras.callbacks.ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6, verbose=1 ), ] # Train the model EPOCHS = 25 history = model.fit( ds_train_prepared, validation_data=ds_val_prepared, epochs=EPOCHS, callbacks=callbacks, verbose=1, )
Plot Training Curves
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) # Accuracy ax1.plot(history.history['accuracy'], label='Train', linewidth=2) ax1.plot(history.history['val_accuracy'], label='Validation', linewidth=2) ax1.set_title('Model Accuracy', fontweight='bold') ax1.set_xlabel('Epoch') ax1.set_ylabel('Accuracy') ax1.legend() ax1.grid(True, alpha=0.3) # Loss ax2.plot(history.history['loss'], label='Train', linewidth=2) ax2.plot(history.history['val_loss'], label='Validation', linewidth=2) ax2.set_title('Model Loss', fontweight='bold') ax2.set_xlabel('Epoch') ax2.set_ylabel('Loss') ax2.legend() ax2.grid(True, alpha=0.3) plt.tight_layout() plt.show()
Evaluation and Metrics
Evaluate the trained model on the held-out test set and generate a detailed classification report with per-class precision, recall, and F1-score.
# Evaluate on test set test_loss, test_acc = model.evaluate(ds_test_prepared, verbose=0) print(f"Test Accuracy: {test_acc:.4f}") print(f"Test Loss: {test_loss:.4f}") # Get predictions for detailed metrics y_true = [] y_pred = [] for images, labels in ds_test_prepared: predictions = model.predict(images, verbose=0) y_pred.extend(np.argmax(predictions, axis=1)) y_true.extend(labels.numpy()) y_true = np.array(y_true) y_pred = np.array(y_pred) # Classification Report print("\n--- Classification Report ---") print(classification_report(y_true, y_pred, target_names=class_names))
Confusion Matrix
# Plot confusion matrix cm = confusion_matrix(y_true, y_pred) fig, ax = plt.subplots(figsize=(12, 10)) disp = ConfusionMatrixDisplay( confusion_matrix=cm, display_labels=class_names ) disp.plot(ax=ax, cmap='Blues', values_format='d', xticks_rotation=45) ax.set_title('EuroSAT Classification - Confusion Matrix', fontsize=14, fontweight='bold', pad=20) plt.tight_layout() plt.show()
Transfer Learning with ResNet50
Transfer learning leverages a pre-trained model (trained on millions of images) and fine-tunes it for our satellite classification task. This typically boosts accuracy by 3-5%.
# Build a ResNet50-based model def build_resnet_model(num_classes=10): """Fine-tune ResNet50 for EuroSAT classification.""" # Load pre-trained ResNet50 (without top classification layer) base_model = keras.applications.ResNet50( weights='imagenet', include_top=False, input_shape=(64, 64, 3), ) # Freeze base model layers initially base_model.trainable = False # Build the full model model = keras.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.BatchNormalization(), layers.Dropout(0.4), layers.Dense(256, activation='relu'), layers.Dropout(0.3), layers.Dense(num_classes, activation='softmax'), ], name="EuroSAT_ResNet50") return model, base_model resnet_model, base_model = build_resnet_model() # Phase 1: Train only the new layers (5 epochs) resnet_model.compile( optimizer=keras.optimizers.Adam(learning_rate=1e-3), loss='sparse_categorical_crossentropy', metrics=['accuracy'], ) print("Phase 1: Training classification head...") resnet_model.fit(ds_train_prepared, validation_data=ds_val_prepared, epochs=5, verbose=1)
Phase 2: Fine-Tune Top Layers
# Unfreeze the last 20 layers of ResNet50 for fine-tuning base_model.trainable = True for layer in base_model.layers[:-20]: layer.trainable = False # Recompile with a lower learning rate resnet_model.compile( optimizer=keras.optimizers.Adam(learning_rate=1e-5), loss='sparse_categorical_crossentropy', metrics=['accuracy'], ) print("Phase 2: Fine-tuning top layers...") print(f"Trainable layers: {sum(1 for l in resnet_model.layers[0].layers if l.trainable)}") history_ft = resnet_model.fit( ds_train_prepared, validation_data=ds_val_prepared, epochs=15, callbacks=[ keras.callbacks.EarlyStopping( monitor='val_loss', patience=4, restore_best_weights=True ), ], verbose=1, ) # Evaluate ft_loss, ft_acc = resnet_model.evaluate(ds_test_prepared, verbose=0) print(f"\nResNet50 Test Accuracy: {ft_acc:.4f}")
Visualizing Predictions
Let us create a visual grid showing the model predictions alongside the ground truth labels, with correct predictions in green and mistakes in red.
# Visualize predictions on test images fig, axes = plt.subplots(4, 6, figsize=(18, 12)) test_batch = next(iter(ds_test_prepared)) images, labels = test_batch preds = resnet_model.predict(images, verbose=0) pred_classes = np.argmax(preds, axis=1) pred_confs = np.max(preds, axis=1) for i in range(24): ax = axes[i // 6][i % 6] ax.imshow(images[i].numpy()) ax.axis('off') true_label = class_names[labels[i].numpy()] pred_label = class_names[pred_classes[i]] confidence = pred_confs[i] * 100 correct = true_label == pred_label color = '#22c55e' if correct else '#ef4444' ax.set_title( f"{pred_label}\n{confidence:.0f}%", fontsize=8, fontweight='bold', color=color ) plt.suptitle('Model Predictions on Test Patches (Green = Correct, Red = Wrong)', fontsize=13, fontweight='bold') plt.tight_layout() plt.show()
Per-Class Accuracy Bar Chart
# Calculate per-class accuracy per_class_acc = [] for i in range(num_classes): mask = y_true == i if mask.sum() > 0: acc = (y_pred[mask] == i).mean() per_class_acc.append(acc) else: per_class_acc.append(0) # Plot colors = ['#22c55e' if a >= 0.9 else '#f97316' if a >= 0.8 else '#ef4444' for a in per_class_acc] fig, ax = plt.subplots(figsize=(12, 6)) bars = ax.barh(class_names, per_class_acc, color=colors, edgecolor='none', height=0.6) for bar, acc in zip(bars, per_class_acc): ax.text(bar.get_width() + 0.01, bar.get_y() + bar.get_height()/2, f"{acc:.1%}", va='center', fontweight='bold', fontsize=10) ax.set_xlim(0, 1.08) ax.set_title('Per-Class Test Accuracy', fontsize=14, fontweight='bold') ax.set_xlabel('Accuracy') ax.grid(axis='x', alpha=0.2) plt.tight_layout() plt.show()
Exporting and Next Steps
Save your trained model so you can reload it later or deploy it in a web application or GIS pipeline.
# Save the model in Keras format resnet_model.save('eurosat_resnet50_classifier.keras') print("Model saved as .keras file") # Save as TFLite for mobile / edge deployment converter = tf.lite.TFLiteConverter.from_keras_model(resnet_model) tflite_model = converter.convert() with open('eurosat_classifier.tflite', 'wb') as f: f.write(tflite_model) print(f"TFLite model size: {len(tflite_model) / 1024 / 1024:.1f} MB") # Download from Colab from google.colab import files files.download('eurosat_resnet50_classifier.keras') files.download('eurosat_classifier.tflite')
What You Learned
- How to load and explore real Sentinel-2 satellite imagery (EuroSAT dataset) in Google Colab
- Building efficient data pipelines with tf.data, caching, and prefetching
- Data augmentation strategies for geospatial images
- Designing and training a custom CNN from scratch
- Transfer learning with ResNet50 for improved accuracy
- Generating confusion matrices and classification reports
- Exporting models for deployment (Keras and TFLite formats)
Next Steps to Explore
- Use the 13-band multispectral version of EuroSAT for richer spectral information
- Experiment with other architectures: EfficientNet, Vision Transformer (ViT)
- Apply Grad-CAM to visualize which image regions the model focuses on
- Integrate the trained model into a web app using TensorFlow.js
- Combine with Google Earth Engine for real-time classification pipelines
- Scale up to larger datasets like BigEarthNet (590,326 patches, 19 classes)