☰
自制Tusimple格式车道线数据集:从标注到训练全流程实践
2026/10/22 14:24:44 网站建设 项目流程

1. 自制Tusimple格式数据集的核心思路

1.1 为什么值得自己造一份数据集

很多做车道线检测的同学,第一步都会去找开源的Tusimple数据集来训练。但实际跑通以后你会发现一个很尴尬的问题:别人采集的是国外高速公路场景,车道线清晰、视野开阔、路面平整,而你自己要做的场景可能是城市窄路、乡镇道路、甚至园区内部道路,直接把开源数据训出来的模型放到这些场景里,检测率会明显下滑。

我做了几个车道线检测项目之后,逐渐养成了一个习惯:不管用什么模型,先做一份贴合自己应用场景的Tusimple格式数据集。原因很简单,模型能不能用,很大程度上不取决于网络结构有多新,而取决于训练数据和你真实场景有多接近。与其在模型上反复调参,不如先把数据地基打牢。

Tusimple数据集格式之所以被广泛使用,是因为它的标注形式简单直观,又贴近真实道路感知需求。它把一条车道线表示为一组离散的点坐标,正好适合深度学习模型直接回归输出。自制数据集的过程,本质上就是按照这套格式,把你采集到的道路图片变成模型能学习的标注文件。

1.2 Tusimple格式到底在描述什么

Tusimple格式的核心不是图片本身,而是一个JSON文件。每一行JSON对应一张图片,描述这张图片里的所有车道线信息。我之前第一次看这个格式的时候也绕了一会儿,后来用一个生活化的类比才彻底搞明白:可以把车道线想象成一条弯曲的铁丝,JSON里的坐标点就是铁丝上等间距打的小孔。

具体到字段,最关键的有四个:

  • raw_file:图片的相对路径,模型训练时靠它找到对应的图。
  • lanes:一个二维数组,每一行代表一条车道线的所有x坐标。
  • h_samples:一组固定的y坐标,和lanes里的每一行一一对应。
  • lane_annotations:每条车道线的属性标注,包括是否有效、是否是实线/虚线等。

这里最容易被忽视的是坐标系的约定。Tusimple数据集的图像尺寸通常为1280×720,h_samples是从y=240开始、每隔10个像素取一个点,一直到y=710,一共48个采样高度。lanes里的每条线,就是在这些y值位置上的x坐标。如果某条车道线在某些位置不存在,就填-2而不是0。这是格式的坑之一,填0会被模型当成x=0位置的线,结果完全错乱。

理解了这个结构,自制数据集的方向就清晰了:先采集图片,然后标注每条车道线在48个高度上的x坐标,最后打包成JSON。听起来步骤不多,但里面有很多细节值得展开说。

2. 素材采集与标注规范

2.1 采集素材的条件和注意点

制作数据集的第一步不是标注,而是采集。素材质量直接决定标注效率和最终模型效果。我踩过的最大一个坑是:第一版数据集用的是不同角度、不同高度的手机随手拍的照片,结果模型训练完以后,在同一段路上测试,有时检测得到有时检测不到。后来才知道是相机安装高度和俯仰角不一致,导致消失点位置飘忽,模型学到的几何规律不统一。

如果你想做一份靠谱的Tusimple格式数据集,采集阶段建议满足下面几个条件:

  • 相机固定安装,不要拿在手里随意拍。可以使用行车记录仪、固定支架的手机或专用摄像头。
  • 安装高度和角度尽量一次定好,所有素材保持一致。前视摄像头通常安装在后视镜附近,高度1.3米左右,俯仰角让画面地平线大致处于图像中部偏上。
  • 覆盖不同场景:直道、弯道、上坡下坡、光线充足和逆光、地面清晰和略脏污,都要有一定比例。
  • 如果要做多车道线检测,尽可能拍到两条到五条车道线,这样标注出来的数据更有训练价值。

采集完的图片建议统一缩放成1280×720。不要偷懒直接使用原始分辨率,因为Tusimple格式的h_samples就是基于1280×720设定的,如果你用其他分辨率,要么自己重新定义采样区间,要么模型输出和格式对不上。

2.2 车道线类型的语义约定与标注点采样

Tusimple格式里有一个lane_annotations字段,里面每条车道线的属性分为五类,分别用数字表示。实际项目里最常用的是前四种:0表示当前车道左线、1表示当前车道右线、2表示相邻车道左线、3表示相邻车道右线。这个语义和车辆当前行驶位置强相关,不是简单地按图片从左到右排序。

我见过不少初学者在这里栽跟头:把所有车道线都标成0或1,模型训练出来以后完全分不清车道归属关系。其实自己标注的时候,心里要有一个基准:以车辆当前所在车道为中心,最靠近车身的左右两条线是0和1,再往外扩展是2和3。

标注点的采样密度,官方格式是每10个像素取一个点。实际操作中,尽量不要为了省事减少采样点数量。采样点太少,弯道部分的曲线拟合会严重失真;采样点太密,标注工作量和标注文件体积又会增加。折中方案是直道上可以隔20个像素标一个控制点,弯道上隔5到10个像素标一个控制点,最后统一在48个h_samples位置插值生成坐标。

这里还有一个很重要但常被忽略的点:无效点填充值为-2。比如某条车道线在图像远端已经超出视野,那么超出部分的x坐标就全部填-2。模型在训练时会通过mask机制忽略这些位置,如果你填0,等于人为制造了一条从图像边缘穿过的假车道线,会严重干扰训练。

3. 标注实操全流程

3.1 标注工具怎么选

Tusimple本身没有公开完整的官方标注工具,所以自制数据集通常有两种路线:一是用现成的通用标注工具标控制点,再写脚本转换;二是自己写一个简单的标注小工具。我个人更推荐第一种,因为通用标注工具更成熟,缩放、拖动、撤销这些基础功能都有,比自己从零写省时间得多。

常见的做法是用LabelMe这类工具在图片上标点位,每个点对应车道线的一个控制点。标注时按一条线的顺序依次打点,点完一条标下一条,最后导出成LabelMe的JSON格式,再通过转换脚本生成Tusimple需要的格式。

如果你项目周期比较长,需要反复标注多批数据,也可以自己写一个轻量标注工具。界面不需要花哨,能展示图片、鼠标点击取点、撤销上一个点、按一组预定义的y值自动插值并保存标注结果就够了。我实际使用后发现,一个顺手的小工具能把标注效率提升两倍以上,尤其是批量标注时,能省掉很多机械重复操作。

3.2 一条车道线从标注到采样的完整过程

以标一条完整车道线为例,具体流程是这样的:

  1. 打开图片,先整体观察有几条车道线需要标注,确定每条线的类型编号。
  2. 从距离车辆最近的位置开始打点,沿着车道线中心线往远处标,打点间隔尽量均匀。
  3. 车道线如果有断裂或者被车辆遮挡,不要跳过整个区域,而是尽量按车道线的走向趋势补点。补不了的,后续插值采样时这部分自然会被填成-2。
  4. 一条线标完后,切换到下一条线,注意类型编号按左右交替来,不要重复。
  5. 全部标完后,检查一遍每条线的起止范围和车道线类别,确认无误再保存。

这里面最关键的是插值步骤。因为你在标注时打的点不可能恰好都在h_samples采样高度上,所以需要根据标注控制点拟合出一条折线或曲线,再在48个固定y值位置取对应的x坐标。直线段可以直接线性插值,弯道建议用样条插值,效果会平滑很多。后续的脚本部分我会给出具体实现。

3.3 标注质量控制与一致性检查

标注质量比标注速度重要得多。我自己有一条经验:与其一口气标100张粗制滥造的图,不如分两轮标100张高质量的图。模型训练对标注错误非常敏感,哪怕只有5%的标注点偏了几个像素,最终检测出来的车道线在弯道上也可能出现明显的锯齿或抖动。

标完一批图之后,建议做一个简单的一致性检查:把所有标注结果叠到原图上,生成一张带车道线覆盖的可视化结果图。肉眼扫一遍,重点关注三条:

  • 车道线是否贴合实际道路线,远处是否有明显飘移。
  • 各条线的类型编号顺序是否正确,有没有出现两条线都是0的情况。
  • 断点处是否有异常跳变,比如某条线在某个y位置x坐标突然从100跳到300。

这些检查看起来很基础,但能挡住大部分低质量数据。可视化检查这一步我在后面也会给出一个极简的脚本,配合OpenCV几行代码就能完成。

4. 生成Tusimple格式JSON的脚本实现

4.1 目录结构与命名规范

在实际项目中,一份标准的数据集目录通常长这样:

dataset/ ├── train/ │ ├── clips/ │ │ ├── 0000/ │ │ │ ├── 00000.jpg │ │ │ ├── 00001.jpg │ │ │ └── ... │ │ ├── 0001/ │ │ │ └── ... │ └── label.json ├── val/ │ ├── clips/ │ │ └── ... │ └── label.json

label.json里的raw_file路径要和目录结构一致。比如训练集第一张图,raw_file写成clips/0000/00000.jpg。路径问题看着小,实际坑不少。我用绝对路径写过一次,结果换了机器训练,路径全部失效,重新改配置浪费了整整一个下午。

4.2 从标注点到Tusimple JSON的转换脚本

下面是我实际用的一个转换脚本,输入是手动标注的控制点文件,输出为Tusimple格式的JSON。控制点文件我用的是极简的自定义格式,每行一个点坐标,用逗号分隔,多条车道线用空行隔开。

import json import os import re from scipy.interpolate import splprep, splev import numpy as np H_SAMPLES = list(range(240, 720, 10)) # 48个采样y坐标 def interpolate_lane(points): """ 对一条车道线的控制点做样条插值。 返回一个数组,每个元素对应H_SAMPLES位置上的x坐标。 """ pts = np.array(points, dtype=np.float64) # 控制点少于2个就返回全-2 if len(pts) < 2: return [-2] * len(H_SAMPLES) # 用样条插值,s=0表示强制通过所有控制点 tck, u = splprep([pts[:, 0], pts[:, 1]], s=0) result_x = [] for y in H_SAMPLES: # 计算当前y值是否在控制点覆盖范围内 if y < pts[:, 1].min() or y > pts[:, 1].max(): result_x.append(-2) else: # 二分查找最近的u值区间,再插值x ratio = (y - pts[:, 1].min()) / (pts[:, 1].max() - pts[:, 1].min()) u_target = u[0] + (u[-1] - u[0]) * ratio x_interp, y_interp = splev(u_target, tck) result_x.append(round(float(x_interp), 1)) return result_x def convert_annotation(annotation_file, image_path, lane_types, output_json): """ annotation_file: 手动标注的控制点文件 image_path: 对应的图片路径,写入raw_file字段 lane_types: 每条车道线的类型列表,如[0, 1, 2] output_json: 输出的Tusimple格式JSON文件路径 """ with open(annotation_file, 'r', encoding='utf-8') as f: content = f.read().strip() # 按空行拆分得到每条车道线的控制点列表 lane_blocks = re.split(r'\n\s*\n', content) lanes = [] lane_annotations = [] for i, block in enumerate(lane_blocks): if not block.strip(): continue points = [] for line in block.strip().split('\n'): x_str, y_str = line.strip().split(',') points.append((float(x_str), float(y_str))) lane_x = interpolate_lane(points) lanes.append(lane_x) lane_annotations.append({ "lane_id": i, "lane_type": lane_types[i] if i < len(lane_types) else 0, "valid": True, "attributes": ["solid" if (i % 2 == 0) else "dashed"] }) entry = { "raw_file": image_path, "lanes": lanes, "h_samples": H_SAMPLES, "lane_annotations": lane_annotations } with open(output_json, 'a', encoding='utf-8') as f: json.dump(entry, f) f.write('\n')

这个脚本的核心逻辑是interpolate_lane函数。它把标注控制点作为样条控制点,在每个h_samples位置求x坐标。这个思路学名叫“按行采样”,就是Tusimple格式的本质——一条车道线不再是一串任意密度点,而是固定y间隔下的等间距点。

脚本里需要特别注意的是控制点覆盖范围的判断。如果某条车道线在y=240位置根本还没出现,比如虚线起始点比较晚,那这个位置的x就直接填-2,绝对不能往前面硬插值。

4.3 数据划分与可视化验证

JSON生成完以后,不要急着丢给模型训练,先做两件事:划分数据集、可视化验证。

划分数据集的原则是尽量按场景分,不要按图片随机分。因为连续视频帧之间有很强的相似性,随机划分会导致训练集和验证集出现同一条道路甚至同一帧的情况,验证指标虚高。我习惯把不同时间、不同路段采集的素材分别编号,按编号整体划分。比如采集了10个片段,8个进训练集、2个进验证集,比例大概80%和20%。

可视化验证代码很简单:

import cv2 import json def visualize_label(image_path, lanes, h_samples): img = cv2.imread(image_path) for lane in lanes: for x, y in zip(lane, h_samples): if x > 0: cv2.circle(img, (int(x), int(y)), 3, (0, 0, 255), -1) cv2.imshow('check', img) cv2.waitKey(0) with open('label.json', 'r') as f: for line in f: entry = json.loads(line) visualize_label(entry['raw_file'], entry['lanes'], entry['h_samples'])

看到红色点在图上连成几条清晰的车道线,数据基本就没问题了。如果某条线的点在弯道上明显偏离车道线中心位置,回到标注文件里修正那几个控制点再重新生成。

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

5.1 高频问题速查表

做数据集制作和转换的这段时间,我整理了一份问题排查表,按出现频率排序。

问题现象可能原因解决方案
训练时Loss爆炸lanes里有大量0值检查无效点是否填成-2,而非0
检测出的车道线在远端消失h_samples范围超过图像高度确认图片尺寸统一为1280×720
车道线类型全部一样标注时未按左右顺序编号重新核对lane_annotations的lane_type
弯道处线形扭曲控制点太少或插值方式不对弯道加密标注点,改用样条插值
图片和标注错位raw_file路径写错统一使用相对路径,目录结构保持一致
模型在验证集分数高但实际效果差训练集和验证集场景重叠按视频片段整体划分,不按帧随机划分

这个表里每一行都是我实际踩过的坑,尤其是第一行,影响最大也最容易犯。很多人在生成JSON时习惯性地把缺失点填0,结果模型把图像最边缘当成车道线,训练根本收敛不了。

5.2 独家避坑技巧:关于标注密度、相机参数和类别平衡

标注密度这块,我想多说几句。Tusimple格式的h_samples是每10像素一个y值,但这不代表你标注控制点时也要每10像素一个点。控制点可以比采样点少很多,直线上两头各一个点就够了,弯道上适当加密。这里的关键是插值质量,建议弯道控制点间距控制在15像素以内,否则样条插值容易在曲率大的地方出现过冲。

相机参数方面,不同摄像头采集的图片,消失点位置差异很大。Tusimple格式本身不要求标定参数,但为了让模型适应不同安装角度,最好在数据集中自然涵盖一些轻微的角度变化。比如有的图片略微下俯,有的略微上抬,这样训练的模型才会有一定鲁棒性。不要所有图片都在同一个固定角度下采集,那样模型学到的几何规律过拟合得很严重。

类别平衡也是一个容易被忽视的问题。如果采集的素材大量是直道,弯道数据很少,模型对弯道的检测能力会明显偏弱。我在实际项目中就吃过亏:训练集里直道占了85%,模型在弯道上几乎每次都在远端把车道线丢掉。后来单独补采了一批弯道素材,把弯道比例提升到30%左右,效果立竿见影。做数据采集规划的时候,一定要先盘算好场景分布,而不是想到什么拍什么。

5.3 大批量标注时的效率提升思路

如果你需要标注的数据量很大,比如几百张甚至上千张,纯手动标注会很痛苦。我的做法是先手动标注20张左右,跑通整个流程,然后用这些标注数据做一个简单的模型,再对剩余图片进行预标注,最后人工修正预标注结果。

预标注的做法并不复杂:把已标注的图片送入一个预训练模型或自己刚训练的小模型,得到初步的车道线点坐标,以叠加可视化形式保存成图片,标注工具加载这些图片时自动带上预标注点,人工只需要拖动错误点位。这能把标注时间压缩到原来的三分之一左右。

这个思路其实和主动学习有点类似,先用少量高质量标注获取初始能力,再用模型辅助扩展数据集规模。对比纯手动标注,单张图的标注时间从3分钟缩短到1分钟以内,总耗时能降低一半以上。

6. 从数据到训练的无缝衔接经验

6.1 模型读取Tusimple格式时容易忽略的细节

很多车道线检测模型都提供了Tusimple数据集的加载接口,但直接加载自制数据时,有几个细节需要和模型代码对齐。

首先是h_samples是否要动态读取。有的模型在DataLoader里写死了h_samples为默认值240到710,如果你改了自己的采样区间,模型不会自动适配。我建议在训练脚本中从JSON第一行读取h_samples,动态传入模型,避免硬编码。

其次是lanes的归一化处理。有的模型把x坐标除以图像宽度缩放到[0,1],有的直接使用像素坐标。自定义数据集时,要确认模型的预处理方式和标注格式匹配。否则模型输入输出与标签坐标维度不统一,训练过程不会报错,但指标会非常诡异,检测出来的点全部偏移。

最后是mask机制。Tusimple格式中-2的无效点在模型里通常对应一个mask矩阵,训练时这些位置的loss会被忽略。有些模型的实现只支持全图mask,有些支持逐点mask。如果你的场景里车道线经常被车辆遮挡,无效点很多,务必确认模型的mask处理能力,否则这些位置会被强行参与loss计算,产生明显的拖拽效应,模型会被遮挡物带偏。

6.2 训练效果验证的一个实用方法

训练完模型后,不要只看验证集指标,建议找一个和训练集完全不同的真实场景做一次快速实测。挑一段没有出现在数据集里的路,录一段视频,跑一遍检测,观察车道线在直道、弯道、光线变化下的表现。

我自己常用的快速验证方式是在车内副驾位置放一台笔记本,把摄像头画面实时送入模型处理,输出重叠了车道线检测结果的画面。这种实时反馈比任何离线指标都更能暴露问题。比如车道线在强逆光下大面积闪烁、弯道远端频繁抖动,这些在验证集指标里看不出来,但在真实场景里一测就能发现。

如果实测效果不理想,不要急着调模型超参,优先检查数据。我会重新做一次标注质量抽样检查,随机抽10张训练图,逐条看车道线和实际线形的贴合度。大多数情况下,问题出在标注误差较大的那几张图上。

6.3 一次完整的数据集迭代周期参考

根据我的经验,一份可用的Tusemple格式数据集从零开始制作,可以按这样的节奏推进:

比如有一周时间,第一天完成素材采集和筛选,第二天标注第一批图片并写好转换脚本,第三天做可视化检查和质量修正,第四天补充弯道和特殊场景素材,第五天完成全部标注并生成最终JSON,第六天训练模型跑一遍完整流程,第七天实测并根据结果决定是否多采一批数据补进去。

这个节奏的关键是第三天必须做可视化检查。很多人在转换脚本写完、JSON生成之后就以为万事大吉了,直接开始训练,结果模型训了一宿,第二天一看效果不对,回头才发现是标注或转换环节出了问题,浪费了大量时间。可视化检查只要半小时,却能避免一整夜无效训练,这笔账怎么算都值。

7. 关于这份这份数据集的几点心得

结合实际项目经验,我想说,自制一份Tusimple格式数据集,价值不只是得到一批标注数据。整个流程走下来,你会对格式的每个字段、每个坐标点的含义有十分深刻的理解。以后再调试模型,遇到检测结果异常,你会更快地判断出到底是不是数据问题。

我个人在操作中的体会是,标注质量远比数据量更重要。你用500张高质量标注数据训练出来的模型,效果往往比2000张粗标注数据更好。因为车道线检测本质上是几何回归任务,标注点位的准确性和一致性直接影响模型学到的车道线分布规律。

另外,自动化脚本和数据规范化这部分,花一点时间做得完整一些,后面能省很多麻烦。我一开始图省事,用临时脚本处理标注,后来换场景加数据时发现脚本完全没法复用,又重新整理了一遍目录结构和转换逻辑。如果能在一开始就规划好目录、脚本和检查流程,后续扩展数据集会非常顺畅。

最后再分享一个小技巧:第一次做完一份数据集后,一定要完整地跑一遍训练和验证流程,记录下各项指标,比如准确率、误检率、每张图的推理耗时。这些数据可以作为基线保存下来,之后每次给数据集新增样本、调整标注策略或更换场景时,都用同一套模型和参数重新训练,然后和基线对比。这样你能非常直观地看到每次变化的收益,而不是拍脑袋觉得“好像数据多了效果应该会变好”。

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

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

立即咨询