ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

QT+PCL+串口实现单线激光雷达实时点云可视化

QT+PCL+串口实现单线激光雷达实时点云可视化 1. 整体方案设计与技术选型1.1 为什么是“QTPCL串口QVtkWidget”这套组合单线激光雷达在AGV导航、安防监控、SLAM建图这些场景里几乎是标配传感器。我最初拿到这个需求时也犹豫过直接用一个现成的上位机软件看数据不就行了为什么非要自己在QT里写一套等到真正调试起来才明白现成工具只能看波形或者简单点云你要做二次开发、要跟自己的算法模块对接、要在界面上叠加业务逻辑没有一套可扩展的框架根本跑不起来。选QT做界面层理由很直接跨平台、信号槽机制天生适合处理串口这种异步数据流、QVTKWidget又能把VTK的渲染窗口嵌进QT的布局系统里。选PCL做点云处理层是因为它把点云的数据结构、滤波、可视化这一整套都封装好了不需要自己从头定义点云格式。串口就更不用说了绝大多数单线激光雷达——不管是思岚的RPLIDAR系列、镭神N10还是北阳的URG系列——都提供串口或类串口USB转串口的通信接口协议本质都是收发二进制帧。这套组合的关键在于数据链路要打通串口收原始字节流解析出距离和角度转成三维坐标塞进PCL点云对象再丢给VTK渲染。任何一环出问题后面的环节全是白搭。所以这个系列我打算拆成几篇来写第一篇先把“串口读到数据并在QVtkWidget里刷出实时点云”这条主链路跑通。1.2 方案选型时的两个关键取舍选型时我纠结过两个点。第一个是在PCL的Visualization模块里直接显示还是用QVTKWidget嵌入显示。一开始图省事直接用pcl::visualization::PCLVisualizer弹个独立窗口显示倒是快但问题也很明显没法跟QT界面融合、按钮和参数面板全得用VTK自己的交互器去画做业务界面非常别扭。后来换了QVTKWidget现在新版PCL里头叫QVTKOpenGLNativeWidget把VTK渲染窗口嵌进QT的主窗口里左侧放控制面板、右侧放点云视图这才是正常上位机的形态。第二个是串口库选QSerialPort还是第三方库。QSerialPort是QT官方模块pro文件里加一句QT serialport就能用信号槽机制跟QT主循环无缝配合数据来了自动发信号不用自己开线程轮询。第三方库虽然也有好用的但没必要引入额外依赖尤其做产品交付的时候少一个依赖就少一堆破事。1.3 这套方案的适用范围和基本盘这套方案适用于绝大多数通过串口/虚拟串口输出的单线激光雷达前提是雷达厂商提供协议文档。整个系统的基本盘就三个模块串口数据收发模块、数据解析与坐标转换模块、点云渲染显示模块。三个模块之间用QT的信号槽衔接耦合度低后面不管换雷达型号还是升级显示方式都只需要改中间那层。这个系列第一篇的目标读者我定位成有一定QT基础、想入门PCL点云可视化、又恰好被串口数据怎么变成点云卡住的人。不夸张地说90%的初学时间都耗在“数据格式对不上”和“怎么让它动起来”这两件事上。2. 环境搭建QT、PCL与QVTKWidget的版本恩怨2.1 编译器、QT版本、PCL版本的匹配关系我做这个项目时用的组合是Windows 10 Visual Studio 2019 QT 5.15.2 PCL 1.11.1 VTK 8.2.0。这个组合不敢说最稳但至少是社区里踩坑最少的一套。PCL 1.11.1官方编译包对应VTK 8.2QT 5.15.2又正好带Qt5VTK的编译支持三者版本能对上。先说编译器。PCL官方Release页面提供了PCL-1.11.1-AllInOne-msvc2019-win64.exe这个包注意它绑定的就是MSVC 2019所以QT的编译器套件必须选msvc2019_64而不是QT自带的MinGW。很多新手在这里卡住QT装的是MinGW版PCL却要求MSVC一跑就报一堆链接错误。查这个错误不用慌先确认三个东西的位数、版本、编译器套件是否统一——64位对64位、release对release、MSVC对MSVC三条都能对上90%的dll错误和链接错误就消失了。然后是PCL安装路径。AllInOne包默认装到C:\Program Files\PCL 1.11.1路径里有空格部分老版本CMake解析会有问题但PCL官方包一般能处理。保险起见我会在系统环境变量里设置PCL_ROOT指向安装目录不包含bin那层再把%PCL_ROOT%\bin和%VTK_ROOT%\bin加进PATH。不然后面运行时疯狂弹出“找不到pcl_common.dll”这种框。2.2 QVTKWidget的获取为什么PCL装在系统里还找不到这里有个隐藏的大坑PCL的AllInOne包虽然自带了VTK但你装了PCL不代表QT的designer面板里就多了QVtkWidget这个控件。我一开始在UI编辑器的左侧控件栏里到处找找了半天也没有。原因很简单QVtkWidget是VTK编译时生成的Qt插件PCL安装包基本不会帮你把插件放到QT的plugins目录里。解决办法有三个不用UI设计器直接在代码里new QVTKWidget然后手动加到QLayout里。最省事我项目里就是这么干的。手动把VTK编译输出目录下的QVTKWidgetPlugin.dll一般在vtk-bin/plugins/designer下复制到QT的plugins\designer目录里这样设计器里就能拖拽使用了。高版本VTK8.2以上改了新接口用的是QVTKOpenGLNativeWidget插件名也变了对应复制QVTKOpenGLNativeWidgetPlugin.dll。提示这个项目里我用的是老接口QVTKWidget因为PCL 1.11自带的VTK 8.2还是这套。如果你用PCL 1.12及以上VTK版本更高就得用QVTKOpenGLNativeWidget代码接口差不少别混着抄。2.3 CMake配置串口模块和全局依赖创建QT项目时pro文件如果跟我一样用qmake需要这样配置QT core gui serialport widgets greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 # PCL INCLUDEPATH $$(PCL_ROOT)/include/pcl-1.11 INCLUDEPATH $$(PCL_ROOT)/include/vtk-8.2 LIBS -L$$(PCL_ROOT)/lib \ -lpcl_common \ -lpcl_io \ -lpcl_visualization \ -lpcl_visualization_release用CMake的话CMakeLists.txt大致是这个骨架cmake_minimum_required(VERSION 3.12) project(LidarViewer) set(CMAKE_CXX_STANDARD 11) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) find_package(QT NAMES Qt5 REQUIRED COMPONENTS Core Gui Widgets SerialPort) find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets SerialPort) find_package(PCL 1.11 REQUIRED COMPONENTS common io visualization) find_package(VTK 8.2 REQUIRED) add_executable(${PROJECT_NAME} main.cpp mainwindow.cpp) target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Gui Qt5::Widgets Qt5::SerialPort ${PCL_LIBRARIES} ${VTK_LIBRARIES} )实际编译时MSVC下还需要加一条target_include_directories把VTK的include给进去因为PCL的AllInOne虽然带了VTK头文件但CMake的find_package(VTK)不一定能找到。当时我踩了这个坑后来直接在CMake里手动指定了VTK路径问题才解决。3. 串口数据读取从打开端口到拼出有效帧3.1 串口参数的确定逻辑串口这层用QT自带的QSerialPort参数很好配。单线激光雷达最常见的出厂配置是115200波特率、8位数据位、1位停止位、无校验、无流控。但不排除有些雷达默认460800甚至更高毕竟一帧转一圈动辄上千个点115200不一定够用。我拿到一台新的雷达时第一件事永远是查协议文档里出厂配置那页然后在QSerialPort里手写创建serialPort new QSerialPort(this); serialPort-setPortName(COM3); serialPort-setBaudRate(QSerialPort::Baud115200); serialPort-setDataBits(QSerialPort::Data8); serialPort-setParity(QSerialPort::NoParity); serialPort-setStopBits(QSerialPort::OneStop); serialPort-setFlowControl(QSerialPort::NoFlowControl); serialPort-setReadBufferSize(1024 * 1024); // 缓冲区给大一点这里有个值得注意的细节串口缓冲区的默认大小可能不够塞一整圈点云数据。单线雷达一圈点云数据量从几百字节到几十KB不等如果缓冲太小数据还没读完就被新数据冲掉解析时就会丢帧。我习惯把读缓冲区设置成8MB以上反正现在内存不缺宁可浪费不能丢失。设备名我写成COM3是测试环境的。更健壮的做法是运行时枚举可用串口列表然后从下拉框里选。QSerialPortInfo::availablePorts()方法返回一个QListQSerialPortInfo能拿到端口名、描述、厂商ID等。实际项目里界面上放一个刷新串口按钮配合这个枚举函数能省掉一半的插拔USB线烦恼。3.2 数据读取的两种姿势与信号槽QSerialPort接收数据有两种常见姿势第一种是阻塞式调用waitForReadyRead()加上设置超时时间。这种适合命令行小工具但在GUI里直接用会卡界面绝对不在上位机里这么干。第二种是信号槽异步方式这是我们的正解connect(serialPort, QSerialPort::readyRead, this, MainWindow::onSerialDataReceived);当串口缓冲区里有数据时QT主循环会在事件循环里触发readyRead信号我们只要在槽函数里读就行了。槽函数里只做一件事把数据append进一个接收缓冲的QByteArray然后交给解析模块去处理。不要在槽函数里做耗时的坐标变换、渲染操作否则界面一样卡。这个模式看起来很基础但它是整个实时显示的基石。串口数据是源源不断的高频数据流如果阻塞式读取QT的界面刷新信号压根挤不进去点云会一顿一顿的。3.3 一帧有效数据怎么拼出来单线雷达的数据帧一般长这样帧头比如0xFA 0x55 数据长度 角度区间信息 N个点的距离值 校验码 帧尾。不同厂商的格式差异很大有的雷达还会把转速、时间戳也塞进帧里。我调试时用的雷达协议是帧头两字节数据部分每两个字节表示一个测量点的距离值单位毫米一帧数据里包含的测量点对应扫描角度的一个扇形区间。完整的算法是先做缓冲拼接再做帧解析虽然代码不复杂但写的时候有几个细节void MainWindow::onSerialDataReceived() { QByteArray bytes serialPort-readAll(); dataBuffer.append(bytes); // 从缓冲区中循环提取完整帧 while (true) { // 1. 寻找帧头 int headIndex findHead(dataBuffer); if (headIndex 0) { // 没找到帧头保留最后2字节防止跨分包截断其余丢弃 if (dataBuffer.size() 2) { dataBuffer.remove(0, dataBuffer.size() - 2); } break; } // 2. 丢弃帧头之前的所有垃圾字节 if (headIndex 0) { dataBuffer.remove(0, headIndex); } // 3. 判断一帧长度是否足够 QByteArray frame parseFrame(dataBuffer); if (frame.isEmpty()) { break; // 数据不够等下一次readyRead再来 } processFrame(frame); } }这段逻辑里最有价值的是“保留最后2字节”这个操作。为什么要保留最后2字节因为串口数据是流式的一帧数据包可能被操作系统分两次发过来第一次收到前半截、第二次收到后半截。如果每次readyRead来了之后傻乎乎地从start搜帧头、找不到就清空缓冲那跨分包的那半帧就永远凑不齐数据解析率可能连50%都不到。保留帧头可能的起始字节等待下次数据到达后再次尝试搜索帧头这是串口编程的基础素养。3.4 校验与容错坏帧别硬解析解析出帧后一定要做校验。常见的有CRC16校验、累加和校验。我见过不少人在做串口解析的时候把校验省略了图省事结果就是偶尔出现一个莫名其妙的跳变点点云画面里冒出一个“飞点”位置在几十米开外把自动导航算法吓得一脚急刹车。校验的实现很简单在processFrame里加上去bool MainWindow::verifyChecksum(const QByteArray frame) { // 假设帧的结构: [头2字节][长度2字节][数据N字节][校验1字节] quint8 sum 0; for (int i 0; i frame.size() - 1; i) { sum static_castquint8(frame.at(i)); } return sum static_castquint8(frame.at(frame.size() - 1)); }如果校验失败直接丢弃这一帧不要把它解析出来的点云渲染到画面上。宁可在刷新率上少几帧也不能把脏数据喂给后面的算法。实测下来加了校验之后整个点云画面的稳定性会有质的提升。4. 数据解析与坐标转换把二进制变成点云4.1 协议帧结构的具体拆解雷达把原始测量数据按协议打包发送比如帧结构是帧头0xA5 0x5A信息字转速、起始角度、结束角度数据单元若干组距离值 信号强度校验8位累加和拿一帧实际抓包数据举例用串口调试助手把A5 5A 20 03 E8 00 00 01 2C 01 90 02 58 ...完整打印出来。其中A5 5A是帧头20 03代表一帧里包含30个测量点E8 00代表起始角度注意大小端实际是0x00E8即232乘上角度分辨率0.5度得到116度接着每两个字节是一个距离值比如01 2C是0x012C即300毫米表示该点距离雷达中心300毫米结尾的校验码是前面所有字节的累加和解析的代码段可以这样写void MainWindow::processFrame(const QByteArray frame) { // 跳过帧头 int offset 2; quint16 pointCount static_castquint8(frame.at(offset)) | (static_castquint8(frame.at(offset 1)) 8); offset 2; quint16 startAngleRaw static_castquint8(frame.at(offset)) | (static_castquint8(frame.at(offset 1)) 8); offset 2; double startAngle startAngleRaw * 0.5; // 角度分辨率0.5度 for (int i 0; i pointCount; i) { quint16 distance static_castquint8(frame.at(offset i * 2)) | (static_castquint8(frame.at(offset i * 2 1)) 8); double angle startAngle i * angleResolution; // 极坐标转XYZ存入点云 AddPointToCloud(distance * 0.001, angle); } }实际解析时务必确认协议文档里的字节序是大端还是小端。低字节在前还是高字节在前搞错一个点所有激光点全部镜像翻转看起来像照镜子一样半天查不出毛病。4.2 极坐标到XYZ的坐标变换雷达测出来的原始数据天然是极坐标一个距离r一个角度θ。但PCL的点云是三维直角坐标所以要做坐标变换公式很简单x r * cos(θ) * cos(φ) y r * sin(θ) * cos(φ) z r * sin(φ)对于单线雷达俯仰角φ通常为0也就是说激光束落在同一个平面内z方向可以忽略不计基本就退化成二维了。写入PCL时只要把z设成0即可void MainWindow::AddPointToCloud(double distance, double angleDeg) { double angleRad qDegreesToRadians(angleDeg); float x static_castfloat(distance * std::cos(angleRad)); float y static_castfloat(distance * std::sin(angleRad)); pcl::PointXYZ point; point.x x; point.y y; point.z 0.0f; cloud-points.push_back(point); }4.3 一帧刷一张图还是逐点累积这里有实时的策略问题是每来一帧点云就把整个点云对象的点清空重刷还是把帧点追加到全局点云里我的做法是按帧清空重建。因为雷达每转一圈点云理论上应该完全覆盖周围一圈如果一直叠加上一圈的数据会跟这一圈混在一起界面上就会出现“拖影”。只有一种情况建议做累积——你需要看固定障碍物的历史轨迹或者做某些占用栅格的调试时。核心代码里我是先把点云对象里的容器清空再装新点cloud-points.clear(); // 在这里执行processFrame一帧的点云写入 cloud-width cloud-points.size(); cloud-height 1; cloud-is_dense false;注意cloud-width和height的设置。在PCL里width表示点的数量、height设为1表示它是无序点云pcl::PointCloudpcl::PointXYZ::Ptr有序点云比如深度相机那种行列结构的height才设为大于1。单线雷达的点云形状说到底就是一圈点无序就够了。但这个字段忘设的话后面调用pcl::visualization的显示函数有概率直接崩掉。4.4 点云内存管理的一个小坑cloud这个pcl::PointCloudpcl::PointXYZ::Ptr我在类成员里声明为pcl::PointCloudpcl::PointXYZ::Ptr cloud;构造函数里初始化cloud pcl::PointCloudpcl::PointXYZ::Ptr(new pcl::PointCloudpcl::PointXYZ); cloud-points.reserve(4096);reserve(4096)这行容易被忽略但非常关键。如果不预留空间points.push_back()会频繁触发动态扩容一次两次没问题一秒钟刷新几十帧、每帧上千个点的时候性能差异肉眼可见。提前告诉vector“我要装大概这么多点”它能一次性分配好内存省去反复拷贝释放的开销。5. QVtkWidget显示让点云在界面上真正动起来5.1 初始化渲染窗口在代码里创建QVtkWidget并挂上PCL的visualizer我是这样写的// mainwindow.h里 QVtkWidget *qvtkWidget; pcl::visualization::PCLVisualizer::Ptr viewer; // mainwindow.cpp的构造函数里 qvtkWidget new QVtkWidget(centralWidget); ui-verticalLayout-addWidget(qvtkWidget); viewer.reset(new pcl::visualization::PCLVisualizer(viewer, false)); qvtkWidget-SetRenderWindow(viewer-getRenderWindow()); viewer-setupInteractor(qvtkWidget-GetInteractor(), qvtkWidget-GetRenderWindow()); viewer-setBackgroundColor(0.1, 0.1, 0.1); viewer-addCoordinateSystem(1.0); viewer-addPointCloud(cloud, lidar_cloud); viewer-setPointCloudRenderingProperties( pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, lidar_cloud);这里有几个参数值得说道说道PCLVisualizer(viewer, false)的第二个参数false表示不在独立的窗口里显示这样我们才能把渲染结果塞进QVtkWidget里。如果忽略了那个参数会弹出一个VTK独立窗口QVtkWidget里反而什么都不显示。addCoordinateSystem(1.0)添加一个1米大小的坐标系参考轴红色X轴、绿色Y轴、蓝色Z轴。因为雷达装好后方向是固定的通常正前方是X轴或者Y轴有了坐标轴才好判断雷达摆放的方向对不对。调试完业务功能后可以注释掉画面上更干净。setPointCloudRenderingProperties把点的大小设成2像素。单线雷达一圈几百上千个点点太大画面会显得糊点太小远处的小障碍物看不见实测2到3像素比较合适。5.2 刷新渲染的三种姿势点云更新了怎么让画面刷新我试过好几种方法性能差异挺大的第一种直接viewer-updatePointCloud(cloud, lidar_cloud)。这是最正宗的PCL方式它会替换原有id对应的点云数据并请求重绘。但如果每帧都调用外加每帧都做clear和push_back实际开销不小。我测下来大概稳定在20到30帧画面还算流畅。第二种配合QT的定时器控制刷新频率。比如串口解析线程每收到一帧就往缓存里写然后一个50毫秒的QTimer去做UI更新QTimer *refreshTimer new QTimer(this); connect(refreshTimer, QTimer::timeout, this, MainWindow::onRefreshCloud); refreshTimer-start(50);这样做的好处是刷新频率可控。如果雷达的帧率是40Hz但显示端每秒只能扛住20Hz的刷新那我索性固定刷新间隔20ms把中间的更新忽略掉避免一次事件循环里塞进太多渲染任务卡死UI。第三种用了QVTKWidget内部的渲染机制每次点云变化时调qvtkWidget-update()让它触发VTK渲染管线的重绘。这个方式比较底层适合你已经非常清楚VTK渲染管线的情况。我在这个项目里采用的是第二种定时器刷新。串口那边一收到完整帧就更新cloud对象UI线程定时把cloud刷到viewer里。这个架构把数据接收和画面渲染解耦了也避免了串口信号槽直接触发渲染导致QT事件堆积的问题。5.3 坐标系方向和雷达安装方向的坑调试时我发现画面上点云形状总是不对雷达正前方的墙显示在侧面。查来查去问题出在角度起始方向跟坐标系的对应关系上。不同雷达的0度角定义不一样有的雷达0度在正前方角度顺时针递增有的正前方是90度角度逆时针递增甚至有的0度在雷达背面。如果在代码里把原始角度直接代入cos和sin坐标就会绕错方向。解决办法很笨但有效——拿着雷达对着已知方向的墙看坐标轴上出现的点在哪个象限反推出角度偏移量。实际项目中我在解析函数里加了一个角度偏移double angleBias 0.0; // 根据实测标定单位度 double finalAngle angleDeg angleBias;每次换一台雷达或者换一个安装方向只需要重新标定一次这个偏移量然后写进配置文件里不用改代码。这个经验对于后面做机器人导航的同事帮助很大因为底盘坐标系跟雷达坐标系通常不重合这个偏置量早晚要用上。5.4 渲染线程与界面线程的关系很多人写串口点云上位机最后卡死或者闪退多半是线程问题。我这里明确地说一下线程模型串口的readyRead信号跑在QT主线程界面线程。如果你在槽函数里只做读数据和拼接缓冲不做渲染主线程就不会卡。点云解析做的是坐标变换、三角函数计算对1000个点量级来说消耗微乎其微放在主线程也不会卡。渲染调用viewer-updatePointCloud也是在主线程做的。所以这个项目不需要额外开线程切忌画蛇添足去开什么“串口接收线程”然后在线程里直接操作UI控件。QT的QThread中直接操作UI会导致未定义行为轻则崩溃重则偶发崩溃极难排查。如果非要独立线程收数据那就通过信号把数据发回主线程由主线程的槽函数再做UI更新。6. 常见问题速查与避坑心得6.1 链接报错、DLL缺失、端口打不开问题现象原因解决方案编译报“无法打开包含文件pcl_visualization/...include路径没指对检查PCL_ROOT路径和版本目录确认include/pcl-1.11被正确添加链接错误LNK2019一堆PCL函数无法解析编译器套件不匹配确认QT用的是msvc2019_64且库路径指向pcl lib目录运行提示找不到pcl_common.dllPATH里没加PCL的bin目录把%PCL_ROOT%\bin加进系统PATH并重启电脑串口开发unknown module(s) in qt: serialport缺少serialport模块确认pro或CMake里正确添加了serialport组件必要时重装QT勾选该模块端口打开失败报“Access Denied”串口被串口调试助手或其他程序占用关闭其他占用串口的工具或拔插USB重新枚举QVtkWidget在UI设计器里拖不进去缺少designer插件按上文方法手动添加插件或直接在代码里动态创建控件6.2 数据断续和“跳变飞点”的排查手段如果点云画面中出现某些点瞬间飞到远处帧率又忽高忽低按这个顺序排查先开串口调试助手用16进制模式直接看原始数据流是否连续。如果助手都一卡一卡的那多半是USB转串口的驱动问题或者线材问题。确认串口的波特率是否准确。有些劣质USB转串口芯片在115200波特率下误码率偏高可以换成FTDI芯片的转换线试一下。检查代码里帧头查找的逻辑。很多时候不是协议理解错了而是缓冲拼接时把同一帧的数据重复解析了两遍。关注校验失败率。可以在代码里临时加一个计数器每秒统计校验失败次数。如果失败率超过1%先看线材屏蔽再看电气干扰。激光雷达的电机转动会产生噪声电源不稳时尤其明显。6.3 性能优化的一些心得第一点是别用cloud-points.clear()然后再push这种做法其实有轻微的性能损耗。我试过直接重建pcl::PointCloud对象更慢因为涉及内存分配。保留对象、清空容器是最合理的。如果追求极致性能可以用swap技巧准备两个点云对象一个用来接收新数据一个用来给viewer渲染接收完成后交换指针。不过实测下来单线雷达这种千点级别的数据量不需要用到这步。第二点是在viewer上启用点云渲染的setUseVBOVTK在后台可以用顶点缓冲对象加速渲染。底层已经默认开启的版本就可以老版本需要手动设置qvtkWidget-GetRenderWindow()-GetRenderers()-GetFirstRenderer()-SetUseShadows(0);第三点是调节QTimer刷新频率。如果发现界面占用CPU过高把刷新周期从20ms改成50msCPU占用能降一半不止画面观感几乎没差别。人眼对25Hz以上的点云刷新就觉得很流畅了没必要跑满雷达帧率。6.4 全局刷新流程串起来写成文字描述整个程序的运行流程是这样的程序启动后构造函数里创建QVtkWidget和PCLVisualizer注册好点云id然后创建QTimer定时器初始40ms刷新一次信号槽都绑定好。用户从下拉框选择串口点击连接按钮触发onConnectButtonClickedQSerialPort按参数打开。雷达上电后开始发数据帧readyRead信号触发槽函数把字节拼入缓冲区。解析线程主线程内在缓冲够长时提取完整帧通过校验后逐点转换坐标写入cloud对象。QTimer的timeout信号触发onRefreshCloud调用viewer-updatePointCloud(cloud, lidar_cloud)画面更新完成一个闭环循环。用户点击断开时关闭串口、停止定时器点云画面保持最后一帧等待下次连接。7. 调试过程中的几个“现场记录”调试时我用一把卷尺做了实测验证把雷达放在走廊中间在雷达正前方1米处立了一把尺子当障碍物代码里打印出角度接近0度时的距离值。实测读数是998毫米、1002毫米、997毫米跟卷尺的1000毫米对得上说明协议解析和坐标转换都在正轨上。然后再把障碍物挪到雷达侧面、距雷达正好0.8米的位置观察点云里对应角度90度方向上的距离值读出来基本相同。这个实验花了几分钟但对整个系统的正确性验证来说价值极高。以后不管改协议解析代码还是换一个型号的雷达先用卷尺做一次静态标定比对着屏幕反复看形状靠谱得多。有一次我把角度字节的高低位反了解析出来角度方向完全反了点云呈镜像分布但距离数值全部正常画面看起来像是雷达在转反方向。这个bug在屏幕上很难一眼察觉因为在空旷环境下镜像不镜像不容易看出来。后来就是靠“把卷尺立在正前方看0度点孤零零在背后”才发现问题。所以那把卷尺在我桌上放了很久一直没撤。另外一个值得分享的经验是日志的重要性。我把串口原始字节、解析出的点数、校验失败次数都输出到界面的日志窗口每条带时间戳。看似简单但排查问题时极其好用。有一次用户反馈“点云偶尔抖动”我让现场把日志发过来一看发现在某个时间段校验失败次数突然暴增时间正好跟现场大功率设备启停吻合一下就定位到是电磁干扰问题换了一根带屏蔽层的串口线就好了跟解析代码完全没关系。这个系列的第二篇我打算重点写一下怎么把点云数据通过PCL的滤波、聚类模块做简单的障碍物识别以及怎么把QVTkWidget改成多视图鸟瞰图侧视图那部分涉及的PCL算法会更多一些到时候再细聊。第一篇能做到“串口数据变成实时刷新的点云画面”这个程度后面做导航和避障就有了一个扎实的可视化地基。
返回列表