简介:本资源是一套面向自动驾驶与智能交通领域开发者的三维路径规划系统C++实现源码,适用于具备C++和Qt基础的中高级开发者学习无人车在复杂地形中的三维环境建模与轨迹生成技术。系统支持TIFF格式高程数据加载、基于Qt DataVisualization的三维场景渲染、JSON格式环境数据持久化,以及开源三维曲线库驱动的路径可视化绘制,覆盖从感知输入到规划输出的完整闭环。压缩包共35个文件,含10个头文件(h/hpp)与10个源文件(cpp)构成核心模块,3个TIFF地形数据样本、2个JSON配置文件、2个Qt翻译文件(ts)及1个UI界面文件,整体44.02MB,结构清晰、模块解耦度高,便于二次开发与算法替换。目前已有89人学习下载,读者可直接运行调试三维可视化效果,深入理解无人车路径规划在真实三维空间中的数据流与渲染逻辑,并复用环境加载、路径绘制等通用组件。
1. 三维路径规划不是“把A到B连条线”,而是让无人车在真实地形上安全、平滑、可执行地移动
很多人以为路径规划就是调个A*或RRT,在二维网格上画条线——但真实无人车面对的是起伏的丘陵、斜坡上的碎石堆、带坡度的停车场坡道,甚至月面模拟地形(项目里就含moon1.tif和moon2.tif)。这个 C++ 实现的三维路径规划系统,核心价值在于:它不抽象成“点-线-面”,而是把高程数据(TIFF)、车辆运动学约束(navxyztheta)、轨迹平滑性(spiral_constraint)和可视化验证(QPlot3D+SurfaceGraph)全链路打通。它用 Qt 框架构建 GUI 交互层,底层用 direct collocation 方法求解带动力学约束的最优轨迹,不是纯几何避障,而是“能开得动”的路径。适合正在做高校无人车课程设计、ROS 仿真后需落地验证、或需要快速搭建三维轨迹验证原型的工程师——尤其当你手头已有激光雷达点云转 TIFF 的 pipeline,或要对接 Apollo/autoware 的三维地图模块时,这套代码能省掉 70% 的可视化胶水代码和 JSON 中间格式适配工作。
2. 环境建模与数据流:从 TIFF 高程图到可序列化的 JSON 地形描述
2.1 TIFF 作为三维环境输入的工程合理性
项目选择 TIFF 格式承载三维环境,并非随意——TIFF 支持 32-bit 浮点像素,能无损保存毫米级高程精度(如crop.tif中每个像素代表地面某点海拔),且被 GDAL、OpenCV、QtImage 广泛支持。关键在于readtiff.h/cpp的实现逻辑:它不简单加载为 QImage,而是提取QImage::bits()后按float*强制 reinterpret_cast,再逐像素映射到三维空间坐标系:
// src/readtiff.cpp 关键片段 bool ReadTiff::load(const QString &path, QVector3D &origin, float &scale) { QImage img = QImage(path); if (img.isNull()) return false; const float *data = reinterpret_cast<const float*>(img.bits()); m_heightMap.clear(); for (int y = 0; y < img.height(); ++y) { for (int x = 0; x < img.width(); ++x) { int idx = y * img.width() + x; float z = data[idx]; // 直接读取浮点高程值 m_heightMap.append(QVector3D(x * scale, y * scale, z)); } } origin = QVector3D(0, 0, 0); // 坐标原点设为左下角 return true; }注意:TIFF 必须是单通道浮点型(而非 8-bit 灰度),否则
reinterpret_cast<float*>会读出乱码。可用gdalinfo crop.tif验证Band 1的Data Type = Float32。
2.2 JSON 地形缓存机制与结构设计
为避免每次启动都重解析 TIFF(耗时且不可控),系统将高程网格序列化为env.json。该 JSON 不是简单 dump 数组,而是采用空间索引优化:
{ "metadata": { "width": 512, "height": 512, "scale": 0.1, "origin": [0.0, 0.0, 0.0] }, "grid": [ [123.45, 124.12, ...], // 第0行z值 [125.67, 126.01, ...], // 第1行z值 ... ] }utils.hpp中的saveEnvToJson()函数使用nlohmann::json库生成此结构,关键参数控制精度:
// utils.hpp void saveEnvToJson(const QVector<QVector<float>>& grid, const QString& path, float scale = 0.1f) { nlohmann::json j; j["metadata"]["width"] = grid.size(); j["metadata"]["height"] = grid.isEmpty() ? 0 : grid[0].size(); j["metadata"]["scale"] = scale; j["metadata"]["origin"] = {0.0f, 0.0f, 0.0f}; for (const auto& row : grid) { std::vector<float> jsonRow(row.begin(), row.end()); j["grid"].push_back(jsonRow); } QFile f(path); f.open(QIODevice::WriteOnly); f.write(j.dump(2).c_str()); // 缩进2空格,便于调试 f.close(); }提示:
j.dump(2)生成可读 JSON,但生产环境建议用j.dump()(无缩进)减小文件体积。env.json体积约为同分辨率 TIFF 的 1.8 倍(因文本编码),但加载速度提升 5~8 倍(免去 TIFF 解码)。
2.3 环境类Environment_NavXYZTheta的运动学耦合设计
environment_navxyztheta.h定义的环境类不是静态栅格,而是与车辆位姿强绑定:NavXYZTheta表示位置(x,y,z)+ 偏航角θ,其getElevationAt()方法支持任意(x,y)插值查询高程:
// environment_navxyztheta.cpp float Environment_NavXYZTheta::getElevationAt(float x, float y) const { int col = static_cast<int>(x / m_scale); int row = static_cast<int>(y / m_scale); if (col < 0 || col >= m_width || row < 0 || row >= m_height) return 0.0f; // 边界外返回0 // 双线性插值,避免阶梯状地形 float dx = x / m_scale - col; float dy = y / m_scale - row; float z00 = m_grid[row][col]; float z10 = m_grid[row][std::min(col+1, m_width-1)]; float z01 = m_grid[std::min(row+1, m_height-1)][col]; float z11 = m_grid[std::min(row+1, m_height-1)][std::min(col+1, m_width-1)]; return z00*(1-dx)*(1-dy) + z10*dx*(1-dy) + z01*(1-dx)*dy + z11*dx*dy; }该设计使路径规划器能实时查询“若车头朝 θ 方向行驶到某点,轮胎是否悬空?”——这是二维规划器无法回答的关键问题。
| 参数 | 作用 | 典型值 | 修改建议 |
|---|---|---|---|
m_scale | 地图分辨率(米/像素) | 0.1 | 城市场景用0.2~0.5,月面模拟用0.05 |
m_grid | 高程二维数组 | QVector<QVector<float>> | 内存敏感场景可改用QVector<float>一维存储 |
| 插值方式 | 影响地形平滑度 | 双线性 | 需硬边沿(如台阶)可切换为 nearest-neighbor |
3. 路径生成与约束求解:Direct Collocation 在三维轨迹优化中的落地
3.1 为什么选 Direct Collocation 而非 RRT* 或 A*
RRT* 在三维高程图上易陷入局部最优(如绕不过山脊),而 A* 需离散化导致轨迹锯齿。本项目planner/planner.h采用 Direct Collocation(直接配点法),将连续轨迹x(t), y(t), z(t), θ(t)离散为 N 个时间点上的状态向量,通过非线性优化求解满足以下约束的平滑曲线:
- 运动学约束:
ẋ = v·cosθ,ẏ = v·sinθ,ż = v·tanφ(φ 为俯仰角,由地形坡度决定) - 几何约束:
|κ| ≤ κ_max(曲率限制,防急转弯) - 螺旋约束:
spiral_constraint.h强制轨迹段为等螺距螺旋线,保证车辆横摆角速度连续 - 避障约束:
z(t) ≥ getElevationAt(x(t),y(t)) + safety_margin
directCollocationSolver.h封装了 IPOPT 求解器接口,但项目未自带 IPOPT 库——需自行编译链接。CMakeLists.txt 中关键配置:
# planner/CMakeLists.txt 片段 find_package(IPOPT REQUIRED) target_link_libraries(planner PRIVATE ipopt) target_include_directories(planner PRIVATE ${IPOPT_INCLUDE_DIRS})注意:IPOPT 编译需启用
--with-mumps(推荐),否则大规模网格(>1024×1024)求解会失败。Ubuntu 下可sudo apt install coinor-libipopt-dev,但版本需 ≥3.14.4。
3.2 轨迹数据格式与 JSON 序列化协议
规划结果不存为二进制,而是mp.json,结构严格对应PlannerResult类:
{ "timestamp": "2022-09-03T16:38:37Z", "points": [ {"x": 0.0, "y": 0.0, "z": 0.0, "theta": 0.0, "v": 1.2}, {"x": 0.5, "y": 0.3, "z": 0.02, "theta": 0.1, "v": 1.5}, ... ], "constraints": { "max_curvature": 0.3, "safety_margin": 0.15 } }planner.cpp中saveTrajectoryToJson()使用相同nlohmann::json流程,但增加校验:
bool Planner::saveTrajectoryToJson(const QString& path) { nlohmann::json j; j["timestamp"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); for (const auto& pt : m_trajectory) { nlohmann::json p; p["x"] = pt.x(); p["y"] = pt.y(); p["z"] = pt.z(); p["theta"] = pt.theta(); p["v"] = pt.velocity(); j["points"].push_back(p); } // 强制校验:至少3个点,且z值单调非减(防倒车下坡) if (j["points"].size() < 3) return false; for (size_t i = 1; i < j["points"].size(); ++i) { if (j["points"][i]["z"] < j["points"][i-1]["z"] - 1e-3) { qWarning() << "Trajectory has illegal descent at point" << i; return false; } } // ... 写入文件 }3.3QPlot3D与SurfaceGraph的双视图协同渲染
路径绘制不依赖 OpenGL 手写 shader,而是复用 Qt DataVisualization 的Q3DSurface(地形) +Q3DScatter(路径点):
// plot3d/QPlot3D.cpp void QPlot3D::plotPath(const QVector<QVector3D>& points) { QScatter3DSeries *series = new QScatter3DSeries; QScatterDataArray *dataArray = new QScatterDataArray; dataArray->resize(points.size()); for (int i = 0; i < points.size(); ++i) { (*dataArray)[i].position = points[i]; (*dataArray)[i].color = QColor(Qt::red); // 路径点为红色 } series->setDataArray(dataArray); m_graph->addSeries(series); // 添加连线(Q3DScatter 不支持线,故用Q3DSurface模拟细柱) QSurface3DSeries *lineSeries = new QSurface3DSeries; QSurfaceDataArray *lineArray = new QSurfaceDataArray; lineArray->resize(1, points.size()); for (int i = 0; i < points.size(); ++i) { QSurfaceDataRow *row = new QSurfaceDataRow; row->resize(1); (*row)[0].setPosition(points[i]); lineArray->append(row); } lineSeries->setDataArray(lineArray); m_graph->addSeries(lineSeries); }提示:
Q3DScatter渲染点云快,但无法画线;Q3DSurface画线需构造“单列多行”数据模拟细柱。实际项目中,若需高性能轨迹动画,应改用QCustom3DItem自定义线段。
4. Qt GUI 集成与跨平台构建:从 CMake 到可执行文件的完整链路
4.1 CMakeLists.txt 的模块化分层设计
项目根目录CMakeLists.txt采用三层次结构,避免 Qt 模块污染:
# 根 CMakeLists.txt cmake_minimum_required(VERSION 3.10) project(3DPathPlanner LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) find_package(Qt5 COMPONENTS Core Widgets Gui DataVisualization REQUIRED) # 子模块独立构建 add_subdirectory(src) add_subdirectory(planner) add_subdirectory(plot3d) # 主可执行文件 add_executable(3DPathPlanner main.cpp mainwindow.cpp src/utils.cpp src/readtiff.cpp # ... 其他源文件 ) target_link_libraries(3DPathPlanner PRIVATE Qt5::Core Qt5::Widgets Qt5::Gui Qt5::DataVisualization planner plot3d )planner/CMakeLists.txt单独管理 IPOPT 依赖,plot3d/CMakeLists.txt封装 Q3D 组件——这种解耦使你可单独测试路径规划器(无 GUI)或替换可视化模块。
4.2 Qt Designer UI 文件的动态绑定逻辑
mainwindow.ui是标准 Qt Designer 设计,但关键交互不在.ui中硬编码,而在mainwindow.cpp的on_actionLoad_Environment_triggered()中动态绑定:
void MainWindow::on_actionLoad_Environment_triggered() { QString path = QFileDialog::getOpenFileName(this, "Load TIFF", "", "TIFF Files (*.tif *.tiff)"); if (path.isEmpty()) return; QVector3D origin; float scale; if (!m_env.load(path, origin, scale)) { QMessageBox::critical(this, "Error", "Failed to load TIFF"); return; } // 动态更新UI状态 ui->statusBar->showMessage(QString("Loaded %1×%2 terrain, scale=%3m/pix") .arg(m_env.width()).arg(m_env.height()).arg(scale)); // 触发3D视图重绘 m_plot3D->updateEnvironment(m_env.getGrid(), scale); }注意:
QStatusBar显示分辨率信息,比弹窗更符合专业工具习惯;updateEnvironment()是QPlot3D的自定义槽函数,内部调用Q3DSurface::setDataSet()刷新地形。
4.3 Windows/Linux/macOS 构建差异与调试技巧
- Windows:需安装 Qt Online Installer(选
Qt 5.15.2 MinGW 7.3 64-bit),CMakeLists.txt中find_package(Qt5 ...)自动定位;IPOPT 用 vcpkg 安装:vcpkg install ipopt:x64-windows - Linux:
sudo apt install qt5-default libqt5datavisualization5-dev,IPOPT 从源码编译(Ubuntu 22.04 自带版本过低) - macOS:Qt 6 不兼容
QtDataVisualization,必须用 Qt 5.15;Xcode 命令行工具需xcode-select --install
调试时重点监控:
qInstallMessageHandler()捕获 Qt 警告(如QOpenGLContext::swapBuffers()失败)QFile::exists("env.json")检查缓存路径是否正确QElapsedTimer测量readtiff.cpp加载耗时(TIFF >100MB 时需异步加载)
5. 实战技巧:如何用这套代码快速验证泊车路径规划算法
5.1 构造泊车场景的 TIFF 数据
泊车路径需处理微小坡度(<5°)和精确车位边界。不用真实激光扫描,用 Python 快速生成parking_lot.tif:
import numpy as np from PIL import Image # 创建 200x200 像素,0.05m/像素的泊车区域 width, height = 200, 200 scale = 0.05 grid = np.zeros((height, width), dtype=np.float32) # 添加车位线(凸起0.01m模拟反光标线) for i in range(50, 150): grid[i, 80] = 0.01 grid[i, 120] = 0.01 # 添加坡道(从y=0到y=50,z从0升至0.3m) ramp = np.linspace(0, 0.3, 50) for i in range(50): grid[i, :] = ramp[i] # 保存为32位浮点TIFF img = Image.fromarray(grid, mode='F') img.save('parking_lot.tif', format='TIFF', compression='none')生成后放入environment/目录,启动程序即可加载——比手动标注点云快 10 倍。
5.2 修改spiral_constraint.h适配泊车低速特性
默认螺旋约束针对高速场景(曲率上限 0.3),泊车需更小曲率:
// planner/spiral_constraint.h class SpiralConstraint { public: SpiralConstraint(float maxCurvature = 0.05f) // 泊车改为0.05 : m_maxCurvature(maxCurvature) {} bool isValid(const QVector3D& p1, const QVector3D& p2, const QVector3D& p3) const { // 计算三点曲率,公式略 float curvature = computeCurvature(p1, p2, p3); return curvature <= m_maxCurvature + 1e-4f; // 容差 } private: float m_maxCurvature; };修改后重新编译planner模块,路径将自动变缓——这是纯 RRT* 难以做到的参数化控制。
5.3 用mp.json验证轨迹可行性
规划完成后,检查mp.json中相邻点的纵向坡度是否超限:
# 提取z坐标序列并计算最大坡度(单位:m/m) jq '.points[].z' mp.json | paste -sd ' ' | \ awk '{for(i=2;i<=NF;i++) d[i]=($i-$((i-1))) * 10; max=0; for(i in d) if(d[i]>max) max=d[i]; print "Max slope:", max}'若输出Max slope: 0.12,表示最大坡度 12%,低于乘用车 20% 爬坡能力,可直接部署。
本文还有配套的精品资源,点击获取