资讯详情

CCTSDB交通标志数据集:YOLO目标检测从格式转换到训练避坑全指南

📅 2026/9/28 15:19:16 | 华诺云谱 👁 阅读
CCTSDB交通标志数据集:YOLO目标检测从格式转换到训练避坑全指南
简介面向目标检测研究者和算法工程师这套CCTSDB交通标志数据集包含1000张真实道路场景图片全部经LabelImg人工精细标注标注框质量高。数据同时提供VOC(xml)、COCO(json)、YOLO(txt)三种格式标签分目录存放可直接导入YOLO系列模型训练适用于交通标志识别、自动驾驶感知等场景。资源包共2000个文件除图片与标签外还包含环境搭建与训练教程的HTML文档、数据集划分的Python脚本以及YAML配置文件整体约237MB。目前已有602人学习下载。配套教程区分Linux和Windows版本覆盖环境配置、数据划分到模型训练全流程划分脚本可灵活生成训练集、验证集和测试集适合快速落地交通标志检测项目。1. CCTSDB交通标志三格式数据集1000张图跑通YOLO全流程对想做交通标志检测的开发者来说CCTSDB是绕不开的公开数据集这个版本把1000张图片和VOC、COCO、YOLO三种标签格式一次配齐旁边还带了划分脚本和训练教程等于把准备数据→切分数据→训练YOLO这条链路包装成了一份可复现的实操包。你能用它解决什么两件事一是快速验证YOLO在交通标志这个垂直场景上的效果二是作为数据预处理和格式转换的练习样本。1000张图不多正好够跑通全部流程别指望一次练出商用水准。适合刚入门目标检测、想完整走一遍YOLO流程的开发者也适合负责数据标注转格式的算法工程师拿来对照自己的脚本哪里有坑。2. CCTSDB三种标签格式拆解VOC、COCO、YOLO各自的坐标系和转换要点这一章我先把三种格式的东西讲透后面写训练脚本的时候就不会因为坐标搞错而翻车。2.1 CCTSDB原始标注长什么样一份txt带来的三个坑CCTSDB原始标注不是每张图配一个标签文件而是把所有标注汇总到一份txt里每个目标一行格式大致是图片名 一个逗号分隔的 xmin,ymin,xmax,ymax,class_id。第一次拿到这份标注大多数人会犯三个错。第一个错是没注意类别编号从几开始有的版本0、1、2对应警告/禁令/指示有的版本从1开始直接套到YOLO里会让所有类别错位第二个错是没做框过滤原始标注里存在少量框宽高只有几个像素的噪声目标转进COCO和YOLO后模型会去拟合这些根本没意义的框第三个错是拿文件名去匹配时没考虑后缀差异导致一张图对不上标签。我的习惯是拿到标注先写三行代码做统计看最大类别号、框宽高分布、文件名匹配率再决定后续怎么处理。这三个统计能挡掉后面训练时八成以上的玄学问题。# 统计原始CCTSDB标注的基本情况再决定怎么转格式 import os SRC_TXT cctsdb_annotations.txt IMG_DIR images max_cls 0 tiny_box 0 matched 0 total_box 0 with open(SRC_TXT) as f: for line in f: parts line.strip().split() if len(parts) 2: continue img_name parts[0] x1, y1, x2, y2, cls [float(v) for v in parts[1].split(,)] total_box 1 max_cls max(max_cls, int(cls)) if x2 - x1 10 or y2 - y1 10: tiny_box 1 if os.path.exists(os.path.join(IMG_DIR, img_name)): matched 1 print(f总框数: {total_box}) print(f最大类别号: {max_cls} - 如果从0开始类别数应为 {max_cls 1}) print(f宽或高小于10像素的噪声框占比: {tiny_box / total_box:.2%}) print(f图片名能匹配上的标注占比: {matched / total_box:.2%})这段脚本的输出直接决定你后面怎么改最大类别号是2就说明原标注从0开始是3就说明从1开始必须整体减一噪声框占比超过1%就值得在转换时加一道过滤。比例很小的话过滤不过滤无所谓但我建议保留这道因为CCTSDB这类从视频抽帧的数据集里远距离小标志经常标出宽高不到10像素的框对模型只有坏影响。2.2 VOC用左上右下、COCO用左上宽高、YOLO用归一化中心一个框三种写法三种标签格式描述的是同一个矩形框但坐标系完全不一样这是整个转换过程里最容易出错的地方。VOC格式每张图一个XML目标框写成xmin、ymin、xmax、ymax单位是像素类别名放在 标签里这就是典型的左上角右下角写法。COCO格式是把整个数据集合成一个JSON目标框写成[x, y, width, height]单位也是像素但语义变成了左上角坐标宽高而且类别id从1开始0要预留给background。YOLO格式每张图一个txt每一行是class_id、x_center、y_center、width、height前四个数全部除以图片宽高做了归一化类别id从0开始。三个格式放在一起看最隐蔽的坑是两个一是VOC到COCO的变换不是把xmax、ymax直接当成宽度高度而是要用xmax减xmin二是YOLO归一化时必须用读取图片后的实际宽高不能用文件名猜出来的宽高。很多人转完格式训练时发现框偏移半个身位十有八九是这两个地方出了问题。我的原则是别搞一堆脚本去互相转换先在内存里把所有框统一成绝对像素坐标[x1, y1, x2, y2]这一份中间态再从这一份中间态分别生成三种格式这样三个输出天然一致。2.3 原始txt转VOC/COCO/YOLO一次性生成脚本与参数说明下面这个脚本是我整理过的版本输入一份原始txt和图片目录输出VOC XML目录、COCO JSON、YOLO txt目录三份产物。脚本里先读原始标注过滤掉宽或高小于10像素的框再用同一份中间态写三个输出。# cctsdb_to_three.py —— 把CCTSDB原始txt转成VOC/COCO/YOLO三份标签 # 原始标注格式每个目标一行内容为 img_name xmin,ymin,xmax,ymax,class_id import os import cv2 import json import xml.etree.ElementTree as ET SRC_TXT cctsdb_annotations.txt IMG_DIR images OUT_VOC labels/voc OUT_YOLO labels/yolo OUT_COCO labels/coco/annotations.json # 类别映射按实际标注来这里假设原标注类别id是0/1/2 CLS_NAMES {0: warning, 1: prohibitory, 2: mandatory} boxes_by_img {} with open(SRC_TXT) as f: for line in f: parts line.strip().split() if len(parts) 2: continue img_name parts[0] coords [float(v) for v in parts[1].split(,)] x1, y1, x2, y2, cls coords if x2 - x1 10 or y2 - y1 10: continue # 丢掉噪声小框 boxes_by_img.setdefault(img_name, []).append((x1, y1, x2, y2, int(cls))) os.makedirs(OUT_VOC, exist_okTrue) os.makedirs(OUT_YOLO, exist_okTrue) os.makedirs(os.path.dirname(OUT_COCO), exist_okTrue) def img_size(img_name): return cv2.imread(os.path.join(IMG_DIR, img_name)).shape[:2] # 1) VOC: 每张图一个xml框是左下/右上绝对像素坐标 for img_name, boxes in boxes_by_img.items(): h, w img_size(img_name) root ET.Element(annotation) ET.SubElement(root, filename).text img_name size ET.SubElement(root, size) ET.SubElement(size, width).text str(w) ET.SubElement(size, height).text str(h) for x1, y1, x2, y2, cls in boxes: obj ET.SubElement(root, object) ET.SubElement(obj, name).text CLS_NAMES[cls] bndbox ET.SubElement(obj, bndbox) ET.SubElement(bndbox, xmin).text str(int(x1)) ET.SubElement(bndbox, ymin).text str(int(y1)) ET.SubElement(bndbox, xmax).text str(int(x2)) ET.SubElement(bndbox, ymax).text str(int(y2)) ET.ElementTree(root).write( os.path.join(OUT_VOC, img_name.rsplit(., 1)[0] .xml), encodingutf-8) # 2) YOLO: 每张图一个txt内容为归一化中心坐标宽高 for img_name, boxes in boxes_by_img.items(): h, w img_size(img_name) with open(os.path.join(OUT_YOLO, img_name.rsplit(., 1)[0] .txt), w) as f: for x1, y1, x2, y2, cls in boxes: cx (x1 x2) / 2 / w cy (y1 y2) / 2 / h bw (x2 - x1) / w bh (y2 - y1) / h f.write(f{cls} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n) # 3) COCO: 整个数据集合成一个json注意category_id比原始类号大1 coco {images: [], annotations: [], categories: []} for cid, cname in CLS_NAMES.items(): coco[categories].append({id: cid 1, name: cname}) ann_id 1 for img_id, (img_name, boxes) in enumerate(boxes_by_img.items()): h, w img_size(img_name) coco[images].append({id: img_id, file_name: img_name, width: w, height: h}) for x1, y1, x2, y2, cls in boxes: coco[annotations].append({ id: ann_id, image_id: img_id, category_id: cls 1, bbox: [x1, y1, x2 - x1, y2 - y1], area: (x2 - x1) * (y2 - y1), iscrowd: 0, }) ann_id 1 with open(OUT_COCO, w) as f: json.dump(coco, f, indent2)逻辑说明脚本先读原始txt并过滤小框得到统一的绝对坐标中间态。COCO部分注意category_id比原始类别号大了1这是COCO规范里0被保留给background导致的YOLO部分的归一化用的是cv2.imread读出来的实际宽高不是文件名后缀推断的。参数说明CLS_NAMES里的键值要按你这份CCTSDB标注的真实类别编号改如果原始标注从1开始就把键改成1、2、3并在写入时整体减一脚本用cv2.imread读图取尺寸图片路径必须和IMG_DIR对得上遇到cv2读不了的图片格式要提前转好。注意不同来源的CCTSDB原始标注类别编号有从0和从1两种。转换前先跑一遍2.1的统计脚本确认最大类别号再写死CLS_NAMES这一步省掉后面大量返工。3. 划分脚本train/val/test怎么切才不翻车数据切分看起来简单实际是数据集正式进入训练前最容易埋雷的一步。3.1 为什么不能直接random.shuffle视频帧数据集的同帧泄漏问题CCTSDB是从视频里抽帧标注的同一个交通标志会在连续好几帧里反复出现这些帧在视觉上非常接近只是在画面里稍稍平移了几个像素。如果你直接把图片列表shuffle后按比例切分完全可能出现同一个标志同时出现在训练集和验证集里。这种同帧泄漏会让验证集的mAP虚高因为在验证集里出现过的目标在训练时已经被模型见过近似版本了。模型上线后在真实视频上表现会明显差于你的验证数字这是很多人离线指标好、上线就翻车的重要原因之一。正确做法是按视频段分桶同一个视频段抽出来的帧必须进同一个集合。怎么判断哪些图片属于同一视频段最简单的方法是看文件名前缀一个视频段的帧通常共用同一个前缀比如video1_0001.jpg到video1_1200.jpg。如果你拿到的是CCTSDB这类帧序列数据集第一步先统计文件名结构找出这个前缀规律再用它分桶。如果实在找不到规律就退一步用按文件名前几位归档的办法把相同首几位字符归到同一个桶宁可比random.shuffle保守不要冒泄漏风险。3.2 按视频段分桶的划分脚本可复用的split_dataset.py下面的脚本把分桶逻辑封装成一个参数--bucket-by-prefix 打开时就按文件名前缀分桶关闭时就退化成随机划分。默认比例是6:2:2适合1000张这种小规模数据集。# split_dataset.py —— 按视频段分桶切分train/val/test # 用法: # python split_dataset.py --img-dir split/images --yolo-dir split/labels \ # --out-dir data --train-ratio 0.6 --val-ratio 0.2 --bucket-by-prefix import os import random import shutil import argparse from collections import defaultdict def get_bucket(fname): # 前缀相同视为同一视频段文件名没有下划线时退化为取前4字符 return fname.split(_)[0] if _ in fname else fname[:4] def main(): ap argparse.ArgumentParser() ap.add_argument(--img-dir, requiredTrue, help图片目录) ap.add_argument(--yolo-dir, requiredTrue, helpYOLO标签目录) ap.add_argument(--out-dir, defaultdata, help输出根目录) ap.add_argument(--train-ratio, typefloat, default0.6) ap.add_argument(--val-ratio, typefloat, default0.2) ap.add_argument(--bucket-by-prefix, actionstore_true, help按文件名前缀分桶防止同帧泄漏) args ap.parse_args() random.seed(42) imgs [f for f in os.listdir(args.img_dir) if f.lower().endswith((.jpg, .png))] if args.bucket_by_prefix: buckets defaultdict(list) for img in imgs: buckets[get_bucket(img)].append(img) keys list(buckets.keys()) random.shuffle(keys) n_train int(len(keys) * args.train_ratio) n_val int(len(keys) * args.val_ratio) split_keys { train: keys[:n_train], val: keys[n_train:n_train n_val], test: keys[n_train n_val:], } split_imgs { name: [img for k in ks for img in buckets[k]] for name, ks in split_keys.items() } print(分桶明细:, {k: len(v) for k, v in buckets.items()}) else: random.shuffle(imgs) n len(imgs) n_train int(n * args.train_ratio) n_val int(n * args.val_ratio) split_imgs { train: imgs[:n_train], val: imgs[n_train:n_train n_val], test: imgs[n_train n_val:], } for split_name, imgs in split_imgs.items(): out_img os.path.join(args.out_dir, split_name, images) out_lbl os.path.join(args.out_dir, split_name, labels) os.makedirs(out_img, exist_okTrue) os.makedirs(out_lbl, exist_okTrue) for img in imgs: shutil.copy(os.path.join(args.img_dir, img), os.path.join(out_img, img)) txt img.rsplit(., 1)[0] .txt src_txt os.path.join(args.yolo_dir, txt) if os.path.exists(src_txt): # 防止个别图没有标注导致训练崩 shutil.copy(src_txt, os.path.join(out_lbl, txt)) print(f{split_name}: {len(imgs)} 张图片) if __name__ __main__: main()逻辑说明分桶模式下先打印每个桶的明细检查是否有某个桶大得离谱一个正常的视频段桶应该在几十到一两百帧左右。切分时对桶列表做shuffle而不是对图片做shuffle这样才能保证同桶帧不跨集合。注意脚本只复制了YOLO格式的标签后面如果还要用COCO做验证需要把COCO的json按同样方式拆分或直接把labels目录用软链接链到data下。参数说明--train-ratio和--val-ratio加起来别超过0.9我强制留至少10%做test因为CCTSDB这套标签没有官方test划分你得自己留一份将来给模型做最终验收。--out-dir下会生成train、val、test三个目录每个目录里再分images和labels两层。这里的训练集/验证集/测试集是按桶粒度算的所以实际图片数比例会和6:2:2有出入属于正常现象。3.3 分类别覆盖检查切完验证每个集合里三类标志都在脚本跑完后不要急着训练先做一次类别覆盖检查。CCTSDB三类里mandatory本身样本就少如果随机划分恰好把mandatory都分到了trainval里一类标志都没有那验证集mAP就会直接缺一类。我用下面这段代码统计每个集合里包含每个类别的图片数量发现某个类别在val或test里一张都没有就回到上一步重新设置随机种子或手动调整分桶。# check_coverage.py —— 检查每个集合的类别覆盖情况 import os from collections import Counter for split in [train, val, test]: lbl_dir fdata/{split}/labels cls_counter Counter() for txt in os.listdir(lbl_dir): with open(os.path.join(lbl_dir, txt)) as f: cls_ids set(line.split()[0] for line in f if line.strip()) for c in cls_ids: cls_counter[c] 1 print(split, dict(cls_counter))这段脚本输出三行train那行正常应该三类都有几百个val和test每类至少要有几十个。如果val里缺了某个类别最简单的方法是把随机种子改一下重新划分或者手动把包含该类别的桶挪几个到val里。总的原则一句话宁可train少几帧也别让val缺类别你后面所有调参决策都依赖val的反馈val不全等于白练。4. YOLOv5/YOLOv8训练CCTSDB配置文件与最小跑通命令格式转换和切分都做完了这一章讲实际的训练。4.1 选v5还是选v81000张这个小规模数据集的决定因素YOLOv5和YOLOv8在CCTSDB这种1000张的小规模数据集上都能收敛选哪个主要看你的后续用途。如果你之后要把模型部署到TensorRT或者用C的推理框架我更推荐v5anchor-based的模型在边界框回归上对小目标相对友好社区里针对v5的部署方案最多踩坑资料也全。如果你只是为了快速出一个验证结果、后面还要反复调结构v8的命令行入口更简洁而且anchor-free设计免去了聚类anchor这一步写实验脚本的时候会省不少心。我的做法是先用v5s跑一版基线因为v5s更小、显存占用低1000张图在单张V100上几分钟一个epoch100轮下来大概一两个小时就能出一版可分析的结果。这里要提醒一点预训练权重直接用官方yolov5s.pt第一次运行train.py会自动下载到权重缓存目录不需要自己单独找下载地址。如果下载太慢可以手动把yolov5s.pt放到项目根目录train.py检测到本地文件后会优先使用。不要用随机初始化从头训练CCTSDB只有1000张图随机初始化很难在这么少的数据上学到通用的纹理特征而coco预训练权重里模型已经见过大量小目标迁移到交通标志这种小目标场景收敛会快得多。4.2 data.yaml和train命令写到目录级别的配置细节先把数据配置写好。这个文件决定训练时去哪找图、找标签、有哪些类别路径写绝对路径最省事可以避开相对路径在某个目录下跑不通的坑。# data.yaml —— 放在你项目的根目录 # path 指向上面划分脚本生成的 data 目录 path: /home/you/cctsdb_split train: train/images val: val/images test: test/images # 类别名顺序严格对应YOLO标签里的class_id names: 0: warning 1: prohibitory 2: mandatoryYOLOv5的训练命令是train.py加参数YOLOv8是yolo train加参数。两个命令都要到各自项目目录下执行第一次跑之前先装依赖。v5的依赖在requirements.txt里v8由ultralytics包统一管理。# YOLOv5 最小跑通命令 # 先准备一份YOLOv5项目代码官方仓库clone或本地已有都行 cd yolov5 pip install -r requirements.txt python train.py \ --data /home/you/cctsdb_split/data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache \ --workers 4 \ --project runs/cctsdb \ --name exp1 # YOLOv8 等价命令 pip install ultralytics yolo train \ data/home/you/cctsdb_split/data.yaml \ modelyolov8s.pt \ imgsz640 \ batch16 \ epochs100 \ projectruns/cctsdb \ nameexp1逻辑说明这两条命令都是标准训练入口差别只在v8把参数从--风格改成了keyvalue风格本质参数一致。--cache参数把1000张图全部缓存进内存可以显著减少数据读取的I/O时间这个规模下很划算如果机器内存小于16G就把--cache去掉或者改用--cache disk缓存到磁盘这样速度略慢但不会撑爆内存。4.3 img/batch/cache/autoanchor这几个参数直接决定训练能不能收敛第一个关键参数是--img。CCTSDB原始图片分辨率一般在1280x960左右目标标志占画面比例不大属于典型的小目标场景。直接用640输入小标志会被压缩到十几个像素模型很难学。我的建议是先试640如果val的AP卡在0.4以下上不去再试一次960显存吃得下的话960往往比640在交通标志上高3到5个点mAP。代价是训练时间翻倍1000张图的规模下完全可接受。第二个参数是--batch。它和显存直接相关别盲目往大了调。V100或A100上可以用648G显存用164G显存降到8或4。batch太小会导致batchnorm统计不稳定train的loss曲线会抖得很难看batch太大在这么少的数据上容易过拟合。一个参考原则让每个batch里的目标数稳定在50个以上1000张图、平均每张图两个目标batch 16已经满足这个条件。第三个参数是anchor的调整。v5的预训练anchor是基于coco 80类统计的对交通标志这种小目标不一定适配。训练前跑一次v5自带的autoanchor工具它会基于你的标注重新聚类得到一批新anchor具体命令是python utils/autoanchor.py --data data.yaml --img 640输出的新anchor会建议更新到模型yaml里。这件事在CCTSDB上收益很明显因为交通标志的宽高比集中在1:1附近和coco里各类物体的分布差异不小。v8是anchor-free不需要做这一步。5. CCTSDB训练避坑指南五个高频翻车现场与修复方法这一章来自我自己和身边同事在这些三格式数据集上反复踩过的坑每条都按现象、原因、解决三步写。5.1 现象loss正常但val_mAP只有0.2 —— 标签坐标系的隐藏不一致训练loss一路下降精准率和召回率曲线也正常但val的mAP0.5只有0.2左右。原因大概率是YOLO标签归一化时除的图片宽高和训练时实际读取到的宽高不一致。CCTSDB从视频抽帧部分jpg带EXIF方向信息你如果用PIL读图取shapePIL会按EXIF自动旋转而cv2.imread不处理EXIF两张图shape可能恰好宽高互换。解决转换脚本里统一用cv2.imread读图再取shape训练前抽20张图做一次标签可视化把YOLO标签画到原图上人工看一眼框是否对齐。这一步花五分钟能挡住后面所有排查时间。5.2 现象第二个epoch开始lossnan —— 空标签图与学习率过大的叠加loss在第一个epoch正常下降到第二个epoch直接变成nan。最常见的原因是两个叠加一是某个batch里混进了没有标签的图片YOLO的loss对空标签会除零二是默认学习率0.01对这个1000张的小数据集偏大超过模型能承受的更新幅度。解决转换格式时把所有没有目标的图片清单删掉同时把lr从0.01降到0.001训练命令里加--lr 0.001。这两个动作做完nan基本不会再出现。5.3 现象mandatory类的AP比另外两类低一截 —— 小目标与样本数双重挤压训练完看per-class APwarning和prohibitory有0.7mandatory只有0.4。原因很简单mandatory的样本数少而且这类标志的平均框尺寸比其他两类小模型在小目标上学到的特征不够。解决先跑autoanchor把anchor改成适配小目标的尺寸再看增强参数里的mosaic有没有打开。在1000张的规模下mosaic能等效扩充训练样本建议保持默认的1.0。如果还不行在val.py结果里看mandatory的recall是不是明显低于其他类是的话单独给mandatory类加权v5里可以改hyp.yaml的cls系数或者干脆复制mandatory的图片做两次重采样让它的样本数和其他类对齐。5.4 现象val集指标好看实拍视频里框乱跳 —— 域差异与增强参数离线验证mAP有0.7但你拿着训练好的模型去测一段实拍视频发现框的置信度忽高忽低位置抖动严重。这是典型的数据域差异CCTSDB是固定视角的相机拍的背景和你实拍的场景相差很大。解决在训练时把增强打开而不是用默认的弱增强。v5的hyp.scratch-low.yaml里默认hsv_h、hsv_s、degrees这些参数很低对小数据集容易过拟合到CCTSDB的特定光照和角度上。我会把degrees从0调到10translate调到0.1hsv的饱和度扰动可以加大一点。增强会让离线指标先掉几个点但上线后的稳定性会好很多。5.5 现象预测框比实际标志大一圈 —— 原始标注本身框就画得松模型精度没问题就是输出的框永远比标志实际边界大一圈。这个原因不在训练而在CCTSDB原始标注上人工标注时框画得比较松没有紧贴目标的边缘。如果你把VOC框画出来看会发现上边缘和左边框普遍离标志有3到5个像素的余量。解决可以在转换时整体收缩框比如把xmin加3像素、xmax减3像素y方向同样处理让模型学习更紧的框。或者不去动标注在推理时把输出的框缩小5%再交给下游业务。我一般选前者因为训练时拟合紧框部署时不用额外加后处理逻辑。6. 用val.py验证每类AP并给CCTSDB做小目标增强6.1 验证命令与结果解读per-class AP和mAP0.5:0.95训练结束后跑一遍验证不要只看train的loss曲线那说明不了泛化能力。python val.py \ --data /home/you/cctsdb_split/data.yaml \ --weights runs/cctsdb/exp1/weights/best.pt \ --img 640 \ --conf-thres 0.001 \ --iou-thres 0.6 \ --save-json命令里把conf-thres降到0.001是为了算mAP时覆盖更多的低置信度预测这样mAP才接近论文里的标准口径。输出文件里会给出每类的mAP0.5和mAP0.5:0.95重点关注margin比较小的那两个类别。如果只有0.4就说明该类的召回不足加样本或调anchor如果AP高但precision低则说明误检多可以考虑把置信度阈值提高到0.3再部署。v8的话用yolo val命令参数名变成data...、model...输出结构和v5基本一致。6.2 想要更高精度两个后续动作切片推理与难例挖掘CCTSDB这套流程跑到这里已经算完整闭环但如果1000张的精度不满足业务需求我一般做两个后续动作。第一个是切片推理把960或1280的输入图切成几块重叠的小图分别检测再合成结果对交通标志这种小目标能再涨3到5个点mAP代价是推理时间成倍增加适合离线场景。第二个是难例挖掘把验证集上误检最多的背景图收集起来比如那些在颜色上和交通标志接近的道路牌、广告牌单独作为一类负样本加入训练。这两个动作做完CCTSDB模型的召回和精度都会上一档。我自己的习惯是每次训练前先抽20张图确认三种格式的标注可视化都对齐了再开跑这个习惯已经帮我躲过了至少十次标签错位的坑。这个方案本身不复杂但每一步的坑都藏在格式切换和划分细节里把基础打扎实后面调起来才顺。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑