🎯 Object Detection Fundamentals
🎯 What is Object Detection?
Object detection is a computer vision task that identifies and locates objects in images or videos. It provides both classification (what) and localization (where) of objects.
Key concept: Object detection outputs bounding boxes with class labels and confidence scores for each detected object.
Deep Explanation
Object detection is a fundamental problem in computer vision. It combines classification and regression — classifying objects and regressing their bounding box coordinates.
Why it matters: Object detection enables autonomous driving, surveillance, medical imaging, and augmented reality applications.
# Object detection inference example
import torch
import torchvision
def detect_objects(image, model, threshold=0.5):
# Preprocess image
image_tensor = preprocess(image)
# Run inference
with torch.no_grad():
predictions = model(image_tensor)
# Extract predictions
boxes = predictions[0]['boxes']
scores = predictions[0]['scores']
labels = predictions[0]['labels']
# Filter by threshold
mask = scores > threshold
boxes = boxes[mask]
scores = scores[mask]
labels = labels[mask]
# Draw bounding boxes
for box, score, label in zip(boxes, scores, labels):
draw_box(image, box, label, score)
return boxes, scores, labels📊 Image Classification
🎯 Classification Foundation
Image classification assigns a single label to an entire image. It's the foundation for object detection, which adds localization.
Classification Details
Architectures: CNNs like ResNet, VGG, EfficientNet. Output: Probability distribution over classes. Training: Cross-entropy loss on labeled images.
Relation to detection: Object detection uses classification to classify each object candidate.
# Image classification (PyTorch)
import torch.nn as nn
class ImageClassifier(nn.Module):
def __init__(self, num_classes=1000):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, 7, stride=2),
nn.ReLU(),
nn.MaxPool2d(3, stride=2),
# ... more layers
)
self.classifier = nn.Sequential(
nn.Linear(2048, 512),
nn.ReLU(),
nn.Linear(512, num_classes)
)
def forward(self, x):
x = self.features(x)
x = x.mean([2, 3]) # Global average pooling
return self.classifier(x)
# Usage
model = ImageClassifier(num_classes=1000)
output = model(image) # Shape: (batch_size, 1000)
probs = torch.softmax(output, dim=1)📍 Localization
🎯 Locating Objects
Localization predicts the bounding box coordinates of an object. It outputs (x, y, w, h) or (x1, y1, x2, y2).
Localization Methods
Regression: Directly predict box coordinates. Anchor boxes: Predict offsets from predefined boxes. Heatmaps: Predict center points and box dimensions.
Loss: L1/L2 loss for coordinates, IoU loss for box overlap.
# Bounding box operations
class BoundingBox:
def __init__(self, x1, y1, x2, y2):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
def area(self):
return (self.x2 - self.x1) * (self.y2 - self.y1)
def iou(self, other):
# Intersection over Union
x1 = max(self.x1, other.x1)
y1 = max(self.y1, other.y1)
x2 = min(self.x2, other.x2)
y2 = min(self.y2, other.y2)
if x2 <= x1 or y2 <= y1:
return 0.0
intersection = (x2 - x1) * (y2 - y1)
union = self.area() + other.area() - intersection
return intersection / union
def to_xywh(self):
# Convert to center x, center y, width, height
cx = (self.x1 + self.x2) / 2
cy = (self.y1 + self.y2) / 2
w = self.x2 - self.x1
h = self.y2 - self.y1
return cx, cy, w, h🔄 Sliding Window
🎯 Traditional Approach
Sliding window is a traditional approach where a classifier is applied to every possible window at multiple scales and locations.
Sliding Window Details
Process: Slide a window across the image, classify each window. Multi-scale: Use different window sizes. Computational cost: Very expensive, especially with many classes.
Improvements: Use image pyramids, efficient classifiers, and region proposals.
# Sliding window implementation
def sliding_window(image, window_size, step_size):
# Generate windows at multiple scales
windows = []
scales = [1.0, 0.75, 0.5, 0.25]
for scale in scales:
scaled_image = resize(image, scale)
h, w = scaled_image.shape[:2]
window_h, window_w = window_size
for y in range(0, h - window_h + 1, step_size):
for x in range(0, w - window_w + 1, step_size):
window = scaled_image[y:y+window_h, x:x+window_w]
windows.append((window, x, y, scale))
return windows
# Apply classifier to each window
def detect_with_sliding(image, classifier):
windows = sliding_window(image, (64, 64), 16)
detections = []
for window, x, y, scale in windows:
score = classifier.predict(window)
if score > threshold:
detections.append({
'box': (x, y, x+64, y+64),
'score': score,
'scale': scale
})
return detections📋 Region-Based CNN
🎯 R-CNN Family
R-CNN uses region proposals to reduce the number of windows. It evolved through Fast R-CNN, Faster R-CNN, and Mask R-CNN.
R-CNN Evolution
R-CNN: Extract region proposals, warp them, classify each. Fast R-CNN: Single forward pass for feature extraction. Faster R-CNN: Region Proposal Network (RPN) integrates proposal generation. Mask R-CNN: Adds instance segmentation.
Performance: High accuracy, but slower than one-stage detectors.
# Faster R-CNN architecture (simplified)
class FasterRCNN(nn.Module):
def __init__(self, backbone, num_classes):
super().__init__()
self.backbone = backbone
self.rpn = RPN(backbone.out_channels)
self.roi_head = RoIHead(backbone.out_channels, num_classes)
def forward(self, x):
# Feature extraction
features = self.backbone(x)
# Region Proposal Network
proposals, rpn_losses = self.rpn(features)
# ROI Pooling and classification
detections, roi_losses = self.roi_head(features, proposals)
return detections, rpn_losses, roi_losses
class RPN(nn.Module):
def __init__(self, in_channels):
super().__init__()
self.conv = nn.Conv2d(in_channels, 512, 3, padding=1)
self.classifier = nn.Conv2d(512, 2 * 9, 1) # 2 classes × 9 anchors
self.regressor = nn.Conv2d(512, 4 * 9, 1) # 4 coords × 9 anchors
self.anchors = generate_anchors()
def forward(self, features):
# Generate proposals
scores = self.classifier(self.conv(features))
offsets = self.regressor(self.conv(features))
proposals = self.decode_anchors(self.anchors, offsets)
return proposals⚡ YOLO
🎯 You Only Look Once
YOLO (You Only Look Once) is a one-stage detector that predicts bounding boxes and class probabilities directly from the full image in a single forward pass.
YOLO Details
YOLOv1: Divides image into grid, each cell predicts boxes. YOLOv2/v3: Added anchor boxes, multi-scale predictions. YOLOv4/v5/v8: Continual improvements in speed and accuracy.
Advantages: Very fast (real-time), simple architecture.
# YOLO-style detection (simplified)
class YOLODetector:
def __init__(self, grid_size=7, num_boxes=2, num_classes=20):
self.grid_size = grid_size
self.num_boxes = num_boxes
self.num_classes = num_classes
def decode_predictions(self, predictions):
# predictions shape: (batch, grid, grid, num_boxes * 5 + num_classes)
batch_size = predictions.shape[0]
detections = []
for b in range(batch_size):
for i in range(self.grid_size):
for j in range(self.grid_size):
cell_pred = predictions[b, i, j]
for box_idx in range(self.num_boxes):
# Extract box coordinates
cx = (j + cell_pred[box_idx*5]) / self.grid_size
cy = (i + cell_pred[box_idx*5+1]) / self.grid_size
w = cell_pred[box_idx*5+2]
h = cell_pred[box_idx*5+3]
conf = cell_pred[box_idx*5+4]
if conf > 0.5:
# Class scores
class_scores = cell_pred[self.num_boxes*5:]
class_id = class_scores.argmax()
class_prob = class_scores[class_id]
detections.append({
'box': (cx-w/2, cy-h/2, cx+w/2, cy+h/2),
'class': class_id,
'confidence': conf * class_prob
})
return detections🚀 SSD
🎯 Single Shot MultiBox Detector
SSD (Single Shot MultiBox Detector) is another one-stage detector that uses multi-scale feature maps and anchor boxes.
SSD Features
Multi-scale: Predictions from different feature map layers. Anchors: Different aspect ratios and scales. Speed: Faster than YOLO (original). Accuracy: Comparable to two-stage detectors.
Architecture: Base network (e.g., VGG) with extra convolutional layers.
# SSD architecture (simplified)
class SSD(nn.Module):
def __init__(self, base_network, num_classes, aspect_ratios):
super().__init__()
self.base = base_network
self.extra_layers = self.create_extra_layers()
self.classifiers = nn.ModuleList()
self.regressors = nn.ModuleList()
self.aspect_ratios = aspect_ratios
# Create prediction heads for each feature map
for feature_map in self.get_feature_maps():
n_channels = feature_map.shape[1]
self.classifiers.append(
nn.Conv2d(n_channels, len(aspect_ratios) * num_classes, 1)
)
self.regressors.append(
nn.Conv2d(n_channels, len(aspect_ratios) * 4, 1)
)
def forward(self, x):
features = []
for layer in [self.base, self.extra_layers]:
x = layer(x)
features.append(x)
predictions = []
for feat, cls_head, reg_head in zip(features, self.classifiers, self.regressors):
# Apply prediction heads
class_pred = cls_head(feat)
box_pred = reg_head(feat)
predictions.append((class_pred, box_pred))
return predictions🏔️ FPN
🎯 Feature Pyramid Network
FPN (Feature Pyramid Network) builds a feature pyramid that improves detection of objects at different scales.
FPN Details
Bottom-up: Standard CNN backbone (ResNet). Top-down: Upsample higher-level features. Lateral connections: Combine bottom-up and top-down features. Multi-scale: Each level handles different object sizes.
Integration: Used in RetinaNet, Mask R-CNN, and many modern detectors.
# FPN implementation (simplified)
class FPN(nn.Module):
def __init__(self, backbone):
super().__init__()
self.backbone = backbone
# Lateral connections
self.lateral = nn.ModuleList([
nn.Conv2d(c, 256, 1) for c in [2048, 1024, 512, 256]
])
# Top-down outputs
self.output = nn.ModuleList([
nn.Conv2d(256, 256, 3, padding=1) for _ in range(4)
])
def forward(self, x):
# Bottom-up features
c5, c4, c3, c2 = self.backbone(x) # from deepest to shallowest
# Top-down path
p5 = self.lateral[0](c5)
p4 = self.lateral[1](c4) + nn.Upsample(scale_factor=2)(p5)
p3 = self.lateral[2](c3) + nn.Upsample(scale_factor=2)(p4)
p2 = self.lateral[3](c2) + nn.Upsample(scale_factor=2)(p3)
# Output features
p5 = self.output[0](p5)
p4 = self.output[1](p4)
p3 = self.output[2](p3)
p2 = self.output[3](p2)
return p2, p3, p4, p5📐 Anchor Boxes
🎯 Anchor-Based Detection
Anchor boxes are predefined bounding boxes used as references. The model predicts offsets from these anchors.
Anchor Design
Scales: Different sizes (e.g., 32, 64, 128, 256). Aspect ratios: 1:1, 1:2, 2:1. Placement: Centered at feature map locations. K-means clustering: Used in YOLO to determine anchor sizes from training data.
Positive anchors: Those with high IoU with ground truth boxes.
# Anchor generation
def generate_anchors(scales=[32, 64, 128], aspect_ratios=[0.5, 1, 2]):
anchors = []
for scale in scales:
for ratio in aspect_ratios:
w = scale * math.sqrt(ratio)
h = scale / math.sqrt(ratio)
anchors.append((-w/2, -h/2, w/2, h/2))
return anchors
# K-means for anchor clustering (YOLO)
def kmeans_anchors(boxes, k=9):
# boxes: list of (w, h) from dataset
# Use k-means to find optimal anchor sizes
# Initialize centroids randomly
centroids = random.sample(boxes, k)
for _ in range(100):
# Assign to clusters
clusters = [[] for _ in range(k)]
for box in boxes:
distances = [iou_distance(box, c) for c in centroids]
closest = distances.index(min(distances))
clusters[closest].append(box)
# Update centroids
for i in range(k):
if clusters[i]:
centroids[i] = np.mean(clusters[i], axis=0)
return centroids🚫 Non-Max Suppression
🎯 Removing Duplicate Detections
Non-Max Suppression (NMS) removes duplicate detections by keeping only the highest-scoring box among overlapping ones.
NMS Process
Step 1: Sort detections by confidence score. Step 2: Select the highest-scoring box. Step 3: Remove boxes with IoU > threshold. Step 4: Repeat until all boxes processed.
Variants: Soft-NMS (weights scores instead of removing), NMS with adaptive thresholds.
# Non-Max Suppression implementation
def non_max_suppression(boxes, scores, iou_threshold=0.5):
# boxes: list of (x1, y1, x2, y2)
# scores: list of confidence scores
if len(boxes) == 0:
return []
# Sort by score descending
indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
keep = []
while indices:
# Keep the best
current = indices.pop(0)
keep.append(current)
# Remove boxes with high IoU
to_remove = []
for i, idx in enumerate(indices):
iou = compute_iou(boxes[current], boxes[idx])
if iou > iou_threshold:
to_remove.append(i)
# Remove in reverse order
for i in reversed(to_remove):
indices.pop(i)
return keep
# Soft-NMS (differentiable)
def soft_nms(boxes, scores, iou_threshold=0.5, sigma=0.5):
# Weight scores instead of removing
keep = []
while boxes:
# Best box
best_idx = scores.argmax()
keep.append(best_idx)
# Update scores of remaining boxes
for i in range(len(boxes)):
if i != best_idx:
iou = compute_iou(boxes[best_idx], boxes[i])
if iou > iou_threshold:
scores[i] *= np.exp(-(iou**2) / sigma)
# Remove the best box
boxes = np.delete(boxes, best_idx, axis=0)
scores = np.delete(scores, best_idx)
return keep📊 Evaluation Metrics
🎯 Measuring Performance
Common metrics include mAP (mean Average Precision), IoU, precision, recall, and F1-score.
Metric Details
mAP: Area under the precision-recall curve, averaged over classes. IoU threshold: Typically 0.5 (mAP@0.5) or multiple thresholds (mAP@[0.5:0.95]). Precision: True positives / (True positives + False positives). Recall: True positives / (True positives + False negatives).
F1-score: Harmonic mean of precision and recall.
# mAP calculation
def compute_map(predictions, ground_truth, iou_threshold=0.5):
# predictions: list of (boxes, scores, labels)
# ground_truth: list of (boxes, labels)
# Group by image
image_results = {}
for pred, gt in zip(predictions, ground_truth):
# Compute true/false positives for each class
pass
# Compute AP per class
aps = []
for class_id in classes:
# Get predictions and ground truth for this class
class_preds = []
class_gts = []
# Sort by score
class_preds.sort(key=lambda x: x['score'], reverse=True)
# Compute precision-recall curve
tp = np.zeros(len(class_preds))
fp = np.zeros(len(class_preds))
for i, pred in enumerate(class_preds):
# Check if any ground truth matches
matched = False
for gt in class_gts:
if compute_iou(pred['box'], gt['box']) > iou_threshold:
if not gt['matched']:
gt['matched'] = True
matched = True
break
if matched:
tp[i] = 1
else:
fp[i] = 1
# Compute precision and recall
tp_cumsum = np.cumsum(tp)
fp_cumsum = np.cumsum(fp)
recall = tp_cumsum / len(class_gts)
precision = tp_cumsum / (tp_cumsum + fp_cumsum)
# Compute AP (area under PR curve)
ap = np.trapz(precision, recall)
aps.append(ap)
return np.mean(aps)📚 Datasets
🎯 Object Detection Datasets
Popular datasets include COCO, Pascal VOC, Open Images, and KITTI.
Dataset Details
COCO: Common Objects in Context - 80 classes, 330K images, 1.5M objects. Pascal VOC: 20 classes, 11K images. Open Images: 600 classes, 9M images. KITTI: Autonomous driving dataset.
Format: COCO JSON format or Pascal VOC XML format.
# COCO dataset loader (simplified)
class COCODataset:
def __init__(self, annotation_path, image_dir):
self.annotation_path = annotation_path
self.image_dir = image_dir
self.load_annotations()
def load_annotations(self):
import json
with open(self.annotation_path, 'r') as f:
self.data = json.load(f)
self.images = {img['id']: img for img in self.data['images']}
self.categories = {cat['id']: cat for cat in self.data['categories']}
# Group annotations by image
self.annotations = {}
for ann in self.data['annotations']:
img_id = ann['image_id']
if img_id not in self.annotations:
self.annotations[img_id] = []
self.annotations[img_id].append(ann)
def get_item(self, idx):
img_info = self.images[idx]
img_path = f"{self.image_dir}/{img_info['file_name']}"
anns = self.annotations.get(idx, [])
boxes = []
labels = []
for ann in anns:
x, y, w, h = ann['bbox']
boxes.append([x, y, x + w, y + h])
labels.append(ann['category_id'])
return img_path, boxes, labels💼 Applications
🎯 Real-World Uses
Object detection powers autonomous driving, surveillance, medical imaging, retail, and augmented reality.
Application Domains
Autonomous driving: Pedestrian, vehicle, and traffic sign detection. Surveillance: Person detection and tracking. Medical: Tumor detection in radiology. Retail: Object counting and inventory management.
AR: Object detection for AR interaction.
# Application: Pedestrian detection
class PedestrianDetector:
def __init__(self, model_path):
self.model = torch.load(model_path)
self.classes = ['person', 'car', 'bicycle', 'motorcycle']
def detect(self, image):
# Run detection
detections = self.model(image)
# Filter for pedestrians
pedestrians = []
for det in detections:
if det['class'] == 'person' and det['confidence'] > 0.7:
pedestrians.append(det)
return pedestrians
def track_pedestrians(self, video_frames):
# Tracking over time
tracks = []
for frame in video_frames:
people = self.detect(frame)
# Apply tracking (e.g., Kalman filter)
tracks = update_tracks(tracks, people)
return tracks🚀 Future Directions
🎯 Emerging Trends
Future directions include real-time on-device detection, transformer-based detectors, and few-shot learning.
Future Trends
On-device: Efficient models for mobile and edge. Transformers: DETR and its variants. Few-shot: Detecting novel classes with few examples. Video: Spatio-temporal detection. 3D: Point cloud object detection.
Foundation models: Large vision-language models for detection.
# Future: DETR (Detection Transformer)
class DETR(nn.Module):
def __init__(self, backbone, num_classes, num_queries=100):
super().__init__()
self.backbone = backbone
self.transformer = nn.Transformer(
d_model=512,
nhead=8,
num_encoder_layers=6,
num_decoder_layers=6
)
self.query_pos = nn.Parameter(torch.randn(num_queries, 512))
self.classifier = nn.Linear(512, num_classes + 1)
self.bbox_predictor = nn.Linear(512, 4)
def forward(self, x):
# Extract features
features = self.backbone(x)
features = features.flatten(2).permute(2, 0, 1)
# Transformer decoder
hs = self.transformer(features, self.query_pos.unsqueeze(1).repeat(1, 1, 1))
# Predict classes and boxes
classes = self.classifier(hs)
boxes = self.bbox_predictor(hs)
return classes, boxes
# Future: EfficientViT for on-device detection
class EfficientDetector:
def __init__(self):
self.model = self.build_efficient_model()
def build_efficient_model(self):
# Lightweight model for edge devices
# Using MobileNet/EfficientNet backbone
pass