Courses Satellite Image Classification with Google Colab
Free Course
Course 02

Satellite Image Classification
with Google Colab

Build a deep learning image classifier from scratch using real Sentinel-2 satellite imagery from the EuroSAT dataset. Learn to load, preprocess, train, evaluate, and visualize land use classification models entirely in the cloud, no local GPU required.

~3 Hours
Google Colab
EuroSAT Real Data
Beginner-Friendly

Course Outline

  1. 01 Introduction to EuroSAT and Remote Sensing Classification
  2. 02 Setting Up Google Colab with GPU Runtime
  3. 03 Loading and Exploring the EuroSAT Dataset
  4. 04 Data Preprocessing and Augmentation
  5. 05 Building a CNN Model with TensorFlow / Keras
  6. 06 Training the Model and Monitoring Performance
  7. 07 Evaluation: Confusion Matrix, Classification Report, and ROC
  8. 08 Transfer Learning with ResNet50
  9. 09 Visualizing Predictions on Satellite Patches
  10. 10 Exporting Your Model and Next Steps
01

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

ClassSamplesDescription
AnnualCrop3,000Seasonal cultivated farmland
Forest3,000Dense tree cover areas
HerbaceousVegetation3,000Grasslands and meadows
Highway2,500Major road infrastructure
Industrial2,500Factories, warehouses, industrial zones
Pasture2,000Livestock grazing areas
PermanentCrop2,500Orchards, vineyards, permanent cultivation
Residential3,000Urban housing areas
River2,500Waterways and river corridors
SeaLake3,000Large water bodies
Dataset Source: EuroSAT is publicly available through TensorFlow Datasets (TFDS) and can also be downloaded from the official repository. Each image is 64x64 pixels with 13 spectral bands (or RGB composite). In this course we use the RGB version for simplicity.

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.

02

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

cell_01_setup.py Python
# 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')))
Expected Output
TensorFlow version: 2.15.0 GPU available: [PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')] Num GPUs: 1

Step 3: Install Dependencies

cell_02_dependencies.py Python
# 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!")
03

Loading the EuroSAT Dataset

TensorFlow Datasets (TFDS) hosts the EuroSAT RGB version. We load it directly with a train/validation/test split.

cell_03_load_data.py Python
# 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()}")
Expected Output
Number of classes: 10 Class names: ['AnnualCrop', 'Forest', 'HerbaceousVegetation', 'Highway', 'Industrial', 'Pasture', 'PermanentCrop', 'Residential', 'River', 'SeaLake'] Train samples: 18900 Val samples: 4050 Test samples: 4050

Visualize Sample Images

cell_04_visualize.py Python
# 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()
What you should see: A 2x5 grid of 64x64 satellite image patches showing different land cover types. Forest images appear dark green, SeaLake is deep blue, Residential shows clear building patterns, and Highway features linear road structures.
04

Data Preprocessing and Augmentation

We normalize pixel values to [0, 1], resize images, batch the dataset, and add data augmentation to improve generalization.

cell_05_preprocessing.py Python
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

cell_06_augmentation_preview.py Python
# 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()
05

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.

cell_07_build_cnn.py Python
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()
Model Architecture Summary
Model: "EuroSAT_CNN" _________________________________________________________________ Total params: 482,186 Trainable params: 481,162 Non-trainable params: 1,024 _________________________________________________________________

Compile the Model

cell_08_compile.py Python
model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=1e-3),
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy'],
)

print("Model compiled. Ready to train!")
06

Training the Model

We train for 25 epochs with early stopping and learning rate reduction callbacks to prevent overfitting.

cell_09_train.py Python
# 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

cell_10_plot_history.py Python
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()
Expected Results: The custom CNN should reach approximately 88-92% validation accuracy after training. You will see training and validation curves converging, which indicates good generalization.
07

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.

cell_11_evaluate.py Python
# 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

cell_12_confusion_matrix.py Python
# 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()
Key insight: Common misclassifications occur between AnnualCrop / PermanentCrop and between HerbaceousVegetation / Pasture, since these classes share similar spectral signatures. Forest and SeaLake typically show near-perfect accuracy.
08

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%.

cell_13_transfer_learning.py Python
# 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

cell_14_fine_tune.py Python
# 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}")
Expected Results: With transfer learning, you should reach approximately 93-96% test accuracy, a significant improvement over the custom CNN. This demonstrates the power of leveraging pre-trained features even for satellite imagery.
09

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.

cell_15_visualize_predictions.py Python
# 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

cell_16_per_class_accuracy.py Python
# 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()
10

Exporting and Next Steps

Save your trained model so you can reload it later or deploy it in a web application or GIS pipeline.

cell_17_export.py Python
# 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)
Dataset Citation: Helber, P., Bischke, B., Dengel, A., & Borth, D. (2019). EuroSAT: A Novel Dataset and Deep Learning Benchmark for Land Use and Land Cover Classification. IEEE Journal of Selected Topics in Applied Earth Observations and Remote Sensing, 12(7), 2217-2226.