ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于YOLOv5的非机动车违停检测:从数据集到部署全流程

基于YOLOv5的非机动车违停检测:从数据集到部署全流程 简介本资源是面向智能交通与城市治理场景的YOLOv5目标检测训练专用数据集聚焦非机动车违规停放识别任务适用于计算机视觉初学者、算法工程师及智慧城市项目开发者。资源包含951张自行车实拍图像jpg与对应PASCAL VOC格式标注文件xml覆盖山地、公路、越野、通勤及共享单车等细分类型属完整10类自行车数据集中的第三类子集标注规范、场景丰富可直接用于YOLOv5模型训练、验证与部署。压缩包共1896个文件大小为138.26MB结构清晰图片与XML一一对应便于批量加载与数据增强。目前已有270人学习下载资源由实战经验丰富的作者chenjing_an整理发布配套数据已按车辆类型系统分类标注质量高、重复率低显著降低数据清洗与预处理成本助力快速构建高精度非机动车识别模型。 我做了好几个基于监控画面的非机动车违停检测项目说实话真正难的不是训练一个”能检测到自行车”的模型而是把数据集、训练流程、判定规则和现场部署这整条链路走通。很多朋友拿着网上找来的bicycles2_images_xmls数据集跑到YOLOv5里一跑看到loss降了就觉得自己搞定了但一上现场就崩——不是把停在划线区域里的车也报成违停就是傍晚光线一暗漏检一半。这篇文章我就以自行车检测为切入点把我怎么用YOLOv5处理这批已标注数据、怎么设计违规判定业务逻辑、怎么处理现场部署的各种坑完整拆开来讲。这个项目适配的人群很广刚接触YOLOv5想训练第一个目标检测模型的入门选手可以把bicycles2数据集当作一个标准练手项目已经在做智慧城市、智慧园区相关方案的技术人员可以参考我后面对业务规则和部署优化的思路。整个链路我尽量讲得细一点从环境的版本选择到标签格式转换再到训练指标怎么判读最后落到”检测框怎么变成一条违停告警”每一步都会说明我为什么这么选。1. 先把这个项目的核心链路拆清楚从数据集到业务判定很多人第一眼看到“yolov5非机动车违规停放已标注数据集机器视觉识别bicycles2_images_xmls”这一串词下意识把它理解成“用YOLOv5训练一个能直接识别违规停车的模型”。这个理解会走弯路。深度学习模型本质上只能做“感知”它认识的是画面里的“自行车”这一类物体至于这辆车停在哪儿、那条区域是不是禁停区域、停在这里算不算违规都需要在模型之后用一套业务规则去判定。这是整个项目里最容易被新手忽略的分界线。我习惯把完整链路拆成五段数据层拿到bicycles2_images_xmls这种已标注数据集理解它的标签格式和类别定义弄清楚里面到底标注了什么。训练层把VOC格式的XML标注转换成YOLOv5需要的TXT格式配置数据集路径跑训练得到能检测“自行车”的模型权重。规则层划定禁停区域把模型输出的检测框和区域做空间关系计算判定当前画面里是否存在违停行为。业务层对判定结果做去重、抽帧、保存证据图、触发告警把单帧的检测结果变成一条可处理的事件记录。部署层把模型导出成更适合推理的格式适配现场摄像头角度、光照条件并不断优化漏检误检。数据层和训练层是基础大部分教程都在讲这两块但真正决定这个项目能不能落地的是规则层和业务层。我见过不少团队模型调得漂漂亮亮的mAP也能看了结果到了现场发现摄像头里的自行车都停在马路牙子上模型框出来了禁停区域外有俩像素压线于是死活不报。这种问题不是模型训练能解决的而是坐标映射和判定规则设计的问题。所以我这篇文章不会只停在“训练出一个模型”而是把从数据集到业务判定的整条链路都走一遍。你按这个顺序做下来至少能拿到一个在自己数据上跑得通、上了现场不丢人的完整方案。2. bicycles2_images_xmls数据集深度解析不是所有XML都一样我做这个项目时拿到的是网上流传较多的bicycles2_images_xmls数据集目录结构非常直观一个images文件夹放JPG图片一个xmls文件夹放对应的Pascal VOC格式标注文件。文件名一一对应比如000001.jpg对应000001.xml。整个数据集标注的目标只有一个类别——自行车bicycle。2.1 Pascal VOC格式里到底藏了什么打开一个XML文件你会发现信息比想象中多一些。它不只是存了一个矩形框坐标还记录了图片尺寸、通道数、目标类别、目标的bounding box顶点坐标甚至还有difficult标记。一个典型的标注长这样annotation folderimages/folder filename000001.jpg/filename size width1280/width height720/height depth3/depth /size object namebicycle/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin426/xmin ymin234/ymin xmax713/xmax ymax618/ymax /bndbox /object /annotation这里的xmin/ymin/xmax/ymax是像素坐标坐标系原点在图片左上角x轴向右y轴向下。这个坐标系约定很重要后面转YOLO格式、做规则判定的时候都要跟它保持一致。difficult标记表示这个目标是否难以辨认默认转YOLO格式的时候通常会跳过difficult1的样本但对bicycles2这种数据集大部分标注的difficult都是0所以基本不用处理。2.2 这个数据集的真实构成与训练陷阱我翻了一下这组数据的整体情况图片分辨率以1280x720和1920x1080为主覆盖了白天、傍晚两种光照场景以街道、园区、小区门口居多自行车有停放的、也有骑行中的还有一部分是斜靠在墙上只露出一半的。单张图片里自行车数量差异很大少的一张一辆多的一张五六辆而且车辆密集时互相遮挡的情况很常见。这里有一个容易踩的坑模型学到的是“自行车”这个类别不是“停放状态”这个属性。如果你希望模型直接输出“这辆车违停了”那一定会失败因为数据集里根本没有“违停”这个标签。数据集里的bicycle既包括停放的也包括骑行的深度学习模型会从这些样本里抽象出“自行车”的通用视觉特征——两个轮子、车架、把手这些。至于它运动还是静止、在哪个区域那是另一层逻辑要做的事。另外一个隐藏问题是数据分布。如果这组数据里大多是靠墙竖着放的自行车模型对“倒在地上”或者“横七竖八叠在一起”的自行车检测效果就比较差。所以拿到数据集后我建议先做一次简单的样本统计比如随机抽几十张图数一数单张图里目标的数量分布、目标框面积占整图的比例。这样做的目的是提前掌握训练难点避免训练完才发现小目标检测能力不足再回头补数据。2.3 用一段脚本快速检查数据集我建议你拿到数据集后先写个十来行的脚本统计一下图片尺寸、目标框大小、每张图的目标数心里有个底。这段脚本不用很复杂用到os和xml.etree.ElementTree就行import os import xml.etree.ElementTree as ET xml_dir xmls box_counts [] box_sizes [] for xml_file in os.listdir(xml_dir): tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() objs root.findall(object) box_counts.append(len(objs)) for obj in objs: bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) w xmax - xmin h ymax - ymin box_sizes.append((w, h)) print(平均每张图目标数:, sum(box_counts) / len(box_counts)) print(最大目标框宽高:, max(box_sizes)) print(最小目标框宽高:, min(box_sizes))统计完你大概能得出两个关键信息一是目标尺度范围如果宽高有大量小于32像素的框说明小目标检测是这个项目的难点二是单图目标数量上限这直接影响你后面训练时的batch大小和NMS参数。我在处理bicycles2数据集时发现有不少远距离拍摄的小目标自行车这直接导致我后面放弃了yolov5n改用yolov5s并开了更高的输入分辨率。3. YOLOv5环境搭建与版本选型这一步翻车最冤训练环境搭建本身不难但版本问题能把人折磨到怀疑人生。我强烈建议你先把YOLOv5的版本、PyTorch版本、Python版本、CUDA版本之间的关系理清楚再动手装不然装到一半发现torch.cuda.is_available()返回False心态就崩了。3.1 选哪个YOLOv5分支YOLOv5的官方仓库在GitHub上主要维护的是master分支。但你需要知道v6.0之前和之后在某些命令上有细微差别例如--cache参数是缓存图片到内存以加速训练。目前绝大多数教程和业务代码都基于v6.0/v7.0这几个稳定版本我自己用的是v6.0标签因为社区生态最成熟遇到问题搜答案容易各种改模型的博客也是基于这个版本的代码。新的master分支代码一直在更新日常使用反而容易出现接口变动。一个实用的做法是clone完代码后直接切到版本标签git clone https://github.com/ultralytics/yolov5.git cd yolov5 git checkout v6.0选版本还有个更现实的原因你在GitHub上搜到的大部分Issue讨论、CSDN博客、知乎文章都是针对v5.0/v6.0/v7.0这几个老版本的。用新版本branch跑出问题去搜可能匹配到的信息都是老版本的却因为代码改动对不上排查起来更痛苦。做项目不是追新稳定可复现最重要。3.2 安装过程与GPU验证依赖安装直接用仓库里现成的requirements.txtpip install -r requirements.txt不过要注意yolov5仓库有时候会附带requirements.txt里面包含一些不常用的包比如notebook这种。对纯训练任务来说装多了也没大碍但如果你在非常精简的环境里部署建议注释掉不需要的部分。反正我本机训练环境就装了torch、torchvision、opencv-python、matplotlib、pandas、requests、pyyaml这几个核心依赖剩下的缺了再补。真正关键的是PyTorch版本要和CUDA版本匹配。以我现在常用的CUDA 11.7为例对应的torch安装命令是pip install torch1.13.1cu117 torchvision0.14.1cu117 --extra-index-url https://download.pytorch.org/whl/cu117装完一定要跑一下这个验证脚本python -c import torch; print(torch.__version__); print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))如果torch.cuda.is_available()返回False九成是torch版本与CUDA版本不匹配或者CUDA压根没装对。这时候别急着重新装两小时的torch先用nvidia-smi看一下驱动支持的CUDA版本再对照PyTorch官方的安装矩阵来选版本。我见过太多人在这步浪费时间最后发现是装成了CPU版的torch。3.3 验证环境先跑一次推理环境装好之后不要直接训练先拿一张包含自行车的图片跑一次官方预训练模型的推理确认整条推理链路是通的python detect.py --weights yolov5s.pt --source data/images/bus.jpg --conf-thres 0.5如果你能正常看到图片里被画出框的结果那么环境基本就没问题了。这时候我建议你先跑一下COCO预训练模型看看它对自行车这一类别的检测效果。COCO数据集里本身有bicycle这个类别所以yolov5s.pt是认识自行车的。这一步的意义在于验证你的环境、依赖、图片读写链路都正常而不是等训练跑了一半才发现读图有问题。4. 把VOC标注转成YOLO格式转换代码、数据划分与路径陷阱YOLOv5训练不认XML标注它只认和图片同名的TXT标注文件每行一个目标格式是class_id x_center y_center width height其中x_center、y_center、width、height都是相对于图片宽高的归一化值取值范围0到1。这一步转换很简单但很多人在归一化时忽略了一个细节导致坐标全错模型却还在跑。4.1 转换代码与归一化的坑以VOC格式的xmin/ymin/xmax/ymax转YOLO格式为例核心公式是x_center ((xmin xmax) / 2) / width y_center ((ymin ymax) / 2) / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height注意这里全是除以图片的宽度和高度而不是除以某个归一化常数。最容易犯的错是分子用像素坐标、分母用了不同数据集的图片宽度导致所有框的位置全部偏移。我习惯写成一段独立的Python脚本统一处理整个目录import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_file, class_names): tree ET.parse(xml_file) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue class_id class_names.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) return lines class_names [bicycle] xml_dir xmls out_dir labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue lines convert_voc_to_yolo(os.path.join(xml_dir, xml_file), class_names) txt_file os.path.join(out_dir, xml_file.replace(.xml, .txt)) with open(txt_file, w) as f: f.write(\n.join(lines))转换完成后我建议随机挑几张图用OpenCV把TXT里的框画回原图和XML的框对比一眼。这一步叫“转换后可视化验证”能最直观地确认坐标正常。不要只对着数字判断画出来看一眼是最快的。4.2 训练集/验证集划分数据划分的默认做法是随机打乱按8:1:1或者9:0.5:0.5分成训练、验证、测试。但要注意一个问题如果bicycles2数据集里包含连拍的图片也就是同一场景相差几秒钟的多张帧随机划分会把高度相似的图片同时分到训练集和验证集里导致验证指标虚高。我在一次实际训练中吃过这个亏验证集的mAP50高达0.93可一到现场摄像头就露馅因为模型在训练时已经“背”过同类场景。所以划分时至少要保证随机种子固定这样每个人复现你结果时才能得到一模一样的划分import random from glob import glob random.seed(42) image_paths glob(images/*.jpg) random.shuffle(image_paths) train_ratio, val_ratio 0.8, 0.1 train_paths image_paths[:int(len(image_paths) * train_ratio)] val_paths image_paths[int(len(image_paths) * train_ratio):int(len(image_paths) * (train_ratio val_ratio))] test_paths image_paths[int(len(image_paths) * (train_ratio val_ratio)):]如果你的项目对同一场景连续帧特别敏感更严谨的做法是按场景分组划分比如按文件名前缀分组保证同一组图片不跨集合。但对bicycles2这个数据集来说文件名没有明显的前缀分组所以固定随机种子就足够了。4.3 data.yaml的路径配置YOLOv5训练时通过data.yaml告诉程序训练集、验证集图片的路径以及类别名。一个常见的错误是路径使用相对路径结果从不同目录启动训练时报Dataset not found。我给出的建议是在data.yaml里直接写绝对路径或者把data.yaml放在yolov5项目根目录下并用相对根目录的路径。train: /your/abs/path/dataset/images/train val: /your/abs/path/dataset/images/val test: /your/abs/path/dataset/images/test nc: 1 names: [bicycle]注意YOLOv5在加载数据时会去train路径下找图片然后根据图片路径自动推断对应的标签路径。默认情况下它会把图片路径里的images替换成labels来定位标注文件。所以你的目录结构要严格保持dataset/ images/ train/ 000001.jpg val/ 000002.jpg labels/ train/ 000001.txt val/ 000002.txt如果路径不匹配YOLOv5会在训练日志里跳过一些图片出现found 100 images, 80 labels这种数字对不上的情况。这时候不要忽略一定要检查目录结构不然训练就变成了在部分数据上盲跑。5. 训练模型超参数策略、训练监控与指标判读数据准备好之后就是训练阶段。这一部分我想说的不只是一条训练命令而是训练前怎么选模型规格、训练中怎么判断模型是否正常收敛、训练后怎么解读那些评估指标。5.1 模型规格和关键超参数的选择YOLOv5有n/s/m/l/x五种规格复杂度依次增大。做自行车检测这种类别的目标检测yolov5s是最平衡的选择在GTX 1660或RTX 3060级别显卡上都能跑得很舒服。如果你是第一次跑建议直接用yolov5s.pt预训练权重作为初始权重不要随机初始化。COCO预训练模型已经学过大量通用特征迁移到自行车检测上能省下大量训练时间收敛也更快。batch-size是一个跟显存直接挂钩的参数。显存不够时宁可降低batch-size或者输入分辨率也不要强行开大batch-size导致OOM。以yolov5s为例RTX 3060 12G显存--batch-size 16 --imgsz 640基本是没问题的。输入分辨率方面我看到bicycles2里有不少小目标如果显存够建议用--imgsz 640不要用--imgsz 416。分辨率越高对小目标越友好代价是训练和推理都变慢。epochs我一般先设100如果数据集只有几百张100个epoch完全够了。你说不定会在第30个epoch就看到mAP不再上升这时候可以提前停掉。想让代码自动帮你判断的话YOLOv5有--patience参数配合早停机制比如设--patience 20表示验证集指标连续20个epoch没有提升就自动停止。完整训练命令示例python train.py \ --data data.yaml \ --weights yolov5s.pt \ --batch-size 16 \ --imgsz 640 \ --epochs 100 \ --name bicycle_exp1 \ --cache--cache参数能把图片一次性加载进内存大幅减少每个epoch的磁盘IO等待。如果图片总量不大几千张以下显存又够开这个参数训练效率提升非常明显。5.2 训练过程中的正常与异常信号训练时YOLOv5会在终端打印一个实时更新的进度表包含box_loss、obj_loss、cls_loss、mAP50、mAP50-95等信息。很多第一次训练的朋友看到loss下降就会很开心但其实更关键的是这几个数值之间是否平衡。正常情况是box_loss和obj_loss整体平稳下降mAP50逐步上升并趋于稳定。如果box_loss一直降但mAP50不涨大概率是数据有问题比如标签错位、图片损坏。我第一次用bicycles2数据集训练时大概到第20个epoch左右mAP50就能到0.8以上后面继续训练缓慢涨到0.88附近。如果训练集很小mAP在初期涨得会更快这不一定说明模型好反而可能是过拟合的前兆。判断过拟合的一个直接方法是看训练集和验证集的mAP差距是否越拉越大比如训练集mAP50到了0.99验证集只有0.75那就是过拟合了。解决办法包括增加数据增强强度、增加训练数据量、使用早停、或者换小一号的模型。5.3 验证结果的正确打开方式训练结束之后YOLOv5会在runs/train/bicycle_exp1/下生成results.png、confusion_matrix.png、F1_curve.png等多张图表。我通常先看results.png里的mAP50和mAP50-95曲线再去看val_batch0_labels.jpg和val_batch0_pred.jpg这两张对比图直观地感受模型预测框和真实标注的差异。这里要提醒一个判断指标的心态mAP50到0.85以上对单类检测任务来说已经可以用mAP50-95可能只有0.6到0.7这个数字低一点不代表模型不能用因为50-95对框的定位精度要求非常苛刻。对于违停检测这种业务定位精度当然重要但更关键的是检测框有没有稳定套住自行车主体而不是追求跟标注框重叠到极致。python detect.py --weights runs/train/bicycle_exp1/weights/best.pt \ --source your_test_image.jpg \ --conf-thres 0.35 \ --iou-thres 0.45--conf-thres就是置信度阈值决定哪个框会被保留。实战中我一般从0.25到0.5之间调试低了会多出误检高了会漏掉一些远距离小目标。这个阈值不一定要在训练阶段定死留到部署阶段针对现场效果再调更合理。6. 从检测框到违停事件判定规则的完整设计模型训练好之后离”识别非机动车违规停放”还差最关键的一步——怎么把测试图片上的检测框转化成一条具体的违规告警。我见过太多人在这里直接卡住不知道代码怎么写也不知道用什么算法判断“这个自行车停在禁停区域”。6.1 违停判定不是目标检测而是空间关系计算模型输出的每个检测框是一个矩形由x1, y1, x2, y2, conf, class_id组成。禁停区域通常是在监控画面里手动标定的一个或多个多边形比如消防通道口、盲道区域、绿化带前的那块地。判定自行车是否违停本质上就是判断“检测框”和“禁停多边形”之间是否有足够大的空间交集。最简单也最常用的规则是取检测框底部中心点作为自行车的触地位置。自行车作为竖直方向的目标它的底部中心点大概对应车轮与地面的接触点把这个点投到画面里的禁停区域多边形内做点是否在凸/凹多边形内的判断如果在区域内则判定违停。判断一个点是否在多边形内有很多现成算法最简单的射线法就可以OpenCV里也有cv2.pointPolygonTest直接能用。如果你觉得底部中心点不够稳可以把规则增强为检测框与禁停多边形的重叠面积占检测框面积的比例超过阈值比如30%时也触发违停。这种规则对横躺在地上的自行车更友好因为横躺的自行车检测框底部中心点不一定落在禁停区但车身整个都压线了。6.2 用多边形标定区域并联动检测结果写代码之前首先要做的一件事是确定禁停区域的像素坐标。我一般对着监控画面截图用图像标注工具直接在图上画多边形记录每个顶点的像素坐标。这样做有两个好处一是直观二是坐标直接和模型输出对齐。如果摄像头固定不动这个多边形坐标就是固定的可以写死在配置里如果摄像头有移动就需要额外的区域标定算法那种情况复杂很多建议项目初期先避开。下面是一个完整的判定伪代码import cv2 # 假设有一个禁停区域多边形顶点按顺序排列 no_parking_polygon [(100, 200), (300, 180), (320, 320), (120, 340)] def is_no_parking(box): x1, y1, x2, y2 box[:4] bottom_center_x (x1 x2) / 2 bottom_center_y y2 point (bottom_center_x, bottom_center_y) result cv2.pointPolygonTest( np.array(no_parking_polygon, dtypenp.int32), point, False ) return result 0 # 对模型输出每个框做判断 for det in predictions: if is_no_parking(det): trigger_alert(det)注意这里我取的是检测框的y2即框底部的y坐标。因为检测框是目标的外接矩形自行车整个目标竖直方向从车轮到车座底部就是车轮着地的位置取底部中心点比取整个框的中心点更有物理意义。如果你对这个物理位置要求更高还可以通过目标跟踪算法跟踪自行车取连续多帧的底部中心点做平均减少单帧抖动。6.3 业务去重与告警触发策略如果你直接把每一帧检测到违停就报警会收到雪崩一样的告警同一辆自行车在摄像头画面里可能停留几十分钟每秒25帧每帧都触发一次。所以必须引入告警去重机制。最朴素的方法是基于位置去重把检测框的中心点坐标和告警时间存下来如果当前帧的违停车中心点和之前某次告警的坐标距离小于一定像素比如50像素并且时间间隔小于某个值比如3分钟就不重复触发。稍微工程化一点可以使用IoU去重将检测框坐标归一化后保存到哈希表键为(图像ID或摄像头ID)。每次新检测先和最近告警框做IoU若IoU大于0.5说明是同一辆车不重复告警。如果超过N秒例如120秒都没有再检测到此框清除该记录。这个逻辑其实就是在检测之上加了一个简易的多目标跟踪器。如果现场同时有多辆违停车那么你可以把告警记录批量归档在一张证据图上把所有的违停框都画出来生成一次总告警而不是一辆车报一次。我在实际项目里都是这么干的运维人员收到一条带现场截图和违停数量的消息处理效率高得多。另外触发判定后一定要回传一张带检测框和禁停区域绘制的JPEG截图作为证据留存。这个截图由判定线程自动抓取不需要额外存储视频流。录像留给NVR截图上只标注业务信息责任判定清晰明了。7. 现场部署的主要坑从摄像头角度到模型推理性能到了部署阶段其实已经不是什么高深技术了更多的是一些现场经验的积累。我把最容易踩的坑按顺序列出来这是我跑过多次现场后总结出来的。7.1 摄像头视角带来的检测差异训练数据里的图片大多是平视或者略俯视的角度而实际监控摄像头通常架设在3到6米高的杆子上俯视角度很大。俯视角度下的自行车两个轮子是椭圆形的车身结构被压缩和目标检测模型训练时看到的侧视图差别很大。这是迁移到现场后mAP下降的最主要原因之一。解决办法有两条路第一条在项目启动时就去现场截取一两个小时的监控画面用这个数据补训模型把现场视角的特征注入模型第二条如果实在拿不到现场数据只能在部署后用小步迭代的方式隔几天把误检漏检的截图收集起来标注后继续训练做模型更新。我倾向于两条腿走路先拿一些现场画面做预研测试评估模型的“水土不服”程度再决定要不要重新采集标注。7.2 小目标和密集遮挡的处理bicycles2数据集里有不少远距离小目标但现场监控中自行车占画面比例通常更小。一辆自行车在1080P画面里可能只有60到80像素高如果模型输入分辨率只有640实际用于检测的特征图里自行车只有十几个像素。要提高小目标检出率从三个方向发力训练时提高输入分辨率到--imgsz 960或1280代价是推理变慢。推理时对画面做分区检测把画面切成四块或九块分别推理再做结果融合。这个方法对小目标提升非常显著但需要处理重复框合并。后处理时降低置信度阈值到0.25再配合区域规则过滤掉误检。密集停放是另一个痛点。一排共享单车停在一起彼此遮挡检测框会互相重叠NMS后可能只保留了几个框导致“明明停了五辆只框出来两辆”的统计偏差。这种情况下NMS的IoU阈值需要适当调高比如从0.45调到0.6保留更多重叠框同时也可以考虑把目标检测改成基于关键点或密度图的方法但工程复杂度会上升。对一般项目来说先接受一些遮挡漏检再靠多视角摄像头弥补往往才是性价比最高的方案。7.3 模型导出与边缘设备推理如果项目要部署到现场边缘盒子而不是跑在GPU服务器上模型导出这一步很关键。YOLOv5官方提供了export.py脚本可以导出ONNX、TensorRT等多种格式。我在这类项目里最常用的是TensorRT在NVIDIA显卡上能获得显著的推理加速在边缘盒子上跑yolov5s的TensorRT FP16模型单帧推理时间可以压到20毫秒以内完全满足实时分析多路视频流的需要。python export.py --weights runs/train/bicycle_exp1/weights/best.pt --include onnx engine --device 0如果是瑞芯微RK3568、RK3588这类平台或者地平线、算能这类国产边缘芯片YOLOv5模型需要先转ONNX再通过各自工具链转成NPU可运行的格式。这类平台的部署更依赖芯片厂商的文档不同厂商的工具链差异很大。建议提前确认好目标平台的推理框架再决定用哪个版本的YOLOv5和导出方式不然容易倒腾一两个星期还在原地打转。7.4 光照与天气的鲁棒性监控是7x24小时跑的晚上光线暗、雨天反光、逆光背光都会让检测效果明显下降。bicycles2数据集里虽然有傍晚样本但显然没有包含足够多的黑夜场景。一个稳妥的做法是部署现场如果允许给摄像头补一些红外或白光照明保证夜间画面有基本的亮度。另一个更实际的做法是在夜间降低检测置信度阈值并配合更宽松的区域规则宁可多报几次误检再人工复核也不希望漏掉真正的违停事件。如果你有积累能力可以考虑在夜间采集数据做专门的夜间模型如果没有我建议在方案交付里明确告诉客户夜间检测率会低于白天这是当前机器视觉识别的客观局限而不是模型的重大缺陷。提前管理预期对项目验收有帮助。8. 几个值得深入的方向以及这个项目教会我的事做完这个项目我一直觉得目标检测本身只是起点。同样的模型、同样的数据集换一套业务规则就能做停车位占用检测、车流统计、共享单车淤积预警这些都是同一套技术底座上的不同应用。如果你手头已经有了一批带检测框的照片不管是bicycles2还是你自己标注的数据都可以顺着这个思路往下扩展。后面如果要做得更深入我会优先尝试三个方向第一把单类检测扩展到多类比如同时检测自行车、电动自行车、三轮车因为现场违停的往往不只是自行车。第二引入一个简单的跟踪模块对不同车辆做ID关联这样不仅能判断违停还能统计违停持续时长业务价值立刻提升一个档次。第三把“禁停区域”做成动态配置的接口支持运维人员远程调整区域而不需要每天改代码重新部署。最后说一个我自己的体会不要迷信模型的指标。mAP再漂亮都不如拿手机拍一段现场视频放到测试环境里跑十分钟更能说明问题。我每次做类似项目都会花大量时间在“测试数据采集”上模拟真实监控视角而不是反复纠缠训练集里的那零点零几的mAP提升。数据不够就补数据规则不对就调规则模型不行才轮到训练层面去折腾。这个顺序颠倒过来项目就会变成无休止的调参循环事倍功半。本文还有配套的精品资源点击获取
返回列表