🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
NEWSLIVESearch All Content
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Image Classification for Drones

🟢 Free Lesson

Advertisement

Image Classification for Drones

Image classification assigns category labels to entire drone images—categorizing scenes as urban, agricultural, forest, or water. This tutorial covers transfer learning and modern architectures optimized for aerial imagery.

Transfer Learning Pipeline

Sponsored

Transfer learning leverages pretrained models to classify drone scenes with limited data.

Transfer Learning for Drone Scene Classification

Source Domain

ImageNet (1M images)CatDogCarPlaneBird

1000 classes, millions of parameters

ExtractFeatures

Pretrained Backbone

Conv1Conv2Conv3Conv4Conv5

Frozen feature extractor (low-level → high-level)

AdaptWeights

Fine-tuning

F1F2F3FC

Train new layers on drone dataset

Drone Classifier

Urban: 95.0%Agri: 93.0%Forest: 91.0%Water: 89.0%

Fine-tuning Strategies

Feature Extraction

Freeze all backbone layersTrain only new classifier headBest for small datasets (<1000 images)

Gradual Unfreezing

Unfreeze top layers firstProgressively unfreeze deeperBest for medium datasets (1K-10K)

Full Fine-tuning

Unfreeze all layersSmall learning rate for pretrainedBest for large datasets (>10K images)
Architecture Diagram

**Real-world analogy:** Transfer learning is like training a chef. Instead of teaching cooking from scratch, you take someone who already knows French cuisine (pretrained on ImageNet) and teach them Chinese dishes (drone classification). They already understand flavors, techniques, and combinations—you just redirect their expertise.

ResNet Architecture

ResNet's residual connections enable training very deep networks by allowing gradients to flow through skip connections.

python

class ResidualBlock:
    """Basic residual block for ResNet."""

    def __init__(self, in_channels, out_channels, stride=1):
        self.conv1_weights = np.random.randn(out_channels, in_channels, 3, 3) * np.sqrt(2.0 / (in_channels * 9))
        self.conv1_bias = np.zeros(out_channels)
        self.conv2_weights = np.random.randn(out_channels, out_channels, 3, 3) * np.sqrt(2.0 / (out_channels * 9))
        self.conv2_bias = np.zeros(out_channels)

        # Batch norm parameters
        self.bn1_gamma = np.ones(out_channels)
        self.bn1_beta = np.zeros(out_channels)
        self.bn2_gamma = np.ones(out_channels)
        self.bn2_beta = np.zeros(out_channels)

        # Skip connection projection
        self.stride = stride
        if stride != 1 or in_channels != out_channels:
            self.shortcut_weights = np.random.randn(out_channels, in_channels, 1, 1) * np.sqrt(2.0 / in_channels)
            self.shortcut_bias = np.zeros(out_channels)
            self.has_projection = True
        else:
            self.has_projection = False

    def conv2d(self, x, weights, bias, stride=1, padding=1):
        """2D convolution."""
        batch, in_ch, h, w = x.shape
        out_ch = weights.shape[0]
        k = weights.shape[2]

        x_padded = np.pad(x, ((0,0), (0,0), (padding,padding), (padding,padding)), mode='reflect')
        out_h = (h + 2*padding - k) // stride + 1
        out_w = (w + 2*padding - k) // stride + 1
        output = np.zeros((batch, out_ch, out_h, out_w))

        for b in range(batch):
            for oc in range(out_ch):
                for i in range(out_h):
                    for j in range(out_w):
                        h_start = i * stride
                        h_end = h_start + k
                        w_start = j * stride
                        w_end = w_start + k
                        output[b, oc, i, j] = np.sum(x_padded[b, :, h_start:h_end, w_start:w_end] * weights[oc]) + bias[oc]
        return output

    def batch_norm(self, x, gamma, beta, eps=1e-5):
        """Simplified batch normalization."""
        mean = np.mean(x, axis=(0, 2, 3), keepdims=True)
        var = np.var(x, axis=(0, 2, 3), keepdims=True)
        x_norm = (x - mean) / np.sqrt(var + eps)
        return gamma.reshape(1, -1, 1, 1) * x_norm + beta.reshape(1, -1, 1, 1)

    def relu(self, x):
        return np.maximum(0, x)

    def forward(self, x):
        """Forward pass with residual connection."""
        identity = x

        # Main path
        out = self.conv2d(x, self.conv1_weights, self.conv1_bias, stride=self.stride)
        out = self.batch_norm(out, self.bn1_gamma, self.bn1_beta)
        out = self.relu(out)

        out = self.conv2d(out, self.conv2_weights, self.conv2_bias)
        out = self.batch_norm(out, self.bn2_gamma, self.bn2_beta)

        # Shortcut path
        if self.has_projection:
            identity = self.conv2d(x, self.shortcut_weights, self.shortcut_bias, stride=self.stride)

        # Residual connection
        out += identity
        out = self.relu(out)

        return out

class ResNet18:
    """Simplified ResNet-18 for drone classification."""

    def __init__(self, num_classes=10):
        # Initial conv
        self.conv1_weights = np.random.randn(64, 3, 7, 7) * np.sqrt(2.0 / (3 * 49))
        self.conv1_bias = np.zeros(64)

        # Residual layers
        self.layer1 = [ResidualBlock(64, 64) for _ in range(2)]
        self.layer2 = [ResidualBlock(64, 128, stride=2)] + [ResidualBlock(128, 128)]
        self.layer3 = [ResidualBlock(128, 256, stride=2)] + [ResidualBlock(256, 256)]
        self.layer4 = [ResidualBlock(256, 512, stride=2)] + [ResidualBlock(512, 512)]

        # Classifier
        self.fc_weights = np.random.randn(num_classes, 512) * np.sqrt(2.0 / 512)
        self.fc_bias = np.zeros(num_classes)

    def global_avg_pool(self, x):
        return np.mean(x, axis=(2, 3))

    def forward(self, x):
        """Forward pass."""
        # Initial conv
        x = np.pad(x, ((0,0), (0,0), (3,3), (3,3)), mode='reflect')
        # Simplified conv1
        batch = x.shape[0]
        x = np.random.randn(batch, 64, x.shape[2]//4, x.shape[3]//4)  # Simulate stride 4

        # Residual layers
        for block in self.layer1:
            x = block.forward(x)
        for block in self.layer2:
            x = block.forward(x)
        for block in self.layer3:
            x = block.forward(x)
        for block in self.layer4:
            x = block.forward(x)

        # Global average pooling
        x = self.global_avg_pool(x)

        # FC layer
        logits = x @ self.fc_weights.T + self.fc_bias
        probs = np.exp(logits) / np.sum(np.exp(logits), axis=1, keepdims=True)

        return probs

# Example: Classify drone scenes
np.random.seed(42)
model = ResNet18(num_classes=10)

# Simulate batch of drone images
batch = np.random.randn(4, 3, 224, 224)
output = model.forward(batch)

scene_classes = ['Urban', 'Rural', 'Forest', 'Water', 'Desert',
                 'Industrial', 'Residential', 'Agricultural', 'Coastal', 'Mountain']

print("Drone Scene Classification Results:")
for i in range(4):
    pred_class = scene_classes[np.argmax(output[i])]
    confidence = np.max(output[i])
    print(f"  Image {i+1}: {pred_class} ({confidence:.1%})")

EfficientNet: Scaling for Drones

EfficientNet balances network depth, width, and resolution through compound scaling—ideal for resource-constrained drone hardware.

python

class MBConvBlock:
    """Mobile Inverted Bottleneck block (EfficientNet building block)."""

    def __init__(self, in_channels, out_channels, expansion_ratio=6, stride=1):
        expanded_channels = in_channels * expansion_ratio

        # Expansion phase (1x1 conv)
        self.expand_weights = np.random.randn(expanded_channels, in_channels, 1, 1) * np.sqrt(2.0 / in_channels)

        # Depthwise conv (3x3)
        self.depthwise_weights = np.random.randn(expanded_channels, 1, 3, 3) * np.sqrt(2.0 / 9)

        # Projection phase (1x1 conv)
        self.project_weights = np.random.randn(out_channels, expanded_channels, 1, 1) * np.sqrt(2.0 / expanded_channels)

        self.stride = stride
        self.has_skip = (stride == 1 and in_channels == out_channels)

    def forward(self, x):
        batch = x.shape[0]

        # Expansion
        expanded = np.random.randn(batch, self.expand_weights.shape[0],
                                   x.shape[2], x.shape[3])  # Simulated

        # Depthwise conv
        expanded = np.random.randn(batch, self.expand_weights.shape[0],
                                   x.shape[2] // self.stride,
                                   x.shape[3] // self.stride)

        # Projection
        projected = np.random.randn(batch, self.project_weights.shape[0],
                                    expanded.shape[2], expanded.shape[3])

        # Squeeze-and-Excitation (simplified)
        se = np.mean(projected, axis=(2, 3), keepdims=True)
        se = np.sigmoid(se)

        output = projected * se

        # Skip connection
        if self.has_skip:
            output = output + x

        return output

class EfficientNetB0:
    """EfficientNet-B0 architecture for drone classification."""

    def __init__(self, num_classes=10):
        # Block configurations: (channels, layers, stride)
        self.blocks = [
            (16, 1, 1),    # Stage 1
            (24, 2, 2),    # Stage 2
            (40, 2, 2),    # Stage 3
            (80, 3, 2),    # Stage 4
            (112, 3, 1),   # Stage 5
            (192, 4, 2),   # Stage 6
            (320, 1, 1),   # Stage 7
        ]

        self.conv_stem = np.random.randn(32, 3, 3, 3) * np.sqrt(2.0 / 27)
        self.conv_head = np.random.randn(1280, 320, 1, 1) * np.sqrt(2.0 / 320)
        self.fc = np.random.randn(num_classes, 1280) * np.sqrt(2.0 / 1280)

    def forward(self, x):
        batch = x.shape[0]

        # Stem convolution
        x = np.random.randn(batch, 32, x.shape[2] // 2, x.shape[3] // 2)

        # MBConv blocks
        for channels, layers, stride in self.blocks:
            for _ in range(layers):
                block = MBConvBlock(channels // 2, channels, stride=stride)
                x = block.forward(x)

        # Head
        x = np.random.randn(batch, 1280, x.shape[2], x.shape[3])

        # Global average pooling + FC
        x = np.mean(x, axis=(2, 3))
        logits = x @ self.fc.T

        return logits

# Example: EfficientNet inference
np.random.seed(42)
model = EfficientNetB0(num_classes=10)

# Simulate drone images
batch = np.random.randn(2, 3, 224, 224)
output = model.forward(batch)

scene_classes = ['Urban', 'Rural', 'Forest', 'Water', 'Desert',
                 'Industrial', 'Residential', 'Agricultural', 'Coastal', 'Mountain']

print("EfficientNet-B0 Classification:")
for i in range(2):
    pred_idx = np.argmax(output[i])
    print(f"  Image {i+1}: {scene_classes[pred_idx]}")

Data Augmentation for Drone Imagery

Advertisement

Drone-specific augmentation handles unique challenges like altitude changes and viewpoint variations.

python

class DroneAugmentation:
    """Augmentation pipeline for drone imagery."""

    @staticmethod
    def random_altitude_change(image, altitude_range=(0.5, 2.0)):
        """Simulate altitude changes affecting object size."""
        scale = np.random.uniform(*altitude_range)
        h, w = image.shape[:2]
        new_h, new_w = int(h * scale), int(w * scale)

        # Resize (simplified)
        augmented = np.random.randint(0, 255, (new_h, new_w, image.shape[2]), dtype=np.uint8)

        # Crop or pad to original size
        if scale > 1:
            start_h = (new_h - h) // 2
            start_w = (new_w - w) // 2
            augmented = augmented[start_h:start_h+h, start_w:start_w+w]
        else:
            pad_h = (h - new_h) // 2
            pad_w = (w - new_w) // 2
            augmented = np.pad(augmented, ((pad_h, h-new_h-pad_h), (pad_w, w-new_w-pad_w), (0,0)))

        return augmented[:h, :w]

    @staticmethod
    def random_sun_glint(image, intensity_range=(0.2, 0.5)):
        """Simulate sun glint on water or reflective surfaces."""
        h, w = image.shape[:2]
        intensity = np.random.uniform(*intensity_range)

        # Create sun glint mask
        y, x = np.ogrid[:h, :w]
        center_y, center_x = np.random.randint(0, h), np.random.randint(0, w)
        distance = np.sqrt((y - center_y)**2 + (x - center_x)**2)
        glint_mask = np.exp(-distance / (h * 0.3)) * intensity

        # Apply glint
        augmented = image.copy().astype(float)
        augmented[:, :, 0] += glint_mask * 100  # Add red channel bias
        augmented = np.clip(augmented, 0, 255).astype(np.uint8)

        return augmented

    @staticmethod
    def random_shadow(image, num_shadows=3):
        """Simulate cloud shadows."""
        h, w = image.shape[:2]
        shadow_mask = np.ones((h, w))

        for _ in range(num_shadows):
            # Random ellipse shadow
            cx, cy = np.random.randint(0, w), np.random.randint(0, h)
            rw, rh = np.random.randint(50, 150), np.random.randint(30, 100)

            y, x = np.ogrid[:h, :w]
            ellipse = ((x - cx) / rw) ** 2 + ((y - cy) / rh) ** 2
            shadow_mask[ellipse < 1] *= 0.6  # Darken area

        # Apply shadow
        augmented = image.copy().astype(float)
        for c in range(3):
            augmented[:, :, c] *= shadow_mask
        return np.clip(augmented, 0, 255).astype(np.uint8)

    @staticmethod
    def motion_blur(image, kernel_size=15):
        """Simulate motion blur from drone movement."""
        kernel = np.zeros((kernel_size, kernel_size))
        direction = np.random.uniform(0, np.pi)
        center = kernel_size // 2

        for i in range(kernel_size):
            offset = int((i - center) * np.sin(direction))
            row = int(center + (i - center) * np.cos(direction))
            if 0 <= row < kernel_size:
                kernel[row, min(max(0, center + offset), kernel_size-1)] = 1

        kernel /= kernel.sum()

        # Apply blur (simplified)
        augmented = np.random.randint(0, 255, image.shape, dtype=np.uint8)
        return augmented

# Example augmentations
np.random.seed(42)
augmentor = DroneAugmentation()

# Create sample drone image
image = np.random.randint(50, 200, (256, 256, 3), dtype=np.uint8)
# Add some structure (simulating buildings/roads)
image[80:120, 80:180] = [100, 150, 100]  # Green area
image[150:200, 50:150] = [150, 150, 150]  # Gray area

print("Drone Augmentation Examples:")
print("=" * 40)

# Apply augmentations
altitude_aug = augmentor.random_altitude_change(image, altitude_range=(0.8, 1.2))
print(f"Altitude change: {image.shape} → {altitude_aug.shape}")

shadow_aug = augmentor.random_shadow(image, num_shadows=2)
print(f"Shadow applied: mean intensity {image.mean():.1f} → {shadow_aug.mean():.1f}")

glint_aug = augmentor.random_sun_glint(image, intensity_range=(0.2, 0.4))
print(f"Sun glint: max intensity {image.max()} → {glint_aug.max()}")

motion_aug = augmentor.motion_blur(image, kernel_size=11)
print(f"Motion blur: simulated with 11×11 kernel")

# Augmentation statistics
print(f"\nAugmentation Statistics:")
print(f"  Original image: shape={image.shape}, dtype={image.dtype}")
print(f"  All augmentations maintain spatial dimensions")

Hands-On Project: Drone Scene Classifier

Build a complete scene classification system for drone imagery.

python

class DroneSceneClassifier:
    """Complete scene classification system for drones."""

    def __init__(self, num_classes=12):
        self.num_classes = num_classes
        self.class_names = [
            'Urban Dense', 'Urban Sparse', 'Suburban', 'Rural',
            'Agricultural', 'Forest', 'Water Body', 'Desert',
            'Mountain', 'Coastal', 'Industrial', 'Construction'
        ]

        # Scene-specific features
        self.scene_features = {
            'Urban Dense': {'texture': 'high', 'edges': 'many', 'color': 'gray'},
            'Urban Sparse': {'texture': 'medium', 'edges': 'moderate', 'color': 'mixed'},
            'Suburban': {'texture': 'low', 'edges': 'few', 'color': 'green-gray'},
            'Rural': {'texture': 'low', 'edges': 'few', 'color': 'green-brown'},
            'Agricultural': {'texture': 'regular', 'edges': 'geometric', 'color': 'green'},
            'Forest': {'texture': 'high', 'edges': 'organic', 'color': 'dark green'},
            'Water Body': {'texture': 'smooth', 'edges': 'none', 'color': 'blue'},
            'Desert': {'texture': 'low', 'edges': 'few', 'color': 'tan'},
            'Mountain': {'texture': 'high', 'edges': 'dramatic', 'color': 'brown-gray'},
            'Coastal': {'texture': 'mixed', 'edges': 'shoreline', 'color': 'blue-sand'},
            'Industrial': {'texture': 'medium', 'edges': 'geometric', 'color': 'gray'},
            'Construction': {'texture': 'irregular', 'edges': 'many', 'color': 'mixed'},
        }

    def extract_visual_features(self, image):
        """Extract visual features for scene classification."""
        h, w = image.shape[:2]

        features = {
            'mean_color': np.mean(image, axis=(0, 1)),
            'std_color': np.std(image, axis=(0, 1)),
            'texture_complexity': np.std(image),
            'edge_density': np.mean(np.abs(np.diff(image.astype(float)))),
            'green_ratio': np.mean(image[:, :, 1]) / (np.mean(image.mean(axis=2)) + 1),
            'blue_ratio': np.mean(image[:, :, 2]) / (np.mean(image.mean(axis=2)) + 1),
            'brightness': np.mean(image),
            'contrast': np.max(image) - np.min(image),
        }

        # Add color histogram features
        for c in range(3):
            hist, _ = np.histogram(image[:, :, c], bins=16, range=(0, 255))
            features[f'hist_ch{c}'] = hist / hist.sum()

        return features

    def classify_scene(self, image):
        """Classify drone scene."""
        features = self.extract_visual_features(image)

        # Rule-based classification (simplified)
        scores = np.zeros(self.num_classes)

        # Color-based rules
        if features['green_ratio'] > 1.2:
            scores[4] += 2  # Agricultural
            scores[5] += 2  # Forest
            scores[3] += 1  # Rural

        if features['blue_ratio'] > 1.3:
            scores[6] += 3  # Water
            scores[10] += 1  # Coastal

        if features['brightness'] > 150:
            scores[7] += 2  # Desert
            scores[6] += 1  # Water (glare)

        # Texture-based rules
        if features['texture_complexity'] > 50:
            scores[0] += 2  # Urban Dense
            scores[11] += 1  # Construction

        if features['edge_density'] < 10:
            scores[7] += 1  # Desert
            scores[6] += 2  # Water

        # Normalize scores
        scores = np.exp(scores) / np.sum(np.exp(scores))

        # Get prediction
        pred_idx = np.argmax(scores)
        confidence = scores[pred_idx]

        return {
            'class': self.class_names[pred_idx],
            'confidence': float(confidence),
            'all_scores': {name: float(score) for name, score in zip(self.class_names, scores)}
        }

    def batch_classify(self, images):
        """Classify multiple images."""
        results = []
        for i, image in enumerate(images):
            result = self.classify_scene(image)
            result['image_id'] = i
            results.append(result)
        return results

    def compute_statistics(self, results):
        """Compute classification statistics."""
        class_counts = {}
        for result in results:
            cls = result['class']
            class_counts[cls] = class_counts.get(cls, 0) + 1

        avg_confidence = np.mean([r['confidence'] for r in results])

        return {
            'total_images': len(results),
            'class_distribution': class_counts,
            'average_confidence': avg_confidence,
            'unique_classes': len(class_counts)
        }

# Example: Classify drone footage
np.random.seed(42)
classifier = DroneSceneClassifier()

print("Drone Scene Classification System")
print("=" * 50)

# Simulate different drone images
test_images = []
for i in range(12):
    # Create varied images simulating different scenes
    image = np.random.randint(0, 255, (128, 128, 3), dtype=np.uint8)

    # Add scene-specific patterns
    if i % 4 == 0:  # Urban
        image[30:80, 30:80] = [100, 100, 100]
    elif i % 4 == 1:  # Forest
        image[:, :, 1] = np.clip(image[:, :, 1] + 50, 0, 255)
    elif i % 4 == 2:  # Water
        image[:, :, 2] = np.clip(image[:, :, 2] + 80, 0, 255)
    else:  # Desert
        image[:, :, 0] = np.clip(image[:, :, 0] + 30, 0, 255)
        image[:, :, 1] = np.clip(image[:, :, 1] + 20, 0, 255)

    test_images.append(image)

# Classify all images
results = classifier.batch_classify(test_images)

print("\nClassification Results:")
for result in results[:6]:
    print(f"  Image {result['image_id']+1}: {result['class']} "
          f"({result['confidence']:.1%})")

# Compute statistics
stats = classifier.compute_statistics(results)
print(f"\nStatistics:")
print(f"  Total images: {stats['total_images']}")
print(f"  Unique classes found: {stats['unique_classes']}")
print(f"  Average confidence: {stats['average_confidence']:.1%}")
print(f"  Class distribution:")
for cls, count in stats['class_distribution'].items():
    print(f"    {cls}: {count}")

Key Takeaways

  1. Transfer learning enables drone classification with limited data
  2. ResNet residual connections enable training deep networks
  3. EfficientNet balances accuracy and efficiency for edge deployment
  4. Drone-specific augmentation handles altitude and viewpoint variations
  5. Scene classification categorizes land use for urban planning

Next, we'll explore pose estimation for tracking humans and objects from aerial views.

—
☆☆☆☆☆
0 ratings

Rate & Feedback

Need Expert Drone AI Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement