Detection Pipeline Overview
Anchor-driven detection frameworks operate through a standardized sequence. First, a dense grid of reference regions is synthesized across the input tensor. Second, a classifier evaluates each reference region for target presence. Third, a regressor adjusts the coordinates of positive regions to align with ground-truth boundaries.
Spatial Overlap and IoU
Spatial overlap between two rectengular regions is quantified using the Intersection over Union (IoU) metric. This ratio divides the overlapping area by the total combined area, providing a normalized similarity score between 0 and 1. It serves as the primary criterion for matching predictions to actual objects and filtering redundant detections.
Ancher Labeling Strategy
Every generated region functions as an independent training instance. Labels are assigned by mapping each region to either a background class or a specific ground-truth rectangle. Due to the dense generation strategy, the dataset natural skews heavily toward negative samples. During training, each anchor requires two distinct targets: a categorical label and a normalized coordinate offset relative to the matched ground truth.
Filtering Predictions with NMS
During inference, overlapping predictions for the same object are filtered using Non-Maximum Suppression (NMS). This greedy algorithm retains the highest-confidence prediction and discards neighboring boxes that exceed a predefined overlap threshold, ensuring a single output per detected object.
Core Implementation
The following modules provide a complete, standalone implementation of the anchor generation, matching, encoding, and inference pipeline using PyTorch.
1. Anchor Grid Generation
import torch
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
def generate_anchor_grid(feature_map, scales, aspect_ratios):
"""Create a dense set of reference boxes centered on each pixel."""
h, w = feature_map.shape[-2:]
device = feature_map.device
n_scales, n_ratios = len(scales), len(aspect_ratios)
anchors_per_pixel = n_scales + n_ratios - 1
scale_t = torch.tensor(scales, device=device)
ratio_t = torch.tensor(aspect_ratios, device=device)
# Normalize coordinates to [0, 1]
step_y, step_x = 1.0 / h, 1.0 / w
cy = (torch.arange(h, device=device) + 0.5) * step_y
cx = (torch.arange(w, device=device) + 0.5) * step_x
grid_y, grid_x = torch.meshgrid(cy, cx, indexing='ij')
grid_y, grid_x = grid_y.reshape(-1), grid_x.reshape(-1)
# Compute normalized widths and heights
norm_w = torch.cat([scale_t * torch.sqrt(ratio_t[0]),
scales[0] * torch.sqrt(ratio_t[1:])]) * h / w
norm_h = torch.cat([scale_t / torch.sqrt(ratio_t[0]),
scales[0] / torch.sqrt(ratio_t[1:])])
# Half dimensions for corner calculation
half_dims = torch.stack([-norm_w, -norm_h, norm_w, norm_h], dim=1).repeat(h * w, 1) / 2.0
# Broadcast center coordinates
centers = torch.stack([grid_x, grid_y, grid_x, grid_y], dim=1).repeat_interleave(anchors_per_pixel, dim=0)
return (centers + half_dims).unsqueeze(0)
2. Bounding Box Visualization
def render_bounding_boxes(ax, box_coords, cls_names=None, palette=None):
"""Overlay rectangular regions on a matplotlib axis."""
def ensure_list(val, fallback=None):
if val is None: return fallback or []
return val if isinstance(val, (list, tuple)) else [val]
cls_names = ensure_list(cls_names)
palette = ensure_list(palette, ['#1f77b4', '#2ca02c', '#d62728', '#9467bd', '#17becf'])
for idx, coords in enumerate(box_coords):
color = palette[idx % len(palette)]
x_min, y_min, x_max, y_max = coords.detach().cpu().numpy()
rect = Rectangle((x_min, y_min), x_max - x_min, y_max - y_min,
fill=False, edgecolor=color, linewidth=2)
ax.add_patch(rect)
if cls_names and idx < len(cls_names):
txt_color = 'white' if color == '#000000' else 'black'
ax.text(x_min, y_min, cls_names[idx], fontsize=8, color=txt_color,
bbox=dict(facecolor=color, edgecolor='none', pad=2))
3. IoU Matrix Computation
def compute_iou_matrix(set_a, set_b):
"""Calculate pairwise Intersection over Union between two box collections."""
def area(boxes):
return (boxes[:, 2] - boxes[:, 0]).clamp(min=0) * (boxes[:, 3] - boxes[:, 1]).clamp(min=0)
area_a = area(set_a)
area_b = area(set_b)
# Intersection coordinates via broadcasting: (N, 1, 2) vs (1, M, 2)
tl = torch.max(set_a[:, None, :2], set_b[:, :2])
br = torch.min(set_a[:, None, 2:], set_b[:, 2:])
inter_wh = (br - tl).clamp(min=0)
inter_area = inter_wh[:, :, 0] * inter_wh[:, :, 1]
union_area = area_a[:, None] + area_b[None, :] - inter_area
return inter_area / union_area.clamp(min=1e-9)
4. Ground Truth Assignment
def match_anchors_to_ground_truth(gt_boxes, prior_boxes, device, match_thresh=0.5):
"""Assign each prior box to a ground-truth rectangle based on overlap."""
n_priors, n_gt = prior_boxes.shape[0], gt_boxes.shape[0]
iou_mat = compute_iou_matrix(prior_boxes, gt_boxes)
assignment = torch.full((n_priors,), -1, dtype=torch.long, device=device)
# 1. Assign priors that exceed the threshold
max_iou, max_idx = torch.max(iou_mat, dim=1)
valid_mask = max_iou >= match_thresh
assignment[valid_mask] = max_idx[valid_mask]
# 2. Ensure every GT box gets at least one prior (greedy max overlap)
col_mask = torch.full((n_priors,), -1, device=device)
row_mask = torch.full((n_gt,), -1, device=device)
for _ in range(n_gt):
flat_idx = torch.argmax(iou_mat)
gt_idx = flat_idx % n_gt
prior_idx = flat_idx // n_gt
assignment[prior_idx] = gt_idx
iou_mat[:, gt_idx] = col_mask
iou_mat[prior_idx, :] = row_mask
return assignment
5. Target Encoding and Preparation
def encode_box_offsets(priors, matched_gt, epsilon=1e-6):
"""Transform coordinate differences into normalized regression targets."""
priors_cwh = torch.cat([(priors[:, :2] + priors[:, 2:]) / 2, priors[:, 2:] - priors[:, :2]], dim=1)
gt_cwh = torch.cat([(matched_gt[:, :2] + matched_gt[:, 2:]) / 2, matched_gt[:, 2:] - matched_gt[:, :2]], dim=1)
delta_xy = 10.0 * (gt_cwh[:, :2] - priors_cwh[:, :2]) / priors_cwh[:, 2:]
delta_wh = 5.0 * torch.log(epsilon + gt_cwh[:, 2:] / priors_cwh[:, 2:])
return torch.cat([delta_xy, delta_wh], dim=1)
def prepare_detection_targets(base_priors, batch_gt):
"""Generate classification labels and regression masks for a batch."""
batch_sz = batch_gt.shape[0]
priors = base_priors.squeeze(0)
device, n_priors = priors.device, priors.shape[0]
batch_deltas, batch_masks, batch_labels = [], [], []
for b in range(batch_sz):
gt = batch_gt[b]
mapping = match_anchors_to_ground_truth(gt[:, 1:], priors, device)
pos_mask = (mapping >= 0).float().unsqueeze(-1).repeat(1, 4)
cls_tgt = torch.zeros(n_priors, dtype=torch.long, device=device)
coord_tgt = torch.zeros((n_priors, 4), dtype=torch.float32, device=device)
pos_indices = torch.nonzero(mapping >= 0).squeeze(-1)
gt_indices = mapping[pos_indices]
cls_tgt[pos_indices] = gt[gt_indices, 0].long() + 1
coord_tgt[pos_indices] = gt[gt_indices, 1:]
deltas = encode_box_offsets(priors, coord_tgt) * pos_mask
batch_deltas.append(deltas.reshape(-1))
batch_masks.append(pos_mask.reshape(-1))
batch_labels.append(cls_tgt)
return torch.stack(batch_deltas), torch.stack(batch_masks), torch.stack(batch_labels)
6. Prediction Decoding
def decode_box_predictions(priors, reg_outputs):
"""Revert normalized offsets back to absolute corner coordinates."""
priors_cwh = torch.cat([(priors[:, :2] + priors[:, 2:]) / 2, priors[:, 2:] - priors[:, :2]], dim=1)
pred_xy = (reg_outputs[:, :2] * priors_cwh[:, 2:] / 10.0) + priors_cwh[:, :2]
pred_wh = torch.exp(reg_outputs[:, 2:] / 5.0) * priors_cwh[:, 2:]
pred_cwh = torch.cat([pred_xy, pred_wh], dim=1)
half_wh = pred_cwh[:, 2:] / 2
return torch.cat([pred_cwh[:, :2] - half_wh, pred_cwh[:, :2] + half_wh], dim=1)
7. Non-Maximum Suppression
def apply_non_max_suppression(coords, confidences, overlap_limit):
"""Filter overlapping detections using a greedy confidence-based approach."""
sorted_idx = torch.argsort(confidences, descending=True)
retained = []
while sorted_idx.numel() > 0:
top = sorted_idx[0]
retained.append(top)
if sorted_idx.numel() == 1:
break
current_box = coords[top].unsqueeze(0)
remaining_boxes = coords[sorted_idx[1:]]
iou_vals = compute_iou_matrix(current_box, remaining_boxes).squeeze(0)
keep_mask = iou_vals <= overlap_limit
sorted_idx = sorted_idx[1:][keep_mask]
return torch.tensor(retained, device=coords.device, dtype=torch.long)
8. Inference Pipeline
def run_detection_pipeline(class_probs, box_deltas, base_priors, nms_thresh=0.5, conf_thresh=0.01):
"""Execute full inference: decode, suppress overlaps, and threshold predictions."""
device, batch_sz = class_probs.device, class_probs.shape[0]
priors = base_priors.squeeze(0)
n_anchors = class_probs.shape[2]
results = []
for b in range(batch_sz):
probs = class_probs[b]
deltas = box_deltas[b].reshape(-1, 4)
max_conf, pred_cls = torch.max(probs[1:], dim=0)
decoded_boxes = decode_box_predictions(priors, deltas)
kept_idx = apply_non_max_suppression(decoded_boxes, max_conf, nms_thresh)
all_idx = torch.arange(n_anchors, device=device)
combined = torch.cat([kept_idx, all_idx])
_, counts = torch.unique(combined, return_counts=True)
suppressed_idx = combined[counts == 1]
final_order = torch.cat([kept_idx, suppressed_idx])
pred_cls[suppressed_idx] = -1
pred_cls = pred_cls[final_order]
max_conf = max_conf[final_order]
decoded_boxes = decoded_boxes[final_order]
low_conf_mask = max_conf < conf_thresh
pred_cls[low_conf_mask] = -1
max_conf[low_conf_mask] = 1.0 - max_conf[low_conf_mask]
batch_out = torch.cat([pred_cls.unsqueeze(1), max_conf.unsqueeze(1), decoded_boxes], dim=1)
results.append(batch_out)
return torch.stack(results)