一、YOLOv3 模型推理过程源码解析
推理过程指的是将输入图像送入训练好的YOLOv3模型,得到模型输出的预测结果。
1. 输入图像预处理 (Preprocessing)
在将图像送入模型之前,通常需要进行一系列的预处理操作,以使其符合模型的输入要求。常见的预处理步骤包括:
- 图像缩放 (Resizing): 将输入图像缩放到模型训练时所使用的尺寸,例如常见的
416x416
或608x608
。这通常涉及到保持图像的宽高比,并在必要时进行填充 (padding)。 - 归一化 (Normalization): 将图像的像素值归一化到
[0, 1]
或[-1, 1]
的范围内。这有助于加速模型收敛和提高性能。常见的归一化方法是将像素值除以255。 - 通道调整 (Channel Adjustment): 确保图像的通道顺序与模型要求的顺序一致,例如从BGR转换为RGB。
- 转换为模型输入格式: 将处理后的图像数据转换为模型所需的张量 (Tensor) 格式,并将其放置在正确的设备上 (CPU或GPU)。
源码示例 (PyTorch):
Python
import torch
import cv2
import numpy as npdef preprocess_image(image_path, input_size):"""预处理输入图像"""img = cv2.imread(image_path)img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)h, w = img.shape[:2]new_h, new_w = input_size# 保持宽高比缩放scale = min(new_h / h, new_w / w)resized_img = cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LINEAR)# 填充top = (new_h - resized_img.shape[0]) // 2bottom = new_h - resized_img.shape[0] - topleft = (new_w - resized_img.shape[1]) // 2right = new_w - resized_img.shape[1] - leftpadded_img = cv2.copyMakeBorder(resized_img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(128, 128, 128)) # 用灰色填充# 转换为Tensor并归一化img_tensor = torch.from_numpy(padded_img).float().permute(2, 0, 1) / 255.0img_tensor = img_tensor.unsqueeze(0) # 添加batch维度return img_tensor, (h, w), scale, (top, left)# 示例用法
image_path = "test.jpg"
input_size = (416, 416)
img_tensor, original_size, scale, padding = preprocess_image(image_path, input_size)# 将Tensor放到设备上 (如果使用GPU)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
img_tensor = img_tensor.to(device)
2. 模型前向传播 (Forward Pass)
预处理后的图像数据被送入加载好的YOLOv3模型中,模型会逐层进行计算,最终在不同的尺度上输出预测结果。YOLOv3通常在三个不同的尺度上进行预测,对应于网络中的三个不同的特征图。
源码示例 (PyTorch):
Python
import torch.nn as nn# 假设已经加载了训练好的YOLOv3模型
# model = ...# 设置模型为评估模式
model.eval()# 关闭梯度计算,减少内存消耗并加速推理
with torch.no_grad():outputs = model(img_tensor)# outputs 是一个包含三个元素的列表,每个元素对应一个尺度的预测结果
# 每个元素的形状通常是 (batch_size, num_anchors_per_scale * (5 + num_classes), grid_size, grid_size)
# 例如: (1, 3 * (5 + 80), 13, 13), (1, 3 * (5 + 80), 26, 26), (1, 3 * (5 + 80), 52, 52)
3. 解析模型输出 (Output Interpretation)
模型的原始输出是三个不同尺度上的特征图。每个特征图上的每个Cell都预测了固定数量的边界框 (由anchors定义)。对于每个预测框,输出包含以下信息:
- 边界框中心坐标偏移量 (tx, ty): 相对于当前Cell的左上角。
- 边界框宽度和高度的对数偏移量 (tw, th): 相对于预定义的anchor尺寸。
- 目标置信度 (objectness score): 表示当前预测框内包含一个物体的概率。
- 类别概率 (class probabilities): 表示当前预测框内的物体属于每个类别的概率。
我们需要将这些原始输出转换为实际的边界框坐标、置信度和类别标签。
源码示例 (PyTorch):
Python
def decode_output(output, anchors, num_classes, input_size):"""解码模型的输出"""batch_size, _, grid_h, grid_w = output.shapestride_h = input_size[0] / grid_hstride_w = input_size[1] / grid_wscaled_anchors = [(a_w / stride_w, a_h / stride_h) for a_w, a_h in anchors]output = output.view(batch_size, len(anchors), num_classes + 5, grid_h, grid_w).permute(0, 1, 3, 4, 2).contiguous()# 中心坐标cx = (torch.sigmoid(output[..., 0]) + torch.arange(grid_w).float().to(output.device).view(1, 1, grid_w, 1)) * stride_wcy = (torch.sigmoid(output[..., 1]) + torch.arange(grid_h).float().to(output.device).view(1, 1, 1, grid_h).permute(0, 1, 3, 2)) * stride_h# 宽度和高度pw = torch.tensor([scaled_anchors[i][0] for i in range(len(anchors))]).to(output.device).view(1, len(anchors), 1, 1)ph = torch.tensor([scaled_anchors[i][1] for i in range(len(anchors))]).to(output.device).view(1, len(anchors), 1, 1)bw = torch.exp(output[..., 2]) * pw * stride_wbh = torch.exp(output[..., 3]) * ph * stride_h# 置信度conf = torch.sigmoid(output[..., 4])# 类别概率pred_cls = torch.sigmoid(output[..., 5:])return torch.cat([cx.unsqueeze(-1), cy.unsqueeze(-1), bw.unsqueeze(-1), bh.unsqueeze(-1), conf.unsqueeze(-1), pred_cls], dim=-1)# 假设定义了anchors和num_classes
anchors = [[[116, 90], [156, 198], [373, 326]],[[30, 61], [62, 45], [59, 119]],[[10, 13], [16, 30], [33, 23]]]
num_classes = 80
input_size = (416, 416)predictions = []
for i, output in enumerate(outputs):decoded_output = decode_output(output, anchors[i], num_classes, input_size)predictions.append(decoded_output.view(output.size(0), -1, num_classes + 5))# 将所有尺度的预测结果合并
predictions = torch.cat(predictions, dim=1)
二、后处理模块源码解析 (Post-processing)
后处理模块的主要任务是根据模型的原始预测结果,过滤掉低置信度的预测框,并使用非极大值抑制 (NMS) 来消除冗余的重叠框,最终得到高质量的检测结果。
1. 置信度过滤 (Confidence Filtering)
首先,我们会根据预测框的目标置信度 (objectness score) 设定一个阈值,将低于该阈值的预测框过滤掉。
源码示例 (PyTorch):
Python
conf_thres = 0.5 # 置信度阈值# 过滤掉置信度低于阈值的预测框
conf_mask = (predictions[:, :, 4] > conf_thres).unsqueeze(-1)
predictions = predictions[conf_mask.repeat(1, 1, predictions.size(-1))].view(predictions.size(0), -1, predictions.size(-1))
2. 类别概率过滤 (Class Probability Filtering)
对于剩余的预测框,我们通常会选择具有最高类别概率的类别作为该框的预测类别。然后,我们可以根据类别概率设定一个阈值,进一步过滤掉低概率的预测。
源码示例 (PyTorch):
Python
prob_thres = 0.4 # 类别概率阈值# 获取每个预测框的最大类别概率和对应的类别索引
max_conf, max_conf_idx = torch.max(predictions[:, :, 5:], dim=-1)
max_conf = max_conf.unsqueeze(-1)
max_conf_idx = max_conf_idx.unsqueeze(-1).float()# 将置信度和类别信息合并到预测结果中
detections = torch.cat([predictions[:, :, :4], max_conf, max_conf_idx], dim=-1)# 过滤掉类别概率低于阈值的预测框
prob_mask = (detections[:, :, 4] > prob_thres).unsqueeze(-1)
detections = detections[prob_mask.repeat(1, 1, detections.size(-1))].view(detections.size(0), -1, detections.size(-1))
3. 非极大值抑制 (Non-Maximum Suppression, NMS)
在经过置信度和类别概率过滤后,仍然可能存在一些重叠的预测框预测到同一个物体。NMS算法的目标是选择其中置信度最高的框,并抑制掉其他与之重叠程度较高的框。
NMS算法的步骤通常如下:
- 对每个类别分别进行NMS。
- 将属于同一类别的所有预测框按照置信度从高到低排序。
- 选择置信度最高的框作为最终的检测结果,并将其加入到最终的检测列表中。
- 计算该框与其他所有框的IoU (Intersection over Union)。
- 将IoU大于某个阈值 (NMS阈值) 的框从列表中移除,因为它们与当前选择的框高度重叠,很可能是同一个物体的重复检测。
- 重复步骤3-5,直到列表为空。
源码示例 (PyTorch):
Python
def non_max_suppression(prediction, conf_thres=0.5, nms_thres=0.4):"""执行非极大值抑制"""# 将预测框的格式从 (center_x, center_y, width, height) 转换为 (x1, y1, x2, y2)box_corner = prediction.new(prediction.shape)box_corner[:, :, 0] = prediction[:, :, 0] - prediction[:, :, 2] / 2box_corner[:, :, 1] = prediction[:, :, 1] - prediction[:, :, 3] / 2box_corner[:, :, 2] = prediction[:, :, 0] + prediction[:, :, 2] / 2box_corner[:, :, 3] = prediction[:, :, 1] + prediction[:, :, 3] / 2prediction[:, :, :4] = box_corner[:, :, :4]output = [None for _ in range(len(prediction))]for i, image_pred in enumerate(prediction):# 过滤掉置信度低的框conf_mask = (image_pred[:, 4] >= conf_thres).squeeze()image_pred = image_pred[conf_mask]if not image_pred.size(0):continue# 获取每个框的置信度和类别分数class_conf, class_pred = torch.max(image_pred[:, 5:], 1, keepdim=True)# 创建包含 (x1, y1, x2, y2, confidence, class_confidence, class_pred) 的检测结果detections = torch.cat((image_pred[:, :5], class_conf.float(), class_pred.float()), 1)# 获取所有唯一的类别unique_labels = detections[:, -1].cpu().unique()if prediction.is_cuda:unique_labels = unique_labels.cuda()for c in unique_labels:# 获取属于当前类别的所有检测结果detections_class = detections[detections[:, -1] == c]# 按照置信度降序排序_, conf_sort_index = torch.sort(detections_class[:, 4], descending=True)detections_class = detections_class[conf_sort_index]# 执行NMSmax_detections = []while detections_class.size(0):# 选择置信度最高的框max_detections.append(detections_class[0].unsqueeze(0))# 如果只有一个框了,就结束if len(detections_class) == 1:break# 计算IoUiou = bbox_iou(max_detections[-1], detections_class[1:])# 移除IoU大于阈值的框detections_class = detections_class[1:][iou < nms_thres]# 将当前类别的NMS结果添加到最终输出中if max_detections:if output[i] is None:output[i] = torch.cat(max_detections, dim=0)else:output[i] = torch.cat((output[i], torch.cat(max_detections, dim=0)), dim=0)return outputdef bbox_iou(box1, box2):"""计算两个 bounding boxes 的 IoU"""b1_x1, b1_y1, b1_x2, b1_y2 = box1[:, 0], box1[:, 1], box1[:, 2], box1[:, 3]b2_x1, b2_y1, b2_x2, b2_y2 = box2[:, 0], box2[:, 1], box2[:, 2], box2[:, 3]# 交集区域的坐标inter_x1 = torch.max(b1_x1, b2_x1)inter_y1 = torch.max(b1_y1, b2_y1)inter_x2 = torch.min(b1_x2, b2_x2)inter_y2 = torch.min(b1_y2, b2_y2)# 交集区域的面积inter_area = torch.clamp(inter_x2 - inter_x1, min=0) * torch.clamp(inter_y2 - inter_y1, min=0)# 并集区域的面积b1_area = (b1_x2 - b1_x1) * (b1_y2 - b1_y1)b2_area = (b2_x2 - b2_x1) * (b2_y2 - b2_y1)union_area = b1_area + b2_area - inter_areaiou = inter_area / (union_area + 1e-6) # 添加一个小的epsilon防止除零return iou# 应用NMS
nms_output = non_max_suppression(predictions, conf_thres=0.5, nms_thres=0.4)# nms_output 是一个列表,每个元素对应一张输入图像的检测结果
# 每个元素是一个形状为 (num_detections, 7) 的Tensor,包含 (x1, y1, x2, y2, objectness_conf, class_conf, class_pred)
4. 后处理结果处理 (Post-NMS Processing)
经过NMS后,我们得到了最终的检测结果。这些结果通常包含边界框的坐标、目标置信度、类别置信度和类别标签。我们可能需要将这些结果转换回原始图像的尺寸,并在图像上绘制边界框和标签。
源码示例 (PyTorch):
Python
def scale_coords(coords, original_size, img_size, padding):"""将预测框的坐标缩放回原始图像尺寸"""h, w = original_sizeimg_h, img_w = img_sizetop, left = paddingscale_w = w / (img_w - left - padding[1])scale_h = h / (img_h - top - padding[0])coords[:, [0, 2]] -= leftcoords[:, [1, 3]] -= topcoords[:, [0, 2]] *= scale_wcoords[:, [1, 3]] *= scale_hreturn coords# 假设我们只有一张输入图像
if nms_output[0] is not None:detections = nms_output[0].cpu()# 将坐标缩放回原始图像尺寸detections[:, :4] = scale_coords(detections[:, :4], original_size, input_size, padding)# 在图像上绘制边界框和标签 (这部分可以使用OpenCV或其他库完成)# ...