一. 课题背景
机械臂引导:3D视觉引导机械臂方案被越来越多的行业认可,如:快递分拣供包、汽车配件上下料、仓储拆码垛、仓配货品拣选。本课题使用海康机器人3D相机产品获取视野中多个无序摆放物品的抓取点位与抓取顺序,物品种类不限。
二. 开发环境介绍及目标
1.Visual Studio 2019
2.PCL、Boost
3.RGB-D感知相机MV-EB435i
4.SDK_Mv3dRgbd_V1.0.0_Win
本课题在Windows平台进行开发, 整个项目由两个部分组成:使用海康RGB-D感知相机MV-EB435i采集三维点云数据,存储成pcd格式点云文件;通过PCL对存储的点云文件进行简单的点云处理,包括直通滤波器滤波,体素滤波器下采样,统计滤波器滤波,法向量计算,点云分割,最终输出点云簇的质心位置,得到视野中无序物品的抓取点位。
三. 解析点云数据及存储
由于相机通过USB接入,在设备类型选择DeviceType_USB:
ASSERT_OK(MV3D_RGBD_GetDeviceList(DeviceType_USB, &devs[0], nDevNum, &nDevNum));
参考SDK中给出的结构体参数,可以得到图像的详细信息:
typedef struct _MV3D_RGBD_IMAGE_DATA_  
 {  
 Mv3dRgbdImageType enImageType; 
 uint32_t nWidth; 
 uint32_t nHeight; 
 uint8_t* pData; 
 uint32_t nDataLen; 
 uint32_t nFrameNum; 
 int64_t nTimeStamp; 
 uint8_t nReserved[16]; 
 } MV3D_RGBD_IMAGE_DATA;
在相机图像MV3D_RGBD_IMAGE_DATA数据中,有图像宽、图像高、相机输出的图像数据、图像数据长度(字节)、帧号、设备上报的时间戳等信息,通过图像高、宽、图像数据长度可以计算图像点数,以供后续使用。为了得到图像数据,对pData指针进行解析,得到图像数据。
由于相机不输出点云数据,采用深度图数据转换成点云数据:
nRet = MV3D_RGBD_MapDepthToPointCloud(handle, &stFrameData.stImageData[i], &stPointCloudImage);
采用PCL的点云结构存储解析得到的相机点云数据,并对明显的无效数据进行过滤:

调用PCL存文件接口,将点云文件存储成pcd格式:
pcl::io::savePCDFileASCII(to_string(num)+".pcd", pointCloud);
四. 点云数据处理及物体位置输出
创建点云数据集,读取点云到pcl的点云结构体中:
pcl::PCDReader reader;
reader.read("1.pcd", *cloud);
对点云进行显示。为了同时显示原始点云,直通滤波器滤波,体素滤波器下采样,统计滤波器滤波,法向量计算,点云分割的结果,建立了三个PCLVisualizer渲染窗口,并将其中一个窗口分成四格同时显示不同滤波器的效果:
int v1(0);
viewer->createViewPort(0.0, 0.5, 0.5, 1.0, v1);
viewer->setBackgroundColor(0, 0, 0, v1);
viewer->addText("sample cloud", 10, 10, "sample cloud", v1);
viewer->addPointCloud<pcl::PointXYZ>(cloud, "cloud", v1);
int v2(0);
viewer->createViewPort(0.5, 0.5, 1.0, 1.0, v2);
viewer->setBackgroundColor(0.3, 0.3, 0.3, v2);
viewer->addText("cloud after PassThrough", 10, 10, "cloud after PassThrough", v2);
viewer->addPointCloud<pcl::PointXYZ>(cloud_after_PassThrough, "cloud_after_PassThrough", v2);
int v3(0);
viewer->createViewPort(0.0, 0.0, 0.5, 0.5, v3);
viewer->setBackgroundColor(0.3, 0.3, 0.3, v3);
viewer->addText("cloud after voxelgrid", 10, 10, "cloud after voxelgrid", v3);
viewer->addPointCloud<pcl::PointXYZ>(cloud_after_voxelgrid, "cloud_after_voxelgrid", v3);
int v4(0);
viewer->createViewPort(0.5, 0.0, 1.0, 0.5, v4);
viewer->setBackgroundColor(0.3, 0.3, 0.3, v4);
viewer->addText("cloud after StatisticalRemoval", 10, 10, "cloud after StatisticalRemoval", v4);
viewer->addPointCloud<pcl::PointXYZ>(cloud_after_StatisticalRemoval, "cloud_after_StatisticalRemoval", v4);
对点云进行滤波。通过直通滤波器滤波过滤z方向上过近或者过远的点云,通过体素滤波器下采样降低点云稠密度,通过统计滤波器滤波过滤离群点。通过上述流水线的串行点云滤波操作,得到更接近期望的点云:
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_after_PassThrough(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PassThrough<pcl::PointXYZ> passthrough;
passthrough.setInputCloud(cloud);
passthrough.setFilterFieldName("z");
passthrough.setFilterLimits(500.0, 3000.0);
passthrough.filter(*cloud_after_PassThrough);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_after_voxelgrid(new pcl::PointCloud<pcl::PointXYZ>);
pcl::VoxelGrid<pcl::PointXYZ> voxelgrid;
voxelgrid.setInputCloud(cloud_after_PassThrough);
voxelgrid.setLeafSize(10.0f, 10.0f, 10.0f);
voxelgrid.filter(*cloud_after_voxelgrid);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_after_StatisticalRemoval(new pcl::PointCloud<pcl::PointXYZ>);
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> Statistical;
Statistical.setInputCloud(cloud_after_voxelgrid);
Statistical.setMeanK(100);
Statistical.setStddevMulThresh(0.15);
Statistical.filter(*cloud_after_StatisticalRemoval);
对滤波后的点云进行法向量计算并显示法向量和点云:
pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>);
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> normalEstimation;
normalEstimation.setInputCloud(cloud_after_StatisticalRemoval);
normalEstimation.setRadiusSearch(100);
pcl::search::KdTree<pcl::PointXYZ>::Ptr kdtree(new pcl::search::KdTree<pcl::PointXYZ>);
normalEstimation.setSearchMethod(kdtree);
normalEstimation.compute(*normals);
viewer1->addPointCloud<pcl::PointXYZ>(cloud_after_StatisticalRemoval, "cloud_after_StatisticalRemoval");
viewer1->addPointCloudNormals<pcl::PointXYZ, pcl::Normal>(cloud_after_StatisticalRemoval, normals, 2, 100, "normals");
viewer1->addCoordinateSystem(1.0);
对点云进行分割。点云分割是根据空间、几何和纹理等特征对点云进行划分,使得同一划分区域内的点云拥有相似的特征 。从点云中提取簇(集群),并将点云索引保存在 cluster_indices 中,通过设置临近搜索的搜索半径(搜索容差)、每个簇(集群)的最小大小、每个簇(集群)的最大大小、点云搜索算法,进行点云分割。
需要注意的是:设置一个合适的聚类搜索半径 ClusterTolerance,如果搜索半径取一个非常小的值,那么一个实际独立的对象就会被分割为多个聚类;如果将值设置得太高,那么多个对象就会被分割为一个聚类,所以需要进行测试找出最合适的ClusterTolerance。
std::vector<pcl::PointIndices> cluster_indices;
pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
ec.setClusterTolerance(20.0); 
ec.setMinClusterSize(2000);    
ec.setMaxClusterSize(250000);  
ec.setSearchMethod(tree);     
ec.setInputCloud(cloud_after_StatisticalRemoval);   
ec.extract(cluster_indices);   
输出点云簇质心位置。通过compute3DCentroid计算分割后点云簇的质心,并显示在点云图像中:
Eigen::Vector4f centroid;  
pcl::compute3DCentroid(*cloud_cluster, centroid); 
std::cout << "Cluster x y z: " << centroid(0) << " " << centroid(1) << " " << centroid(2) << std::endl;
pcl::PointXYZ center;
center.x = centroid(0);
center.y = centroid(1);
center.z = centroid(2);
viewer2->addSphere(center, 20, 255, 0, 0, "sphere"+to_string(j));
五. 代码
1、解析点云数据及存储:
main.cpp:
#include <iostream>
#include <fstream>
#include <algorithm>
#include <stdio.h>
#include "../common/common.hpp"
#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
using namespace std;
int main()
{
    ASSERT_OK(MV3D_RGBD_Initialize());
    unsigned int nDevNum = 0;
    ASSERT_OK(MV3D_RGBD_GetDeviceNumber(DeviceType_USB, &nDevNum));
    LOGD("MV3D_RGBD_GetDeviceNumber success! nDevNum:%d.", nDevNum);
    ASSERT(nDevNum);
    
    std::vector<MV3D_RGBD_DEVICE_INFO> devs(nDevNum);
    ASSERT_OK(MV3D_RGBD_GetDeviceList(DeviceType_USB, &devs[0], nDevNum, &nDevNum));
    for (unsigned int i = 0; i < nDevNum; i++)
    {
        LOG("Index[%d]. SerialNum[%s] IP[%s] name[%s].\r\n", i, devs[i].chSerialNumber, devs[i].SpecialInfo.stNetInfo.chCurrentIp, devs[i].chModelName);
    }
    
    void* handle = NULL;
    unsigned int nIndex = 0;
    ASSERT_OK(MV3D_RGBD_OpenDevice(&handle, &devs[nIndex]));
    LOGD("OpenDevice success.");
    
    ASSERT_OK(MV3D_RGBD_Start(handle));
    LOGD("Start work success.");
    int num = 0;
    bool exit_flag = TRUE;
    MV3D_RGBD_FRAME_DATA stFrameData = { 0 };
    while (exit_flag)
    {
        pcl::PointCloud<pcl::PointXYZ> pointCloud;
        
        int nRet = MV3D_RGBD_FetchFrame(handle, &stFrameData, 5000);
        
        if (MV3D_RGBD_OK == nRet)
        {
            
            for (int i = 0; i < stFrameData.nImageCount; i++)
            {
                
                if (ImageType_Depth == stFrameData.stImageData[i].enImageType)
                {
                    MV3D_RGBD_IMAGE_DATA stPointCloudImage;
                    
                    nRet = MV3D_RGBD_MapDepthToPointCloud(handle, &stFrameData.stImageData[i], &stPointCloudImage);
                    
                    float* pPtr = (float*)stPointCloudImage.pData;
                    int nPointNum = stPointCloudImage.nDataLen / (sizeof(float) * 3);
                    
                    LOGD("Start write Points of %d framenum!", stPointCloudImage.nFrameNum);
                    for (int nPntIndex = 0; nPntIndex < nPointNum; ++nPntIndex)
                    {
                        if (pPtr[nPntIndex * 3 + 0] > 6000 || pPtr[nPntIndex * 3 + 1] > 6000 || pPtr[nPntIndex * 3 + 2] > 6000)continue;
                        if (pPtr[nPntIndex * 3 + 0] != 0 || pPtr[nPntIndex * 3 + 1] != 0 || pPtr[nPntIndex * 3 + 2] != 0) {
                            pcl::PointXYZ point;
                            point.x = pPtr[nPntIndex * 3 + 0];
                            point.y = pPtr[nPntIndex * 3 + 1];
                            point.z = pPtr[nPntIndex * 3 + 2];
                            pointCloud.points.push_back(point);
                        }
                    }
                    pointCloud.width = pointCloud.points.size();
                    pointCloud.height = 1;
                    pcl::io::savePCDFileASCII(to_string(num)+".pcd", pointCloud);
                    LOGD("_MapDepthToPointCloud() Run Succeed: framenum (%d) height(%d) width(%d)  len (%d)!", stPointCloudImage.nFrameNum,
                        stPointCloudImage.nHeight, stPointCloudImage.nWidth, stPointCloudImage.nDataLen);
                }
                
                if (MV3D_RGBD_OK != nRet)
                {
                    break;
                }
            }
        }
        
        if (_kbhit())
        {
            exit_flag = FALSE;
        }
        num++;
    }
    ASSERT_OK(MV3D_RGBD_Stop(handle));
    ASSERT_OK(MV3D_RGBD_CloseDevice(&handle));
    ASSERT_OK(MV3D_RGBD_Release());
    LOGD("Main done!");
    return 0;
}
CMakeLists.txt:
cmake_minimum_required(VERSION 3.9)
set(CMAKE_CXX_STANDARD 11)
project(storepointscloud)
set(BOOST_ROOT "C:/PCL 1.12.1/3rdParty/Boost")
find_package(Boost REQUIRED
             COMPONENTS system filesystem thread)
find_package(PCL 1.3 REQUIRED)
set(SOURCE_FILES main.cpp)
include_directories(Include/)
include_directories(common/include)
include_directories(
    ${PCL_INCLUDE_DIRS}
    ${Boost_INCLUDE_DIRS}
    )
link_directories(Lib/Win64)
link_directories(common/lib/Win64)
link_directories(
    ${PCL_LIBRARY_DIRS}
    ${BOOST_LIBRARY_DIRS}
    )
add_definitions(
    ${PCL_DEFINITIONS}
    )
add_compile_definitions(NOMINMAX)
add_executable(storepointscloud ${SOURCE_FILES})
target_link_libraries(storepointscloud 
    ${PCL_LIBRARIES}
    ${Boost_LIBRARIES}
    )
target_link_libraries(storepointscloud Mv3dRgbd glfw3 opengl32 glu32)
2、点云数据处理及物体位置输出:
main.cpp:
#include <iostream>
#include <fstream>
#include <algorithm>
#include <stdio.h>
#include<pcl/point_types.h>
#include <pcl/io/pcd_io.h>
#include <pcl/visualization/cloud_viewer.h>
#include<pcl/filters/passthrough.h>  
#include<pcl/filters/voxel_grid.h>  
#include<pcl/filters/statistical_outlier_removal.h>   
#include <pcl/filters/conditional_removal.h>    
#include <pcl/filters/radius_outlier_removal.h>   
#include <pcl/features/normal_3d.h>
#include <pcl/ModelCoefficients.h>
#include <pcl/filters/extract_indices.h>
#include <pcl/kdtree/kdtree.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/segmentation/extract_clusters.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <Eigen/Core>
#include <pcl/common/transforms.h>
using namespace std;
int main(int argc, char** argv)
{
    vtkObject::GlobalWarningDisplayOff();
    
    
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::PCDReader reader;
    reader.read("1.pcd", *cloud);    
    boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(new pcl::visualization::PCLVisualizer("3D Viewer:pre-process"));
    boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer1(new pcl::visualization::PCLVisualizer("3D Viewer:normals"));
    boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer2(new pcl::visualization::PCLVisualizer("3D Viewer:segmentation"));
    viewer->initCameraParameters();
    
    int v1(0);
    viewer->createViewPort(0.0, 0.5, 0.5, 1.0, v1);
    viewer->setBackgroundColor(0, 0, 0, v1);
    viewer->addText("sample cloud", 10, 10, "sample cloud", v1);
    viewer->addPointCloud<pcl::PointXYZ>(cloud, "cloud", v1);
    std::cout << "原始点云数据点数:" << cloud->points.size() << std::endl;
    
    
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_after_PassThrough(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::PassThrough<pcl::PointXYZ> passthrough;
    passthrough.setInputCloud(cloud);
    passthrough.setFilterFieldName("z");
    passthrough.setFilterLimits(500.0, 3000.0);
    
    passthrough.filter(*cloud_after_PassThrough);
    
    int v2(0);
    viewer->createViewPort(0.5, 0.5, 1.0, 1.0, v2);
    viewer->setBackgroundColor(0.3, 0.3, 0.3, v2);
    viewer->addText("cloud after PassThrough", 10, 10, "cloud after PassThrough", v2);
    viewer->addPointCloud<pcl::PointXYZ>(cloud_after_PassThrough, "cloud_after_PassThrough", v2);
    std::cout << "直通滤波后点云数据点数:" << cloud_after_PassThrough->points.size() << std::endl;
    
    
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_after_voxelgrid(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::VoxelGrid<pcl::PointXYZ> voxelgrid;
    voxelgrid.setInputCloud(cloud_after_PassThrough);
    voxelgrid.setLeafSize(10.0f, 10.0f, 10.0f);
    voxelgrid.filter(*cloud_after_voxelgrid);
    
    int v3(0);
    viewer->createViewPort(0.0, 0.0, 0.5, 0.5, v3);
    viewer->setBackgroundColor(0.3, 0.3, 0.3, v3);
    viewer->addText("cloud after voxelgrid", 10, 10, "cloud after voxelgrid", v3);
    viewer->addPointCloud<pcl::PointXYZ>(cloud_after_voxelgrid, "cloud_after_voxelgrid", v3);
    std::cout << "体素化网格方法后点云数据点数:" << cloud_after_voxelgrid->points.size() << std::endl;
    
    
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_after_StatisticalRemoval(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::StatisticalOutlierRemoval<pcl::PointXYZ> Statistical;
    Statistical.setInputCloud(cloud_after_voxelgrid);
    Statistical.setMeanK(100);
    Statistical.setStddevMulThresh(0.15);
    
    Statistical.filter(*cloud_after_StatisticalRemoval);
    
    int v4(0);
    viewer->createViewPort(0.5, 0.0, 1.0, 0.5, v4);
    viewer->setBackgroundColor(0.3, 0.3, 0.3, v4);
    viewer->addText("cloud after StatisticalRemoval", 10, 10, "cloud after StatisticalRemoval", v4);
    viewer->addPointCloud<pcl::PointXYZ>(cloud_after_StatisticalRemoval, "cloud_after_StatisticalRemoval", v4);
    std::cout << "统计分析滤波后点云数据点数:" << cloud_after_StatisticalRemoval->points.size() << std::endl;
    
    
    pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>);
    pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> normalEstimation;
    normalEstimation.setInputCloud(cloud_after_StatisticalRemoval);
    
    normalEstimation.setRadiusSearch(100);
    
    
    pcl::search::KdTree<pcl::PointXYZ>::Ptr kdtree(new pcl::search::KdTree<pcl::PointXYZ>);
    normalEstimation.setSearchMethod(kdtree);
    normalEstimation.compute(*normals);
    viewer1->addPointCloud<pcl::PointXYZ>(cloud_after_StatisticalRemoval, "cloud_after_StatisticalRemoval");
    
    viewer1->addPointCloudNormals<pcl::PointXYZ, pcl::Normal>(cloud_after_StatisticalRemoval, normals, 2, 100, "normals");
    viewer1->addCoordinateSystem(1.0);
    
    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>);
    tree->setInputCloud(cloud_after_StatisticalRemoval);
    
    std::vector<pcl::PointIndices> cluster_indices;
    pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
    ec.setClusterTolerance(20.0); 
    ec.setMinClusterSize(2000);    
    ec.setMaxClusterSize(250000);  
    ec.setSearchMethod(tree);     
    ec.setInputCloud(cloud_after_StatisticalRemoval);   
    ec.extract(cluster_indices);        
    
    int j = 0;
    for (std::vector<pcl::PointIndices>::const_iterator it = cluster_indices.begin();it != cluster_indices.end(); ++it) {
        
        pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_cluster(new pcl::PointCloud<pcl::PointXYZ>);
        const std::vector<int>& indices = it->indices;
        for (std::vector<int>::const_iterator pit = indices.begin(); pit != indices.end(); ++pit)
            cloud_cluster->points.push_back(cloud_after_StatisticalRemoval->points[*pit]);
        cloud_cluster->width = cloud_cluster->points.size();
        cloud_cluster->height = 1;
        cloud_cluster->is_dense = true;
        std::cout << "PointCloud representing the Cluster: " << cloud_cluster->points.size() << " data points."
            << std::endl;
        std::stringstream ss;
        ss << "cloud_cluster_" << j;
        Eigen::Vector4f centroid;  
        pcl::compute3DCentroid(*cloud_cluster, centroid); 
        std::cout << "Cluster x y z: " << centroid(0) << " " << centroid(1) << " " << centroid(2) << std::endl;
        pcl::PointXYZ center;
        center.x = centroid(0);
        center.y = centroid(1);
        center.z = centroid(2);
        viewer2->addSphere(center, 20, 255, 0, 0, "sphere"+to_string(j));
        
        pcl::visualization::PointCloudColorHandlerRandom<pcl::PointXYZ> single_color(cloud_cluster);
        viewer2->addPointCloud<pcl::PointXYZ>(cloud_cluster, single_color, ss.str());
        viewer2->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, ss.str());
        j++;
    }
    std::cout << "cloud size: " << cluster_indices.size() << std::endl;
    while (!viewer->wasStopped()) {
        viewer->spinOnce(1000);
    }
    return (0);
}
CMakeLists.txt:
cmake_minimum_required(VERSION 3.9)
set(CMAKE_CXX_STANDARD 11)
project(pointscloudprocessing)
set(BOOST_ROOT "C:/PCL 1.12.1/3rdParty/Boost")
find_package(Boost REQUIRED
             COMPONENTS system filesystem thread)
find_package(PCL 1.3 REQUIRED)
find_package(VTK REQUIRED)
set(SOURCE_FILES main.cpp)
include_directories(Include/)
include_directories(common/include)
include_directories(
    ${PCL_INCLUDE_DIRS}
    ${Boost_INCLUDE_DIRS}
    ${VTK_INCLUDE_DIRS}
    )
link_directories(Lib/Win64)
link_directories(common/lib/Win64)
link_directories(
    ${PCL_LIBRARY_DIRS}
    ${BOOST_LIBRARY_DIRS}
    ${VTK_LIBRARY_DIRS}
    )
add_definitions(
    ${PCL_DEFINITIONS}
    )
add_compile_definitions(NOMINMAX)
add_executable(pointscloudprocessing ${SOURCE_FILES})
target_link_libraries(pointscloudprocessing 
    ${PCL_LIBRARIES}
    ${Boost_LIBRARIES}
    ${VTK_LIBRARIES}
    )
六. 运行结果
从控制台查看相机参数和图像信息:

存储得到的点云文件:

第一个渲染窗口的原始点云,直通滤波器滤波,体素滤波器下采样,统计滤波器滤波结果:

法向量计算结果:

点云分割结果:

点云质心位置:


七. 总结
本课题采用PCL对海康机器人3D相机产品野中点云进行处理和获取多个无序摆放物品的抓取点位,但是实际拍摄物体种类较少,实验场景单一,整体还有待优化。
代码地址:https://github.com/hahaha1014/HaikangXiling.git