☰
自制Tusimple格式车道线数据集全流程实战:从采集标注到模型训练闭环
2026/10/22 8:59:52 网站建设 项目流程

1. 内容整体设计与思路拆解

1.1 这张格式到底长什么样

先别急着动手拍视频、找工具,我第一次做tusimple数据集的时候踩了个大坑:我以为标注完了就是“数据集”了,结果模型训练一跑就崩,报错全是格式不匹配。所以第一步,我们必须把tusimple的存储格式彻底吃透。

tusimple格式源自知名自动驾驶数据集Tusimple的标注规范,它是一种“基于采样点”的车道线表示方法。每个样本对应一张图片和一个同名JSON标注文件。JSON里核心有三个字段:

{ "lanes": [ [-2, -2, ... , 721, 631, ...], [-2, -2, ... ] ], "h_samples": [240, 250, ..., 710], "raw_file": "clips/0530/1492626226036821934/20.jpg" }

看着有点抽象,我拆开讲。

h_samples是一个一维数组,表示一系列固定的纵坐标(y坐标),比如从图片中间开始每隔10个像素取一条水平线,一直到图片底部。原始Tusimple数据集用720p图片,h_samples通常从240开始(去掉天空和远景),步长10,到710结束。

lanes是一个二维数组,每一个子数组代表一条车道线。子数组的长度必须和h_samples完全一致,第i个值就是这条车道线在h_samples[i]这个高度上的横坐标(x坐标)。如果车道线在这个高度不存在(比如虚线断掉了、或者还没出现在画面里),对应的x值就用 -2 填充。

raw_file就是当前图片的相对路径。

核心逻辑用一句话概括:固定y轴采样,逐个x轴取值,缺失用-2补。这种格式的好处是特别规整,模型读取的时候不用解析多边形,直接按索引取值就能构造训练张量,非常适合做基于回归或分类的车道线检测。

1.2 为什么单选tusimple格式

现在开源的车道线标注格式不少,比如著名的CULane格式、YOLO格式的实例分割标注、还有各种自定义的json格式。我并不是说tusimple是万能的,但它有几个特点让它成为入门和中期研发很实用的选择。

第一,坐标系简单。CULane用多边形区域表示车道线,还要额外区分实线虚线颜色,标注工作量直接翻倍。tusimple只标中心线点列,标注负担小很多。

第二,模型兼容性极好。目前主流车道线模型,像经典的车道线检测网络,很多代码仓库的数据加载器都是直接按照tusimple格式来写的。有些项目甚至只认这个格式,你要是用别的格式就得自己改数据加载代码,调试成本不小。做自己的数据集本来就是为了适配自己的场景,没必要在底层格式上给自己造轮子。

第三,方便做质量检查。因为数据是规整的数组,一行代码就能把标注结果可视化,比如把x坐标和y坐标画到原图上,一眼就能看出标得准不准。如果用多边形标注,可视化检查反而要多写不少代码。

当然它也有限制:tusimple格式保存的是车道线的“中心线”,没有车道线宽度、颜色、虚实这类属性信息。如果你的项目需要做精细的车道线分类,比如区分白实线黄虚线,那这个格式就不太够用,需要自己扩展字段。我在后面会讲怎么在兼容原格式的前提下做扩展。

1.3 制作流程的整体规划

做一个tusimple格式自己的数据集,完整链路是:场景设计 -> 原始数据采集 -> 标注 -> 格式转换 -> 可视化验证 -> 切分数据集 -> 模型闭环验证。

很多教程只讲到标注和转换就结束了,但实际踩下来你会发现,真正决定数据集能不能用的,是后面那几步。你有没有试过折腾两周做出来5000张图,放进模型训练,结果准确率还不如直接用公开数据集?问题往往出在场景设计不合理或者验证环节缺失。

正确的做法是先小额试跑,比如先采集50张图,手动标完、转换完、可视化检查完,再批量去做。这样可以早发现流程漏洞,避免到后期返工。我见过有人一口气标了两千张,结果发现raw_file路径写错了,所有样本训练时都读不到图,那种绝望我懂。

2. 原始数据采集与场景设计

2.1 相机选型与安装要点

制作车道线数据集,原始图像的质量直接决定标注上限。不要想着靠后期算法去拯救模糊、过曝的画面。我自己试下来,比较靠谱的方案是运动相机或者行车记录仪,分辨率至少要1080p,如果能上2.7K更好。关键点是固定焦距、固定光圈、关闭自动美颜和广角矫正。有些运动相机自带广角畸变矫正,如果开着,标注坐标和实际物理位置会对不上。

安装位置建议在车辆挡风玻璃中线偏下,高度大致在仪表台附近,视角水平略微向下压5度左右。之所以这么装,是为了让画面中车道线的走势更接近自动驾驶场景的前视视角。装太高了像无人机视角,装太低近处车道线又容易出画。

安装角度在整个采集过程中绝对不能动。注意,我说的是绝对。因为tusimple标注的坐标体系是基于图像像素的,如果相机角度变了,同一个物理车道线在不同图像里的像素位置就会变,模型学到的映射关系就乱套了。所以采集过程中最好用硬固定支架,别用吸盘式的,颠簸路面上容易移位。

2.2 场景多样性是数据集的生命线

很多人做自己数据集时最容易犯的错,就是只在上下班固定路线采集。搞出来一千张图,看着挺多,其实场景高度雷同——晴天、白天、同一条路。这样的数据集拿去微调模型,一到阴天或者路上换了车道线颜色,模型立刻退化。

我在实践中会列一个场景清单,采集的时候逐项打卡:

  • 光照条件:晴天、阴天、逆光、黄昏、夜间开灯、夜间无路灯
  • 天气状况:干燥路面、小雨、雨后反光、薄雾
  • 道路类型:城市双向车道、高速公路、乡村单车道、隧道、高架上下匝道
  • 车道线状况:清晰白线、磨损黄线、新刷油漆反光强烈、水泥路面标线、沥青路面标线
  • 交通状况:空旷、车流密集、旁边有大车遮挡

注意,不要为了追求多样性故意去危险驾驶。采集数据永远要在安全合法的前提下进行,比如副驾有人帮忙观察路况。

还有一个很实际的经验:采集视频时不要把每一条都录太长。tusimple格式在训练时是按“clips”即连续帧片段来组织的,每段最长也就十几秒。我的习惯是每个场景录10到20秒,停一停再录下一条,这样后期切clips很方便,也方便做时序增强。

2.3 视频抽帧的时机与节奏

拿到原始视频素材后,不要每一帧都用。相邻两帧之间的车道线位置变化很小,如果全部灌进数据集,会导致训练集和验证集之间高度相似,模型看起来很牛,一测真实场景就露馅。实操上我通常会每秒抽2到3帧,也就是间隔10到15帧抽一帧。

具体用FFmpeg命令抽帧的话,一行就能搞定:

ffmpeg -i input.mp4 -vf "fps=2.5" -q:v 2 frames/frame_%04d.jpg

这里fps=2.5表示每秒抽2.5帧。-q:v 2表示输出高质量JPEG。抽完后务必快速浏览一遍,把模糊帧、镜头被遮挡的帧、车辆转弯导致视角剧变的帧筛掉,这些帧会让标注变得很困难。

另外有一个细节:为了保证模型能学到时序信息(很多车道线模型会用连续帧做时序融合),抽帧时要保证帧结构连续。比如一个10秒的视频,每秒抽2.5帧,一共25帧,这25帧就是一个完整的clip,不要中途断开。后面做训练集测试集划分的时候,也是按clip为单位切,不能把同一个clip的帧同时放到训练集和测试集里,否则就是数据泄漏。

3. 标注工具选型与标注实操

3.1 手工标注工具对比与选择

标注这一步没什么捷径,想省时间的可以看看下面的工具对比。我前前后后试过不少,列几个代表性的:

工具优点缺点适用场景
labelme开源免费,多边形/线段标注都支持,社区大标注效率一般,导出格式需要二次转换小批量、自定义格式
某在线标注平台多人协作方便,有辅助标注模型涉及数据上传,有保密顾虑团队协作、数据量数百小时级
基于SAM的辅助标注脚本自动化程度高,点一下就能出掩膜转线对车道线这种细长结构容易飘,需要手动修正有编程能力,追求效率
自研标注脚本完全控制逻辑,可加半自动追踪开发成本高,需要调试界面长期做数据,规模上千张

我的建议是:如果只是几百张的验证级数据集,直接用labelme足够。如果是几千张的正式训练集,写一个基于交互式分割模型的半自动标注工具能省一半时间,后面我会讲具体思路。

注意,使用在线平台之前,务必确认你采集的图像数据不涉及敏感信息。比如拍到了路人面部、车牌号、特殊建筑等,要么做脱敏处理,要么干脆别用在线平台。自己做数据集虽然是技术活,数据合规这根弦不能松。

3.2 车道线标注的规范与边界

很多人以为标注就是沿着车道线点几个点,大错特错。标注规范如果定不清楚,两个标注员标出来的结果能差出十万八千里,模型训练出来效果自然稀碎。

我在团队里推行过一套标注规范,核心原则如下:

  • 只标车道线中心线。不标边缘,不画宽度,沿着白色实线的几何中心点即可。
  • 按可见部分标注。虚线车道线,只标线段部分,线段之间的空隙对应位置全部填 -2。不要脑补把空隙也连上。虚线的语义本来就包括“此处应断开”,模型需要学到这个特征。
  • 遮挡处理。如果有车辆遮挡了车道线,被遮挡区域也填 -2,不要猜着画。这一点和虚线类似——不确定就不标。
  • 端点规则。车道线延伸出画面底部边缘时,标到最后一个采样点即可;顶部通常超出h_samples范围,不用管。
  • 忽略干扰线。路面上有水渍、阴影、旧线痕迹形成的“假线”,坚决不标。宁可少标一条,不要标错一条,标错线对模型的误导远大于漏标。

这些规范看着不起眼,但决定了数据集的最终质量。我曾经因为“虚线和实线混合路段是否补全”这个问题没定清楚,两个标注员对同一个弯道的标注结果差了40个像素,后来模型在那个弯道直接画出了S形曲线。所以说,编码前先定规范,比写代码更重要。

3.3 标注效率提升的实战技巧

如果一张一张点,平均每张图要花3到5分钟,做一千张图就是50个小时打底,这还没算返工。所以我后来换了一套思路:

第一,利用连续帧做初始标注。在一个clip里,先标第一帧,然后用简单的光流或特征点跟踪预估下一帧的车道线位置,人工只做微调。我自己写过一个简陋版本:取上一帧标注点周围的小窗口,在当前帧用模板匹配找新位置。效果虽然不如深度跟踪算法,但能减少一半的点击量。

第二,放大倍率统一。标注时固定图像缩放级别,比如始终放大到200%再点,这样不同标注员之间点的精度会一致很多。

第三,高频错误自动提示。我写了个小脚本,每标注完一张图就检查:车道线数量是否在2到6条之间、每个点的x坐标是否落在图像宽度范围内、车道线是否出现剧烈抖动(相邻采样点x差超过80像素)。有明显问题的直接弹窗提醒。这个小脚本救了我很多次,因为人工标注在疲劳状态下很容易点歪。

指导原则就是:让标注员只做“选择题”,不要做“计算题”。所有能用程序自动判断的东西,都不要靠肉眼。

4. 格式转换与自动化工具链

4.1 从标注结果到tusimple格式的转换逻辑

假设你用了labelme标注,导出得到的是带points的JSON文件,格式大概是每根线一串[x, y]点对。现在需要转成tusimple那种“固定y采样、缺失填-2”的数组。这一步我自己写了个Python脚本,核心逻辑如下:

import json def points_to_tusimple_lane(points, h_samples): # points: [(x1,y1), (x2,y2), ...] # 先按y从小到大排序 points = sorted(points, key=lambda p: p[1]) lane_x = [] idx = 0 # 对每个固定采样高度y,找到最接近的标注点x for y in h_samples: while idx < len(points) and points[idx][1] < y: idx += 1 if idx < len(points) and abs(points[idx][1] - y) <= 5: lane_x.append(round(points[idx][0], 1)) else: lane_x.append(-2) return lane_x

这里要注意几个关键细节:

  • 采样高度集合h_samples必须全局统一。原版tusimple是240到710,步长10。如果你自己的图片分辨率不是720p,需要自己重新定采样集合。比如1080p的图像,可以做360到1070,步长10或者20。这直接决定了后续模型读取时的高度范围,不一致的话训练直接崩。
  • 容差范围不要设太大。上面的例子用了5像素容差,即标注点若不在采样高度附近,此位置算缺失。实际中标注点很难恰好落在整数高度上,5像素是比较合理的折中。
  • x坐标用浮点还是整数。原版tusimple里x值看起来是整数,其实模型读取时会转成浮点。我建议保留一位小数,减少量化误差,因为车道线本身是连续的,强行取整会让弯道处出现阶梯感。

4.2 图片路径与文件名组织规范

格式正确只是第一步,文件组织没定好,后面读数据一样会踩坑。tusimple格式对路径有约定:raw_file必须是相对路径,并且和标注JSON的存放位置保持特定结构。

我的目录结构是这样的:

data_root/ clips/ 001/ frame_0001.jpg 0001.json ... 002/ ... label.json label_1.json ...

其中每个JSON文件名和图片名一致,raw_file字段写成"clips/001/frame_0001.jpg"。训练时数据加载器会以data_root为基准拼接完整路径,如果我们生成的raw_file不是这个相对路径,就会读到不存在的文件。

另外,因为tusimple官方格式中,同一个clip的标注会汇总到根目录下的一个label.json里,而每个图片单独的同名JSON是我自己额外输出的,专门用来做单张检查。模型真正读取时其实只认根目录下的汇总文件。所以转换完成后,最好额外生成一份汇总label.json,格式是把所有单帧的JSON内容按序放到一个大数组里。

用代码实现汇总很简单:

import json label_list = [] for clip_id in ["001", "002", ...]: for img_name in clip_images[clip_id]: sample = { "raw_file": f"clips/{clip_id}/{img_name}.jpg", "lanes": ..., "h_samples": h_samples } label_list.append(sample) with open("label.json", "w") as f: json.dump(label_list, f)

注意JSON文件不要用ensure_ascii=True,路径里面有中文的话会被转义成unicode,某些加载器不认。我用json.dump(label_list, f, ensure_ascii=False),养成习惯。

4.3 可视化验证脚本:一分钟看出一千张图的毛病

这一步太重要了,我甚至觉得它比标注本身还重要。转换完的标注数据,如果不做可视化,你根本不知道里面藏了多少个 -2、多少个坐标错位。

可视化脚本的思路分两步。第一步,把每张图像上的车道线点画出来。这样一个简单的脚本就能快速检查出整体质量:

import cv2 import json import numpy as np def visualize_label(img_path, label): img = cv2.imread(img_path) h_samples = label["h_samples"] for lane_x in label["lanes"]: pts = [] for x, y in zip(lane_x, h_samples): if x > 0: # 跳过-2的缺失点 pts.append((int(round(x)), int(y))) if len(pts) >= 2: for i in range(len(pts) - 1): cv2.line(img, pts[i], pts[i+1], (0, 0, 255), 3) return img

第二步更重要:批量扫描出所有“可疑样本”。所谓可疑样本包括:某条车道线的有效点数少于3个(这种样本对训练几乎没有正向作用)、两条车道线在某个高度间距小于5像素(大概率标重了)、车道线在连续帧之间突变超过100像素(可能是标注抖动或追踪失败)。

把这些可疑样本单独导出到一个文件夹,我只需要翻一遍这个文件夹,就能判断这个批次的数据要不要打回重标。相比把所有图片都看一遍,效率提升非常明显。

5. 常见问题与排查技巧实录

5.1 车道线数量不一致导致模型batch崩溃

新手做tusimple格式数据集,最常遇到的报错是模型训练时“每条样本的车道线数量不一致”。原版tusimple数据集中,每张图的车道线数量从2到6不等,很多数据加载器设计成“先读取所有标注,再按最大数量做mask对齐”。如果你的数据集中出现了某张图只有1条线,或者0条线,代码里又没做mask处理,那训练时必然张量形状冲突。

解决思路有两个方向。第一,在数据上做过滤:确保每张图至少标注2条车道线。如果画面中确实只有1条线(比如乡间小路),这种样本直接删掉,或者并入特殊标签集。第二,在代码里做对齐:读取标注时统一生成一个置信度mask,有效点为1,缺失点为0,模型只对有效点做loss反传。这个mask在可视化验证阶段就可以提前生成并检查。

我个人的习惯是数据上和代码上都做双重保险,因为换模型代码仓库时,你不知道对方的数据加载器是否处理了异常车道线数量。

5.2 虚线车道线的断裂处理失误

很多标注员第一次标虚线时,会把所有线段连成一条完整的线,然后模型就学出了“虚线处也要连续”的错觉。更坑的是,有些标注员在相邻两帧里对同一条虚线线的“断”和“续”处理不一致,导致同一clip里同一物理车道线一会儿连续一会儿断裂。

遇到这个问题,我的经验是在标注规范里明确写:只看当前帧,虚线只标当前可见的油漆块。同时写一个后处理脚本,自动检测同一条车道线在连续三帧内的有效点数量变化率。如果某帧有效点数量比前后帧少了一半以上,大概率是虚线误标或漏标,人工抽出来检查。

最终模型训练出来的效果,虚线处的断点位置会和标注高度一致。如果你希望模型对虚线的断裂有更强的鲁棒性,可以在转换阶段以小概率对车道线做随机“模拟断裂”,这属于数据增强的范畴了,后续可以展开。

5.3 标注坐标与图像尺寸不匹配

这个问题极其常见,尤其是用了不同来源的抽帧工具后。比如你抽帧输出的是1920x1080的图,但标注工具默认读的是缩略图预览,标注时就基于小图操作,坐标范围和原图差了2倍都不止。结果训练时看到车道线全跑到图片外面去了。

排查方法很简单:加载任意一张图和它的标注,打印出图像宽高和所有x坐标的最大值、最小值,如果超出图像宽度范围,说明确实存在坐标基准不一致。解决办法只能重新标注或者按比例缩放坐标:

scale_x = img_width / annot_width new_x = old_x * scale_x

这里挺多代码仓库要求x和h_samples必须严格遵守,其实缩放后的小数位精度保留到0.1比较合适,太大容易破坏连续性。

5.4 JSON编码与逗号污染

做批量数据处理时,常见的是用字符串拼接方式生成JSON,然后手滑漏了一个逗号或引号,或者图片路径里带了特殊字符。我遇到过路径里有一个&符号,导致JSON解析失败,排查了半小时才发现是路径本身包含特殊字符。

保险的做法是:不手工拼JSON,全部用Python的json库来生成和写入。如果你必须用脚本处理已有JSON,先json.load()验证一下,不要直接正则替换。养成“数据全局改完,先跑一个validator”的习惯,可以省很多半夜排查的时间。

写个小验证函数放在转换脚本最后:

def validate_tusimple(label_path, img_root): with open(label_path, "r", encoding="utf-8") as f: data = json.load(f) for sample in data: img_path = os.path.join(img_root, sample["raw_file"]) if not os.path.exists(img_path): print(f"FAIL: image not found: {sample['raw_file']}") return False h_len = len(sample["h_samples"]) for lane in sample["lanes"]: if len(lane) != h_len: print(f"FAIL: lane length mismatch in {sample['raw_file']}") return False print("ALL OK") return True

5.5 数据集不平衡:弯道样本不足

做了一批数据后,统计一下各样本的“车道线曲率”分布。我做过一次统计,发现直道样本占了80%,弯道样本只有20%,结果模型在直道很准,一进高速匝道的弯道就飞线。原因是采集路线上直道太长了,弯道占比少。

应对方法是做“困难样本过采样”:在转换脚本里按曲率给样本分组,把弯道样本复制几份(或者做水平翻转增强)放进训练集。注意测试集绝不能做这种重复。

另外水平翻转是个挺实用的增强手段,它可以把右弯变左弯,相当于凭空多出一倍弯道样本。有些模型输出本身对左右车道有区分,所以使用翻转时要小心标签也要对应翻转。

6. 模型闭环验证与数据集迭代

6.1 用现成训练代码跑通验证

数据集做完后,别急着发论文或者交付给团队,先在至少一个公开代码仓库上跑通整个训练流程。我之所以强调“至少一个”,是为了隔离问题:如果模型完全没有收敛,到底是数据格式问题还是模型超参问题,可以换个仓库对比确认。

跑通验证的步骤一般是:

  1. 将数据集按clip划分为 train/val/test,比例建议8:1:1。
  2. 改数据加载器里的数据根目录和h_samples配置。
  3. 用一个小学习率和较小batch size,训练10到20个epoch。
  4. 观察loss下降曲线和验证集上可视化输出。

如果一切正常,训练集loss应该稳定下降,验证集上画出来的车道线应该和原图标注基本对齐。如果loss不降,优先检查明天的数据加载环节——打印一批输入张量和标注张量,肉眼看形状和数值是否合理。我曾经有两次“模型不收敛”,最后都是因为数据加载器里读错了图片路径,不是模型的问题。

6.2 量化指标与实际感受

tusimple格式的评测通常用准确率(accuracy)和误检率(false positive rate),计算方式简单讲就是把预测的车道线坐标和GT坐标比较,在固定误差阈值内算对。自己做数据集时,建议同时做两个层面的评估:

  • 数值层面:跑一下标准评测代码,得到准确率和误检率。
  • 视觉层面:随机抽一批测试集结果图,人工看图检视车道线的走向是否合理。

有很多模型准确率看着不错,但可视化却发现车道线在弯道内侧“穿透”到对面车道了。这类问题数值上不一定体现得很明显,但实际驾驶场景里非常致命。所以我每次做完数据集,都会专门抽几十张弯道场景的图做目检。

6.3 数据集的持续迭代机制

一次采集很难覆盖所有工况,所以我倾向于把数据集制作看成一个循环迭代的过程,而不是一次交付。每次模型在某个场景失败,就去收集对应的难例数据,标完补充进数据集。

这样做有几个好处:一是数据量和场景覆盖持续增加,模型鲁棒性逐步提升;二是标注规范和工具链越用越顺手,后期标注成本会明显下降;三是每次迭代都可以用上一版模型来给新数据做自动预标注,人工只要负责修正,效率比冷启动阶段高很多。

迭代到一定阶段后,记得冻结一个版本作为基准数据集,之后模型的改造、对比、调参都基于这个冻结版本,否则不同版本的数据集混在一起没法做实验对照。

7. 踩坑心得与一些补充

最后分享一条我要强调的经验:制作自己的数据集,本质是建立一套可复用的工具链和标注规范,而不是一口气标多少张图。我第一版数据集做了半个月,大部分时间浪费在没有验证标准的瞎标注和重复劳动上。后来把这套流程建起来以后,再做第二个场景的数据集,从原始视频到可用训练集,一周左右就能完成。

在实际操作中,践行的步骤是:先在一个小样本上把采集到训练全链路跑通,再规模化执行;每种工况采集前,先在场景清单里打勾;每次转换完,跑一遍可视化验证和统计脚本再进入训练环节。这些习惯能帮你避开我踩过的绝大多数坑。

另外补充一句关于数据安全的:自己采集的数据如果是用在商业项目里,务必确认采集过程符合当地法规和平台规范,涉及公共道路的影像数据要做好必要的脱敏处理。这不是刻板说教,而是做技术工作绕不开的现实问题。

如果你后续想把这个格式往更多方向扩展,比如加入车道线类型属性、加入车道中心线偏移量、或者和其他传感器数据对齐等,可以在JSON里加自定义字段,只要核心字段保持不变,兼容性就不会受影响。这个方向上可玩的东西很多,我后面会再单独写一篇扩展实践的分享。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询