乐于分享
好东西不私藏

Qt + PCL 点云可视化与处理软件开发详解

Qt + PCL 点云可视化与处理软件开发详解

一、背景与概述

Qt和PCL(Point Cloud Library)结合使用,能够为许多应用场景提供强大的图形界面和点云数据处理能力。Qt提供了优秀的跨平台图形界面支持,而PCL提供了点云处理的各种算法和工具。将这两者结合起来,可以在三维重建、机器人感知、自动驾驶、工业检测、文化遗产数字化、医学图像分析等领域构建出功能强大的应用程序。

PCL中的3D可视化模块建立在VTK(Visualization Toolkit)之上,而Qt提供了完善的界面框架,这构成了Qt+VTK+PCL联合开发的底层逻辑。

二、开发环境配置

2.1 版本选择

Qt与PCL结合开发时,推荐以下版本组合:

  • Qt 5.15 / Qt 6.x:最新的LTS版本提供更好的稳定性和性能
  • **PCL 1.12+ / 1.14+**:建议使用PCL 1.12或更高版本以获得更好的VTK兼容性
  • VTK 9.x:PCL的可视化模块依赖VTK,推荐使用VTK 9.0及以上版本

从VTK 8.2开始,经典的QVTKWidget已被标记为废弃,QVTKOpenGLNativeWidget成为官方推荐的选择。

2.2 环境安装

Windows平台

使用vcpkg进行安装是最便捷的方式:

vcpkg install pcl[core,visualization]:x64-windows
vcpkg install vtk[qt]:x64-windows

也可以直接下载PCL的exe安装包(需与VS版本匹配,例如VS2022使用msvc2022版本),注意安装过程中将PCL添加到环境变量。

Linux平台

sudo apt install libpcl-dev libvtk9-dev
sudo apt install qtcreator

2.3 CMakeLists.txt配置

推荐使用CMake构建项目,这在PCL生态中是标准做法。一个标准的CMakeLists.txt配置如下:

cmake_minimum_required(VERSION 3.8)
project(PCLViewer)

set(CMAKE_CXX_STANDARD 14)

# 查找Qt6(若使用Qt5则将Qt6改为Qt5)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets OpenGL)

# 查找PCL
find_package(PCL REQUIRED COMPONENTS common io visualization)
include_directories(${PCL_INCLUDE_DIRS})
link_directories(${PCL_LIBRARY_DIRS})
add_definitions(${PCL_DEFINITIONS})

# 查找VTK
find_package(VTK REQUIRED)
include(${VTK_USE_FILE})

# 添加头文件和源文件
set(HEADERS mainwindow.h pclviewer.h)
set(SOURCES main.cpp mainwindow.cpp pclviewer.cpp)

# 生成Qt的moc文件
qt6_wrap_cpp(HEADERS_MOC ${HEADERS})

add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS_MOC})

target_link_libraries(${PROJECT_NAME}
    Qt6::Core Qt6::Widgets Qt6::OpenGL
    ${PCL_LIBRARIES}
    ${VTK_LIBRARIES}
)

关键说明:find_package(PCL REQUIRED)会在系统中查找PCL库并将其引入项目,REQUIRED表示如果未找到PCL库则会发生错误并停止构建。

2.4 .pro文件配置(传统qmake方式)

如果使用qmake构建,.pro文件配置如下:

QT += core gui widgets opengl
TARGET = PCLViewer
TEMPLATE = app

# PCL配置
INCLUDEPATH += /usr/include/pcl-1.12 /usr/include/vtk-9.1
LIBS += -lpcl_common -lpcl_io -lpcl_visualization \
        -lvtkCommonCore-9.1 -lvtkRenderingOpenGL2-9.1

# Eigen3支持
INCLUDEPATH += /usr/include/eigen3

# 推荐使用CMake而非qmake

三、PCLVisualizer 可视化与交互详解

PCLVisualizer是PCL点云可视化和交互的核心类,建立在VTK之上。通过PCLVisualizer提供的封装接口,用户可以实现基于事件的交互操作、拾取响应、视角控制等高级可视化需求。

3.1 基本功能

点云显示

viewer->addPointCloud<pcl::PointXYZ>(cloud, "cloud_id");

点云动态更新

viewer->updatePointCloud(cloud, "cloud_id");

颜色和样式设置

// 设置点的大小
viewer->setPointCloudRenderingProperties(
    pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3"cloud_id");

// 设置点云颜色(RGB)
viewer->setPointCloudRenderingProperties(
    pcl::visualization::PCL_VISUALIZER_COLOR, 1.00.00.0"cloud_id");

几何体渲染:PCLVisualizer还支持渲染线条、平面、立方体、球体、坐标轴等几何图元。

3.2 交互功能

PCLVisualizer内置了鼠标旋转、缩放、平移等基础交互能力。此外,它还提供了多种事件回调注册机制:

  • 鼠标点选:registerPointPickingCallback
  • 框选:registerAreaPickingCallback
  • 键盘事件:registerKeyboardCallback
  • 鼠标移动/点击事件:通过VTK扩展实现(需继承vtkInteractorStyle)

3.3 摄像机视角控制

viewer->setCameraPosition(
00-3,    // 相机位置
000,     // 视点中心
0-10// 相机“上”方向
);

3.4 点选拾取示例

viewer->registerPointPickingCallback([](const pcl::visualization::PointPickingEvent& event, void*) {
float x, y, z;
    event.getPoint(x, y, z);
std::cout << "Picked point: " << x << ", " << y << ", " << z << std::endl;
});

3.5 多视口支持

PCLVisualizer可以在同一个窗口中显示多个视图:

int v1, v2;
viewer->createViewPort(0.00.00.51.0, v1);
viewer->createViewPort(0.50.01.01.0, v2);
viewer->addPointCloud(cloud1, "cloud1", v1);
viewer->addPointCloud(cloud2, "cloud2", v2);

四、Qt与PCL点云显示的核心原理

PCL的点云显示基于VTK渲染引擎,而VTK提供了嵌入Qt控件的机制。核心思路是:创建一个vtkGenericOpenGLRenderWindowvtkRenderer,将它们绑定到Qt的QVTKOpenGLNativeWidget控件上,再将这个渲染窗口传递给PCLVisualizer。

关键点:避免双窗口问题。错误示范会导致PCLVisualizer自己创建一个独立的原生渲染窗口,而非嵌入Qt界面。正确的做法是在构造PCLVisualizer时就传入已准备好的渲染器和渲染窗口。

五、完整代码示例:基于Qt+PCL的点云可视化软件

5.1 项目结构

├── CMakeLists.txt
├── main.cpp
├── mainwindow.h
├── mainwindow.cpp
├── mainwindow.ui
├── pclviewer.h
└── pclviewer.cpp

5.2 mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include<QMainWindow>
#include<QPushButton>
#include<QLabel>
#include<QFileDialog>
#include<pcl/point_cloud.h>
#include<pcl/point_types.h>
#include<pcl/visualization/pcl_visualizer.h>
#include<vtkGenericOpenGLRenderWindow.h>
#include<vtkRenderer.h>
#include<QVTKOpenGLNativeWidget.h>

QT_BEGIN_NAMESPACE
namespace Ui { classMainWindow; }
QT_END_NAMESPACE

classMainWindow :public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
voidon_openButton_clicked();
voidon_saveButton_clicked();
voidon_voxelFilterButton_clicked();

private:
voidsetupUI();
voidinitPCLVisualizer();
voidupdatePointCloudDisplay();

    Ui::MainWindow *ui;
    QVTKOpenGLNativeWidget *visualizerWidget;

// PCL可视化器
    pcl::visualization::PCLVisualizer::Ptr viewer;

// 点云数据
    pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud;
    pcl::PointCloud<pcl::PointXYZRGB>::Ptr filteredCloud;
};

#endif// MAINWINDOW_H

5.3 mainwindow.cpp

#include"mainwindow.h"
#include"ui_mainwindow.h"
#include<QHBoxLayout>
#include<QVBoxLayout>
#include<QStatusBar>
#include<QMessageBox>
#include<pcl/io/pcd_io.h>
#include<pcl/io/ply_io.h>
#include<pcl/filters/voxel_grid.h>
#include<vtkGenericOpenGLRenderWindow.h>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , cloud(new pcl::PointCloud<pcl::PointXYZRGB>)
    , filteredCloud(new pcl::PointCloud<pcl::PointXYZRGB>)
{
    ui->setupUi(this);
    setupUI();
    initPCLVisualizer();
}

MainWindow::~MainWindow()
{
delete ui;
}

voidMainWindow::setupUI()
{
// 创建可视化控件
    visualizerWidget = new QVTKOpenGLNativeWidget(this);
    visualizerWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

// 创建按钮
    QPushButton *openButton = new QPushButton("打开点云"this);
    QPushButton *saveButton = new QPushButton("保存点云"this);
    QPushButton *filterButton = new QPushButton("体素滤波"this);

    connect(openButton, &QPushButton::clicked, this, &MainWindow::on_openButton_clicked);
    connect(saveButton, &QPushButton::clicked, this, &MainWindow::on_saveButton_clicked);
    connect(filterButton, &QPushButton::clicked, this, &MainWindow::on_voxelFilterButton_clicked);

// 工具栏布局
    QHBoxLayout *toolbarLayout = new QHBoxLayout();
    toolbarLayout->addWidget(openButton);
    toolbarLayout->addWidget(saveButton);
    toolbarLayout->addWidget(filterButton);
    toolbarLayout->addStretch();

// 主布局
    QVBoxLayout *mainLayout = new QVBoxLayout();
    mainLayout->addLayout(toolbarLayout);
    mainLayout->addWidget(visualizerWidget);

    QWidget *centralWidget = new QWidget(this);
    centralWidget->setLayout(mainLayout);
    setCentralWidget(centralWidget);

    setWindowTitle("PCL点云可视化工具");
    resize(1024768);
}

voidMainWindow::initPCLVisualizer()
{
// 【关键步骤】创建VTK渲染窗口和渲染器,避免双窗口问题
    vtkSmartPointer<vtkGenericOpenGLRenderWindow> renderWindow =
        vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New();
    vtkSmartPointer<vtkRenderer> renderer =
        vtkSmartPointer<vtkRenderer>::New();
    renderWindow->AddRenderer(renderer);

// 将渲染窗口设置给Qt控件
    visualizerWidget->setRenderWindow(renderWindow);

// 创建PCLVisualizer,传入预先准备好的renderer和renderWindow
// 第四个参数为false表示不创建新的交互器,使用Qt控件提供的交互器
    viewer.reset(new pcl::visualization::PCLVisualizer(
        renderer, renderWindow, "viewer"false));

// 设置背景色为黑色
    viewer->setBackgroundColor(000);
}

voidMainWindow::on_openButton_clicked()
{
    QString fileName = QFileDialog::getOpenFileName(this,
"打开点云文件""",
"点云文件 (*.pcd *.ply);;PCD文件 (*.pcd);;PLY文件 (*.ply);;所有文件 (*)");

if (fileName.isEmpty())
return;

// 清空之前的点云
    cloud->clear();

// 加载点云文件
std::string filePath = fileName.toStdString();
int result;

if (fileName.endsWith(".pcd", Qt::CaseInsensitive)) {
        result = pcl::io::loadPCDFile<pcl::PointXYZRGB>(filePath, *cloud);
    } else {
        result = pcl::io::loadPLYFile<pcl::PointXYZRGB>(filePath, *cloud);
    }

if (result < 0) {
        QMessageBox::warning(this"错误""无法加载点云文件: " + fileName);
return;
    }

    statusBar()->showMessage(QString("已加载点云,共 %1 个点").arg(cloud->size()));
    updatePointCloudDisplay();
}

voidMainWindow::on_saveButton_clicked()
{
if (cloud->empty()) {
        QMessageBox::warning(this"警告""没有可保存的点云数据");
return;
    }

    QString fileName = QFileDialog::getSaveFileName(this,
"保存点云文件""",
"PCD文件 (*.pcd);;PLY文件 (*.ply)");

if (fileName.isEmpty())
return;

std::string filePath = fileName.toStdString();

if (fileName.endsWith(".pcd", Qt::CaseInsensitive)) {
        pcl::io::savePCDFileBinary(filePath, *cloud);
    } else {
        pcl::io::savePLYFileBinary(filePath, *cloud);
    }

    statusBar()->showMessage("点云已保存至: " + fileName);
}

voidMainWindow::on_voxelFilterButton_clicked()
{
if (cloud->empty()) {
        QMessageBox::warning(this"警告""没有点云数据可进行滤波");
return;
    }

// 体素滤波降采样
    pcl::VoxelGrid<pcl::PointXYZRGB> voxelGrid;
    voxelGrid.setInputCloud(cloud);
    voxelGrid.setLeafSize(0.01f0.01f0.01f);  // 体素大小
    voxelGrid.filter(*filteredCloud);

// 更新点云
    cloud->swap(*filteredCloud);

    statusBar()->showMessage(QString("体素滤波完成,点云从 %1 降至 %2 个点")
        .arg(cloud->size()).arg(filteredCloud->size()));

    updatePointCloudDisplay();
}

voidMainWindow::updatePointCloudDisplay()
{
if (!viewer || cloud->empty())
return;

// 移除旧的显示内容
    viewer->removeAllPointClouds();

// 添加点云到可视化器
    viewer->addPointCloud<pcl::PointXYZRGB>(cloud, "cloud");

// 设置点云属性
    viewer->setPointCloudRenderingProperties(
        pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2"cloud");

// 添加坐标系(可选)
    viewer->addCoordinateSystem(1.0);

// 强制刷新渲染窗口
    visualizerWidget->renderWindow()->Render();
}

5.4 main.cpp

#include"mainwindow.h"
#include<QApplication>
#include<QSurfaceFormat>

intmain(int argc, char *argv[])
{
QApplication app(argc, argv);

// 设置OpenGL版本(对于现代VTK是必需的)
    QSurfaceFormat format;
    format.setDepthBufferSize(24);
    format.setStencilBufferSize(8);
    format.setVersion(32);
    format.setProfile(QSurfaceFormat::CoreProfile);
    QSurfaceFormat::setDefaultFormat(format);

    MainWindow window;
    window.show();
return app.exec();
}

5.5 代码关键点说明

(1)避免双窗口问题

代码中最关键的地方在initPCLVisualizer()函数中。如果没有正确构造PCLVisualizer,会导致一个独立于Qt窗口的PCL原生渲染窗口弹出,而Qt控件区域却是空白的。正确的做法是:先创建vtkGenericOpenGLRenderWindowvtkRenderer,将它们设置给Qt的QVTKOpenGLNativeWidget控件,然后在PCLVisualizer构造时传入这两个对象,第四个参数设为false

(2)点云更新机制

viewer->removeAllPointClouds();      // 移除旧的点云
viewer->addPointCloud(...);          // 添加新的点云
visualizerWidget->renderWindow()->Render();  // 强制刷新渲染窗口

(3)体素滤波

体素滤波是点云处理中最常用的降采样方法之一。它把点云空间划分成规则的3D网格,并用每个体素的质心代替体素内所有点,从而达到降采样的目的。

六、扩展功能建议

基于上述基础框架,可以进一步扩展以下功能模块:

  1. 点云滤波:添加统计滤波、半径滤波、直通滤波等更多滤波方式,这是点云预处理的重要环节
  2. 点云配准:集成ICP(迭代最近点)、4PCS等配准算法实现点云对齐
  3. 点云分割:实现平面分割、欧几里得聚类分割等
  4. 特征提取:计算法线、FPFH、PFH等点云特征描述子
  5. 实时流处理:从摄像头或激光雷达实时读取点云数据并动态显示
  6. 多线程处理:将耗时的点云处理算法放在独立线程中执行,保持界面响应流畅

七、常见问题与解决方案

1. 双窗口问题

原因:PCLVisualizer默认会为自己创建一个独立的vtkRenderWindow,而非使用Qt控件提供的窗口。

解决:构造PCLVisualizer时传入预先为Qt控件准备的renderer和renderWindow,第四个参数设为false。

2. 编译找不到QVTKOpenGLNativeWidget头文件

原因:VTK编译时未开启Qt支持。

解决:编译VTK时需勾选VTK_Group_ENABLE_Qt选项。

3. OpenGL版本不兼容导致显示异常

在main函数中设置QSurfaceFormat,指定OpenGL Core Profile版本(3.2以上),确保与VTK的渲染要求兼容。

八、总结

本文详细介绍了Qt与PCL结合开发点云可视化软件的完整流程,涵盖环境配置、核心原理、关键代码和扩展建议。Qt+PCL组合的强大之处在于:Qt提供了成熟的跨平台GUI框架,PCL提供了丰富的点云处理算法,VTK则提供了高效的三维渲染引擎——三者有机结合能够构建功能完善的3D点云处理应用。通过本文的代码示例,读者可以快速搭建起一个包含点云加载、显示、滤波和保存的完整工具框架,并在此基础上继续扩展更多高级功能。