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
Transfer learning leverages pretrained models to classify drone scenes with limited data.
**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.
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.
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
Drone-specific augmentation handles unique challenges like altitude changes and viewpoint variations.
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.
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
- Transfer learning enables drone classification with limited data
- ResNet residual connections enable training deep networks
- EfficientNet balances accuracy and efficiency for edge deployment
- Drone-specific augmentation handles altitude and viewpoint variations
- Scene classification categorizes land use for urban planning
Next, we'll explore pose estimation for tracking humans and objects from aerial views.