航点数据结构设计:从零搭建无人车的“记忆单元”
做无人车开发,说白了就是让车知道“我在哪”、“要去哪”、“怎么去”。而航点,就是这三个问题的核心载体。今天我们来聊聊航点的数据结构设计——这是整个任务系统的基石。
我记得刚入行那会儿,有个项目因为航点数据格式没统一,导致导航模块和动作执行模块各说各话,联调时出了不少问题。从那以后,我对数据建模这件事就格外上心。
航点类:无人车的“坐标+指令”组合
一个航点,至少得包含三样东西:位置、朝向、动作。位置告诉车去哪,朝向告诉车怎么停,动作告诉车到了之后干什么。
我个人习惯把航点类设计成这样:
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class ActionType(Enum):
"""动作类型枚举"""
STOP = "stop" # 停车
LOAD = "load" # 装载
UNLOAD = "unload" # 卸载
WAIT = "wait" # 等待
CHARGE = "charge" # 充电
CUSTOM = "custom" # 自定义动作
@dataclass
class Waypoint:
"""航点数据类"""
x: float # 横坐标(米)
y: float # 纵坐标(米)
yaw: float # 朝向角(弧度)
action: ActionType # 到达后的动作
speed: Optional[float] = None # 通过该点的目标速度
tolerance: float = 0.5 # 到达判定阈值(米)
你可能会问:为什么用 dataclass?嗯,这里有个小故事。我之前用普通类写航点,每次都要手写 __init__、__repr__、__eq__,代码又臭又长。dataclass 自动帮你生成这些,省心不少。
核心要点:航点不只是坐标,它是“位置+行为”的复合体。设计时要把动作类型作为一等公民,而不是事后补丁。
为什么用 dataclass?我的实战体会
Python 3.7 引入的 dataclass,简直就是为数据建模量身定做的。我总结了几点好处:
- 代码量减少 60%:不用写模板代码,专注业务逻辑
- 类型注解清晰:IDE 自动补全,减少低级错误
- 不可变可选:设置 frozen=True 可以防止意外修改
- 开箱即用的序列化:配合 asdict() 方法,转字典超方便
举个例子,我在项目中经常需要批量创建航点:
from dataclasses import asdict
# 创建一条路径
path = [
Waypoint(0.0, 0.0, 0.0, ActionType.STOP),
Waypoint(10.0, 0.0, 0.0, ActionType.WAIT, speed=2.0),
Waypoint(10.0, 5.0, 1.57, ActionType.UNLOAD),
Waypoint(0.0, 5.0, 3.14, ActionType.LOAD),
]
# 转成字典列表,方便存储
path_dicts = [asdict(wp) for wp in path]
小技巧:如果你需要频繁比较航点是否相等,可以在 dataclass 里加上 order=True 参数,这样就能直接排序了。
序列化与反序列化:让航点“活”起来
航点数据不能只活在内存里,得能存能读。JSON 和 YAML 是两种最常用的格式。我一般这么选:
| 格式 | 优点 | 缺点 | 我的使用场景 |
|---|---|---|---|
| JSON | 解析快、兼容性好 | 不支持注释 | 运行时配置、网络传输 |
| YAML | 可读性强、支持注释 | 解析稍慢 | 人工编辑的航点文件 |
JSON 序列化实战
import json
from dataclasses import asdict
def waypoints_to_json(waypoints: list[Waypoint]) -> str:
"""航点列表转JSON字符串"""
data = []
for wp in waypoints:
wp_dict = asdict(wp)
wp_dict['action'] = wp.action.value # 枚举转字符串
data.append(wp_dict)
return json.dumps(data, indent=2, ensure_ascii=False)
def json_to_waypoints(json_str: str) -> list[Waypoint]:
"""JSON字符串转航点列表"""
data = json.loads(json_str)
waypoints = []
for item in data:
item['action'] = ActionType(item['action']) # 字符串转枚举
waypoints.append(Waypoint(**item))
return waypoints
# 使用示例
json_str = waypoints_to_json(path)
print(json_str)
# 输出:
# [
# {
# "x": 0.0,
# "y": 0.0,
# "yaw": 0.0,
# "action": "stop",
# "speed": null,
# "tolerance": 0.5
# },
# ...
# ]
注意:枚举类型在 JSON 里没有原生支持。我踩过这个坑——直接序列化枚举会报错。解决办法就是手动转成字符串,反序列化时再转回来。
YAML 序列化实战
YAML 的可读性确实好,适合人工编辑。我曾经用 YAML 管理过一套包含 200 多个航点的测试路线,编辑起来比 JSON 舒服多了。
import yaml
def waypoints_to_yaml(waypoints: list[Waypoint]) -> str:
"""航点列表转YAML字符串"""
data = []
for wp in waypoints:
wp_dict = asdict(wp)
wp_dict['action'] = wp.action.value
data.append(wp_dict)
return yaml.dump(data, default_flow_style=False, allow_unicode=True)
def yaml_to_waypoints(yaml_str: str) -> list[Waypoint]:
"""YAML字符串转航点列表"""
data = yaml.safe_load(yaml_str)
waypoints = []
for item in data:
item['action'] = ActionType(item['action'])
waypoints.append(Waypoint(**item))
return waypoints
# 使用示例
yaml_str = waypoints_to_yaml(path)
print(yaml_str)
# 输出:
# - action: stop
# speed: null
# tolerance: 0.5
# x: 0.0
# y: 0.0
# yaw: 0.0
# - action: wait
# speed: 2.0
# tolerance: 0.5
# x: 10.0
# y: 0.0
# yaw: 0.0
我的习惯:开发阶段用 YAML,方便调试和修改。部署到实车时转成 JSON,追求解析性能。
知识体系总览
下面这张图,把航点数据结构的核心脉络梳理清楚了:
避坑指南:我踩过的三个坑
做航点数据结构设计,有几个地方特别容易出问题。我把自己踩过的坑分享给你:
- 浮点数精度问题:我曾经用 == 比较两个航点的坐标,结果因为浮点数精度,明明相同的点被判为不同。后来统一用 tolerance 阈值比较,再没出过问题。
- 枚举序列化遗漏:有一次部署时忘了处理枚举类型,结果 JSON 解析直接报错。现在我在序列化函数里都会加个类型检查。
- YAML 缩进错误:YAML 对缩进敏感,团队里有人用 tab 有人用空格,导致文件解析失败。后来统一用 2 空格缩进,并在 CI 里加了格式检查。
总结一下:航点数据结构看似简单,但设计得好不好,直接影响到后续的任务规划、路径执行、数据存储。用 dataclass 建模,配合 JSON/YAML 序列化,这套组合拳我用了好几年,稳得很。
好了,航点数据结构就聊到这。代码都在上面了,你可以直接拿去用。有什么问题,欢迎交流。
公众号:蓝海资料掘金营,微信deep3321