PyTorch实现YOLOv3-tiny:从Darknet权重转换到摄像头实时目标检测

发布时间:2026/10/9 22:28:15
PyTorch实现YOLOv3-tiny:从Darknet权重转换到摄像头实时目标检测
简介一份基于PyTorch的YOLOv3-tiny轻量级目标检测实现面向需要在边缘设备或实时场景中部署检测模型的开发者可帮助快速完成模型定义、数据准备、训练与推理的闭环。压缩包共22个文件以Python脚本为主模型结构、预处理、损失计算、训练微调与推理等辅以类别名文件、示例图片、字体和说明文档整体仅1.17MB目录划分清晰便于按需取用。资源覆盖了从锚点聚类、LMDB数据库构建到预训练/微调/推理的完整流程包含网络架构定义、数据预处理、锚点计算、损失函数计算等核心模块并带有网络架构图、聚类结果等可视化素材便于理解YOLOv3-tiny的设计思路同时提供测试脚本和示例图片可直接运行验证检测效果。已有49人学习下载适合希望基于PyTorch定制轻量检测项目的研究者与工程人员。1. PyTorch 实现 YOLOv3-tiny这份 zip 到底解决什么问题先给结论这份 PyTorch 实现 YOLOv3-tiny 的工程包不是那种只有模型结构和一行 predict 的玩具 demo而是一个把 Darknet 格式的轻量检测模型完整搬进 PyTorch 的落地资源——里面有 cfg 网络定义、权重转换脚本、单图推理和 OpenCV 视频流调用示例。它专门解决图像识别里算力有限但需要实时检测的场景CPU 笔记本、树莓派、旧显卡或者需要在边缘设备上跑目标检测的机器学习项目。YOLOv3-tiny 比完整版 YOLOv3 少了约 90% 的卷积层推理速度快一个数量级代价是 mAP 降低。适合两类人想快速把检测模型部署到本地摄像头的开发者以及想读懂轻量检测网络每一层怎么写的初学者。如果你追求最高精度这个包不适合如果你要的是能跑起来、能改参数、能看效果它比从零写 Darknet 解析省一周时间。2. YOLOv3-tiny 结构拆解轻量模型凭什么在图像识别里立足2.1 主干网络7 层卷积换来的是算力友好完整版 YOLOv3 的 Darknet-53 有 53 个卷积层而 YOLOv3-tiny 只保留了 7 个卷积层和 6 个 max-pooling 层。这 7 层卷积不是拍脑袋砍出来的前几层负责提取边缘、纹理这类底层特征后几层在逐步下采样的同时把通道数从 16 拉高到 1024。输入 416×416 的图经过前 6 层卷积池化后特征图缩到 13×13最后一层卷积把通道数压到和检测头匹配的维度。由于卷积核数量少、层数浅forward 一次的计算量大约只有完整版 YOLOv3 的十分之一在 CPU 上也能跑到每秒十几帧。资源包里的 cfg 文件值得先读一遍。每个 convolutional 块的 filters、size、stride 定义了模型骨架PyTorch 实现会逐行解析这个 cfg 来构建网络而不是在代码里硬编码每一层。我一般建议拿到这个包先别急着跑打开 cfg 数一遍卷积 池化的交替次数你就知道后面权重转换脚本为什么要求严格按层序读取了。2.2 两个检测头13×13 和 26×26 各管什么YOLOv3-tiny 牺牲了 YOLOv3 的三个尺度检测头只保留两个。13×13 的特征图对应 32 倍下采样感受野大负责检测大目标26×26 的特征图是 16 倍下采样从第 4 层引出一条旁路卷积后上采样再与主干特征拼接负责中等偏小的目标。每个检测头锚定 3 个先验框。资源包里的 anchor 顺序是部署阶段最容易翻车的地方特征图尺度anchor 值像素相对 416 输入偏向目标26×26(10,14) (23,27) (37,58)小目标13×13(81,82) (135,169) (344,319)大目标anchor 顺序不能改因为 cfg 里 tiny 版本的 mask 规定得清清楚楚前三个 anchor 给 26×26 头后三个给 13×13 头。如果你在代码里把 anchors 顺序写反了模型不会报错但小目标会大面积漏检。这类问题用单张测试图看输出很难发现只能用正常图能检出、小目标图全灭来反推。2.3 zip 包里通常该有的文件先核对再动手一个能完整复现的 PyTorch 版 YOLOv3-tiny 工程核心文件应该不少于以下几类文件作用缺了会怎样yolov3-tiny.cfg网络结构定义模型无法构建model.pycfg 解析 前向传播无模型utils.pyletterbox、解码、NMS推理结果没法看yolov3-tiny.weightsDarknet 官方预训练权重没法直接出检测结果detect.py单图 / 视频推理入口只能调 API 不能实操convert_weights.pyDarknet 权重转 PyTorch 格式权重加载报错有些包还自带一张 sample.jpg 拉通流程。如果少了 convert_weights.py说明权重可能已经转成了 .pt 或 .pth 格式直接加载反而是省事版本。拿到包的第一步不是读代码而是先确认权重文件格式和 cfg 是否存在这两个是后续所有操作的地基。3. 环境搭建与权重转换让 Darknet 权重在 PyTorch 里跑起来3.1 依赖版本与设备检查PyTorch 的版本对这类老工程影响很大。YOLOv3-tiny 最初写的代码多基于 PyTorch 1.x 的 API如果你装了 2.x 版本大概率会遇到torch.nn.functional.interpolate的 align_corners 参数问题以及torch.no_grad()行为差异。我一般的做法是先建独立虚拟环境降低翻车成本conda create -n yolotiny python3.8 conda activate yolotiny pip install torch1.13.1 torchvision0.14.1 pip install opencv-python numpy如果你只有一台 CPU 机器不建议装最新版 PyTorch因为这套代码在 1.13 上测试最充分。装完之后先跑一段设备检查代码确认 Python 能看见正确的计算设备import torch, cv2 print(PyTorch:, torch.__version__) print(CUDA available:, torch.cuda.is_available()) print(OpenCV:, cv2.__version__) if torch.cuda.is_available(): print(GPU:, torch.cuda.get_device_name(0))逻辑说明第一行导入核心库并把版本打印出来方便你判断是不是踩了版本兼容的坑。第二行在没装 GPU 版 PyTorch 时CUDA 会显示 False此时推理会落到 CPU速度慢正常。OpenCV 版本建议 ≥4.5部分老包在cv2.dnn里暗坑较多但纯摄像头读取没影响。3.2 权重文件解析Darknet 二进制格式的读法Darknet 的 .weights 文件是自定义二进制格式不是简单的 tensor 序列。前 5 个 int32 是 header版本号、训练迭代等元信息后面按 cfg 层顺序依次存储有 batch_normalize 的卷积层先写 BN 的 bias、weight、running_mean、running_var再写卷积权重没有 BN 的层直接写卷积 bias 再写权重。任何一点顺序错位模型加载后输出就是随机噪声。下面是转换脚本的核心片段在真实工程里我会把完整逻辑抽成一个函数import numpy as np import torch def load_darknet_weights(model, weights_path, cfg_blocks): fp open(weights_path, rb) # 跳过 darknet 的 5 个 int32 头部数据全部存为 float32 的 little-endian header np.fromfile(fp, dtypenp.int32, count5) print(header:, header) for block in cfg_blocks: if block[type] convolutional: conv model # 假设 model 按 cfg 顺序暴露了卷积层引用 if block.get(batch_normalize, 0): bn get_bn_layer_for(conv) bn.bias.data.copy_(read_np(fp, bn.bias.numel())) bn.weight.data.copy_(read_np(fp, bn.weight.numel())) bn.running_mean.data.copy_(read_np(fp, bn.running_mean.numel())) bn.running_var.data.copy_(read_np(fp, bn.running_var.numel())) # 卷积权重按 [out_ch, in_ch, kh, kw] 顺序排布 shape [conv.out_channels, conv.in_channels, conv.kernel_size[0], conv.kernel_size[1]] conv.weight.data.copy_( read_np(fp, int(np.prod(shape))).reshape(shape) ) if not block.get(batch_normalize, 0): conv.bias.data.copy_(read_np(fp, conv.bias.numel())) fp.close() return model def read_np(fp, count): return torch.from_numpy( np.fromfile(fp, dtypenp.float32, countcount) )逻辑说明函数按 cfg 的模块顺序逐个读取BN 参数必须在卷积权重之前读因为 Darknet 在磁盘上的排列就是这种顺序。read_np每次读固定数量的 float32 并转成 torch tensor保证内存连续。链接顺序route/shortcut层不占权重跳过即可否则偏移量对不上。参数说明cfg_blocks是从 cfg 文件解析出的模块列表每个元素是一个 dict含有 type、filters、size、stride、batch_normalize 等字段。转换后建议顺手把权重存成 .pth 文件下次直接torch.load省得每次都要解析二进制文件。3.3 验证一张图从头到尾的推理流水线权重加载成功不等于推理正确必须跑一张已知内容的图验证。下面是完整的单图推理流程我把预处理、前向、后处理拆开写方便定位问题出在哪一段import cv2, torch from model import Darknet from utils import letterbox, decode_outputs, nms device cuda if torch.cuda.is_available() else cpu model Darknet(yolov3-tiny.cfg).to(device) model load_darknet_weights(model, yolov3-tiny.weights, parse_cfg()) model.eval() # 1. 预处理保持宽高比的 letterbox长边缩到 416 img cv2.imread(sample.jpg) img_letter, scale, pad_w, pad_h letterbox(img, 416) # 2. 张量转换BGR - RGB - CHW - [1,3,416,416]再除以 255 tensor torch.from_numpy( img_letter[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) ) / 255.0 # 3. 前向推理模型返回两个尺度的原始预测张量 with torch.no_grad(): outputs model(tensor.to(device)) # 4. 后处理解码 tx/ty/tw/th 置信度过滤 NMS boxes decode_outputs(outputs, anchors, conf_thres0.5) keep nms(boxes, iou_thres0.45)逻辑说明letterbox不是简单 resize而是把图按长边缩到 416短边补灰避免目标变形导致检测率下降这是整个检测流程中最容易被忽略的预处理细节。除以 255 即可不要额外套用 ImageNet 的 mean/std 标准化那会直接毁掉检测效果。decode_outputs把网络的原始坐标偏移量换算成实际 bboxnms做类别内的重叠框抑制。参数说明conf_thres是置信度阈值0.5 表示只保留网络判定概率超过 50% 的框精度需求高就调到 0.6召回需求高就降到 0.3。iou_thres是 NMS 的 IoU 阈值0.45 意味着两个框重叠超过 45% 就视为同一个目标主题重叠严重时调高到 0.5。跑完如果能在图上画出若干个合理框说明权重转换和预处理链路都没问题可以进下一步做摄像头接入。4. OpenCV 摄像头实时检测把模型塞进视频流4.1 模型只在启动时加载一次新手最容易犯的错是在每帧循环里重复执行model Darknet(...)和load_darknet_weights(...)。模型构建和权重读取都是 CPU 上的重量级操作重复做会让帧率掉到个位数。正确做法是初始化阶段加载一次模型循环里只做预处理、推理、后处理model build_tiny_model(yolov3-tiny.cfg, yolov3-tiny.weights) # 只执行一次 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ok, frame cap.read() if not ok: break dets detect_tiny(model, frame, conf_thres0.5, iou_thres0.45) frame draw_boxes(frame, dets) cv2.imshow(yolov3-tiny-live, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明detect_tiny内部封装了 letterbox、模型前向、解码、NMS 整条链路每帧只调用推理函数。VideoCapture(0)在笔记本上是内置摄像头如果插了 USB 摄像头且驱动没排好序可能是 1 或 2。waitKey(1)里按q退出是 OpenCV 的标准键控模式没有这个的话窗口会卡死。参数说明CAP_PROP_FRAME_WIDTH设 640 而不是 1920能大幅减少每帧处理耗时因为摄像头分辨率越高letterbox 后要 resize 的像素就越多。如果你的机器 CPU 不够强降到 480×360 是更务实的选择。注意cap.read()返回的两个值ok为 False 时必须 break否则后面处理一帧黑图或空图直接崩。4.2 拖动条调节 conf 和 NMS 阈值调参是图像识别部署里最玄学的部分写死在代码里每次改都要重启效率太低。OpenCV 的createTrackbar可以在窗口里直接拖阈值看效果win yolov3-tiny-live cv2.namedWindow(win) cv2.createTrackbar(conf, win, 50, 100, lambda x: None) cv2.createTrackbar(iou, win, 45, 100, lambda x: None) while True: ok, frame cap.read() if not ok: break conf cv2.getTrackbarPos(conf, win) / 100.0 iou cv2.getTrackbarPos(iou, win) / 100.0 dets detect_tiny(model, frame, conf_thresconf, iou_thresiou) frame draw_boxes(frame, dets) cv2.imshow(win, frame)逻辑说明拖动条数值范围是 0-100实际使用时除以 100 还原成 0.0-1.0 的阈值。这样做的好处是可以在不重启进程的情况下实时观察漏检和误检的平衡点尤其是检测远处小目标时把 conf 降低的同时往往要调高 iou两者是联动关系。参数说明conf越高漏检越多但误检越少iou越高重叠框保留越多同一目标可能出现多个框。初始建议 conf 在 0.4-0.5iou 在 0.45 上下。4.3 视频保存与帧率统计短时间看检测效果可以但要验证模型稳定性就得录一段带标注的视频并统计真实 FPS。我习惯在循环里加上时间戳和 VideoWriterwriter cv2.VideoWriter( result.avi, cv2.VideoWriter_fourcc(*XVID), 20.0, (frame_width, frame_height) ) prev_time cv2.getTickCount() frame_count 0 while True: ok, frame cap.read() if not ok: break frame draw_detections(frame, model) writer.write(frame) frame_count 1 if frame_count % 30 0: now cv2.getTickCount() fps 30.0 / ((now - prev_time) / cv2.getTickFrequency()) print(avg fps:, round(fps, 1)) prev_time now writer.release()逻辑说明VideoWriter 的编码器用 XVID 最稳mp4 在部分 OpenCV 版本里需要额外编解码库容易黑屏。FPS 统计用getTickCount计算真实耗时每 30 帧打一次平均值避免单帧抖动。这里没有把检测耗时的细节拆开因为实际瓶颈往往不在模型而在摄像头读取和画框的 I/O 上。5. 常见问题从转换到部署的六个翻车现场5.1 加载权重报 size mismatch层顺序没对齐现象load_darknet_weights执行到一半报RuntimeError: size mismatch或者模型能加载但输出全乱。原因cfg 解析出来的模块列表和你 PyTorch 模型里self.layers的顺序不一致。最常见的是 route 层和 upsample 层被错误地当成卷积层处理导致指针偏移错位后面的权重全部加载到错误的张量上。解决在加载前打印一遍 cfg 中所有卷积层的 filters 和 kernel_size与模型 state_dict 里每层 shape 逐一比对。用torch.weights.keys()列出所有参数名再把 cfg 阻塞列表序号和 keys 序号做成映射表两边数目一致才继续。5.2 全图无检测框预处理不是 normalize是除以 255现象同一个 cfg、同一个权重在 torchvision 里跑分类模型好好的一跑检测全图一个框都没有或者框里全是背景类别。原因很多人习惯性套用 ImageNet 预训练的标准化transforms.Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225])。YOLOv3-tiny 在 Darknet 里训练时只做了 0-255 到 0.0-1.0 的缩放从来没做过 mean-std 标准化输入分布一变整个网络的卷积输出全乱了。解决预处理只做/255.0不做任何 mean-std 归一化。确认你是在 BGR 转 RGB 之后再转 CHW不要先转再翻转通道顺序。这个坑我用血泪经验确认过光排查就花了一个晚上。5.3 小目标全灭anchor 顺序和输入分辨率没对齐现象行人、汽车这种大目标检测没问题但是远处的瓶子、小猫小狗全漏检confidence 输出几乎为 0。原因26×26 检测头的 anchor 是(10,14)(23,27)(37,58)如果代码里把 anchors 顺序写成 13×13 的在前后处理解码时对应关系错乱前面的小 anchor 配到了大特征图感受野对不上。另一种可能是你直接把输入尺寸从 416 改到 608但没有同步调整 letterbox 的分辨率导致 anchor 相对尺度严重偏移。解决确认 cfg 中mask3,4,5对应 26×26 的 anchormask6,7,8对应 13×13。改分辨率时先把 anchor 按比例缩放或干脆直接随机插值到新尺寸。用一张只有小目标的图做回归测试保证改动后有框再调其他参数。5.4 GPU 显存溢出元凶往往在输入 batch 和中间缓存现象单图推理正常但连续跑摄像头或视频流时CUDA out of memory程序崩溃。原因每帧采样到 GPU 张量后没及时释放torch.no_grad()忘加导致中间变量被完整保存或者检测头的上采样层在每次 forward 都分配新的中间缓存。解决在推理循环外加with torch.no_grad():并把输入张量明确 pinned 或直接复用一块内存。我一般在循环开始前先torch.cuda.empty_cache()兜底不要每帧都调性能会变差但可以在进程初始化时调一次。5.5 摄像头卡顿到无法接受每帧重建模型 后处理瓶颈现象画面播是能播但 FPS 只有 3-5操作明显卡顿CPU 占用率 100%。原因两个主要因素。一是代码里把build_model或load_darknet_weights写进了 while 循环模型每次重新初始化二是后处理里decode_outputs和nms用了 Python 双层 for 循环遍历所有候选框CPU 图像识别时这部分开销甚至超过 CNN 本身。解决模型构建绝对只做一次后处理尽量向量化用torchvision.ops.nms替代手写循环。如果还不够把摄像头分辨率降到 640×480letterbox 目标改 320这个包支持 320 输入但 mAP 会降一点。实测在笔记本 CPU 上能稳定跑到 15-20 FPS。5.6 NMS 把目标压没了类别间抑制方向搞反现象两个不同类别的目标重叠人和自行车NMS 后只剩一个框另一个类别直接消失。原因用了 class-agnostic NMS所有类别的框放在一起按 IoU 抑制重叠度超过阈值时置信度低的那个类别框被删掉。图像识别里这个行为偶尔会被误以为模型漏检。解决改成 class-wise NMS先按类别分组组内做 IoU 抑制组间互不干扰。torchvision.ops.nms本身支持传入 scores但需要你手动给每个框加类别标签按类别循环调用。这个细节在内置工具函数里往往隐藏得很深不排查代码根本不会意识到。6. 进阶CPU 上跑 tiny 的三个提速技巧先把 416×416 推理的耗时拆开看模型 forward 约占 45%letterbox 和 BGR-RGB 转换约占 15%后处理解码和 NMS 约占 40%。很多人以为瓶颈在模型其实后处理常被忽略。提速第一步是在确认已用torch.no_grad()的前提下把解码和 NMS 从 Python 循环改成批量张量运算这一步通常能直接提升 30% 帧率。第二步如果模型和权重允许把网络整体转成半精度推理model model.half() tensor tensor.half() # 输入也转半精度否则报类型不匹配逻辑说明Volta 架构之后的 GPU 对 FP16 有专门加速即使 CPU 上不支持加速half 也能减半内存带宽占用推理速度有小幅提升。注意前提是你的输入张量和模型参数都在同一个 dtype否则matmul直接报错。我一般不建议在 CPU 上用半精度实测提升不明显反而有精度损失。第三步是 ONNX 导出把模型固化成计算图再用 OpenVINO 或 ONNX Runtime 跑。导出的核心是固定输入尺寸和 anchor 顺序dummy_input torch.randn(1, 3, 416, 416) torch.onnx.export( model, dummy_input, yolov3-tiny.onnx, opset_version11, input_names[input], output_names[out_13, out_26] )逻辑说明导出时模型内部的所有分支都会被固化成节点之后不再依赖 PyTorch 环境。ONNX Runtime 在 CPU 上的推理速度通常比 PyTorch eager 模式快 20%-50%尤其在 Intel 平台配合 OpenVINO 时提升更明显。参数说明opset_version 不能太低11 在大多数部署环境中兼容性最好input_names和output_names必须与运行时读取的 name 一致否则推理时报输入名称错误。踩过的最后一个坑是视频抽帧策略很多检测场景并不需要逐帧检测比如监控画面 10 秒人可能只走几步。我现在的做法是每 2 帧检测一次中间帧直接复用上一次的检测结果把节省的算力用来提高置信度阈值。这套组合下来原来 8 FPS 的视频流能稳到 18-20 FPS且肉眼几乎分辨不出延迟差异。从那以后我每次拿到新的训练好的检测模型都会先跑一遍 profile 脚本把 forward 和后处理的耗时占比打出来再决定优化方向——而不是一上来就换模型结构。这个 zip 包让我把 YOLOv3-tiny 从听说过变成能改能用希望你也能从中找到自己的部署节奏。本文还有配套的精品资源点击获取