之前用OpenGL+Qt的方式进行点云可视化,奈何OpenGL太高级,太底层了,什么都要自己搞,虽然最后也搞出来一套,但是花了太大的力气才搞完,现在在回过头看看以前的代码,好多都要想一下才能明白。
最近项目要用到PCL进行点云处理,然后了解了PCL使用vtk进行可视化。然后研究了一下vtk,过程中走了不少弯路,现在把一些经验总结一下,算是给自己这段时间的研究一个交代。
我用的环境是VS2019 + Qt5.13 + PCL 1.11 + 自己编译的VTK8.2,基本上算是最新的了。
PCL的安装相对简单,直接去官网下载安装即可,安装完后如下图所示。其中,PCL用到的第三方库都在3rdParty文件夹中。安装完后需要重新编译VTK,因为安装版的VTK是不支持Qt插件的。
我们下载PCL对应的VTK版本,这里一定要下载与PCL对应的VTK源码,然后编译即可,具体编译可参考其他文章,这里没什么难度的。
在编译VTK8.2的时候我遇到的一个错误是:export EXPORT or TARGETS specifier missing,解决办法是:打开VTK目录下的cmakelist.txt文件,复制156~157到154~155,最后,cmakelist.txt看起来像这样:
# Add a virtual target that can be used to build all compile tools.
add_custom_target(vtkCompileTools)
if (_vtk_compiletools_targets)
list(REMOVE_DUPLICATES _vtk_compiletools_targets)
export(TARGETS ${_vtk_compiletools_targets}
FILE ${VTK_BINARY_DIR}/VTKCompileToolsConfig.cmake)
add_dependencies(vtkCompileTools ${_vtk_compiletools_targets})
endif()unset(_vtk_targets)
unset(_vtk_compiletools_targets)
unset(_vtk_all_targets)
编译完后把VTK的动态库,静态库和头文件替换PCL中的VTK即可,编译完VTK8.2应该像这样,其中,plugins文件夹中存放的是Qt设计师的控件动态库,但是我从来没用过Qt设计师,所以,这个对我没什么用。然后有一个QVTKWidget头文件,有了这个头文件就表示你的VTK此时可支持嵌入在Qt中显示了。
网上找的资料绝大部分都是基于Qt设计师,把VTK的Qt控件提升为VTKWidget,然后各种操作。但是这篇文章我想用纯代码来实现。一方面我从来不用Qt设计师,另一方面我是基于VS+Qt插件来写的,在VS使用QT设计师很麻烦。
PCL可视化通过PCL的可视化类管理实现,PCL可视化类PCLVisualizer负责管理数据,QVTKWidget才是真正渲染数据的地方,类似于画布。所以,可视化类一定要设置渲染窗口,即最终要把数据显示在哪里。关键代码如下:
this->SetRenderWindow(_viewer->getRenderWindow());
有了可视化的容器,我们现在还不清楚到底怎么把数据刷到QVTKWidget上,刚才说过,PCL可视化器是管理数据的,所以,我们想显示谁就把谁添加到PCL可视化器上。
_viewer->addPointCloud(_cloud);
最终,我们的核心代码如下:
void PCLViewer::initVtk()
{
_viewer->addPointCloud(_cloud);
_viewer->setBackgroundColor(0.0, 0.0, 0.0);
_viewer->setPointCloudRenderingProperties(
pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1.0);
_viewer->setPointCloudRenderingProperties(
pcl::visualization::PCL_VISUALIZER_COLOR, 1.0, 1.0, 1.0);
cb_args.clicked_points_3d = clicked_points_3d;
cb_args.viewerPtr = pcl::visualization::PCLVisualizer::Ptr(_viewer);
_viewer->registerPointPickingCallback(
click_point_callback, (void*)&cb_args); // 注册鼠标拾取点回调函数
this->SetRenderWindow(_viewer->getRenderWindow()); // 设置PCLViewer可视化器渲染窗口
}
void PCLViewer::updateData(const QVector>& data)
{
_cloud.reset(new pcl::PointCloud);
std::async(std::launch::async, [=]() {
for (int i = 0; i < data.size(); ++i)
{
for (int j = 0; j < data[0].size(); ++j)
{
_cloud->points.push_back(
pcl::PointXYZ(data[i][j].x(),
data[i][j].y(), data[i][j].z()));
}
}
}).get();
_viewer->updatePointCloud(_cloud, pointID);
_viewer->resetCamera();
this->update();
}
其中,PCLViewer继承自QVTKWidget,定义如下:
class PCLViewer : public QVTKWidget
{
Q_OBJECT
public:
PCLViewer(QVTKWidget* parent = nullptr);
public slots:
void updateData(const QVector>& data);
protected:
virtual QSize minimumSizeHint() const override; // 作为子窗口时设置子窗口最小尺寸
virtual QSize sizeHint() const override; // 作为子窗口时设置子窗口默认尺寸
private:
void initVtk();
private:
pcl::PointCloud::Ptr _cloud; // 点云
std::string pointID; // 点云ID
pcl::visualization::PCLVisualizer::Ptr _viewer; // pcl可视化器
pcl::PointCloud::Ptr clicked_points_3d; // 存储鼠标拾取点
};
实际上,PCLViewer就是Qt设计师把VTKWidgets提升为类的操作后的类。有了PCLViewer窗口控件,我们就可以把他放到QMainWindow窗口上合适的位置了。
鼠标点拾取背后的原理实际上比较复杂的,是一个从二维到三维映射的过程,结合光线追踪,从相机原点出发,与世界坐标下的第一个交点就是鼠标拾取的点。但是PCL可视化器为我们封装了这个过程,不需要我们自己去计算。PCL可视化器捕获鼠标位置是通过回调函数实现的,我们定义相应的回调函数即可。
// 拾取鼠标位置回调函数声明
void point_pick_callback(
const pcl::visualization::PointPickingEvent& event, void* args);
void point_pick_callback(const pcl::visualization::PointPickingEvent& event,
void* args)
{
struct pointpick_args* data =
(struct pointpick_args*)args;
if (-1 == event.getPointIndex())
{
return;
}
pcl::PointXYZ cur_point;
event.getPoint(cur_point.x, cur_point.y, cur_point.z);
data->clicked_points_3d->points.push_back(cur_point);
// 开始画拾取点
pcl::visualization::PointCloudColorHandlerCustom red(
data->clicked_points_3d, 255, 0, 0);
data->viewerPtr->removePointCloud("clicked_points");
data->viewerPtr->addPointCloud(
data->clicked_points_3d, red, "clicked_points");
data->viewerPtr->setPointCloudRenderingProperties(
pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 10, "clicked_points");
qDebug() << cur_point.x << " " << cur_point.y << " " << cur_point.z;
}
最终,我们的软件实现的效果如下:
最终的工程代码可按下面的连接下载:
pcl+Qt可视化工程代码
最近有小伙伴想把鼠标点选的点在控件中显示,而不是在控制台上显示。具体实现起来就犯难了,主要是回调函数不支持类普通成员函数,要么回调函数是全局函数,要么是类的静态成员函数。全局函数和类的静态成员函数都不支持发送信号,鼠标点选点更新后不知道如何才能通知到控件让其感知到,那么到底要怎样才能把点选点发送到具体的控件实例上呢显示呢?
最近抽空看了一下PCL提供的注册回调函数原型,发现其参数可以是函数对象,如下图所示。这样我们可以把类的成员函数封装成函数对象,然后就可以愉快地注册类的成员函数作为回调函数了。
关键代码如下:
// 把类成员函数封装成函数对象
std::function fun =
std::bind(&PCLViewer::point_pick_callback, this, std::placeholders::_1);
_viewer->registerPointPickingCallback(fun);
最后,我把点显示在QLineEdit控件上,最终具体效果如下图所示:
工程代码中包含了我编译好的VTK8.2动态库,如果你想自己编译也可以,遇到的问题可在下面评论中指出,大家一起交流学习。
最后再次强调一下,PCL与依赖的第三方的库版本一定要匹配。