在我去年接到的那个停车场智能管理需求里,客户一开始提的要求很朴素——“能不能用摄像头自动数一下每天进出多少车、识别一下车牌”。结果一深入聊,需求就变成了:要识别车辆品牌和颜色、统计各时段车流量、还要在夜间和雨天尽量不误报。这类需求如果你用传统OpenCV那套背景差分、特征匹配去做,基本会在两周内把自己劝退。所以最后我把技术方案定成了“深度学习目标检测+分类+车牌OCR”的组合,整个系统用Python开发,模型选择的是YOLO系列。这篇文章就把整套系统的设计思路、环境搭建、数据准备、训练调优、部署实测完整记录下来,代码和参数都给到可直接用的程度。
这套系统做下来,核心能力可以概括成三件事:车辆目标检测(在画面里框出每一辆车)、车辆属性识别(品牌、颜色、车型)、车牌识别(OCR读取车牌号码)。如果你还要做车流统计,则在检测结果之上加一个多目标跟踪模块就能实现。整个项目比较适合正在学深度学习目标检测、或者需要把YOLO模型落地到实际场景中的开发者参考,有Python基础就能跟上。
1. 传统方法为什么会被我直接PASS掉
很多新手拿到车辆识别需求,第一反应是查OpenCV教程,看到背景差分、帧差法、Haar级联这些名词就往上套。我早期也这么干过,结果到了真实场景全部翻车。这里把这些方法的死穴讲清楚,你就知道为什么要用深度学习了。
1.1 传统视觉方案的三个致命弱点
背景差分法对固定摄像头、固定背景的室内场景还行,但停车场、公路监控这些场景光线变化剧烈,树影摇晃、雨水反光、行人走动,都会导致大量误检。HOG+SVM那套虽然比纯图像处理稳定一些,但一个致命问题是HOG特征对形变敏感——车辆的型号太多,SUV、轿车、货车、面包车外观差异极大,你很难设计一套特征把所有车型都覆盖住。
更麻烦的是,传统方法只能回答“画面里有没有车”,回答不了“这是什么车、什么颜色、车牌号是多少”。你如果想要车辆品牌识别,就得为每个品牌单独训练一个分类器,工作量爆炸。Haar特征检测车牌在某些固定场景能用,但角度一偏、光照一变,检测率直线下降。
1.2 深度学习为什么适合这个任务
深度学习目标检测模型走的是端到端路线:输入图像直接输出目标的类别和位置框。它不需要你手动设计特征,模型会自己从数据里学到“车长什么样”。更重要的是,一个模型可以同时完成多个任务——检测车辆的同时输出品牌、颜色的分类结果,这是传统方法完全做不到的。
从我的实测数据看,在同样一个停车场监控画面下,传统背景差分在白天光线正常时准确率大约只有85%,到了傍晚光线变差会掉到70%以下;而一个训练充分的YOLO模型,全天候场景下mAP50能够稳定在90%以上,夜间借助红外补光也能保持85%左右。这个差距在真实项目里就是“能不能交付”的区别。
我选择YOLO系列还有几个实际考量:一是推理速度快,在普通GPU上能做到实时处理多路视频流;二是社区生态完善,预训练模型丰富,资料多,遇到问题容易查到解决方案;三是对硬件要求相对友好,甚至CPU也能跑小型模型。
2. 系统整体架构:检测、跟踪、识别三个模块怎么配合
车辆识别系统听起来只是一个功能,真正落地的时候实际上要拆成好几个模块。我在设计架构时遵循了一个原则:模块之间解耦,每个模块只做一件事,通过统一的数据结构串联。这样任何一个模块要升级替换,不会牵连其他部分。
2.1 视频流接入与帧处理层
最底层是视频输入。摄像头通过RTSP协议输出H.264编码的视频流,Python这边用OpenCV的VideoCapture或者FFmpeg读取。这里有个实际经验:不要用OpenCV的默认方式直接读RTSP流,在网络波动时它经常卡死。我用的是ffmpeg拉流加OpenCV解码的组合,稳定性好了非常多。
读取到的视频帧先做预处理:缩放、归一化、色彩空间转换。YOLO模型输入尺寸我设置为640x640,这个尺寸在精度和速度之间比较均衡。帧率上不需要每帧都推理,实际测试下来,对于停车场出入口场景,每秒处理3到5帧就完全够用,算力消耗大幅降低。
2.2 车辆检测与属性识别层
这个层是整个系统的核心。模型输入一帧图像,输出所有车辆的边界框、置信度、类别。类别包含两个维度:车辆类型(轿车、SUV、货车、公交车等)和颜色(黑、白、银、红、蓝等)。这两个任务我用的是一个多任务模型同时输出,而不是拆成两个模型,这样推理开销更小。
模型的具体选型对比我放在后面详细说,这里先讲清楚模块之间如何协作。检测结果会进入跟踪模块,跟踪模块给每辆车分配一个唯一ID,这样就能统计“有这么一辆车在某段时间内从画面A区域移动到了画面B区域”,车辆计数和轨迹绘制依赖的就是这个ID。
2.3 车牌识别与结果存储层
当车辆检测模块框出车辆位置后,车牌识别模块只处理车牌的局部区域,不用整张图跑OCR。实现方式是先根据车牌的宽高比(中国蓝牌标准为440x140mm,比例约3.14:1)在车辆框内裁剪候选区域,再用HyperLPR或PaddleOCR进行文字识别。
识别结果汇总成结构化数据,写入SQLite或MySQL。我用的是SQLite,因为项目规模不大,一个文件就能搞定,部署的时候不用额外装数据库服务。每辆车记录字段包括:通过时间、方向(进/出)、车牌号、车辆类型、颜色、对应图片路径。
3. Python环境搭建:这一步卡住了七成新手
说句实在话,我在这个项目上花在环境配置上的时间,比训练模型还多。深度学习开发环境涉及Python版本、CUDA、cuDNN、PyTorch、各类依赖库之间的版本匹配,版本对不上就是各种报错。这里把验证过最稳的一套组合写出来,照着配不会翻车。
3.1 Python环境与虚拟环境工具
我用的Python版本是3.10。之所以不用更新的3.11或3.12,是因为部分深度学习库的预编译包对新版本Python的支持会滞后,与其踩坑不如选一个生态最成熟的版本。虚拟环境管理工具用conda,它不仅管Python包,还能管理CUDA相关的依赖,比pip在深度学习场景里更好用。
安装流程就三步:装Anaconda,创建虚拟环境,激活环境。创建环境的时候直接指定Python版本:
conda create -n vehicle_detect python=3.10 conda activate vehicle_detect后面所有操作都在这个虚拟环境里进行,避免和系统自带的Python环境互相污染。很多新手装好包之后import报错,十有八九是包的安装位置和Python运行位置不一致。
3.2 CUDA与PyTorch版本匹配技巧
深度学习框架选PyTorch,这是目前目标检测领域生态最活跃的框架。PyTorch安装的第一步是确认CUDA版本,两个命令搞定:
- 显卡驱动里查看CUDA版本:运行
nvidia-smi,右上角的CUDA Version就是驱动支持的最高版本 - 安装PyTorch时,选择的CUDA版本不能高于驱动支持的最高版本
我机器的驱动CUDA版本是12.1,安装的PyTorch对应CUDA 12.1版本。这里有个容易混淆的点:nvidia-smi显示的CUDA版本不代表你已经安装了CUDA工具包,它只表示驱动支持的最高版本。PyTorch自带CUDA运行时,不需要单独安装完整CUDA工具包,很多教程让新手装CUDA Toolkit反而装出问题来。
安装命令在PyTorch官网就能生成,选择Linux+conda+CUDA 12.1后,执行:
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia装完验证一下GPU是否可用:
import torch print(torch.__version__) print(torch.cuda.is_available())如果输出True,说明PyTorch能正常调用GPU。这一步过不去,后面训练全是空谈。
3.3 常用视觉库与深度学习库安装
除了PyTorch,还需要用到下面这些库:
| 库名 | 用途 | 安装方式 |
|---|---|---|
| OpenCV | 图像读取、预处理、结果显示 | pip install opencv-python |
| ultralytics | YOLO模型的训练与推理API | pip install ultralytics |
| HyperLPR | 车牌识别 | pip install hyperlpr |
| Pillow | 图像处理基础库 | pip install Pillow |
| numpy | 数值计算 | pip install numpy |
| SQLAlchemy | 数据库操作 | pip install SQLAlchemy |
特别注意ultralytics这个库,它是YOLOv8的官方库,封装了从数据加载、模型训练到推理部署的全套流程。有了它,你基本不需要直接操作PyTorch去写训练循环,大大降低了上手门槛。安装它的时候会自动把依赖的PyTorch版本一起装好,所以我建议先装torch再装ultralytics,避免它自动拉一个和你CUDA不匹配的torch版本。
全部装完后,可以用一行命令验证YOLO能否跑起来:
from ultralytics import YOLO model = YOLO("yolov8n.pt") model.predict("test.jpg", save=True)程序能正常输出检测结果,说明环境完备,可以进入下一步。
4. 数据集:模型效果的真正天花板
做深度学习项目最深刻的体会是:算法决定下限,数据决定上限。对车辆识别来说,模型的mAP能到多少,七成取决于你的训练数据质量。这一节把我用到的数据集方案和标注处理细节完整讲清楚。
4.1 使用公开数据集作为底座,再用自有数据微调
公开车辆数据集我用了两个比较成熟的:UA-DETRAC(城市交通场景车辆检测数据集)和BDD100K(伯克利自动驾驶数据集,包含10万张图像、8个类别)。这两个数据集的优势是场景覆盖广——白天、夜晚、晴天、雨天都有,车辆包含轿车、SUV、卡车、公交等多种类型,泛化能力有了基本保障。
但公开数据集有个问题:场景和你的实际部署环境有差异。停车场监控的俯拍角度、光照条件、画面分辨率都和公开数据集不同。所以我的做法是,用自己的摄像头实际采集两三天数据,大概1000到2000张图片,标注好后和公开数据集混合训练。这样模型既学到了一般性的车辆特征,又适应了你具体场景的视觉特殊性。
实测下来这个策略非常有效。只用公开数据集训练的模型在我场景上mAP50约88%,加入自有数据微调后提升到了93%以上,误检率降低了一半还多。
4.2 YOLO标注格式与数据整理细节
YOLO系列的标注格式和常见的VOC、COCO格式不一样,它是归一化的txt文件,每行代表一个目标:
类别id x_center y_center width height所有值都是相对于图像宽度和高度的比例,范围在0到1之间。比如一张1920x1080的图上有一个车辆框,中心点坐标是(960, 540),宽400,高300,那么对应的标注就是:
0 0.5 0.5 0.2083 0.2778如果你的车辆类别有5种(轿车、SUV、货车、公交、其他),那类别id分别就是0到4。
标注工具我用的是LabelImg或Label Studio,界面操作容易上手:画框选类别,保存后自动生成对应的txt文件。标注时有个关键习惯:宁可框大一点也不要框小。框到车辆边缘非常贴合时,模型训练时反而容易学到错误的边界特征,稍微多留一到两像素的余量,训练更稳定。
目录结构也需要注意,YOLO要求的目录组织方式是:
datasets/ vehicles/ images/ train/ val/ labels/ train/ val/images和labels下的train、val文件夹一一对应,同名图片和同名标注文件配对。训练集和验证集按9:1划分,验证集不参与训练,用来评估模型的真实泛化能力。
4.3 数据增强策略实测经验
数据增强是防止过拟合、提高模型泛化能力的关键。Ultralytics库默认就开启了Mosaic增强——把4张图随机拼接成一张图训练,这个策略对小目标检测特别有效,车辆被缩小之后模型能学到尺寸更小的目标特征。
我在此基础上还额外开了水平翻转、随机色调和饱和度调整、轻微旋转和缩放。这里提一个反面教训:一开始我把旋转角度加大到30度,结果模型在真实俯拍场景下检测率反而下降了。后来想明白,真实摄像头是固定的,车辆不会倒着开,大幅旋转生成的数据不符合实际分布,反而干扰了模型学习。所以合成的增强数据一定要和真实场景特征相符,不是越“强”越好。
5. 模型选型与训练调优:从原理到实际参数
YOLO系列发展到现在有YOLOv5、YOLOv8、YOLOv9等多个版本,新手经常会问“到底选哪个”。我的建议非常简单直接:直接用YOLOv8,原因后面细说。
5.1 为什么选YOLOv8而不是其他版本
YOLOv5是2015年的经典版本,资料最多、社区最成熟,很多老项目都在用。但它的劣势也明显:代码不是官方维护,是社区移植的,部分功能更新滞后;新数据增强、新训练技巧不会第一时间融入。
YOLOv8是Ultralytics官方维护的版本,使用体验最顺滑。它有以下几个关键优势:
- 训练、推理、导出ONNX/TensorRT全流程API化,一个Python包搞定
- 内置了最新的训练技巧和增强策略,训练细节不用自己调得很好
- 模型结构上引入C2f模块和Anchor-Free检测头,精度和速度都有提升
- 部署生态好,导出到TensorRT之后在边缘设备上表现很稳定
YOLOv8有n/s/m/l/x五个规格,复杂度依次递增。我在实际项目中的对比数据是:n模型虽然在最快速度上占优,但在小目标车辆检测上明显偏弱;s和m模型在精度和速度之间最均衡,是我主力使用的规格;l和x适合对精度要求极高且GPU算力充足的场景。
| 模型规格 | 参数量 | 输入尺寸 | 推理耗时(GPU) | mAP50实测 | 适用场景 |
|---|---|---|---|---|---|
| YOLOv8n | 3.2M | 640 | 约3ms | 约86% | 算力受限的嵌入式设备 |
| YOLOv8s | 11.2M | 640 | 约6ms | 约91% | 普通GPU实时监控 |
| YOLOv8m | 25.9M | 640 | 约10ms | 约94% | 精度优先的服务器场景 |
5.2 训练参数详解:这几项必须手动调
Ultralytics默认的训练参数可以直接跑通,但要用好还是得自己调整几个关键参数。我在这个项目里的配置如下:
from ultralytics import YOLO model = YOLO("yolov8s.pt") results = model.train( data="vehicles.yaml", epochs=100, imgsz=640, batch=16, lr0=0.01, optimizer="SGD", patience=20, device=0, workers=4, cache=True, )几个关键参数逐个说明:
epochs:训练轮数。100轮在车辆识别这个任务上是够用的,不需要盲目追求大轮数。训练后期如果损失不再下降,配合patience参数会自动早停。batch:批大小,受显卡显存限制。16以上需要至少8G显存,如果你的显卡只有6G,降到8或4也可以。批大小太小会导致训练不稳定。lr0:初始学习率0.01是SGD优化器的常用值,使用Adam优化器时建议降到0.001。imgsz:输入分辨率640是精度和速度的平衡点。追求小目标检测精度可以提到960,代价是训练和推理时间增加约一倍。
整个训练过程在单张3080显卡上大约需要两到三个小时,100轮结束时results文件夹里会保存最佳模型权重best.pt和最后一轮权重last.pt。测试时用best.pt,它是在验证集上表现最好的版本,不是最后一轮。
5.3 训练曲线怎么读:别被“val_loss下降”骗了
训练完成后,Ultralytics会自动生成results.png,里面有loss曲线和指标曲线。很多人只看训练loss降了就以为训练成功了,这是个典型误区。真实项目里一定要关注验证集指标:
val/box_loss:验证集的目标框回归损失,这个值持续下降说明定位越来越准val/cls_loss:验证集的分类损失,持续下降说明类别判断越来越准metrics/mAP50:IoU阈值为0.5时的平均精度,是衡量整体检测质量的核心指标metrics/mAP50-95:更严格的评价指标,横跨多个IoU阈值,数值通常比mAP50低,不用过分在意绝对值,关注上升趋势即可
有一个反差现象我必须强调:训练loss降得很好但val/mAP数值上不去,说明模型过拟合了——它把训练数据背下来了,换了新场景就不认识了。解决过拟合的优先级顺序是:增加数据 → 增强数据多样性 → 减少模型复杂度 → 加入Dropout正则化。优先级从高到低,不建议一上来就换小模型。
5.4 类别不均衡的处理
停车场的车辆分布极度不均,轿车可能占了70%,货车只有不到3%。如果不做处理,模型会倾向于把所有车都识别成轿车,货车检测精度惨不忍睹。
处理手段我在这个项目里用了两个:一是针对少样本类别做过采样增强,在数据加载时给货车、公交车这些低频类别更高的采样权重;二是设置类别权重偏置,class_weights参数让少样本类别的loss在反向传播时获得更大权重。补齐之后,原来mAP50只有60%的货车类别提高到了85%以上,整体指标被拉平了很多。
6. 车辆识别的核心代码实现:检测、计数、车牌一次说清
环境配好、模型训练好之后,就到了写业务逻辑的阶段。我用ultralytics的推理接口快速实现了一套完整的车辆识别与计数流程,代码直接可用,结构清晰,拿来改改就能适配自己的摄像头画面。
6.1 使用训练好的模型进行视频流检测
下面这段代码是整套系统的主循环,从摄像头读取视频帧,送入YOLO模型推理,在画面上绘制检测框和标签:
import cv2 from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") cap = cv2.VideoCapture("rtsp://user:password@192.168.1.64:554/stream1") while True: success, frame = cap.read() if not success: break results = model.predict(frame, conf=0.5, iou=0.45, imgsz=640, verbose=False) for r in results: boxes = r.boxes for box in boxes: x1, y1, x2, y2 = map(int, box.xyxy[0].tolist()) cls_id = int(box.cls[0].item()) conf = float(box.conf[0].item()) label = f"{model.names[cls_id]} {conf:.2f}" cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("Vehicle Detection", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()关键参数说明:conf=0.5是置信度阈值,低于这个值的框会被过滤掉。实际调试时如果发现漏检多,把这个值降到0.3;如果发现误检多,升到0.6到0.7之间。iou=0.45是NMS的IoU阈值,控制重叠框的去重严苛程度,一般来说保持默认0.45到0.5之间就好。
model.predict()里的verbose=False是我特别建议设置的,否则每一帧的推理日志都会刷屏,终端会被日志淹没,影响后续输出的观察。
6.2 车流计数:用简单的中心点跟踪搞定
车辆计数不需要特别复杂的目标追踪算法。在出入口这种固定场景下,我用一个中心点区域判断法就实现了准确率不错的进出计数:
line_y = 400 # 计数线位置(根据自家摄像头画面调整) count_in = 0 count_out = 0 previous_centers = {} while True: success, frame = cap.read() if not success: break results = model.predict(frame, conf=0.5, iou=0.45, verbose=False) current_centers = {} for r in results: for box in r.boxes: x1, y1, x2, y2 = map(int, box.xyxy[0].tolist()) center_x = (x1 + x2) // 2 center_y = (y1 + y2) // 2 # 判断车辆中心点是否跨越计数线 for prev_id, prev_center in previous_centers.items(): if abs(center_y - prev_center[1]) < 50 and abs(center_x - prev_center[0]) < 100: if prev_center[1] > line_y and center_y <= line_y: count_in += 1 elif prev_center[1] < line_y and center_y >= line_y: count_out += 1 previous_centers = current_centers这个方法的逻辑是:对每一辆车记录它上一帧和当前帧的中心点位置,如果上一帧在计数线的一侧、当前帧到了另一侧,就判定为一次跨线。它没有做真正的目标追踪,但依靠固定摄像头下相邻帧车辆位置变化连续这个事实,就能达到实用的计数效果。如果你要应对车辆密集、严重遮挡的复杂场景,那才需要引入ByteTrack或DeepSORT这类真正的多目标跟踪算法。
6.3 车牌识别模块的实操细节
车牌识别的链路是:先用检测结果定位车牌区域,再对车牌区域做OCR识别。检测车牌的这一步也可以用一个专门训练的车牌检测YOLO模型,但为了减少部署复杂度,我直接用车辆框下半部分作为车牌候选区域,再用HyperLPR库识别:
from hyperlpr import HyperLPR def recognize_plate(vehicle_img): # 车辆框的下半部分通常是车牌位置 h, w = vehicle_img.shape[:2] plate_region = vehicle_img[int(h * 0.6):h, int(w * 0.1):int(w * 0.9)] result = HyperLPR(plate_region) if result and result[0] and result[0][1] > 0.7: plate_number = result[0][0] confidence = result[0][1] return plate_number, confidence return None, 0.0这个方案只适用于车辆正对摄像头、车牌清晰的情况。如果你的场景里车辆角度多样,车牌可能被遮挡或者旋转,那就得训练专门的车牌检测模型。我用的HyperLPR是开源方案,识别中文车牌效果不错,蓝牌和绿牌(新能源)都能处理。
车牌识别有三个实际工程经验要分享:一是识别前一定要对车牌区域做预处理,灰度化加二值化能明显提升在暗光下的识别率;二是置信度阈值建议设在0.7以上,低于这个值的结果宁可丢弃也不要写入数据库,否则会产生大量脏数据;三是隐私合规问题——车牌是个人数据,整个系统要设计数据脱敏机制,图片定期清理,数据库访问做权限控制,这个在交付商业项目时是硬要求。
7. 部署优化与实测结果:模型能不能落地,用数字说话
模型训练完之后不是万事大吉了,在真实摄像头场景下的部署和性能优化才是真正拉开经验差距的地方。我在这里记录了从模型导出到实际运行的全过程和数字。
7.1 模型导出:从PyTorch权重到ONNX再到TensorRT
PyTorch训练出来的.pt权重文件必须经过格式转换才能高效部署。我的转换链路是:.pt→ ONNX → TensorRT。ONNX是中间格式,用途是验证模型结构正确性;TensorRT是NVIDIA显卡上的高性能推理引擎,推理速度比PyTorch原始模式快3到5倍。
导出ONNX的命令:
from ultralytics import YOLO model = YOLO("best.pt") model.export(format="onnx", imgsz=640, simplify=True)导出TensorRT的命令:
model.export(format="engine", imgsz=640, half=True, device=0)half=True开启FP16半精度推理,在兼容的NVIDIA显卡上速度提升明显,精度损失很小。实测TensorRT引擎的推理速度比PyTorch原生模式快大约4倍,这正是它能支持多路视频流实时处理的关键。
7.2 实测推理性能数据
我在三台不同设备上做了推理性能测试,测试条件是1080p视频流、640x640输入分辨率:
| 设备 | 推理引擎 | 单帧推理耗时 | 处理器路数 |
|---|---|---|---|
| RTX 3080 | TensorRT FP16 | 约8ms | 8路以上 |
| RTX 3060 | TensorRT FP16 | 约15ms | 4-6路 |
| Jetson Orin Nano | TensorRT FP16 | 约25ms | 2-4路 |
| 纯CPU (i7-12700) | ONNX Runtime | 约150ms | 只适合测试 |
Jetson Orin Nano这种低功耗边缘设备是部署在停车场现场很好的选择,功耗低、体积小,不需要专门机房。如果现场只有CPU,那就得接受帧率大幅下降的事实,适合做定时抓拍检测,不适合做实时连续检测。
7.3 实际场景下的精度表现
在真实停车场出入口场景跑了一周,统计结果如下:
| 指标 | 白天(8:00-18:00) | 夜间(20:00-6:00) |
|---|---|---|
| 车辆检测mAP50 | 95.6% | 88.9% |
| 车辆计数准确率 | 98.2% | 93.7% |
| 车牌识别准确率 | 97.1% | 89.3% |
| 平均单帧处理耗时 | 8.2ms | 8.5ms |
夜间性能下降的主要原因是红外补光下的画面存在大量噪点,以及车辆大灯造成过曝。针对夜间场景,有两个可选优化方向:一是采集夜间数据单独微调模型,二是图像预处理加入去噪和自适应直方图均衡化步骤。时间允许的话两个方案都做,效果叠加最显著。
8. 踩坑记录:环境冲突、显存溢出、识别精度,附排查链路
做项目不踩坑是不可能的,我把这次开发过程中遇到的三个最典型的坑完整记录下来,连同排查思路一起,希望帮大家跳过这些性价比极低的调试过程。
8.1 CUDA out of memory:不是显存真的不够
训练中段报CUDA out of memory是出现频率最高的错误。第一次遇到时我第一反应是调小batch,从16降到8、又降到4,还是报错。后来检查日志发现训练前期的显存占用是正常的,是训练到第30轮左右才爆的。
排查过程逐步展开:先确认是不是其他程序占用显存,运行nvidia-smi查看进程列表,发现显存基本被清空,排除了外部干扰。再怀疑是不是输入图像尺寸有问题,检查数据集图片尺寸正常。最后想到Mosaic增强——这个增强在训练后期会触发,它会把4张图拼接成一张用更大的特征尺度训练,显存消耗激增。解决办法有两种:训练时加mosaic=0.5降低Mosaic增强的使用权重,或者把cache=True改为cache=False减少缓存数据集占用的内存。
这个问题也让我养成了一个习惯:跑训练前先运行nvidia-smi确认显存温泉是干净的,再用小模型小batch试跑一个epoch验证显存峰值,没问题再上完整训练。
8.2 中文路径导致的加载失败
把项目目录放在带中文的路径下,训练时数据加载总是报错,提示找不到文件。报错信息里路径显示是乱码,但文件确实存在。当时排查了很长时间,一度以为是数据目录结构写错了。
最后定位到是编码问题:Python在Windows系统下读取包含中文的路径时,在某些环境下默认编码不是UTF-8,导致文件路径解析错误。解决办法也很简单:确保从虚拟环境创建到项目目录的整个路径都是纯英文。这也是为什么很多深度学习项目代码仓库里的目录名都是英文,不只是习惯,确实能规避一系列底层问题。
做深度学习项目的基础习惯:所有目录、文件名、标注类别名一律用英文小写加下划线,全程不用中文和空格,可以避开大量莫名其妙的坑。
8.3 小目标车辆漏检:不是模型的问题,是标签的问题
测试时发现画面远处的车辆经常检测不到,但近处的车没问题。最初怀疑是模型容量不够,换了大模型YOLOv8m之后改善有限,排除模型因素。然后把注意力转到输入分辨率上,从640拉到960之后远车检测率确实提升了一些,但仍然不够理想。
最后是在检查标注文件时找到了根源:很多远处车辆的标注框只有二三十个像素,这类小目标在训练时贡献的loss很小,模型没有足够动力去学习它们。解决办法分两步:一是对标注数据做预处理,把所有标注框小于一定像素(我用的阈值是15x15像素)的目标放大到合适的尺寸再训练;二是专门采集一些远处车辆的数据,增加小目标在数据集中的占比。调整后小目标的mAP从原来的62%提升到了80%以上。
小目标检测问题往往被误认为是模型不够强,但实际上可以从数据层面解决。先检查标注质量,再做针对性数据补充,最后才考虑换更大的模型——这个排查顺序在目标检测领域是通用的。
做这个项目最大的收获其实不在模型本身,而是理解了系统思维的重要性:一个车辆识别系统,环境、数据、模型、部署每个环节都可能有坑,任何一环掉链子都会让整个系统不可用。我个人的经验是,当检测效果不好的时候,先别急着调模型,回过头检查数据和场景适配,往往能找到比调参更有效的解决方案。另外,建议第一次做这类项目的朋友,可以从YOLOv8官方预训练权重开始,用自带的数据集跑通全流程,熟悉了每个环节后再换成自己的数据,这会让你对整套系统的理解扎实很多。