公司动态
广告牌识别数据集构建与YOLOv8训练全流程实战
简介目标检测模型的落地效果很大程度上取决于训练数据的质量与标注规范。在垂直场景中数据工程往往比模型调参更关键。本文从数据集构建的基础概念出发解析了从图像采集、清洗、预处理到COCO JSON标注格式的完整链路并深入探讨了错误标注如何导致训练集loss异常下降等常见问题。同时结合COCO预训练权重迁移学习的原理展示了基于YOLOv8训练广告牌识别模型的高效配置与实践经验。通过合理的场景覆盖、精确的标注规范和科学的训练集划分单类目标检测在商场、建筑、道路等真实环境中可达到mAP50超0.9的稳定效果为户外广告监测、智能巡检等应用提供了可复用的技术方案。无论是数据工程入门还是目标检测实战本文的链路与避坑指南都极具参考价值。 先交代一下背景。我平时主要做目标检测方向的项目落地去年接了一个户外广告监测的需求甲方要求对商场外墙、建筑立面、道路两侧的广告牌做到自动识别和定位。吭哧吭哧跑了一个多月数据、标注、训练、调优全走了一遍最后沉淀出了一个8157张图片的广告牌识别数据集其中7137张作为训练集分辨率统一处理到640x640标注格式用的是COCO JSON。这篇就把整个数据集从采集、清洗、标注到训练落地的完整链路拆开讲一遍包括踩过的坑和能直接抄的配置给正在做类似数据工程或者想用目标检测做广告牌识别的朋友参考。1. 项目整体设计与数据集构成1.1 为什么要单独做一个广告牌识别数据集广告牌识别这个任务表面上看就是一个单类目标检测但真正做起来会发现市面上几乎没有现成的、能直接用的数据集。公开数据集里要么是通用物体COCO、VOC里面广告牌只是零星出现样本量少得可怜要么是街景语义分割数据集比如Cityscapes标注粒度是像素级做检测还得自己转换和处理。更麻烦的是广告牌本身的外观差异极大——商场外墙的巨型LED屏、写字楼的灯箱字牌、路边的小型指示牌、公交站台的广告灯箱形态、尺寸、光照条件完全不是一个量级通用模型在这种数据上几乎必翻车。所以这个项目做数据集的时候核心思路不是凑够8000张图就完事而是按照场景维度去构建样本分布。商场的、建筑的、道路边的三大类场景按差不多 4:3:3 的比例采集确保模型见到的广告牌是多样化的而不是只认识某一类特定场景下的广告牌。分类能识别这些广告牌本质上靠的是数据分布足够贴近真实世界。1.2 数据集规模与文件构成这个数据集最终是8157张图片划分情况如下项目数量总图片数8157张训练集7137张剩余用于验证/测试1020张图像分辨率640x640像素标注格式COCO JSON目标类别广告牌单类训练集占比大约87.5%这个比例在单类检测任务里是合理的。如果是多类别或者类别间样本不均衡训练集比例可能需要重新调整但对于单类目标训练集多一点模型见过的场景就多一点泛化会更好。COCO JSON格式的组织方式一句话总结就是一个json文件里包含了所有图片的路径信息、尺寸信息、标注框信息、类别映射信息。Ultralytics YOLO系列、Detectron2、MMDetection这些主流框架都支持COCO格式后续做训练、评估、部署的生态非常顺。具体字段结构下面专门讲。2. 数据采集与图像预处理的完整链路2.1 采集场景规划为什么必须覆盖商场、建筑、道路三类广告牌识别的难点不在识别本身而在广告牌的定义边界和场景多样性。同样是广告牌商场外立面的广告牌往往是巨幅灯箱或者LED屏面积大、亮度高但存在反光和视角畸变写字楼和公寓楼外墙的广告牌通常是条幅或者亚克力字牌形态细长尺度变化大道路两侧的广告牌更复杂有高炮广告离地面很高的大牌子、路灯杆挂旗、公交站台灯箱尺寸从几十厘米到几十米都有。采集的时候我按三个维度做了规划场景维度商场、商圈、写字楼群、住宅底商、城市主干道、高速出入口、高架桥周边、公交站台天气/时段维度白天顺光、白天逆光、阴天、黄昏、夜间灯光点亮状态拍摄角度维度平视、仰视、俯视、正对、侧对为什么要刻意做这个矩阵因为我踩过只收集好看的广告牌照片的坑。第一次做小规模测试的时候训练集里全是商场顺光正拍的照片模型看起来效果不错但一换到夜间或者逆光场景AP直接掉了二十多个点就是典型的场景过拟合。实际采集来源包括自己拍摄、街景截图、合作方提供的历史巡查照片以及一些公开来源的图像。合计筛选了接近2万张原始图片最终保留8157张剔除比例接近60%。2.2 图像清洗与预处理原图五花八门分辨率从几百到几千都有拍摄设备从手机到监控摄像头都有。所以清洗和预处理是决定数据集质量的关键一步我按顺序做了这几件事第一轮去重。用感知哈希算法pHash计算图片相似度配合人工抽检把同一广告牌的连续帧、相似角度的重复图剔除避免标注样本重复性太高。第二轮清晰度筛选。直接用Laplacian算子计算图像方差方差低于阈值的模糊图直接淘汰。广告牌本身在画面里可能是远距离、小目标如果原图就模糊标注出来也是噪声样本。第三轮遮挡度评估。广告牌被树木、电线杆、车辆遮挡超过50%的图片原则性剔除。当然轻微遮挡是保留的否则模型学不会抗遮挡能力。第四轮分辨率统一。统一缩放到640x640。注意这里不是简单地resize而是采用letterbox保持长宽比填充或者中心裁剪方式。广告牌是细长形状的居多如果直接拉伸到640x640广告牌的比例会变形模型学到的形状特征就歪了。实际用的是letterbox在缩放后的图片四周补灰边确保广告牌比例不变。640x640这个分辨率的选择也做一个说明这是YOLO系列模型最常用的输入尺寸兼容性最好。和训练显存的平衡也相关如果改成1280x1280精度会提升但显存消耗会翻好几倍训练成本划不来。对于广告牌这种中等尺寸目标不是那种极端小的目标640x640是性价比最高的选择。3. COCO JSON标注格式解析与核心字段说明3.1 为什么选择COCO JSON而不是YOLO TXT现在做目标检测标注格式主要就是两种COCO JSON和YOLO TXT。我这次选择COCO JSON是因为它是目前通用性最好的标注交换格式。YOLO TXT比较简单每个txt文件对应一张图一行一个目标格式是class x_center y_center width height归一化坐标。这种格式和YOLO系列绑定太紧如果后面想换到MMDetection或者Detectron2又得转换一次。COCO JSON的好处在于一个json文件承载所有标注信息管理方便包含images、annotations、categories三大部分结构化程度高支持areas、iscrowd等额外属性扩展性好大模型和框架直接支持比如Ultralytics YOLOv8、YOLOv11可以直接训练COCO格式如果后续想从COCO转YOLO格式用脚本转换非常容易后面训练章节会给出转换代码。但从YOLO转COCO就没那么直观所以我建议数据源头统一用COCO。3.2 COCO JSON的完整结构拆解一个标准的COCO JSON文件最顶层是一个字典包含以下关键字段{ images: [ { id: 1, file_name: ad_001.jpg, width: 640, height: 640 } ], annotations: [ { id: 1, image_id: 1, category_id: 1, bbox: [x, y, width, height], area: 1000, iscrowd: 0 } ], categories: [ { id: 1, name: billboard } ] }逐个解释一下images数组记录每一张图片的id、文件名、宽高。注意这里的宽高必须是原始图片或者预处理后的真实宽高标注的bbox坐标就是基于这个尺寸的像素坐标不是归一化坐标。annotations数组记录每个标注框的信息。image_id表示这个框属于哪张图bbox是[左上角x, 左上角y, 框宽, 框高]单位是像素area是框面积iscrowd表示这个目标是否是一组密集目标广告牌这类单目标一般填0。categories数组记录类别映射。id要从1开始name是类别名。如果是多类别就添加多条记录。一个容易踩的坑是bbox坐标越界。如果图像缩放后标注框的坐标超出了图片边界比如负坐标或者宽高超出图片范围训练时一些框架会报错或者产生nan loss。所以标注生成之后最好做一次合法性检查把越界的框裁剪或者剔除。3.3 标注规范的制定与质检流程标注规范对于数据质量的影响非常大这个项目里我定了几条硬性规则广告牌的定义画面中出现的主要广告信息载体包括灯箱、LED屏、喷绘布、字牌、条幅等凡是以商业广告信息为主要内容的载体都标。不标的对象纯建筑标识楼名、公司铭牌、交通指示牌、公益宣传栏除非带商业广告内容、画面中的广告内容占整体比例很小的远景广告牌。最小标注尺寸标注框的宽或者高小于20像素的目标不标注。这样的目标在640x640输入下对训练的贡献几乎可以忽略反而会变成噪声。标注完成之后做了双重质检第一轮是程序化检查检查每个bbox坐标是否越界、宽高是否为0、是否重复标注同一区域、图片文件名是否都能对应上。第二轮是人工抽检随机抽取20%的图片可视化标注结果人工检查框是否贴合目标、是否有漏标误标。这里补充一个经验标注框比目标本身大10%-15%其实问题不大模型能学得过来但框明显偏到目标以外比如把旁边的招牌也框进去了这种错误标注会让训练loss降不下来。热搜词里正好有错误标注会导致数据标注模型训练集loss降不下来吗答案是如果错误标注集中出现在某些难例上确实会导致loss异常。所以质检环节不可省。4. 训练集划分策略与YOLO系列模型适配4.1 训练集与验证集的划分思路数据集8157张训练集7137张剩下1020张作为验证/测试集。这里需要强调的是划分绝对不是随机打乱。广告牌识别和通用物体检测最大的区别在于同一块广告牌可能出现在多张连续拍摄的照片中如果随机划分那同一块广告牌的图片可能既进了训练集也进了验证集验证结果会虚高。正确做法是在图片采集层面就按广告牌位置/场景去重之后再做划分比如某商场外墙的五张照片要么全进训练集要么全进验证集保证验证集里的目标在训练集里没有出现过。简单做法是采集时按地点-时间给图片分组然后以组为单位划分。这样验证集评估出来的模型能力才是真正的泛化能力而不是记忆能力。划分比例上7137/8157大约87.5%的训练集这个比例略高但考虑到广告牌场景相对单一问题不大。如果你后续要加类别或者场景扩展建议将训练集比例降到80%留更多数据做验证。4.2 适配YOLOv8/YOLOv5/YOLOv11的数据格式转换COCO JSON格式好归好但YOLO系列训练时需要的是YOLO TXT格式。所以这里需要做一个转换。我直接用一段Python脚本实现COCO转YOLO的格式转换import json import os def coco_to_yolo(coco_json_path, output_dir): with open(coco_json_path, r) as f: coco_data json.load(f) # 建立image_id到文件名的映射 img_id_to_info {} for img in coco_data[images]: img_id_to_info[img[id]] img # 建立category_id到索引的映射YOLO类别id从0开始 cat_id_to_idx {} for idx, cat in enumerate(coco_data[categories]): cat_id_to_idx[cat[id]] idx os.makedirs(output_dir, exist_okTrue) # 按图片分组标注 img_annotations {} for ann in coco_data[annotations]: img_id ann[image_id] img_annotations.setdefault(img_id, []).append(ann) for img_id, anns in img_annotations.items(): img_info img_id_to_info[img_id] width img_info[width] height img_info[height] txt_name os.path.splitext(img_info[file_name])[0] .txt txt_path os.path.join(output_dir, txt_name) with open(txt_path, w) as f: for ann in anns: cat_idx cat_id_to_idx[ann[category_id]] x, y, w, h ann[bbox] # 转为归一化中心点坐标 x_center (x w / 2) / width y_center (y h / 2) / height w_norm w / width h_norm h / height # 防止越界导致的训练异常 x_center min(max(x_center, 0), 1) y_center min(max(y_center, 0), 1) f.write(f{cat_idx} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n)转换过程中有一个非常关键的坑YOLO类别索引是从0开始的而COCO是从1开始的而且如果存在超类别情况比如categories里id是1、3、5有跳号直接把COCO的category_id当成YOLO类别索引就会错乱。上面代码里用enumerate重新建立了连续索引可以避免这个问题。4.3 640x640输入分辨率的训练配置训练的时候在ultralytics库的环境下配置一个data.yaml文件就够# ad_billboard.yaml path: /path/to/ad_billboard_dataset train: images/train val: images/val nc: 1 names: 0: billboard然后模型训练直接用YOLOv8的命令行或者Python APIyolo detect train data/path/to/ad_billboard.yaml modelyolov8s.pt epochs100 imgsz640 batch16 device0训练过程中的几个参数经验imgsz640和数据集分辨率保持一致不用额外resize训练效率最高。预训练权重用yolov8s.ptCOCO预训练而不是从头训练。广告牌虽然不在COCO的80类里但预训练权重已经学到了丰富的底层视觉特征边缘、纹理、形状迁移学习能让收敛速度提升好几倍最终精度也更高。batch尺寸取决于显存16G显存跑yolov8sbatch16比较合适。如果增大到batch32会明显OOM得往下降imgsz。epochs单类任务收敛速度快一般60-100个epoch就足够不需要像COCO那样跑300轮。我实测到80个epoch左右AP就基本平稳了。5. 完整训练流程与模型评估5.1 环境准备与依赖安装训练环境我用的是Ubuntu 20.04 NVIDIA GPU方案显卡是RTX 309024G显存CUDA版本11.8。依赖安装直接用pip搞定pip install ultralyticsultralytics这个库已经集成了YOLOv5/8/11的训练、验证、导出接口强烈建议用这个自己从头写训练代码完全是浪费时间。安装完之后用yolo命令行直接可以跑。如果没有GPU用CPU训练也不是不行但收敛速度会让人崩溃。一个小数据集7137张训练图用CPU跑一个epoch可能要十几分钟用GPU只要不到一分钟。如果只有CPU环境建议先减少epoch缩到20只是验证流程通不通不要指望出精度结果。5.2 训练过程监控训练跑起来之后ultralytics会输出每个epoch的loss、精度Precision、召回率Recall、mAP50、mAP50-95等指标。下面是我实际训练过程的几个关键观测点box_loss、cls_loss、dfl_loss这三个loss在前10个epoch下降很快之后进入缓慢下降期这是正常现象。如果某个loss出现震荡上升大概率是学习率太大。mAP50这个指标对于广告牌识别来说比mAP50-95更有参考价值。mAP50计算的是IoU阈值0.5时的平均精度允许一定的框位置误差。广告牌检测在业务上框大概对就够用了不需要像素级精确所以mAP50做到0.9以上mAP50-95做到0.7以上这个模型就可以上线。验证集结果每轮epoch结束后会在验证集上跑一遍重点关注验证集mAP有没有掉。如果训练集loss还在降验证集mAP开始下降那就是过拟合了需要提前停止或者加重数据增强。训练结束后的best.pt和last.pt都在runs/detect/train/weights/目录下best.pt是验证集指标最好的权重做推理部署要用这个文件。5.3 模型评估与业务指标换算模型训练完之后用一段脚本在验证集上做批量推理统计业务指标漏检率验证集中实际有广告牌的图片模型没有检出的比例误检率模型检测出广告牌的区域但实际不是广告牌的比例定位准确率检测框和人工标注框的IoU大于0.5的比例实际跑下来业务指标和学术指标之间的差距比较明显。mAP50是0.91但落到业务上夜间广告牌漏检偏多逆光下的广告牌误检率偏高。这说明一个很现实的问题数据集场景分布决定模型能力的上限。如果业务使用场景里有大量夜间图片训练集里就必须加入足够比例的夜间样本。5.4 数据增强与难例挖掘对于广告牌识别这个小目标、多样化外观的任务数据增强策略和通用检测任务有区别。ultralytics默认开了hsv增强、平移、缩放等但在我的实验里以下几个增强参数影响最大yolo detect train ... hsv_h0.015 hsv_s0.7 hsv_v0.4 translate0.1 scale0.5 fliplr0.5scale0.5模拟广告牌在不同距离下的尺度变化对尺度泛化帮助很大。hsv_v0.4亮度变化增强让模型更好适应白天、黄昏、夜间的亮度差。fliplr0.5水平翻转增加样本多样性。另外一个值得做的是难例挖掘。第一轮模型训练完之后在验证集上找漏检样本把这些样本加入训练集重新训练。这种做法在没有更多原始数据的情况下是提升模型上限最有效的方法。第二次训练之后我记得夜间广告牌的漏检率从15%降到了6%左右。6. 常见问题与避坑指南6.1 COCO JSON标注文件解析报错常见报错场景是json.load直接抛JSONDecodeError。排查思路如下用VS Code或者在线JSON格式化工具打开文件看是不是标准的JSON语法。检查文件编码推荐统一用UTF-8无BOM格式。Windows下生成的txt/json容易带BOM头Python的json.load遇到BOM会报错需要在打开文件时加encodingutf-8-sig。另外一个是json文件几百MB打不开或者加载慢的问题。图像数量大的时候annotations数组里几万条记录很正常json的解析会慢一些。可以用orjson替代标准库的json实测解析速度提升5-10倍。6.2 训练loss不下降的可能原因检查标注是否错乱尤其是bbox坐标是否和类别对应、是否出现全零坐标。用可视化脚本把标注框画到图片上随机抽200张看看问题一眼就能看出来。学习率过大默认lrf参数有时候在自定义数据集上收敛不好尝试把学习率调低比如lr00.005。类别不平衡如果数据集里广告牌数量悬殊比如商场大广告牌占80%路边小广告牌占20%模型会偏向学大广告牌特征小广告牌容易漏检。可以考虑对少样本场景做过采样。6.3 广告牌检测中的小目标和遮挡问题广告牌识别最典型的难例是两种一是远处的小广告牌目标在图上只有几十个像素二是被树木、电线杆遮挡的广告牌。处理建议小目标把输入分辨率从640提到960或者1280通常会带来几个点的AP提升代价是训练时间增加。如果业务上主要检测近处广告牌可以不调。遮挡训练集中保留一定比例的遮挡样本模型会学到抗遮挡能力。但如果遮挡太严重超过50%面积建议还是让模型放弃检测不要硬标。6.4 从COCO训练权重迁移的注意事项热搜词里提到coco预训练权重我想提醒一下COCO的预训练权重是80类通用检测器它的特征提取网络部分可以直接用但检测头部分只对80类有效。广告牌这个新类别输出层的分类数量是1和COCO的80不一致所以ultralytics在加载预训练权重时会自动跳过热启动不匹配的层。这个机制是正常的不用担心。但从YOLOv8n一直选到YOLOv8x不同体量模型的推理速度和精度差异很大广告牌识别是相对简单的单类任务YOLOv8s是性价比最高的档位YOLOv8m以上提升的AP有限但推理速度下降明显部署到边缘设备可能扛不住。7. 数据集的局限与后续扩展方向7.1 当前数据集的明显短板这个数据集最主要的问题在于单一类别。只能识别广告牌这个整体概念不能区分灯箱、LED屏、喷绘布、字牌等子类别。如果业务需要区分广告牌类型需要在标注上更细粒度。地域局限。采集的场景主要是城市环境城乡结合部的广告牌形态差异大模型在这些场景下的表现需要额外验证。季节覆盖不足。数据集里夏季和秋季图片偏多冬季有积雪覆盖和春季树木茂密导致遮挡加重的样本不足。天气覆盖不足。雨雾天气的图片是最大短板目前模型在雨天的鲁棒性没有测试过。这些都是后续扩充数据集时需要优先补充的方向。7.2 从单模型到业务系统的扩展路径广告牌识别只是一个基础能力落到真实业务中通常还需要以下扩展广告牌内容OCR识别检测到广告牌之后接OCR模型提取广告文案内容用于合规审查。广告牌结构化入库把识别结果位置、时间、图片快照写入数据库建立广告牌档案做变化检测。多帧视频流处理从视频流中抓取广告牌的最优画面避免重复检测降低计算成本。定期自动巡检无人机或巡查车辆摄像头上部署模型自动发现新出现的广告牌或破损广告牌。对于这几个扩展方向当前数据集本质上还是单帧静态图片检测如果要做视频流处理还需要补充连续帧的时序数据。最后说点实在的做完这个数据集和训练全流程我个人最大的体会是广告牌识别这种垂直场景任务数据工程的工作量占了七成模型训练只占三成。你可能花两周打磨标注规范比花两周调模型参数带来的收益大得多。尤其是标注质量宁可少标一百张图也不要标错五十张。如果你也准备做类似的数据集我建议第一步先用LabelImg或者X-AnyLabeling把标注规范固定下来标个100张图、跑通整个流程再大规模铺开。另外640x640分辨率、COCO JSON格式、YOLOv8s预训练权重、80个epoch这四个起步配置能帮你少走很多弯路。这批8157张广告牌识别数据集训出来的模型在我本地的验证集上mAP50能到0.9以上覆盖商场、建筑、道路边的大部分常见广告牌形态拿来直接接业务验证完全够用了。本文还有配套的精品资源点击获取