1. 项目概述:为什么选择Unity+Autoware制作高精地图?
如果你正在自动驾驶、机器人或者智慧交通领域摸索,大概率听说过“高精地图”这个词。它不再是简单的导航路线,而是包含了车道线精确几何、交通标志、路面坡度、甚至路沿石高度的三维数字模型,是自动驾驶车辆的“超级记忆”。传统的高精地图制作,往往依赖昂贵的专业测绘设备和复杂的商业软件,门槛高、流程长,让很多研究团队和初创公司望而却步。
那么,有没有一种方法,能让我们利用手头更常见的工具,快速搭建一个高精地图的制作与验证流程呢?这就是“Unity+Autoware联合实战”的核心价值。Unity,大家熟知的是它的游戏开发能力,但其强大的3D场景编辑、实时渲染和物理引擎,让它成为了一个绝佳的高精地图“编辑器”和“仿真验证平台”。而Autoware,作为自动驾驶领域最著名的开源软件栈之一,它定义了一套完整的高精地图数据格式(如Lanelet2/Vector Map),并提供了感知、定位、规划等一系列算法。
将两者结合,思路就清晰了:在Unity中,我们可以利用其直观的可视化界面和丰富的插件,高效地创建、编辑三维道路模型,并导出为Autoware能够识别的格式;然后,在Autoware中加载这张地图,进行自动驾驶算法的仿真测试,验证地图的准确性和实用性。这个过程形成了一个从“创作”到“应用”的闭环,特别适合算法前期开发、教学演示和特定场景(如园区、停车场)的快速地图构建。
我之所以花时间研究这套流程,是因为在实际项目中,我们经常需要针对测试场地制作临时的高精地图。等待专业测绘周期太长,而用代码硬写地图数据又极其繁琐且容易出错。Unity的所见即所得编辑方式,极大地提升了效率;再通过C#脚本进行数据转换和导出,整个流程就变得可控且可重复。接下来,我就把这套经过实战检验的“五步法”和完整的C#代码分享给你,无论你是自动驾驶工程师、机器人研究者,还是对3D地理信息感兴趣的学生,都能快速上手。
2. 核心工具链与工作流程拆解
在动手之前,我们需要理清整个工具链的构成和各部分的分工。这不是一个单一软件的操作,而是一个小型的系统工程。
2.1 Unity的角色:不仅仅是编辑器
很多人对Unity的认知停留在游戏层面,但在工业仿真和数字孪生领域,它早已是重要工具。在本项目中,Unity承担了三个核心角色:
- 三维场景建模平台:你可以使用Unity内置的Cube、Plane等基本几何体,或者导入专业的道路、建筑3D模型资产,来搭建道路环境。通过Transform组件精确控制位置、旋转和缩放,可以构建出车道宽度、曲率、坡度等关键属性。
- 高精地图逻辑标注工具:高精地图不仅需要几何,更需要语义。例如,一条车道需要被标记为“可行驶车道”,一个交通牌需要关联其类型(限速、停止)。我们通过在Unity游戏对象(GameObject)上添加自定义的C#脚本组件(Component)来实现这种逻辑标注。比如,给一个代表车道的长条形Cube挂上
LaneComponent脚本,并在脚本中设置车道ID、转向类型、限速等信息。 - 数据导出与格式转换器:这是连接Unity和Autoware的关键。我们需要编写C#脚本,遍历场景中所有被标注过的游戏对象,将它们的位置、旋转、缩放以及自定义的语义信息,按照Autoware Vector Map或Lanelet2 XML的格式要求,组织并写入到一个文本文件(如
.csv或.xml)中。Unity强大的脚本系统和文件IO功能让这一切变得可行。
2.2 Autoware的角色:标准的制定者与验证者
Autoware在这里主要不是用来编辑地图,而是作为地图数据的“消费者”和“标准检验器”。
- 提供数据格式标准:Autoware开源代码中明确定义了其支持的高精地图格式,主要是Vector Map和Lanelet2。我们的导出脚本必须以这两种格式之一为目标进行编写。Vector Map格式相对直观,它将地图元素分解为点(Point)、线(Line)、车道(Lane)、路标(Pole)等一系列CSV文件。理解这种数据结构是编写导出器的前提。
- 提供仿真验证环境:这是最关键的一步。当我们在Unity中导出了地图文件后,需要将其放入Autoware的运行环境中。通过Autoware的
map_tools等工具加载地图,并使用其rviz可视化插件进行查看。如果能正确显示车道、交通标志,并且自动驾驶模块(如路径规划)能基于此地图工作,那就证明我们的Unity制作的地图是有效的。如果显示错乱或规划失败,我们就需要回到Unity检查模型精度或数据导出逻辑。
2.3 五步工作流全景图
整个流程可以精炼为以下五个步骤,它们构成了一个迭代优化的闭环:
- Unity场景搭建与模型准备:在Unity中创建或导入基础道路环境的三维模型。
- 地图语义逻辑标注:为场景中的模型添加自定义C#脚本,赋予其高精地图语义(车道线类型、交通牌ID等)。
- C#脚本开发与数据导出:编写核心导出脚本,将标注好的场景数据转换为Autoware格式。
- Autoware环境配置与地图加载:在Ubuntu系统中配置Autoware,并加载导出的地图文件进行可视化。
- 功能验证与迭代优化:在Autoware中运行简单的定位或规划demo,验证地图可用性,并根据问题反馈回Unity修改。
这个流程的优势在于,可视化编辑降低了几何创建的难度,程序化导出保证了数据的准确性和批量处理能力,而在成熟框架中验证则确保了产出物的工业实用性。
3. 实战第一步:Unity场景准备与基础建模
现在,我们进入实战环节。首先从Unity工程开始。
3.1 创建项目与基础设置
打开Unity Hub,创建一个新的3D项目,命名为“HDMapCreator”。版本建议选择Unity 2021.3 LTS或2022.3 LTS,这些长期支持版本稳定性和社区资源都比较好。
创建后,进行几项关键设置:
- 单位系统(Unit):高精地图通常使用米(meter)作为单位。确保你的项目单位是米制。在Unity中,1个单位默认代表1米,这很方便,但最好在
Edit -> Project Settings -> Physics中确认一下。 - 坐标系:Unity是左手坐标系(Y轴向上),而许多自动驾驶领域的数据(如激光雷达点云)可能使用右手坐标系(Z轴向上)。Autoware的Vector Map默认使用ENU(东-北-天)坐标系,对应X-East, Y-North, Z-Up。为了简化,我们通常在Unity中也采用类似的约定:将道路平面放在X-Z平面上(Unity的“地面”平面),Y轴向上代表高度。这样在导出数据时,只需要处理坐标系轴的映射(如Unity的X->东, Z->北, Y->天),逻辑更清晰。
- 场景尺度:建议先从一个简单的交叉路口或一段直道开始,范围控制在200m x 200m以内。不要一开始就尝试制作大规模城市地图。
3.2 使用基本几何体构建道路模型
对于原型验证,我们完全可以使用Unity自带的立方体(Cube)和平面(Plane)来搭建道路。
- 创建地面:在Hierarchy面板右键 -> 3D Object -> Plane,重命名为“Ground”。将其Scale设置为(100, 1, 100),作为一个大的基础地面。
- 创建车道:车道本质是一个扁长的立方体。
- 创建一个Cube,重命名为“Lane_Straight_01”。
- 在Inspector面板中,将Transform的Position设为(0, 0.01, 0),使其略微高于地面,避免渲染时Z-fighting(深度冲突)。
- 将Scale设置为(50, 0.02, 3.5)。这里,X=50代表车道长度50米,Z=3.5代表车道宽度3.5米(标准车道宽度),Y=0.02代表很薄的路面厚度。
- 在Material面板,给它赋一个深灰色的材质,以区分于地面。
- 创建车道线:车道线可以用更细长的白色立方体表示。
- 创建另一个Cube,重命名为“Line_White_01”。
- 设置Position为(0, 0.02, 1.75)(假设贴在车道边缘),Scale为(50, 0.005, 0.15)。0.15米宽的实线或虚线是常见的。
- 赋予一个白色的材质。
- 组织场景结构:为了管理方便,建议在Hierarchy中创建几个空的GameObject作为文件夹,例如“Roads”、“Lanes”、“Signs”,然后将对应的模型拖入其中。清晰的场景结构对后续编写遍历脚本至关重要。
注意:用基本几何体建模精度有限,主要用于验证流程。对于真实项目,建议导入使用专业工具(如Blender, 3ds Max)制作的道路模型FBX文件,或者使用Unity的ProBuilder等高级建模工具进行更精细的编辑。
3.3 关键技巧:使用Prefab(预制件)提高效率
当你需要创建大量相似元素时,比如一排间隔相同的虚线,或者多个相同的停车标志,手动复制和调整位置极其低效。这时一定要使用Prefab。
- 将你精心调整好的一个模型(如一个标准的“虚线线段”Cube)从Hierarchy拖到Project面板的Assets文件夹中,它就创建成了一个Prefab。
- 之后,你可以直接从Project面板拖拽这个Prefab到场景中复用。
- 如果需要修改所有虚线的样式(比如颜色或宽度),你只需要在Project面板中修改原始Prefab,所有场景中的实例都会自动更新(除非某些属性被实例单独覆盖了)。
这个技巧在制作大规模、规则化的地图元素时,能节省海量时间。
4. 实战第二步:为地图元素注入灵魂——C#语义标注脚本
模型有了,但它们现在对计算机来说只是一堆“形状”。我们需要告诉程序:“这个长立方体是编号为101的车道,最高限速60km/h;那个小立方体是编号为201的停止线。” 这需要通过编写C#脚本来实现。
4.1 设计基础数据类
首先,在Unity项目的Assets/Scripts文件夹下(如果没有就创建一个),我们创建一些基础的数据类,用来表示高精地图的通用属性。这些类不继承自MonoBehaviour,是纯粹的C#类。
// HDMapElement.cs - 高精地图元素基类 using System.Collections.Generic; using UnityEngine; namespace HDMapToolkit { // 元素类型枚举 public enum ElementType { Point, Line, Lane, StopLine, RoadSign, CrossWalk, // ... 可根据需要扩展 } // 基础元素类,包含ID和类型 [System.Serializable] public class HDMapElement { public int Id; // 全局唯一ID public ElementType Type; public Vector3 Position; // 世界坐标(Unity坐标系) // 可以添加更多通用属性,如置信度、时间戳等 } }4.2 实现核心标注组件
接下来,创建继承自MonoBehaviour的脚本组件,我们将把这些组件挂载到场景中的GameObject上。
车道组件示例 (LaneComponent.cs):
// LaneComponent.cs - 挂载在表示车道的GameObject上 using UnityEngine; namespace HDMapToolkit { public class LaneComponent : MonoBehaviour { [Header("车道基本属性")] public int LaneId = 1001; // 车道ID public float SpeedLimit = 60.0f; // 限速 (km/h) public int LaneNumber = 1; // 车道编号 public bool IsLeftLane = false; // 是否左转专用道 public bool IsRightLane = false; // 是否右转专用道 [Header("关联信息 (需在编辑器中关联)")] public LaneComponent LeftAdjacentLane; // 左侧相邻车道 public LaneComponent RightAdjacentLane; // 右侧相邻车道 public StopLineComponent AssociatedStopLine; // 关联的停止线 public List<RoadSignComponent> RelatedSigns; // 相关的交通标志 // 在Unity编辑器场景视图中绘制Gizmos,便于可视化调试 private void OnDrawGizmosSelected() { Gizmos.color = Color.blue; // 绘制一个包围盒 Gizmos.DrawWireCube(transform.position, transform.lossyScale); // 绘制车道方向箭头(假设物体朝向代表车道方向) DrawArrow.ForGizmo(transform.position, transform.forward * 5, Color.green); } // 一个简单的箭头绘制辅助类 public static class DrawArrow { public static void ForGizmo(Vector3 pos, Vector3 direction, Color color, float arrowHeadLength = 0.25f, float arrowHeadAngle = 20.0f) { Gizmos.color = color; Gizmos.DrawRay(pos, direction); Vector3 right = Quaternion.LookRotation(direction) * Quaternion.Euler(0, 180 + arrowHeadAngle, 0) * new Vector3(0, 0, 1); Vector3 left = Quaternion.LookRotation(direction) * Quaternion.Euler(0, 180 - arrowHeadAngle, 0) * new Vector3(0, 0, 1); Gizmos.DrawRay(pos + direction, right * arrowHeadLength); Gizmos.DrawRay(pos + direction, left * arrowHeadLength); } } } }交通标志组件示例 (RoadSignComponent.cs):
// RoadSignComponent.cs - 挂载在表示交通标志的GameObject上 using UnityEngine; namespace HDMapToolkit { public class RoadSignComponent : MonoBehaviour { public enum SignType { SpeedLimit, // 限速 Stop, // 停止 Yield, // 让行 // ... 其他类型 } [Header("标志属性")] public int SignId = 2001; public SignType Type = SignType.SpeedLimit; public float Value = 60.0f; // 对于限速牌,这里就是限速值 [Header("关联车道")] public LaneComponent AffectedLane; // 这个标志作用于哪条车道 private void OnDrawGizmosSelected() { Gizmos.color = Color.yellow; Gizmos.DrawSphere(transform.position, 0.5f); // 可以绘制一条线连接到受影响的车道,便于检查关联关系 if (AffectedLane != null) { Gizmos.DrawLine(transform.position, AffectedLane.transform.position); } } } }创建好这些脚本后,将其分别拖拽到场景中对应的车道Cube和交通标志模型上,然后在Inspector面板中填写或选择相应的属性值,并建立它们之间的关联(比如把停止线组件拖到车道的AssociatedStopLine字段里)。
实操心得:在Unity编辑器中建立对象间的关联(如车道关联标志)时,务必仔细检查。一个常见的错误是关联错了对象,导致导出的地图数据逻辑错误。善用
OnDrawGizmosSelected方法绘制辅助线和图标,能极大提升在复杂场景中排查关联关系的效率。
5. 实战第三步:核心引擎——C#地图数据导出器
这是整个流程的技术核心。我们需要编写一个“导出器”脚本,它能够遍历场景中所有带有我们自定义组件(LaneComponent,RoadSignComponent等)的GameObject,收集它们的信息,并按照Autoware Vector Map的CSV格式写入到文件中。
5.1 理解Autoware Vector Map格式
Autoware的Vector Map由多个CSV文件组成,每个文件代表一种地图元素。我们主要关注以下几个:
point.csv: 存储所有点的三维坐标 (ID, B, L, H) 或 (ID, x, y, z)。B, L, H通常是经纬度和海拔,我们简化使用局部笛卡尔坐标(x,y,z)。line.csv: 存储由两个点构成的线 (ID, BPID, FPID)。BPID是起点ID,FPID是终点ID。lane.csv: 存储车道信息 (ID, LnID, BLID, FLID, ...)。LnID是车道线ID,BLID/FLID是前后车道ID,用于连接。roadsign.csv: 存储交通标志信息 (ID, VID, LinkID, ...)。VID是标志所在位置的点ID,LinkID通常关联到车道或线。
我们的导出器需要生成这些CSV文件。为了简化,我们采用一种策略:先为所有需要记录位置的对象(如车道中心点、标志位置点)生成point.csv,再根据点之间的关系生成line.csv和lane.csv等。
5.2 编写地图导出管理器 (HDMapExporter.cs)
这个脚本可以挂在一个空GameObject上,或者通过编辑器菜单触发。
// HDMapExporter.cs - 地图导出管理器 using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; using UnityEditor; // 注意:使用了Editor命名空间,这意味着它只能在Unity编辑器环境下运行 namespace HDMapToolkit { public class HDMapExporter : MonoBehaviour { [Header("导出设置")] public string ExportDirectory = "ExportedHDMap"; public string MapName = "MyTestMap"; public bool UseLocalCoordinates = true; // 是否使用局部坐标(原点为Unity世界原点) // 用于存储导出过程中生成的数据 private List<VectorMapPoint> points = new List<VectorMapPoint>(); private List<VectorMapLine> lines = new List<VectorMapLine>(); private List<VectorMapLane> lanes = new List<VectorMapLane>(); private List<VectorMapRoadSign> roadSigns = new List<VectorMapRoadSign>(); // ... 其他元素列表 // Vector Map 数据结构的简单表示类 private class VectorMapPoint { public int pid; public double x; public double y; public double z; } private class VectorMapLine { public int lid; public int bpid; public int fpid; } private class VectorMapLane { public int lnid; public int did; public int blid; public int flid; public string laneType; } private class VectorMapRoadSign { public int id; public int vid; public int linkId; public string signType; public float value; } // 核心导出方法 public void ExportToVectorMapFormat() { // 0. 重置数据 points.Clear(); lines.Clear(); lanes.Clear(); roadSigns.Clear(); // 1. 收集场景中所有标注了的地图元素 LaneComponent[] allLanes = FindObjectsOfType<LaneComponent>(); RoadSignComponent[] allSigns = FindObjectsOfType<RoadSignComponent>(); // ... 查找其他类型的组件 // 2. 处理车道,生成点和车道数据 ProcessLanes(allLanes); // 3. 处理交通标志 ProcessRoadSigns(allSigns); // 4. 写入CSV文件 WriteCSVFiles(); Debug.Log($"高精地图导出完成!文件保存在: {GetExportFullPath()}"); } private void ProcessLanes(LaneComponent[] laneComponents) { int pointIdCounter = 1; int lineIdCounter = 1; int laneIdCounter = 1; Dictionary<LaneComponent, int> lanePointIdMap = new Dictionary<LaneComponent, int>(); // 记录车道中心点ID foreach (var lane in laneComponents) { // 为每个车道创建一个中心点(这里简化处理,实际应根据车道形状取多个点) Vector3 worldPos = lane.transform.position; Vector3 exportPos = ConvertToTargetCoordinate(worldPos); var point = new VectorMapPoint { pid = pointIdCounter, x = exportPos.x, y = exportPos.z, // 注意:Unity Z 轴对应 Vector Map 的 y (北向) z = exportPos.y // Unity Y 轴对应 Vector Map 的 z (天向) }; points.Add(point); lanePointIdMap[lane] = pointIdCounter; pointIdCounter++; // 创建车道线(这里简化,假设每个车道对应一条线) // 实际应根据车道左右边界线生成line // 此处仅为示例,生成一条从车道起点到终点的线(需要起点和终点两个点) // 我们假设车道的transform.forward方向为车道方向,取起点和终点 Vector3 startPos = lane.transform.position - lane.transform.forward * lane.transform.lossyScale.x / 2; Vector3 endPos = lane.transform.position + lane.transform.forward * lane.transform.lossyScale.x / 2; var startPoint = new VectorMapPoint { pid = pointIdCounter++, ... }; // 转换坐标并添加 var endPoint = new VectorMapPoint { pid = pointIdCounter++, ... }; points.Add(startPoint); points.Add(endPoint); var line = new VectorMapLine { lid = lineIdCounter, bpid = startPoint.pid, fpid = endPoint.pid }; lines.Add(line); // 创建车道记录 var vectorLane = new VectorMapLane { lnid = lane.LaneId, // 使用我们自定义的ID did = line.lid, // 关联的线ID blid = 0, // 前车道ID,需要根据实际连接关系填写,这里先填0 flid = 0, // 后车道ID laneType = lane.IsLeftLane ? "left" : (lane.IsRightLane ? "right" : "straight") }; lanes.Add(vectorLane); laneIdCounter++; lineIdCounter++; } // 二次遍历,建立车道之间的连接关系 (BLID, FLID) // 这里需要根据你的场景布局逻辑来写,例如通过位置判断或预先在组件中设置连接关系 // 这是一个简化示例,假设车道顺序排列 for (int i = 0; i < lanes.Count; i++) { if (i > 0) lanes[i].blid = lanes[i - 1].lnid; // 前车道 if (i < lanes.Count - 1) lanes[i].flid = lanes[i + 1].lnid; // 后车道 } } private void ProcessRoadSigns(RoadSignComponent[] signComponents) { int signIdCounter = 3001; foreach (var sign in signComponents) { Vector3 worldPos = sign.transform.position; Vector3 exportPos = ConvertToTargetCoordinate(worldPos); // 为标志创建一个点 var point = new VectorMapPoint { pid = points.Count + 1, x = exportPos.x, y = exportPos.z, z = exportPos.y }; points.Add(point); // 创建交通标志记录 var roadSign = new VectorMapRoadSign { id = sign.SignId, vid = point.pid, // 关联的点ID linkId = sign.AffectedLane != null ? sign.AffectedLane.LaneId : 0, // 关联的车道ID signType = sign.Type.ToString(), value = sign.Value }; roadSigns.Add(roadSign); signIdCounter++; } } private Vector3 ConvertToTargetCoordinate(Vector3 unityWorldPos) { if (UseLocalCoordinates) { // 如果使用局部坐标,直接返回Unity世界坐标(原点为场景原点) return unityWorldPos; } else { // 如果需要转换为真实世界坐标(如UTM),这里需要实现坐标转换函数 // 例如:return GPSConverter.UnityToUTM(unityWorldPos); Debug.LogWarning("真实世界坐标转换未实现,使用局部坐标。"); return unityWorldPos; } } private void WriteCSVFiles() { string fullPath = GetExportFullPath(); if (!Directory.Exists(fullPath)) { Directory.CreateDirectory(fullPath); } // 写入 point.csv WriteCSV(Path.Combine(fullPath, "point.csv"), points, p => $"{p.pid},{p.x:F3},{p.y:F3},{p.z:F3},0,0"); // 最后两个0是Vector Map格式的占位符 // 写入 line.csv WriteCSV(Path.Combine(fullPath, "line.csv"), lines, l => $"{l.lid},{l.bpid},{l.fpid},0"); // 最后0是占位符 // 写入 lane.csv WriteCSV(Path.Combine(fullPath, "lane.csv"), lanes, ln => $"{ln.lnid},{ln.did},{ln.blid},{ln.flid},{ln.laneType},0,0,0"); // 写入 roadsign.csv WriteCSV(Path.Combine(fullPath, "roadsign.csv"), roadSigns, rs => $"{rs.id},{rs.vid},{rs.linkId},{rs.signType},{rs.value:F1}"); // ... 写入其他需要的CSV文件 } private void WriteCSV<T>(string filePath, List<T> dataList, System.Func<T, string> lineFormatter) { using (StreamWriter sw = new StreamWriter(filePath, false, Encoding.UTF8)) { // 写入表头 (需要根据Vector Map官方格式定义) sw.WriteLine("PID,B,L,H,Bx,Lx,Ref"); // point.csv 表头示例 // sw.WriteLine("LID,BPID,FPID,BLID"); // line.csv 表头示例 // 注意:不同的CSV文件表头不同,这里需要严格按照Autoware Vector Map定义来写 foreach (var item in dataList) { sw.WriteLine(lineFormatter(item)); } } Debug.Log($"已生成文件: {filePath}"); } private string GetExportFullPath() { return Path.Combine(Application.dataPath, "..", ExportDirectory, MapName); } // 在Unity编辑器中添加一个菜单项,方便触发导出 [MenuItem("HDMap Tools/Export Current Scene to Vector Map")] static void ExportFromMenu() { GameObject exporterObj = new GameObject("HDMapExporter"); HDMapExporter exporter = exporterObj.AddComponent<HDMapExporter>(); exporter.ExportToVectorMapFormat(); DestroyImmediate(exporterObj); // 执行完后删除临时对象 } } }重要提示:以上代码是一个高度简化的示例框架,用于阐述核心逻辑。真实的Autoware Vector Map格式包含更多字段(如
BLID2,FLID2,JCT,LaneNum等),并且点、线、车道之间的关联关系更为复杂。你需要根据Autoware官方文档中map_tools包提供的格式定义,来完善这个导出器。例如,一个车道通常关联左右两条边界线(DLID,DLID2),而不是一条中心线。
5.3 坐标系转换的坑
代码中ConvertToTargetCoordinate方法暗示了一个关键问题:坐标系转换。Unity的坐标系(左手系,Y向上)与Autoware常用的ENU坐标系(右手系,Z向上或北东地)可能不同。在我们的简化处理中,我们做了映射:Unity (X, Y, Z) -> Vector Map (x, y, z) = (Unity.X, Unity.Z, Unity.Y)。这意味着我们把Unity的X轴当作东,Z轴当作北,Y轴当作天。
这只是一个约定!你必须确保在Autoware中加载地图时,使用了相同的坐标系约定来解释这些数据。否则,你的地图可能会旋转或颠倒。在复杂的项目中,可能需要引入更专业的坐标转换库(如ProjNet)来处理WGS84经纬度到局部笛卡尔坐标的转换。
6. 实战第四步:在Autoware中加载与验证地图
地图数据导出后,我们切换到Autoware环境进行验证。假设你已经在Ubuntu系统上安装好了Autoware(例如通过Docker或源码编译)。
6.1 准备Autoware运行环境
- 启动Autoware:根据你的安装方式,启动Autoware。如果是Docker,运行类似
./run.sh的命令;如果是源码,在workspace下执行source install/setup.bash && ros2 launch autoware_launch logging_simulator.launch.xml。 - 配置RViz:Autoware通常使用RViz进行可视化。确保加载了正确的配置,能够显示
Vector Map或Lanelet2 Map。
6.2 导入并加载导出的地图文件
- 文件传输:将Unity导出的整个文件夹(包含
point.csv,line.csv,lane.csv,roadsign.csv等)拷贝到Autoware环境中的某个路径下,例如~/autoware_map/my_test_map/。 - 修改地图配置文件:Autoware需要通过一个
lanelet2_map.osm或vector_map.csv的索引文件来加载地图。对于Vector Map,通常需要一个vector_map.csv文件,其内容就是各个CSV文件的路径列表。- 创建一个
vector_map.csv文件,内容如下:point,~/autoware_map/my_test_map/point.csv line,~/autoware_map/my_test_map/line.csv lane,~/autoware_map/my_test_map/lane.csv roadsign,~/autoware_map/my_test_map/roadsign.csv
- 创建一个
- 加载地图:在Autoware的RViz界面中,找到加载地图的选项或使用ROS2服务调用。例如,对于Vector Map,你可能需要发布一个包含地图文件路径的ROS话题,或者使用
map_tools中的节点来加载。
6.3 可视化检查与问题排查
如果一切顺利,你将在RViz中看到你从Unity中创建的车道、标志等元素。但第一次成功加载的概率不高,常见问题包括:
- 什么都看不到:检查RViz中对应的显示插件(Display)是否添加并启用,话题(Topic)名称是否正确。检查
vector_map.csv中的文件路径是否正确。查看Autoware终端的错误日志,通常会有加载失败的详细原因。 - 地图元素位置错乱:这极大概率是坐标系问题。确认Unity导出时和Autoware加载时对坐标轴的解释是否一致。尝试在导出脚本中调整坐标映射顺序(如交换Y和Z),或者在Autoware的加载参数中设置坐标系转换。
- 车道连接关系断裂:在RViz中,车道应该是一条条连续的线段。如果出现断裂,说明在导出
lane.csv时,BLID(前车道ID)和FLID(后车道ID)没有正确设置。你需要回到Unity,确保LaneComponent脚本中正确设置了LeftAdjacentLane和RightAdjacentLane(对于同向车道),并且在导出器ProcessLanes方法中正确地根据这些关联生成了连接关系。 - 交通标志没有关联到车道:检查
roadsign.csv中的linkId字段是否正确填写了目标车道的ID。在Unity中,确保RoadSignComponent的AffectedLane字段被正确赋值。
排查技巧:从简单到复杂。先只导出一个简单的“点”和“线”,在Autoware中确认能正确显示且位置无误。然后再逐步添加车道、标志等更复杂的元素和关联关系。这种增量式的验证能帮你快速定位问题所在层。
7. 实战第五步:功能验证与流程优化
地图能正确显示只是第一步,更重要的是它能否支撑自动驾驶功能。
7.1 基础功能验证:定位与路径规划
- 设置模拟车辆:在Autoware中,你可以设置一个模拟的车辆初始位置(通过
Initial Pose工具在RViz中点击)。 - 设置目标点:使用
Goal Pose工具在地图上点击另一个点作为目的地。 - 运行规划模块:启动Autoware的全局路径规划模块(如
lane_planner)。如果地图数据正确,规划器应该能基于车道网络,生成一条从起点到终点的可行路径,并在RViz中以绿色或红色的线显示出来。 - 观察结果:
- 成功:路径连贯地沿着你绘制的车道中心线延伸。恭喜,你的地图基本可用。
- 失败:路径断裂、跳到地图外、或者根本不生成。这通常意味着车道网络(
lane.csv)的连接性有问题,或者车道属性(如laneType)设置不正确,导致规划器认为某些车道不可行驶。
7.2 流程优化与经验总结
经过几轮“Unity编辑 -> 导出 -> Autoware验证 -> 发现问题 -> 回Unity修改”的迭代后,你会积累大量经验,并需要对流程进行优化:
- 开发Unity编辑器扩展:手动为每个模型添加组件、填写ID、建立关联非常耗时。可以开发自定义的Editor Window,提供批量选择、自动编号、可视化连接线绘制等功能,大幅提升在Unity中的标注效率。
- 实现增量导出:当只修改了场景的一小部分时,重新导出全部数据并复制到Autoware环境很麻烦。可以设计导出器只导出发生变化的部分,或者开发一个简单的网络接口,让Unity直接向运行中的Autoware仿真环境发送地图更新数据。
- 引入版本控制:地图数据(CSV文件)是纯文本,非常适合用Git等工具进行版本管理。每次大的修改都做一个提交,便于回溯和协作。
- 制定标注规范:对于团队项目,必须制定统一的标注规范。例如:车道ID的编码规则、坐标原点的选取、模型与真实尺寸的比例(必须为1:1)、不同语义类型使用的颜色或Prefab模板等。
我个人在实际操作中的体会是,这套流程最大的价值在于快速原型验证。当我们需要在一个新的测试场地验证某个感知或规划算法时,用专业测绘车出图可能要等一周,而用这套方法,一个下午就能做出可用的、厘米级精度的局部高精地图,让算法开发几乎可以立即开始。它的精度可能不如专业测绘,但对于算法逻辑验证、仿真测试来说,已经完全足够。更重要的是,这个过程让你对高精地图的数据结构、元素关联有了极其深刻的理解,这是只看文档无法获得的。
最后再分享一个小技巧:在Unity中建模时,可以尝试录制一段车辆在场景中行驶的屏幕录像,然后用视频处理软件提取车辆轨迹的像素坐标,再通过标定反算出世界坐标,作为生成车道中心线点的数据来源。这相当于用“视频测绘”的方式辅助建模,在某些情况下比手动摆放更高效。