尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Qt + librviz + ROS打造一体化点云显示终端

Qt + librviz + ROS打造一体化点云显示终端 简介在机器人可视化开发中3D点云显示与交互界面往往是上位机系统的核心能力。传统做法是独立运行rviz窗口再配合自研Qt面板但这种方式在界面整合、自动化控制与现场部署上存在明显局限。librviz作为rviz的库化形态将渲染面板以Qt组件形式嵌入自研程序通过VisualizationManager和RenderPanel提供完整的API控制能力既保留了点云、激光扫描、模型等全部Display类型又实现了显示与控制逻辑的无缝融合。基于Ubuntu 20.04与ROS Noetic环境结合Qt 5.12和PCL点云库开发者能够定制出符合业务需求的一体化显示终端广泛应用于机器人调试、产品级上位机交付及教学演示等场景。本文围绕librviz的工程实践从环境配置、CMake构建、核心封装到点云性能优化系统梳理嵌入式可视化方案的技术路线与常见坑点为构建高效、稳定的机器人可视化系统提供参考。1. 为什么要把rviz塞进自研Qt界面librviz的价值边界先说一下我为什么会做这个项目。之前调试一台移动机器人传感器有激光雷达和深度相机上位机需要实时显示3D点云、切换视角、叠加机器人模型同时还希望和采集程序、参数面板放在同一个窗口里。最开始的做法很简单直接开一个独立rviz窗口另开一个Qt程序做控制面板两边各管各的。用着用着问题就出来了rviz窗口一旦多了切来切去非常容易点错自研面板和rviz之间如果要做联动还得靠ROS topic传消息更麻烦的是交付给现场人员使用的时候让他们单独去操作rviz不熟悉的人很容易误触。后来就决定把rviz直接嵌到自己的Qt主窗口里做成一个真正的“一体化显示终端”也就是标题里说的Qt librviz ROS方案。1.1 librviz、rviz之间到底是什么关系先说概念。librviz是rviz的库化形态本质上就是把rviz这个独立程序的核心渲染和交互能力拆出来作为一套可嵌入的Qt组件对外提供。你写的Qt程序通过#include rviz/visualization_manager.h、#include rviz/render_panel.h这些头文件把rviz的渲染面板当作一个普通的QWidget加入到自己的窗口布局里然后通过rviz::VisualizationManager来管理显示内容。它和直接启动rviz程序最大区别在于librviz给了你全部控制权添加Display、切换Fixed Frame、设置视角、读取鼠标坐标这些都是通过API来操作而不是解析命令行或者靠用户手动点界面。用一句话概括rviz是可以直接用的成品车librviz是给你一个发动机和底盘让你按自己的需求造一辆专属车。这里面有一个值得注意的点——librviz并不是rviz的简化版它几乎包含了rviz所有的显示类型。点云、激光扫描、机器人模型、地图、路径、相机图像这些Display类型在librviz里都能用并不存在“嵌入版功能缩水”的情况。真正的区别只是在程序里你要自己写逻辑去创建Display、绑定Topic而不是靠鼠标在图形界面里点“Add”。1.2 这个方案解决的三个核心问题第一是界面业务一体化。点云显示、控制按钮、状态监控、数据录制全部放在同一个窗口操作人员只需要理解你设计的界面不需要去学rviz的使用习惯。第二是自动化控制。你可以用程序控制视角切换、读取鼠标选中的点坐标、根据程序状态动态加载不同Topic这在独立rviz里很难做到。第三是部署方便。嵌入式的界面配合你的主程序一起发布现场人员拿到的是一个“完整工具”而不是一堆需要手动打开的ROS程序。当然也要说清楚边界。如果你只是做算法验证自己开着rviz调一调显示效果那完全没必要用librviz直接rviz图形界面更快。如果你的目标是做产品级的上位机、交付给客户使用的监控软件、或者需要深度定制的教学演示工具那librviz就是绕不开的选择。2. 环境准备Qt、ROS、PCL版本匹配的那些坑标题里提到的三个组件Qt、librviz、ROS版本匹配是第一个门槛。我在这个项目上反复折腾过两轮最开始用的是ROS Melodic配Qt5.9后面换到Ubuntu 20.04 ROS Noetic Qt5.12整体才稳定下来。下面把版本选型和环境搭建的关键步骤写清楚。2.1 版本选型与验证方法RoboStudio系列里librviz对ROS版本的依赖非常敏感因为它会链接ROS的很多核心库比如roscpp、rviz、visualization_msgs这些库在不同ROS版本里API有变化。最稳妥的思路是ROS主版本和Qt主版本遵循系统默认。Ubuntu 20.04官方源里自带的是Qt 5.12.8ROS Noetic官方支持的就是这个组合这也是我推荐的最省心配置具体版本组合可以参考下面的表格。系统ROS版本Qt版本推荐程度备注Ubuntu 18.04MelodicQt 5.9.5可行老项目兼容首选Ubuntu 20.04NoeticQt 5.12.8首选官方支持、库最全Ubuntu 22.04HumbleQt 5.15不推荐依赖差异大需大量patch验证当前环境最简单的方法在终端依次执行qmake --version echo $ROS_DISTRO rosversion rviz如果三个命令都能正常输出说明基础环境就绪。需要注意一个重要细节不要自己手动从Qt官网下载安装包替换系统的Qt尤其是不要安装Qt6版本进入这个组合。ROS Noetic的库是按照Qt5编译的你用Qt6的qmake去编译librviz相关代码链接阶段会报一堆莫名其妙的undefined reference错误。如果项目必须用新版Qt那代价是把rviz相关的库全部自行基于Qt6重新编译工程量非常大普通人不要碰。2.2 创建工作空间和依赖安装环境准备好之后建议创建一个独立的工作空间专门放这个界面工程不要和已有的ROS功能包混在一起。这样做的好处是隔离依赖避免因为装了其他乱七八糟的包导致librviz的版本被覆盖。我习惯的目录结构如下mkdir -p ~/rviz_ui_ws/src cd ~/rviz_ui_ws/src catkin_init_workspace然后安装librviz开发所需的依赖包sudo apt-get install ros-noetic-rviz ros-noetic-visualization-msgs \ ros-noetic-interactive-markers ros-noetic-geometry-msgs \ ros-noetic-sensor-msgs ros-noetic-pcl-ros \ libqt5svg5-dev libqt5x11extras5-dev这里有一个很关键的点ros-noetic-rviz这个包不带dev头文件开发librviz还需要ros-noetic-rviz对应的头文件它通常随包一起装到/opt/ros/noetic/include/rviz/目录下面。如果catkin_make的时候提示找不到rviz/visualization_manager.h说明你的ROS安装不完整需要先完整安装桌面版sudo apt-get install ros-noetic-desktop-full2.3 CMakeLists的写法这里最容易卡壳librviz工程的CMakeLists.txt写法相对固定我直接分享一个能编译通过的骨架照着抄基本不会出问题。cmake_minimum_required(VERSION 3.0.2) project(rviz_ui) set(CMAKE_CXX_STANDARD 14) set(CMAKE_INCLUDE_CURRENT_DIR ON) find_package(catkin REQUIRED COMPONENTS roscpp rviz visualization_msgs geometry_msgs sensor_msgs pcl_ros pcl_conversions ) find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets ) catkin_package( INCLUDE_DIRS include LIBRARIES rviz_ui CATKIN_DEPENDS roscpp rviz visualization_msgs geometry_msgs sensor_msgs ) include_directories( include ${catkin_INCLUDE_DIRS} ${Qt5_INCLUDE_DIRS} ) set(SOURCES src/main.cpp src/main_window.cpp src/display_wrapper.cpp ) set(HEADERS include/main_window.h include/display_wrapper.h ) add_executable(rviz_ui_node ${SOURCES} ${HEADERS}) target_link_libraries(rviz_ui_node ${catkin_LIBRARIES} Qt5::Widgets Qt5::Gui Qt5::Core )注意一点如果用catkin_make编译务必确保在package.xml里声明了build_dependrviz/build_depend和exec_dependrviz/exec_depend否则依赖解析会失败。我一开始就是漏了这一步在编译时正常运行时提示找不到rviz的相关动态库排查了很久才想起来是package.xml的问题。3. 核心代码组织从QMainWindow到rviz显示面板这个项目的代码结构不复杂核心就三个文件main.cpp、main_window.cpp、display_wrapper.cpp。下面逐个讲解每个文件的作用和关键代码保证你能直接复制到自己的工程里。3.1 main.cpp初始化ROS节点librviz的基础是ROS节点所以程序启动时首先要初始化ROS。这里有一个细节ros::init必须在创建QApplication之前还是在之后我的实测结论是先创建QApplication再调用ros::init也可以但先ros::init能避免一些信号处理冲突。建议按下面的顺序#include QApplication #include ros/ros.h #include main_window.h int main(int argc, char** argv) { QApplication app(argc, argv); ros::init(argc, argv, rviz_ui_node); MainWindow w; w.show(); return app.exec(); }注意ros::init的节点名称必须是唯一的如果你同时启动了多个这个程序第二个会启动失败提示node name冲突。可以在节点名后面加时间戳或者进程ID来避免比如rviz_ui_node_ std::to_string(getpid())。3.2 main_window.h和main_window.cpp主窗口骨架主窗口继承自QMainWindow核心是持有一个DisplayWrapper对象在窗口初始化时创建显示面板。关键代码// main_window.h #ifndef MAIN_WINDOW_H #define MAIN_WINDOW_H #include QMainWindow #include QScopedPointer class DisplayWrapper; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); ~MainWindow(); private: QScopedPointerDisplayWrapper display_wrapper_; }; #endif// main_window.cpp #include main_window.h #include display_wrapper.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { display_wrapper_.reset(new DisplayWrapper(this)); setCentralWidget(display_wrapper_-getWidget()); setWindowTitle(点云显示终端 - Qt librviz); resize(1280, 800); } MainWindow::~MainWindow() { }这个骨架非常简单实际项目中你可以在centralWidget之外再堆控制面板、状态栏、工具栏等但核心显示区域就是display_wrapper_这个组件。3.3 display_wrapper.cpp最关键的封装这是整个项目的核心负责创建rviz的RenderPanel、VisualizationManager并提供添加Display的方法。完整代码如下// display_wrapper.h #ifndef DISPLAY_WRAPPER_H #define DISPLAY_WRAPPER_H #include QWidget #include QScopedPointer namespace rviz { class RenderPanel; class VisualizationManager; } class DisplayWrapper : public QWidget { Q_OBJECT public: explicit DisplayWrapper(QWidget *parent nullptr); ~DisplayWrapper(); QWidget* getWidget(); void addDisplay(const QString display_type, const QString name, bool enabled true); private: rviz::RenderPanel* render_panel_; rviz::VisualizationManager* manager_; }; #endif// display_wrapper.cpp #include display_wrapper.h #include QVBoxLayout #include rviz/render_panel.h #include rviz/visualization_manager.h #include rviz/display.h DisplayWrapper::DisplayWrapper(QWidget *parent) : QWidget(parent) , render_panel_(nullptr) , manager_(nullptr) { QVBoxLayout* layout new QVBoxLayout(this); layout-setContentsMargins(0, 0, 0, 0); render_panel_ new rviz::RenderPanel(this); layout-addWidget(render_panel_); manager_ new rviz::VisualizationManager(render_panel_); render_panel_-initialize(manager_-getSceneManager(), manager_); manager_-initialize(); manager_-startUpdate(); // 设置默认固定坐标系 manager_-setFixedFrame(map); } DisplayWrapper::~DisplayWrapper() { if (manager_) { manager_-stopUpdate(); delete manager_; manager_ nullptr; } } QWidget* DisplayWrapper::getWidget() { return this; } void DisplayWrapper::addDisplay(const QString display_type, const QString name, bool enabled) { rviz::Display* display manager_-createDisplay(display_type, name, enabled); if (!display) { qWarning(创建Display失败: %s, qPrintable(display_type)); } }这段代码里面有三个容易被忽略的关键点第一次写的时候我全踩过。第一个是render_panel_-initialize(manager_-getSceneManager(), manager_)这一句必须在manager_创建之后、manager_-initialize()之前调用。如果顺序反了面板会显示一片黑没有任何报错。这算是最典型的“不报错但不出图”问题。第二个是manager_-startUpdate()这个函数启动了rviz内部的渲染循环。很多初学者忘了调它或者调用了但放在initialize()之前结果就是界面能打开但点云、机器人模型全部不刷新。正确的顺序是创建manager、initialize manager、startUpdate这三步缺一不可。第三个是析构函数。必须调用manager_-stopUpdate()之后再delete manager_否则会偶发崩溃尤其是在程序退出时。这个问题在rviz的issue区也被人提过很多次原因是渲染线程还在跑你在主线程把manager删了渲染线程去访问一个已经释放的对象直接segmentation fault。4. 点云显示与交互扩展从静态浏览到动态控制封装好DisplayWrapper之后下一步就是往里面加实际要显示的内容。对于点云显示通常的做法是创建rviz/PointCloud2类型的Display然后订阅传感器数据或者从bag里面回放。下面详细说。4.1 添加点云Display的三种方式第一种是在初始化时直接通过代码添加display_wrapper_-addDisplay(rviz/PointCloud2, Cloud, true);第二种是更精细的控制先创建Display再设置具体的Topic名和显示参数rviz::Display* cloud_display manager_-createDisplay(rviz/PointCloud2, Cloud, true); if (cloud_display) { cloud_display-subProp(Topic)-setValue(/velodyne_points); cloud_display-subProp(Style)-setValue(Points); cloud_display-subProp(Size (Pixels))-setValue(3); }subProp是rviz属性系统的核心接口每个显示类型都有对应的属性树。PointCloud2的属性树里常用节点包括Topic、Style、Size (Pixels)、Color Transformer、Color Constant等。修改属性后需要调用cloud_display-setEnabled(true)来触发更新。第三种是开放给用户在界面上加一个下拉框或者LineEdit让用户手动输入Topic名然后程序动态创建Display。这种方式更适合产品化因为不同场景下点云的Topic名可能不同写死在代码里不够灵活。4.2 关于Topic名的一个隐藏坑使用librviz显示点云时rviz默认只订阅std_msgs::Header里面frame_id与当前Fixed Frame一致的PointCloud2消息。如果你的点云frame_id是velodyne而Fixed Frame设置成了map点云就不会显示rviz也不会报错。此时要么把Fixed Frame改成velodyne要么保证TF树里有velodyne到map的坐标变换。我之前调试时花了不少时间才意识到是这个原因因为程序没崩溃、界面正常、Topic也有数据但点云就是不见。实际调试时可以打印当前frame_idrostopic echo /velodyne_points -n 1 | grep frame_id如果显示的不是预期的Fixed Frame就按上面的思路处理。4.3 交互工具与视角控制rviz的面板本身支持鼠标交互默认是左键旋转、中键平移、滚轮缩放这部分功能在librviz中天然存在不需要额外开发。但如果你要控制视角切换比如一键回到俯视图、正视图就需要用API来设置。通过代码切换视角核心是获取当前rviz的rviz::ViewController对象。实践中我使用的方式如下rviz::ViewController* controller manager_-getViewController(); if (controller) { controller-subProp(Distance)-setValue(10.0); controller-subProp(Yaw)-setValue(0.0); controller-subProp(Pitch)-setValue(0.8); controller-subProp(Focal Point)-setValue(QVariant::fromValue(Ogre::Vector3(0, 0, 0))); }其中Yaw和Pitch对应的是球坐标的方位角Distance是相机到Focal Point的距离单位是米。这种方式适合设置固定视角比如“俯瞰全局”或者“跟踪机器人后方”。如果要做视角跟随比如点云数据实时移动时相机自动跟随物体运动就不能只设置一次了需要每次收到新消息时更新Distance之外的焦点。更高效的做法是保持一个定时器每100ms把当前相机的位姿同步到机器人当前位姿代码逻辑并不复杂核心就是QTimer::singleShot(100, this, [this]() { rviz::ViewController* controller manager_-getViewController(); controller-subProp(Focal Point)-setValue(QVariant::fromValue(current_position)); // 重新启动定时器 });4.4 坐标系管理当面板里同时存在多个传感器多传感器融合时面板里往往要同时显示雷达点云和深度相机点云它们的frame_id不同就需要在rviz里设置Fixed Frame为全局坐标系比如map或odom并保证TF树正确发布。librviz的TF管理是自动的只要有/tf和/tf_static话题在发布rviz内部就会自动维护坐标变换关系。但如果你只写了一个静态坐标变换比如把velodyne固定在base_link前方0.5米在librviz里也需要借助tf2_ros::StaticTransformBroadcaster来发布否则rviz拿到了点云数据也不知道该怎么画。我的建议是这种静态变换统一放在一个独立的ROS节点里启动不要在界面程序里写死这样便于统一管理。5. 实测中的性能问题与解决思路点云显示最让人头疼的就是性能。一次显示几十万甚至上百万个点任何计算机都会卡。我实测过几种典型场景整理出下面的性能对照表方便你有个直观印象。点数个帧率fps体验5万以下60流畅10万~20万30~40基本流畅可操作50万15~25能看交互有迟滞100万以上10以下掉帧明显缩放卡顿从表里能看出20万点是一个分界线。实际项目里点云数据动辄就是雷达一圈几万点深度相机一帧几十万点如果不做处理界面很快就会卡成PPT。5.1 降采样最实用的性能优化手段在数据源头降采样是最推荐的方案。PCL里的VoxelGrid就是一个非常成熟的下采样工具它把空间划分成一个个小立方体网格每个网格内保留一个代表点。源码层面的用法如下#include pcl/filters/voxel_grid.h #include pcl_conversions/pcl_conversions.h // 假设cloud是pcl::PointCloudpcl::PointXYZI::Ptr pcl::VoxelGridpcl::PointXYZI voxel; voxel.setInputCloud(cloud); voxel.setLeafSize(0.05f, 0.05f, 0.05f); // 5cm体素 pcl::PointCloudpcl::PointXYZI::Ptr filtered(new pcl::PointCloudpcl::PointXYZI); voxel.filter(*filtered);LeafSize设置很关键。设置太小比如1cm几乎不会减少点数性能提升有限设置太大比如50cm则会把细节全抹掉。我做过测试在10米范围内做建图时0.05f比较合适对近距离的桌面级点云0.01f到0.02f之间效果更好。这个参数没有绝对标准要根据你传感器的量程和场景来调。体素大小的选择原则很简单保证点云应用不塌细节的前提下让点数尽可能少。降采样之后再配合rviz/PointCloud2里面的Decay Time属性设置点云在画面中的滞留时间既能保障帧率又保留环境连续性。Decay Time单位是秒设成0表示每一帧都清空设成大于0就能看到点云叠加效果这在机械臂避障场景里很好用。5.2 显示端的优化减少渲染负担如果降采样还不够就得在显示端做文章。rviz的PointCloud2 Display自带两个和性能相关的属性Style和Size (Pixels)。Style选Points时每个点渲染为一个像素比选Squares或Tiles要快很多Size (Pixels)尽量保持在1~3之间不要开大。实测同一点云Points Size1比Squares Size3渲染速度快了将近2倍。还可以在Display的Color Transformer里选FlatColor而不是AxisColor或Intensity因为后者的颜色计算需要遍历每个点做映射CPU和GPU开销都会增加。当你的场景对颜色精度要求不高时直接设置一种固定颜色能节省大量渲染时间。5.3 线程模型不要让UI线程卡死我见过不少二次开发的程序把耗时的点云处理直接写在主线程里结果界面一操作就假死。正确做法是把耗时任务放到工作线程处理完再通过信号槽通知UI线程更新。Qt的信号槽机制天然跨线程安全所以架构上可以这样设计// 工作线程中处理PCL void Worker::processCloud(pcl::PointCloudpcl::PointXYZI::Ptr input) { pcl::VoxelGridpcl::PointXYZI voxel; voxel.setInputCloud(input); voxel.setLeafSize(0.05f, 0.05f, 0.05f); pcl::PointCloudpcl::PointXYZI::Ptr filtered(new pcl::PointCloudpcl::PointXYZI); voxel.filter(*filtered); emit cloudProcessed(filtered); // 通过信号发回UI线程 }UI线程收到cloudProcessed信号后再调用pcl_conversions::toROSMsg或者直接发布成PointCloud2消息由rviz Display接收显示。这样处理线程和界面线程互不干扰是保证交互流畅的基础。6. 进阶方向多视图、离屏渲染与状态同步基础的点云显示界面跑通后就有余力去琢磨一些更贴近实际需求的功能了。下面几个方向是我们在产品迭代过程中实际做过的可以给你一个扩展的参考。6.1 多视图同步显示有些场景需要同时看俯视图和立体视角或者同时显示全局地图和局部点云。librviz支持在同一个程序中创建多个VisualizationManager和RenderPanel每个面板独立管理自己的Display集合和视角。我建议用两个构造函数不同的DisplayWrapper对象各自设置不同的Fixed Frame一个固定map另一个固定base_link这样就能实现“全局局部”同屏显示。多视图同步操作时让两个面板的ViewController同步即可做一个信号槽连接操作频繁的时候定时器同步比实时同步更省资源。6.2 离屏渲染与截图保存如果你想定时把当前点云画面存成图片或者做远程监控画面推送就需要离屏渲染。librviz本身没有直接提供截图API但Ogre渲染引擎提供了RenderTarget的writeContentsToFile方法。实践中可以通过给RenderPanel的viewport注册一个render listener在渲染完成后抓取像素并保存为QImage。这个方案在工程上可行但代码量不小而且要注意OpenGL上下文和线程问题。更省事的方式是用Qt的QWidget::grab()抓取面板内容只是它依赖窗口可见性和系统合成器离屏时不一定准确。6.3 与机器人状态联动librviz嵌入带来的另一个好处是当你点击点云中的某个点时可以得到该点的3D坐标进而可以联动机械臂规划、或者实现“点选建图”。具体做法是订阅rviz的rviz::SelectionManager该管理器会提供选中物体的位置信息。实现路径稍微曲折一些但确实可行。实际项目中用它做过一个“点击选点→计算距离→反馈给底盘停障”的小功能演示效果非常好。6.4 大规模点云场景的分块加载如果场景大到整个园区点云数据轻松超过几千万点单纯靠VoxelGrid降采样也不够用。这时候就需要分块加载把大场景按空间划分成多个块只把相机视锥体附近的点云发布给rviz远处的点云不加载或者用很粗糙的降采样。这种“LOD式”的点云管理策略在室外机器人场景很管用。如果你的数据和场景正好符合这种形态建议从第一步就考虑好坐标分区和块管理机制否则后期再重构工作量很大。7. 部署与发布从开发机到目标机器代码调试完毕最后一步就是部署。ROS程序部署比普通Qt程序麻烦一点因为依赖的ROS包、动态库、消息定义都要跟着走。常用的做法是准备一个启动脚本里面带上source /opt/ros/noetic/setup.bash、source ~/rviz_ui_ws/devel/setup.bash然后运行你的节点。现场机器如果没有编译环境可以用catkin_make install把整个工作空间装到指定目录再把install目录拷贝过去。依赖的库要么用ldd排查后逐一复制要么在目标机器上用apt提前装好。我只分享一个通用原则依赖越少越容易部署动态链接库全带齐了最省心。发布时有个细节不要在界面程序里保留调试日志、不要用绝对路径去读取配置文件统一用~/.config/your_app或者环境变量来管理工作目录。否则从开发机挪到现场机器很容易出找不到资源文件的问题。最后再分享一个小经验用librviz做开发一定要保持ROS程序和Qt界面程序的调试通道畅通。我习惯在界面上加一个简单的QPlainTextEdit作为日志窗口把rviz的Warning、Error信息重定向进去这样比用终端看日志更直观。调试时能立刻定位是TF问题、Topic问题还是Display配置问题省去大量抓耳挠腮的时间。希望这篇整理能帮你少走弯路直接做出一套既能用又好看的ROS点云显示界面。本文还有配套的精品资源点击获取
返回列表