4. YOLOv3 单图检测结果绘制与保存

任务目标

本案例要求解析YOLO的.cfg配置文件和.weights权重文件,理解网络结构参数,并完成模型加载。通过本案例,你将掌握YOLO模型文件的结构与解析方法,为后续目标检测任务奠定基础。

环境依赖

  • OpenCV ≥ 4.4(需包含dnn模块)
  • Python ≥ 3.7
  • YOLOv4配置文件(yolov4.cfg)与权重文件(yolov4.weights)

核心实现

以下代码完成三个核心任务:解析.cfg文件提取网络层参数、读取.weights文件头部信息、使用OpenCV DNN模块加载模型并验证网络结构。

import cv2
import numpy as np
from collections import OrderedDict

def parse_cfg(cfg_path):
    """解析YOLO .cfg文件,返回层参数字典列表"""
    with open(cfg_path, 'r') as f:
        lines = f.read().split('\n')
    
    layers = []
    current_layer = None
    
    for line in lines:
        line = line.strip()
        if not line or line.startswith('#'):
            continue
        if line.startswith('['):
            if current_layer is not None:
                layers.append(current_layer)
            current_layer = OrderedDict()
            current_layer['type'] = line.strip('[]')
        else:
            if current_layer is not None and '=' in line:
                key, value = line.split('=')
                current_layer[key.strip()] = value.strip()
    
    if current_layer is not None:
        layers.append(current_layer)
    
    return layers

def parse_weights_header(weights_path):
    """读取.weights文件头部,返回版本号与网络参数数量"""
    with open(weights_path, 'rb') as f:
        header = np.fromfile(f, dtype=np.int32, count=4)
        if header[0] == 0:
            version = 1
            num_layers = header[1]
        else:
            version = header[0]
            num_layers = header[1]
        return version, num_layers

def load_yolo_model(cfg_path, weights_path):
    """加载YOLO模型并返回网络对象"""
    net = cv2.dnn.readNetFromDarknet(cfg_path, weights_path)
    return net

if __name__ == "__main__":
    cfg_path = "yolov4.cfg"
    weights_path = "yolov4.weights"
    
    # 1. 解析配置文件
    layers = parse_cfg(cfg_path)
    print(f"配置文件共包含 {len(layers)} 层")
    
    # 打印前3层与最后3层结构
    for i, layer in enumerate(layers[:3]):
        print(f"层 {i}: type={layer['type']}, 参数={dict(layer)}")
    print("...")
    for i, layer in enumerate(layers[-3:]):
        idx = len(layers) - 3 + i
        print(f"层 {idx}: type={layer['type']}, 参数={dict(layer)}")
    
    # 2. 解析权重文件头部
    version, num_layers = parse_weights_header(weights_path)
    print(f"权重文件版本: {version}, 层数: {num_layers}")
    
    # 3. 加载模型
    net = load_yolo_model(cfg_path, weights_path)
    
    # 4. 验证网络结构
    layer_names = net.getLayerNames()
    print(f"OpenCV加载网络层数: {len(layer_names)}")
    
    # 获取输出层
    output_layers = [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]
    print(f"输出层: {output_layers}")
    
    # 检查网络是否可用
    dummy_input = np.zeros((1, 3, 416, 416), dtype=np.float32)
    net.setInput(dummy_input)
    outputs = net.forward(output_layers)
    print(f"前向传播输出特征图数量: {len(outputs)}")
    for i, out in enumerate(outputs):
        print(f"输出 {i}: 形状={out.shape}")

运行与验证

运行方式:

# 下载YOLOv4模型文件(若未准备)
wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights
wget https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4.cfg

# 执行脚本
python yolo_parser.py

验证判定标准:

  1. 配置文件解析输出应包含约162层(YOLOv4标准结构),且每层包含type字段(如convolutional、route、yolo等)
  2. 权重文件头部版本号应为0(表示使用旧版格式),层数应与cfg解析结果一致
  3. OpenCV加载后网络层数应与cfg解析层数一致(或略有差异,因OpenCV会合并部分层)
  4. 输出层应包含3个yolo层,前向传播输出特征图数量为3,形状分别为(1, 255, 13, 13)、(1, 255, 26, 26)、(1, 255, 52, 52)(输入416×416时)

关键说明

  • cfg文件解析逻辑按行处理,以[type]标记新层开始,key=value格式存储参数
  • weights文件头部前4个int32值:第一个为版本号(0表示旧版),第二个为层数,后两个为训练相关参数(可忽略)
  • OpenCV的readNetFromDarknet内部已实现cfg与weights的完整解析,本案例的解析函数用于教学理解文件结构
  • 若输出层数量或形状与预期不符,请检查cfg与weights文件是否匹配(版本需一致)

常见问题

  • 加载失败:确认OpenCV版本≥4.4,且文件路径正确
  • 层数不匹配:cfg与weights必须来自同一模型版本,混用会导致加载错误
  • 内存不足:YOLOv4权重约245MB,确保系统有足够内存