最近折腾MFC中的PCL点云显示,折腾了很久,尝试过OpenGL方案、VTK方案等,最终整理出一套最优的方案,决定使用VTK来显示点云。博主将显示整理成一个CVtkViewer类,该类继承自CStatic,直接修改Picture控件变量的类型就可以实现显示,对点云和图像接口进行封装,使用较为方便和安全。并且使用VTK显示的类封装可以在多个窗口分别显示,如左图所示可以在两个以上的窗口分别显示不同的点云,如果使用OpenGL来实现会相当麻烦。还有就是VTK这种上层接口封装非常全面,OpenGL太偏底层了,比如VTK会自动设置合适的相机位置,而OpenGL实现同样的效果就需要自己解算位置,这也是博主放弃OpenGL的重要原因。
在这篇博客之前,你需要对VTK开发有基本了解,博主使用的是VTK7.0,PCL1.8.0,博客中对博主遇到的编译错误、运行错误等都做了详细解释,希望能有帮助。
博主参考了大量资料,前排放一些有价值的参考链接:
http://blog.csdn.net/wishchin/article/details/45887561
http://blog.csdn.net/wishchin/article/details/45951543
http://blog.sina.com.cn/s/blog_e7783fcf0102xqip.html
http://tieba.baidu.com/p/5131946762
创建CVtkViewer类
下面提供了CVtkViewer类的源码,这里对源码做简单的解释:
1.该类继承自CStatic,支持CStatic的几乎所有操作,但是MoveWindow会改变位置,不会改变大小,所以类中有对MoveWindow的重载
2.显示接口有两个,一个是自定义的RGBXYZ点云裸数据接口,ReadPointCloud,另一个是vtkImageData接口,vtkImageData不仅支持二维图像,也支持三维图像,更多消息请移步百度
3.注意头文件中这样一段代码#include <vtkAutoInit.h> VTK_MODULE_INIT(vtkRenderingOpenGL) VTK_MODULE_INIT(vtkInteractionStyle) VTK_MODULE_INIT(vtkRenderingFreeType),你可以先去掉这一段代码,在Debug模式下看看VTK是否有错误消息,如果没有错误消息就不要加这几行代码,这是由于使用本地编译器编译VTK时有些宏没有定义,会导致一些错误。vtk还有很多奇葩错误,参见我前一篇博客。
4.点云显示的接口,有一段注释掉了,如果你只想显示单色点云,那么可以使用被注释掉的那段代码,如果想显示彩色点云,就不需要修改
5.头文件最后还注释掉了一个类,如果你想多串口同步移动相机,可以通过这个类同时向多个窗口发送命令,详细参见开头给出的链接,虽仍然博主想到其它更简单的方法,但是没有博主的项目没有需求,就没有加进来。
6.在Debug模式下,关闭窗口时会有VTK Error 1208,应该和OpenGL或者Render有关,目前还没有查出来,如果你解决了这个问题,非常希望你能留言告知,这是这个类中(目前发现的)唯一的瑕疵,博主将非常感激。
好了,粘代码吧,首先是头文件:
#pragma once
#include "afxwin.h"
#include <vtkResliceCursor.h>
#include <vtkResliceCursorWidget.h>
#include <vtkPlane.h>
#include <vtkPlaneSource.h>
#include <vtkPlaneWidget.h>
#include <vtkImagePlaneWidget.h>
#include <vtkResliceCursorThickLineRepresentation.h>
#include <vtkResliceCursor.h>
#include <vtkCommand.h>
#include <vtkViewport.h>
#include <vtkViewDependentErrorMetric.h>
#include <vtkSmartPointer.h>
#include <vtkRenderer.h>
#include <vtkRendererSource.h>
#include <vtkRenderingOpenGLModule.h>
#include <vtkRenderWindow.h>
#include <vtkWin32OpenGLRenderWindow.h>
#include <vtkWin32RenderWindowInteractor.h>
#include <vtkPolyVertex.h>
#include <vtkUnstructuredGrid.h>
#include <vtkDataSetMapper.h>
#include <vtkActor.h>
#include <vtkProperty.h>
#include <vtkLookupTable.h>
#include <vtkFloatArray.h>
#include <vtkPointData.h>
#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL)
VTK_MODULE_INIT(vtkInteractionStyle)
VTK_MODULE_INIT(vtkRenderingFreeType)
using Pointf = struct _Pointf{
float x;
float y;
float z;
float r;
float g;
float b;
};
class CVtkViewer : public CStatic
{
DECLARE_DYNAMIC(CVtkViewer)
public:
CVtkViewer();
virtual ~CVtkViewer();
vtkSmartPointer<vtkActor> actor;
void ReadPointCloud(std::vector<Pointf>&);
public:
//3.2 重载CvtkView类PreSubclassWindow()函数和OnPaint()函数
//PreSubclassWindow函数负责创建VTK可视化管线,OnPaint()函数负责客户区内场景渲染。
//vtkAcor,vtkRenderer,vtkRenderWindow,vtkRenderWindowInteractor四个部分。当然根据需要还可以设置vtkRenderWindowInteractorStyle,以及光照,材质,颜色等。
//在CvtkView类头文件中定义相关对象,并在PreSubclassWindow函数中实例化和构建可视化管线
void PreSubclassWindow();
void SetImageData(vtkSmartPointer<vtkImageData> ImageData);
void SetupReslice();
void MoveWindow(CRect);
private:
vtkSmartPointer< vtkImagePlaneWidget > m_ImagePlaneWidget;
vtkSmartPointer< vtkResliceCursorWidget> m_ResliceCursorWidget;
vtkSmartPointer< vtkResliceCursor > m_ResliceCursor;
vtkSmartPointer< vtkResliceCursorThickLineRepresentation > m_ResliceCursorRep;
vtkSmartPointer<vtkRenderer> m_Renderer;
vtkSmartPointer<vtkRenderWindow> m_RenderWindow;
vtkSmartPointer<vtkImageData> m_ImageData;
//m_Direction为方向标志,取值分别为0,1和2,分别代表X轴,Y轴和Z轴方向,
int m_Direction;
protected:
DECLARE_MESSAGE_MAP()
};
//当用户改变图像切分的坐标轴时(旋转坐标轴或者平移坐标系),图像切分平面会产生相应的改变,
//如果将新的切分平面更新到二维视图的vtkImagePlaneWidget对象中,即可实现三维视图的同步更新操作。
///基于以上设计,实现一个vtkCommand子类,来监听vtkResliceCursorWidget::ResliceAxesChangedEvent消息,并实现相应的更新操作。
//class vtkResliceCursorCallback : public vtkCommand
//{
//public:
// static vtkResliceCursorCallback *New()
// {
// return new vtkResliceCursorCallback;
// }
//
// CVtkViewer* view[4];
//
//public:
// void Execute(vtkObject *caller, unsigned long /*ev*/,
// void *callData)
// {
// vtkResliceCursorWidget *rcw = dynamic_cast<vtkResliceCursorWidget *>(caller);
// if (rcw)
// {
// for (int i = 0; i < 3; i++)
// {
// vtkPlaneSource *ps =
// static_cast<vtkPlaneSource *>(view[i]->GetImagePlaneWidget()->GetPolyDataAlgorithm());
//
// ps->SetOrigin(view[i]->GetResliceCursorWidget()->
// GetResliceCursorRepresentation()->GetPlaneSource()->GetOrigin());
// ps->SetPoint1(view[i]->GetResliceCursorWidget()->
// GetResliceCursorRepresentation()->GetPlaneSource()->GetPoint1());
// ps->SetPoint2(view[i]->GetResliceCursorWidget()->
// GetResliceCursorRepresentation()->GetPlaneSource()->GetPoint2());
//
// view[i]->GetImagePlaneWidget()->UpdatePlacement();
// view[i]->Render();
// }
// view[3]->Render();
// }
//
// }
//
// vtkResliceCursorCallback() {}
//};
然后是cpp文件:
#include "stdafx.h"
#include "VtkViewer.h"
IMPLEMENT_DYNAMIC(CVtkViewer, CStatic)
CVtkViewer::CVtkViewer()
{
//在实例化时需要注意,该视图类在默认情况下渲染的是vtkResliceCursorWidget对象的输出,
//因此需要为vtkResliceCursorWidget对象指定相应的vtkRenderer对象,
//m_ResliceCursorWidget->SetInteractor(m_RenderWindow->GetInteractor());
//m_ResliceCursorWidget->SetDefaultRenderer(m_Renderer);
}
CVtkViewer::~CVtkViewer()
{
}
void CVtkViewer::ReadPointCloud(std::vector<Pointf>& pointcloud){
int n = pointcloud.size();
vtkSmartPointer<vtkPoints> points = vtkSmartPointer<vtkPoints>::New();
points->SetNumberOfPoints(n);
vtkSmartPointer<vtkLookupTable> lut = vtkSmartPointer<vtkLookupTable>::New();
lut->SetNumberOfTableValues(n);
lut->Build();
for (int i = 0; i < n; ++i){
points->InsertPoint(i, pointcloud[i].x, pointcloud[i].y, pointcloud[i].z);
lut->SetTableValue(i, pointcloud[i].r, pointcloud[i].g, pointcloud[i].b);
}
vtkSmartPointer<vtkPolyVertex> polyVertex = vtkSmartPointer<vtkPolyVertex>::New();
polyVertex->GetPointIds()->SetNumberOfIds(n);
vtkSmartPointer<vtkFloatArray> pointsScalars = vtkSmartPointer<vtkFloatArray>::New();
pointsScalars->SetNumberOfTuples(n);
for (int i = 0; i < n; ++i){
polyVertex->GetPointIds()->SetId(i, i);
pointsScalars->InsertValue(i, i);
}
vtkSmartPointer<vtkUnstructuredGrid> grid = vtkSmartPointer<vtkUnstructuredGrid>::New();
grid->Allocate(1, 1);
grid->SetPoints(points);
grid->GetPointData()->SetScalars(pointsScalars);
grid->InsertNextCell(polyVertex->GetCellType(), polyVertex->GetPointIds());
vtkSmartPointer<vtkDataSetMapper> mapper = vtkSmartPointer<vtkDataSetMapper>::New();
mapper->SetInputData(grid);
mapper->ScalarVisibilityOn();
mapper->SetScalarRange(0, n-1);
mapper->SetLookupTable(lut);
m_Renderer->RemoveActor(actor);
actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
actor->GetProperty()->SetRepresentationToPoints();
actor->GetProperty()->SetPointSize(2);
m_Renderer->AddActor(actor);
m_Renderer->SetBackground(0, 0, 0);
m_Renderer->ResetCamera();
m_RenderWindow->Render();
// 点云全部显示同样的颜色
/* vtkPoints * points = vtkPoints::New();
int n = pointcloud.size(); int idx = 0;
for (int i = 0; i < n; ++i){
points->InsertPoint(i, pointcloud[i].x, pointcloud[i].y, pointcloud[i].z);
}
vtkPolyVertex * polyvertex = vtkPolyVertex::New();
polyvertex->GetPointIds()->SetNumberOfIds(n);
for (int i = 0; i < n; ++i){
polyvertex->GetPointIds()->SetId(i, i);
}
vtkUnstructuredGrid * grid = vtkUnstructuredGrid::New();
grid->SetPoints(points);
grid->InsertNextCell(polyvertex->GetCellType(), polyvertex->GetPointIds());
vtkDataSetMapper * map = vtkDataSetMapper::New();
map->SetInputData(grid);
vtkActor * actor = vtkActor::New();
actor->SetMapper(map);
actor->GetProperty()->SetColor(0.194, 0.562, 0.75);
m_Renderer->AddActor(actor);
m_Renderer->SetBackground(0, 0, 0);
m_Renderer->ResetCamera();
m_RenderWindow->Render();
map->Delete();
grid->Delete();
actor->Delete();
points->Delete();
polyvertex->Delete();*/
}
void CVtkViewer::PreSubclassWindow()
{
// TODO: Add your specialized code here and/or call the base class
CRect rect;
GetClientRect(rect);
m_Renderer = vtkSmartPointer<vtkRenderer>::New();
m_RenderWindow = vtkSmartPointer<vtkRenderWindow>::New();
m_RenderWindow->SetParentId(this->m_hWnd);
m_RenderWindow->SetSize(rect.Width(), rect.Height());
m_RenderWindow->AddRenderer(m_Renderer);
if (m_RenderWindow->GetInteractor() == NULL)
{
vtkSmartPointer<vtkRenderWindowInteractor> RenderWindowInteractor =
vtkSmartPointer<vtkRenderWindowInteractor>::New();
RenderWindowInteractor->SetRenderWindow(m_RenderWindow);
RenderWindowInteractor->Initialize();
}
m_RenderWindow->Start();
CStatic::PreSubclassWindow();
}
void CVtkViewer::SetImageData(vtkSmartPointer<vtkImageData> ImageData)
{
if (ImageData == NULL) return;
m_ImageData = ImageData;
SetupReslice();
}
void CVtkViewer::SetupReslice()
{
if (m_ImageData == NULL) return;
int dims[3];
m_ImageData->GetDimensions(dims);
//////////////////////////////////////////////////////////////////////////
m_ImagePlaneWidget->SetInputData(m_ImageData);
m_ImagePlaneWidget->SetPlaneOrientation(m_Direction);
m_ImagePlaneWidget->SetSliceIndex(dims[m_Direction] / 2);
m_ImagePlaneWidget->On();
m_ImagePlaneWidget->InteractionOn();
//////////////////////////////////////////////////////////////////////////
m_ResliceCursor->SetCenter(m_ImageData->GetCenter());
m_ResliceCursor->SetImage(m_ImageData);
m_ResliceCursor->SetThickMode(0);
//m_ResliceCursorRep->GetResliceCursorActor()->GetCursorAlgorithm()->SetResliceCursor(m_ResliceCursor);
//m_ResliceCursorRep->GetResliceCursorActor()->GetCursorAlgorithm()->SetReslicePlaneNormal(m_Direction);
m_ResliceCursorWidget->SetEnabled(1);
m_Renderer->ResetCamera();
//////////////////////////////////////////////////////////////////////////
double range[2];
m_ImageData->GetScalarRange(range);
m_ResliceCursorWidget->GetResliceCursorRepresentation()->
SetWindowLevel(range[1] - range[0], (range[0] + range[1]) / 2.0);
m_ImagePlaneWidget->SetWindowLevel(range[1] - range[0], (range[0] + range[1]) / 2.0);
}
void CVtkViewer::MoveWindow(CRect rect){
m_RenderWindow->SetSize(rect.Width(), rect.Height());
CStatic::MoveWindow(rect);
}
BEGIN_MESSAGE_MAP(CVtkViewer, CStatic)
END_MESSAGE_MAP()
在对话框类中使用
首先拖几个Picture Control进来,然后修改控件ID为IDC_VTK1,这里小心IDC_STATIC是不能接收消息的,必须修改成其它的,然后右键添加环境变量为CStatic,然后将其改为CVtkViewer类型
CVtkViewer m_vtk1; CVtkViewer m_vtk2; CVtkViewer m_vtk3; CVtkViewer m_vtk4;

这时候你可以编译下,会发现这些Picture Control直接变成了黑色,这就说明VTK窗口工作了。
然后就是添加电源数据显示了,在对话框窗口的OnPaint函数中进行绘制,使用方法非常简单,就像下面这样
m_vtk2.ReadPointCloud(pointclouds[1]); m_vtk2.MoveWindow(rect); m_vtk2.ShowWindow(SW_SHOW);
这是博主目前一个软件的显示效果图:

好了,最后还是非常感谢开头链接给出的参考代码。
您好,用您这个方法显示的时候出现了一个很奇怪的问题,单个的txt点云数据显示没有问题,但是我把两个Txt文件内的数据按顺序存放到数组里面,最后显示出来的点云图像会分成独立的两块,请问这可能是什么原因造成的呢?
如果只是单个点,直接粘贴应该没有问题,如果带有其它信息就不一样了。
比如点云pcd文件是有格式的,有些情况点云的顺序表征了结构,又比如有的点云按照三角化时的顺序排列。
感谢您的回信,我提出的问题,txt文件中就只有点云坐标, 而且使用matlab显示出来也很正常,所以这个问题很奇怪。 现在是这样,我分多次扫描出来的几个点云坐标数据存放到txt文件中,例如point0.txt到point5.txt, 那我想要只显示其中的几个点云, 例如要同时在同一个picture control控件中显示point2.txt和point4.txt中的点云信息, 有什么方法可以做到呢? 现在看起来txt直接拼接存在一点问题,只是还没有搞清楚原因。
向您请教一个问题, 为什么我开了线程后,程序跑到m_RenderWindow->Render后会出错呢?wglMakeCurrent failed in MakeCurrent error, 请求资源在使用中。 出错0xC0000005