六轴机械臂标定

一、简介

为了实现机器人能够高精度的抓取,或者按照工件外轮廓形状,走特定的轨迹,需要进行一系列的标定工作;具体标定流程可以分为以下四步:

二、标定流程

1、前期准备

本文中使用2D相机为海康相机:MV-CS050-10UM

(1)、确定好棋盘格标定板的尺寸(棋盘格方格边长、标定板尺寸);

(2)、保证机器人现场各项硬件设备均正常运行,设备包括(2d相机、3D相机、相机硬件连接线,网络,相机软件,标定板等);

(3)、根据相机情况,以及实际拍照距离,调整合适的光圈(光圈越大,拍照景深越浅,反之则相反 ),将棋盘格放置到视野范围内,相机连接到MVS软件中,调整焦距直至看到棋盘格的焦点,如下图,这里相机镜头为16mm C 口定焦工业镜头, 物距为40cm左右

2、开始标定

步骤1:相机标定(矫正相机畸变,求解内外参)

注意:这里采用的眼在手上,相机装在机械臂末端上,眼在手外时,固定相机,标定板装到机械臂末端上,移动标定板;

(1)、固定标定板 (12*9,单格6mm)整个标定过程中不能动 ,调整相机位置,确保标定板完整出现在相机视野中且图像清晰

(2)、控制机器人多角度移动相机,**采集 15-20 张不同姿态的标定板图像以及模态姿态坐标(姿态覆盖平移、旋转、倾斜,避免图像重复,姿态尽量变化大)**尽量将标定板置为在视野上下左右,以及四个角,中间等位置;

bash 复制代码
//末端位姿数据X,Y,Z,Rx,Ry,Rz
1051.6,  -516.6,  960.6,  -53.6,  86.7, -127.2  
997.5,  -457.8,  960.6,  -69.8,  86.7,  -127.2
1240.6,,  -457.8,   1047.9,  -172.2,  66.8,  -24.2
1122.8, -721.5, 980.6, -130.8, 77.8, -37
1134.2, -362.3, 980.2, -149.4, 84.9, -56.9
1426.7, -668.6, 1079.8, 178.1, 56.6, 13.8 
1356.6, -423.7, 1052.5, 127.4, 53.9, 49.8
1473.0, -210.4, 967.6, 87.7, 5.6, 84.1
1115.7, -812.7, 959.3, -77.2, 46.5, -91.4
1416.6, -399.0, 1075.6, 136.3, 51.9, 47.4
1486.6, -450.2, 1107.2, 117.3, 14.7, 85.9
1528.8, -236.0, 961.5, 91.3, 6.6, 83.0
1537.3, -211.8, 950.4, 88.4, 6.7, 83.5
1123.2, -870.6, 935.2, -87.2, 30.5, -92.1
1333.6, -416.3, 1070.4, 135.7, 53.8, 53.3
1422.5, -205.8, 978.9, 98.1, 27.0, 78.3
1494.0, -186.1, 998.6, 91.7, 6, 92.9
1118.6, -718.9, 953.0, -84.8, 55.2, -91.7

(3)、相机软件中要清楚看到标定板的角点,计算相机的内参矩阵(焦距、主点坐标)和畸变系数(径向 + 切向畸变)

(4)、验证标定结果:用标定后的参数矫正一张测试图像,观察直线是否无畸变,角点检测误差是否在允许范围内(一般≤0.5 像素);

(5)、保存相机内参和畸变系数,外参;

(6)、坐标系关系介绍:

坐标系转换关系:

f:表示焦距;

dx: 每一像素代表多少毫米;

u0:表示图片中心点的u轴像素;

v0:表示图片中心点的v轴像素;

核心代码:
cpp 复制代码
std::vector<cv::Mat> EyeInHandCal::Cam_Calibration_Corner(std::vector<cv::Mat> images, // 采集的图片数据
                                                          cv::Size board_size,  // 棋盘格纵横向角点数
                                                          cv::Size2f square_size, // 棋盘格单格格的尺寸
                                                          std::string filename )// 结果存放文件
{
    std::vector<std::vector<cv::Point2f>> image_points_seq;
    int image_count = 0;
    cv::Size image_size(0,0); // 图像尺寸

    for (int i = 0; i < images.size(); i++)
    {
        image_count++;
        cv::Mat imageInput = images[i];
        if (image_count == 1)
        {
            image_size.width = imageInput.cols;
            image_size.height = imageInput.rows;
        }
        std::vector<cv::Point2f> image_points_buf;

        // 1. 粗检测棋盘格内角点
        if (false == findChessboardCorners(imageInput, board_size, image_points_buf))
        {
            std::cout << "None " << i << "find the corners in the image!" << std::endl;
        }
        else
        {
            // 角点检测到,
            // 2、亚像素级细化角点坐标(提升标定精度的核心操作)
            // 像素级角点精度不足以支撑高精度相机标定,亚像素细化能显著降低重投影误差,提升标定精度。
            cv::cornerSubPix(imageInput, // 输入图像
                             image_points_buf, // 输入输出:粗检测角点 → 细化后角点
                             cv::Size(5, 5),  // 搜索窗口大小
                             cv::Size(-1, -1), // 死区大小(-1表示无死区)
                             // 迭代终止条件:迭代次数30次 或 精度0.01像素
                             cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.01)
                            );

            image_points_seq.push_back(image_points_buf);
        }
    }

    //以下是摄像机标定    
    std::vector<std::vector<cv::Point3f>> object_points; // 所有图像对应的3D坐标集合

    /* 初始化标定板上角点的三维坐标 */
    int i = 0, j = 0, t = 0;
    for (t = 0; t < image_points_seq.size(); t++) // 遍历所有图片数据
    {
        std::vector<cv::Point3f> tempPointSet; // 单张图像的3D坐标集合
        for (i = 0; i < board_size.height; i++) // 遍历棋盘格的行
        {
            for (j = 0; j < board_size.width; j++) // 遍历棋盘格的列
            {
                cv::Point3f realPoint(0, 0, 0);// 计算每个角点的3D坐标:基于单个格子的物理尺寸
                realPoint.x = i * square_size.width; // X轴坐标
                realPoint.y = j * square_size.height; // Y轴坐标
                realPoint.z = 0;    // 棋盘格在XY平面,Z=0
                tempPointSet.push_back(realPoint);
            }
        }
        object_points.push_back(tempPointSet);
    }

    /*内外参数*/
    cv::Mat cameraMatrix = cv::Mat(3, 3, CV_32FC1, cv::Scalar::all(0));// 内参矩阵(3x3)
    cv::Mat distCoeffs = cv::Mat(1, 5, CV_32FC1, cv::Scalar::all(0));// 畸变系数(1x5)
    std::vector<cv::Mat> tvecsMat;// 平移向量集合(每张图像一个 3x1 向量)
    std::vector<cv::Mat> rvecsMat;// 旋转向量集合(每张图像一个 3x1 向量)

    /* 开始标定 */
    // 重投影误差(像素):评估标定精度的核心指标,越小越好(理想值 < 1 像素)
    double rmsproj = cv::calibrateCamera(
        object_points,// 输入:所有图像的3D世界坐标
        image_points_seq,// 输入:所有图像的2D图像坐标
        image_size,// 输入:图像尺寸
        cameraMatrix,// 输出:内参矩阵
        distCoeffs,// 输出:畸变系数
        rvecsMat,// 输出:旋转向量(外参)
        tvecsMat);// 输出:平移向量(外参)

    std::cout << "camera the reprojection error is:" << rmsproj << std::endl;
    std::cout << "camera calibration complete!\n";

    // 相机内参和畸变系数是相机固有属性,一旦标定完成,可重复使用(除非相机硬件改动)
    std::string camPathStr = filename + "/cam_calibration.yml";
    fs::path rel_path(camPathStr);
    fs::path abs_path = fs::absolute(rel_path);  // 转换为绝对路径
    std::string abs_path_str = abs_path.string();

    cv::FileStorage fs(abs_path_str, cv::FileStorage::WRITE);
    if (fs.isOpened())
    {
        fs << "camera_matrix" << cameraMatrix; // 写入内参矩阵
        fs << "dist_coeffs" << distCoeffs;// 写入畸变系数
        fs.release();
    }

    // 将外参转换为 4×4 齐次变换矩阵,返回结果
    std::vector<cv::Mat> Pose_cam_board; // 存储所有图像的4x4位姿矩阵
    for (i = 0; i < rvecsMat.size(); i++)
    {
        cv::Mat rot;
        // 1. 罗德里格斯变换:旋转向量(3x1)→ 旋转矩阵(3x3)
        cv::Rodrigues(rvecsMat[i], rot);
        // 每张图片下的,平移分量
        const double* tvec_data = tvecsMat[i].ptr<double>(0);
        // 2. 构造4x4齐次变换矩阵 「标定板坐标系(世界坐标系) → 相机坐标系」的刚性变换
        // 矩阵格式:[R | T]
        //          [0 | 1]
        cv::Mat RT = (cv::Mat_<double>(4, 4) << 
            rot.at<double>(0, 0), rot.at<double>(0, 1), rot.at<double>(0, 2), tvec_data[0],
            rot.at<double>(1, 0), rot.at<double>(1, 1), rot.at<double>(1, 2), tvec_data[1],
            rot.at<double>(2, 0), rot.at<double>(2, 1), rot.at<double>(2, 2), tvec_data[2],
            0, 0, 0, 1);// 齐次矩阵最后一行固定为 [0,0,0,1]
        Pose_cam_board.push_back(RT);
    }
    return Pose_cam_board;
}

步骤2:手眼标定(建立相机与机器人坐标系映射)

根据机器人位姿数据和图像中标定板的位姿,计算相机坐标系相对于机器人工具坐标系的转换矩阵(手眼矩阵)

控制机器人移动到任意未参与标定的位姿,通过手眼矩阵计算标定板在机器人基坐标系中的位置,与实际测量值对比,误差需满足项目要求(如≤0.1mm)。

情况1、眼在手上的标定方式:

标定板上的点到机器人基座的标定关系下:

眼在手上在整个标定过程中,有两组变量是固定的,标定板到机器人基底是固定的、相机到机器人末端的关系是固定的,因此每组数据转换满足下面规则:

这里只需要求得矩阵,这是个定值:

核心代码:
cpp 复制代码
// 拆解矩阵
void EyeInHandCal::HomogeneousMtr2RT(cv::Mat& HomoMtr, cv::Mat& R, cv::Mat& T)
{
    cv::Rect R_rect(0, 0, 3, 3);
    cv::Rect T_rect(3, 0, 1, 3);
    R = HomoMtr(R_rect);

    T = HomoMtr(T_rect);
}

// 拼接手眼矩阵
cv::Mat EyeInHandCal::R_T2HomogeneousMatrix(const cv::Mat& R, const cv::Mat& T)
{
    cv::Mat HomoMtr;
    cv::Mat_<double> R1 = (cv::Mat_<double>(4, 3) <<
        R.at<double>(0, 0), R.at<double>(0, 1), R.at<double>(0, 2),
        R.at<double>(1, 0), R.at<double>(1, 1), R.at<double>(1, 2),
        R.at<double>(2, 0), R.at<double>(2, 1), R.at<double>(2, 2),
        0, 0, 0);
    cv::Mat_<double> T1 = (cv::Mat_<double>(4, 1) <<
        T.at<double>(0, 0),
        T.at<double>(1, 0),
        T.at<double>(2, 0),
        1);
    cv::hconcat(R1, T1, HomoMtr);//矩阵拼接
    return HomoMtr;
}

// 手眼标定
cv::Mat EyeInHandCal::Calibrate_HandEye(std::vector<cv::Mat> Pose_cam_board,
    std::vector<cv::Mat> Pose_tool, std::string& filename)
{
    //数据声明
    // 机器人工具→基坐标系:旋转矩阵(3x3)和平移向量(3x1)的集合
    std::vector<cv::Mat> R_gripper2base;
    std::vector<cv::Mat> T_gripper2base;

    // 标定板→相机坐标系:旋转矩阵(3x3)和平移向量(3x1)的集合
    std::vector<cv::Mat> R_target2cam;
    std::vector<cv::Mat> T_target2cam;

    // 相机→机器人工具坐标系:最终求解的旋转矩阵(3x3)和平移向量(3x1)
    cv::Mat R_cam2gripper = cv::Mat(3, 3, CV_64FC1);
    cv::Mat T_cam2gripper = cv::Mat(3, 1, CV_64FC1);

    // 相机→机器人工具坐标系:齐次变换矩阵(4x4,由旋转矩阵+平移向量组合而成)
    cv::Mat Homo_cam2gripper;
    // 齐次变换矩阵缓存(输入参数的备份)
    std::vector<cv::Mat> Homo_target2cam;
    std::vector<cv::Mat> Homo_gripper2base;

    cv::Mat tempR, tempT, temp;
    //计算标定板与相机间的齐次矩阵(旋转矩阵与平移向量)
    for (int i = 0; i < Pose_cam_board.size(); i++)
    {
        temp = Pose_cam_board[i];
        Homo_target2cam.push_back(temp);
        //齐次转rt 4x4 齐次矩阵 → 3x3 旋转矩阵 + 3x1 平移向量
        HomogeneousMtr2RT(temp, tempR, tempT);
        R_target2cam.push_back(tempR);
        T_target2cam.push_back(tempT);
    }

    for (int j = 0; j < Pose_tool.size(); j++)
    {
        temp = Pose_tool[j];
        Homo_gripper2base.push_back(temp);
        HomogeneousMtr2RT(temp, tempR, tempT);
        R_gripper2base.push_back(tempR);
        T_gripper2base.push_back(tempT);
    }
    ////TSAI计算速度最快(试一下标定方法)
    cv::calibrateHandEye(R_gripper2base, T_gripper2base, R_target2cam, T_target2cam, R_cam2gripper, T_cam2gripper, cv::CALIB_HAND_EYE_TSAI);

    Homo_cam2gripper = R_T2HomogeneousMatrix(R_cam2gripper, T_cam2gripper);

    std::cout << "Hand-eye matrix (eye on hand)" << Homo_cam2gripper << std::endl;

    std::string handEyePathStr = filename + "/HandInEye_calibration.yml";
    std::filesystem::path rel_path(handEyePathStr);
    std::filesystem::path abs_path = fs::absolute(rel_path);  // 转换为绝对路径
    std::string handEyeAbs_path_str = abs_path.string();

    cv::FileStorage fs1(handEyeAbs_path_str, cv::FileStorage::WRITE);
    if (fs1.isOpened())
    {
        fs1 << "calibration_result" << Homo_cam2gripper;
        fs1.release();
    }
    return Homo_cam2gripper;
}

即为所求未知矩阵,其他矩阵都为已知值。相机内外参及手眼标定完成后,后续如果整体结构和位置未发生变化,则不再需要进行标定。

通过上述可以求得手眼矩阵为:

bash 复制代码
   data: [ -3.0800181771835300e-02, -3.5519725622691251e-01, 9.3428382088725859e-01, 4.7502537069954926e+01, // x =  4.7502537069954926
       9.9952085943240698e-01, -1.3812516610564485e-02, 2.7699565779723656e-02, -7.0661239637111962e+01, // y = -7.0661239637111962
       3.0660010313520725e-03, 9.3468931926804211e-01, 3.5545249483428831e-01, 2.0935566047297883e+02,  //z = 209.35566047297883, 单位为mm
       0, 0, ., 1. ]
       // 有了x,y, z, 可以实际测量一下2D相机相对于机械臂末端的位置信息,从而验证标定是否正确
       
情况2、眼在手外的标定方式:

**整个标定过程中,相机到机器人基座的位置关系是固定的,标定板到末端也是固定的,**标定板固定在机械臂末端上,移动 15-20个不同位姿,可以获取到各个角点的的RT变化矩阵,同时根据末端位姿可以获取机器人的;

从上面的公式变换中,可以得出以下等式关系:

从这里不难看出只需要求出定值矩阵

步骤3:3D标定

前面的标定,仅仅获得了2d相机看到物体的2D信息,但是实际用机械臂,进行码垛抓取时,还需要3D的Z方向信息,因而需要3D标定

完成相机和机械臂的标定后,开始进行工件标定。移动机械臂,在3D相机的合适成像范围内采集点云数据(本文中使用的是梅卡曼德为1m-2m,1.5m最好);

3D标定的核心目的:

(1)修正相机(采集相机)的镜头畸变;

(2)求解**「结构光投射器坐标系」与「相机坐标系」的相对位姿(外参)**,建立两者的精准映射;

(3)建立"光栅图案的相位/灰度信息"与"真实物理深度"的对应关系,生成深度查询表(LUT);

(4) 最终输出"像素坐标→3D物理坐标(X,Y,Z)"的精准转换模型;

(1)、点云位姿数据采集

在采集数据的时候,尽量采集特征点比较多的区域,方便后续的配准定位,使用3D相机拍照的同时,也要记录好机器人末端的数据;

修正相机(采集相机)的镜头畸变;

求解「结构光投射器坐标系」与「相机坐标系」的相对位姿(外参),建立两者的精准映射------>手眼矩阵;

采集15-20组数据,由于整个标定过程中标定板始终固定,所以等式左边是个定值

这里同理,整个标定过程中要求的定值为:

该矩阵为3D相机的手眼标定矩阵;

核心代码:
cpp 复制代码
//3d相机标定 非均匀圆点标定板使用下列算法
std::vector<cv::Mat> EyeInHandCal::Cam_Calibration_Circle(std::vector<cv::Mat> images, // 输入:采集的多张标定板图像
                                                          cv::Size board_size,// 输入:标定板的圆点网格尺寸(如cv::Size(8,5)表示8列5行)
                                                          cv::Size2f square_size, // 输入:圆点中心的物理间距(单位:mm,width/x方向,height/y方向)
                                                          std::string filename)// 输入:保存标定结果的目录路径
{
    std::vector<std::vector<cv::Point2f>> image_points_seq;

    int image_count = 0;
    cv::Size image_size;
    std::vector<cv::Point2f> image_points_buf;

    for (int i = 0; i < images.size(); i++)
    {
        image_count++;
        cv::Mat imageInput = images[i];
        if (image_count == 1)
        {
            image_size.width = imageInput.cols;
            image_size.height = imageInput.rows;
        }

        // 初始化Blob检测器参数(针对圆点特征优化)
        cv::SimpleBlobDetector::Params params;

        params.thresholdStep = 10;//阈值越小检测越精确,计算速度变慢
        params.filterByInertia = true;//斑点圆度的限制变量,默认是不限制 开启圆度过滤(保证只检测圆形斑点)
        params.minInertiaRatio = 0.5f;// 最小圆度(0.5表示至少是"半椭圆"以上的圆形)
        params.filterByColor = true; //按颜色过滤(只检测白色圆点)
        params.blobColor = 255;//255表示只提取白色斑点,0表示只提取黑色斑点
        params.filterByArea = true;//按面积过滤(排除过小的噪声斑点)
        params.minArea = 500;// 最小斑点面积(像素)

        // 创建Blob检测器,用于圆点中心提取
        cv::Ptr<cv::FeatureDetector> detector = cv::SimpleBlobDetector::create(params);

        // 核心:检测非均匀圆点网格的中心(CALIB_CB_ASYMMETRIC_GRID表示不对称网格)
        if (false == findCirclesGrid(imageInput, board_size, image_points_buf, cv::CALIB_CB_ASYMMETRIC_GRID, detector))
        {
            std::cout << "No circle center detected!" << std::endl;
        }
        else
        {
            image_points_seq.push_back(image_points_buf); // 保存检测到的像素坐标
            std::cout << "NO:" << i << ", image dot detection completed" << std::endl;
        }
    }

    //以下是摄像机标定    
    std::vector<std::vector<cv::Point3f>> object_points;

    /* 初始化标定板上角点的三维坐标 */
    for (int t = 0; t < image_points_seq.size(); t++)
    {
        std::vector<cv::Point3f> tempPointSet;
        for (int i = 0; i < board_size.height; i++)// 遍历标定板行数
        {
            for (int j = 0; j < board_size.width; ++j)// 遍历标定板列数
            {
                cv::Point3f realPoint;
                // 非均匀网格的核心坐标规则:奇数行的列坐标偏移半个间距
                realPoint.x = (2 * j + (i % 2)) * square_size.width;
                realPoint.y = i * square_size.height;
                realPoint.z = 0;// 标定板在世界坐标系的Z=0平面

                tempPointSet.push_back(realPoint);
            }
        }
        object_points.push_back(tempPointSet);// 保存每幅图像对应的世界坐标
    }

    
    // 初始化内参、畸变系数、外参存储变量
    cv::Mat cameraMatrix = cv::Mat(3, 3, CV_32FC1, cv::Scalar::all(0));// 相机内参矩阵(3x3)
    cv::Mat distCoeffs = cv::Mat(1, 5, CV_32FC1, cv::Scalar::all(0));// 畸变系数(k1,k2,p1,p2,k3)
    std::vector<cv::Mat> tvecsMat;// 平移向量(每幅图像的相机→标定板的平移)
    std::vector<cv::Mat> rvecsMat;// 旋转向量(每幅图像的相机→标定板的旋转)
    /* 开始标定 */
    double rmsproj = cv::calibrateCamera(
        object_points,// 标定板世界坐标
        image_points_seq,// 圆点像素坐标
        image_size,// 图像尺寸
        cameraMatrix,// 输出:相机内参矩阵
        distCoeffs,// 输出:畸变系数包含径向畸变 (k1,k2,k3) 和切向畸变 (p1,p2),修正镜头畸变;
        rvecsMat,// 输出:畸变系数
        tvecsMat);// 输出:每幅图像的平移向量

    std::cout << "Calibration complete!\n";

    std::string cam3dPathStr = filename + "/cam_calibration.yml";

    fs::path rel_path(cam3dPathStr);
    fs::path abs_path = fs::absolute(rel_path);  // 转换为绝对路径
    std::string cam3d_abs_path_str = abs_path.string();

    cv::FileStorage fs(cam3d_abs_path_str, cv::FileStorage::WRITE);
    if (fs.isOpened())
    {
        fs << "camera_matrix" << cameraMatrix;
        fs << "dist_coeffs" << distCoeffs;
        fs.release();
    }

    // 将旋转向量+平移向量转换为4x4 RT矩阵(相机→标定板的位姿)
    std::vector<cv::Mat> Pose_cam_board;
    for (i = 0; i < rvecsMat.size(); i++)
    {
        cv::Mat rot;
        cv::Rodrigues(rvecsMat[i], rot);// 旋转向量→旋转矩阵(3x3)
        // 构建4x4齐次变换矩阵(RT)
        cv::Mat RT = (cv::Mat_<double>(4, 4) << rot.at<double>(0, 0), rot.at<double>(0, 1), rot.at<double>(0, 2), tvec_data[0],
            rot.at<double>(1, 0), rot.at<double>(1, 1), rot.at<double>(1, 2), tvec_data[1],
            rot.at<double>(2, 0), rot.at<double>(2, 1), rot.at<double>(2, 2), tvec_data[2],
            0, 0, 0, 1);
        Pose_cam_board.push_back(RT);
    }

    return Pose_cam_board;
}
(2)、手眼标定

这里手眼标定逻辑跟2D一致

步骤4、工件标定

用3D相机采集工件数据,尽量集中在特征点较多的区域,采集到的点云数据为待配准点云数据,模型文件作为目标点云数据,进行点云配准(fpfh),描述点云局部结构,步骤如下所示:

点云配准目的是得到,3D相机与产品点云的转化关系矩阵;

点云配准后得到的矩阵,就是工件在3D相机下的变换关系;因为标定中末端位姿是确定的,因而满足以下转换:

核心代码:
cpp 复制代码
// 计算点云法向量
// 计算每个点的法向量(描述点云表面的朝向),是 FPFH 特征提取的必要前提。
pcl::PointCloud<pcl::Normal>::Ptr Registration::computeNormals(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud,
    int k, bool rever)
{
    pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>);
    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>);
    // 多线程法向量估计(NormalEstimationOMP比普通版快)
    pcl::NormalEstimationOMP<pcl::PointXYZ, pcl::Normal> norm_omp;
    norm_omp.setInputCloud(cloud);          // 输入点云
    norm_omp.setSearchMethod(tree);         // 用KdTree加速邻域搜索
    norm_omp.setKSearch(k);                 // 取每个点的k个近邻计算法向量(k=10)
    norm_omp.setNumberOfThreads(8);         // 8线程加速
    norm_omp.compute(*normals);             // 计算法向量

    if (rever){ // 反转法向量方向(解决法向量朝向不一致的问题)
        for (auto& n : normals->points)
        {
            n.normal_x *= -1.0f;
            n.normal_y *= -1.0f;
            n.normal_z *= -1.0f;
        }
    }

    return normals;
}


//提取点云特征
pcl::PointCloud<pcl::FPFHSignature33>::Ptr Registration::computeFpfhWithNormals(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud,
    const pcl::PointCloud<pcl::Normal>::Ptr& normals, int k)
{
    pcl::PointCloud<pcl::FPFHSignature33>::Ptr fpfh(new pcl::PointCloud<pcl::FPFHSignature33>);
    // 多线程FPFH特征估计
    pcl::FPFHEstimationOMP<pcl::PointXYZ, pcl::Normal, pcl::FPFHSignature33> fpfh_est;
    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree_fpfh(new pcl::search::KdTree<pcl::PointXYZ>);
    fpfh_est.setNumberOfThreads(8);
    fpfh_est.setInputCloud(cloud);              // 输入点云
    fpfh_est.setInputNormals(normals);          // 输入法向量
    fpfh_est.setSearchMethod(tree_fpfh);        // KdTree加速
    fpfh_est.setKSearch(k);                     // num必须大于等于k, 取k个近邻计算特征(k=10)
    fpfh_est.compute(*fpfh);                    // 输出33维FPFH特征

    return fpfh;
}


Eigen::Matrix4f Registration::registration(const std::string& model_path,
    const std::string& cloud_path)
{
    pcl::PointCloud<pcl::PointXYZ>::Ptr target_cloud(new pcl::PointCloud<pcl::PointXYZ>);
    if (pcl::io::loadPLYFile<pcl::PointXYZ>(model_path, *target_cloud) == -1)
    {
        std::cout << "Failed to load the model point cloud file!" << std::endl;
    }

    pcl::PointCloud<pcl::PointXYZ>::Ptr source_cloud(new pcl::PointCloud<pcl::PointXYZ>);
    if (pcl::io::loadPLYFile<pcl::PointXYZ>(cloud_path, *source_cloud) == -1)
    {
        std::cout << "Failed to load camera point cloud file!" << std::endl;
    }

    //点云滤波
    /*pcl::PointCloud<pcl::PointXYZ>::Ptr target_cloud_filtered = voxelFilter(target_cloud, 0.05);
    pcl::PointCloud<pcl::PointXYZ>::Ptr source_cloud_filtered = voxelFilter(source_cloud, 0.05);*/

    //计算点云法线
    // 计算法向量(目标点云不反转,源点云反转)
    pcl::PointCloud<pcl::Normal>::Ptr target_cloud_normals = computeNormals(target_cloud, 10);
    pcl::PointCloud<pcl::Normal>::Ptr source_cloud_normals = computeNormals(source_cloud, 10, true);

    // 提取FPFH特征
    pcl::PointCloud<pcl::FPFHSignature33>::Ptr target_fpfh = computeFpfhWithNormals(target_cloud, target_cloud_normals, 10);
    pcl::PointCloud<pcl::FPFHSignature33>::Ptr source_fpfh = computeFpfhWithNormals(source_cloud, source_cloud_normals, 10);

    //粗配准
    pcl::PointCloud<pcl::PointXYZ>::Ptr sac_result(new pcl::PointCloud<pcl::PointXYZ>);

    //没有必要的话,参数尽量不要进行调整
    pcl::SampleConsensusPrerejective<pcl::PointXYZ, pcl::PointXYZ, pcl::FPFHSignature33> sac_ia;
    sac_ia.setInputSource(source_cloud);
    sac_ia.setSourceFeatures(source_fpfh);
    sac_ia.setInputTarget(target_cloud);
    sac_ia.setTargetFeatures(target_fpfh);

    // 核心参数(无需轻易调整)
    sac_ia.setMaximumIterations(300);           // 最大迭代次数(越多越准,越慢)
    sac_ia.setNumberOfSamples(3);               // 每次迭代随机采样3个点(估计变换矩阵的最小点数)
    sac_ia.setCorrespondenceRandomness(5);      // 每个源点取5个候选匹配点(提升鲁棒性)
    sac_ia.setSimilarityThreshold(0.3f);        // 法向量相似度阈值(越小匹配越严格)
    sac_ia.setMaxCorrespondenceDistance(3);     // 匹配点的最大空间距离(单位:米)
    //sac_ia.setInlierFraction(0.25f);//内点所占最小比例

    sac_ia.align(*sac_result);                  // 执行粗配准
    Eigen::Matrix4f coarseTrans = sac_ia.getFinalTransformation();  // 粗配准变换矩阵

    pcl::IterativeClosestPoint<pcl::PointXYZ, pcl::PointXYZ> icp;
    // 输入:粗配准后的源点云 + 目标点云
    pcl::PointCloud<pcl::PointXYZ>::Ptr reg_result(new pcl::PointCloud<pcl::PointXYZ>);

    ////设置ICP参数
    icp.setInputSource(sac_result);
    icp.setInputTarget(target_cloud);

    // ICP核心参数
    icp.setMaximumIterations(300);                      // 最大迭代次数
    icp.setRANSACIterations(100);                       // RANSAC迭代次数(剔除外点)
    icp.setRANSACOutlierRejectionThreshold(0.05);       // RANSAC外点剔除阈值(米)
    icp.setMaxCorrespondenceDistance(3);                // 匹配点最大距离(米)
    icp.setTransformationEpsilon(0.0000001);            // 变换矩阵的最小变化量(收敛条件)
    icp.setEuclideanFitnessEpsilon(0.000001);           // 点云间的最小均方误差(收敛条件)

    //执行ICP
    icp.align(*reg_result);

    Eigen::Matrix4f transMatrix = icp.getFinalTransformation(); // 精配准变换矩阵

    // 最终变换矩阵 = 精配准矩阵 * 粗配准矩阵(矩阵乘法顺序不能反!)
    Eigen::Matrix4f final_matrix = transMatrix * coarseTrans;

    std::cout << "The final transformation is:" << final_matrix << std::endl;

    return final_matrix;
}

}

三、总结

以上就是机器人标定相关的核心技术逻辑与实践要点啦。从相机标定的坐标系统一,到点云配准的变换矩阵求解,再到矩阵求逆在坐标转换中的关键作用,每一步都是实现机器人精准感知与运动控制的基石。希望这些内容能为正在深耕机器人标定领域的你提供些许参考~

相关推荐
xwz小王子几秒前
PNAS:神经形态机器人电子皮肤
网络·人工智能·机器人
CyanMind1 小时前
强化学习观测项详解之——重力投影
学习·机器人
富唯智能1 小时前
不止于替代:富唯复合机器人,定义无人化工厂的柔性生产力
机器人
博图光电1 小时前
博图通用机器人“眼+脑”——赋能动力锂电池模组智能制造
机器人·制造
沫儿笙1 小时前
安川机器人二保焊省气阀
人工智能·机器人
猿饵块2 小时前
机器人--dh参数
机器人
机器觉醒时代2 小时前
定义下一代机器人训练?智元 SOP:VLA 模型真实世界分布式在线后训练的关键突破
分布式·机器人·ai大模型·人形机器人
欧阳天羲3 小时前
PyTorch 入门:搭建机器人目标检测模型(识别障碍物 )
pytorch·目标检测·机器人
初学大模型4 小时前
我们从神经网络进化过程的角度从新审视神经网络
人工智能·机器人
浩子智控5 小时前
什么是企业机器人流程自动化RPA
机器人·自动化·rpa