几何尺寸与公差论坛

 找回密码
 注册
查看: 180|回复: 3

用开源PCL和c++和eigen编写一组离散点拟合最小二乘圆柱

  [复制链接]
发表于 2025-4-11 15:06:13 | 显示全部楼层 |阅读模式
#include <iostream>
#include <vector>
#include <Eigen/Dense>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/common/common.h>
#include <limits>

// Step 1: PCA to get axis and centroid
void computeAxisAndCentroid(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud,
                             Eigen::Vector3f& centroid,
                             Eigen::Vector3f& axis) {
    centroid = Eigen::Vector3f::Zero();
    for (const auto& pt : cloud->points) {
        centroid += pt.getVector3fMap();
    }
    centroid /= static_cast<float>(cloud->size());

    Eigen::MatrixXf centered(cloud->size(), 3);
    for (size_t i = 0; i < cloud->size(); ++i) {
        centered.row(i) = cloud->points.getVector3fMap() - centroid;
    }

    Eigen::Matrix3f cov = centered.transpose() * centered;
    Eigen::SelfAdjointEigenSolver<Eigen::Matrix3f> eig(cov);
    axis = eig.eigenvectors().col(2).normalized();
}

// Step 2: Project each point to axis and get 2D radial vector
void projectToCrossSection(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud,
                           const Eigen::Vector3f& centroid,
                           const Eigen::Vector3f& axis,
                           std::vector<float>& radialDistances) {
    radialDistances.clear();
    for (const auto& pt : cloud->points) {
        Eigen::Vector3f vec = pt.getVector3fMap() - centroid;
        Eigen::Vector3f proj = vec.dot(axis) * axis;
        Eigen::Vector3f perp = vec - proj;
        radialDistances.push_back(perp.norm());
    }
}

// Step 3: Calculate min and max radius
void computeCylindricityError(const std::vector<float>& radialDistances,
                              float& min_radius,
                              float& max_radius,
                              float& cylindricity) {
    min_radius = std::numeric_limits<float>::max();
    max_radius = std::numeric_limits<float>::lowest();
    for (float r : radialDistances) {
        if (r < min_radius) min_radius = r;
        if (r > max_radius) max_radius = r;
    }
    cylindricity = max_radius - min_radius;
}

int main() {
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);

    // 添加模拟圆柱点数据(带一些随机扰动)
    float true_radius = 5.0f;
    float height = 10.0f;
    for (float z = -height / 2; z <= height / 2; z += 0.2f) {
        for (float theta = 0; theta < 2 * M_PI; theta += M_PI / 30) {
            float noise = 0.05f * ((rand() % 100) / 100.0f - 0.5f);
            float x = (true_radius + noise) * std::cos(theta);
            float y = (true_radius + noise) * std::sin(theta);
            cloud->points.emplace_back(x, y, z);
        }
    }

    Eigen::Vector3f centroid, axis;
    computeAxisAndCentroid(cloud, centroid, axis);

    std::vector<float> radialDistances;
    projectToCrossSection(cloud, centroid, axis, radialDistances);

    float min_r, max_r, cylindricity;
    computeCylindricityError(radialDistances, min_r, max_r, cylindricity);

    std::cout << "Centroid: " << centroid.transpose() << std::endl;
    std::cout << "Axis Direction: " << axis.transpose() << std::endl;
    std::cout << "Min Radius: " << min_r << std::endl;
    std::cout << "Max Radius: " << max_r << std::endl;
    std::cout << "Cylindricity Error (GD&T): " << cylindricity << std::endl;

    return 0;
}
 楼主| 发表于 2025-4-11 15:08:27 | 显示全部楼层
加入 PCL Viewer 的可视化代码

在前面代码的基础上,加入以下内容:

#include <pcl/visualization/pcl_visualizer.h>
#include <thread>

// 可视化函数
void visualize(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud,
               const Eigen::Vector3f& centroid,
               const Eigen::Vector3f& axis,
               float min_radius,
               float max_radius,
               float height = 10.0f) {
    pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("Cylinder Fit Viewer"));
    viewer->setBackgroundColor(0, 0, 0);
   
    // 显示原始点云
    pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> cloud_color(cloud, 255, 255, 255);
    viewer->addPointCloud<pcl::PointXYZ>(cloud, cloud_color, "cloud");
    viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, "cloud");

    // 显示拟合轴线
    Eigen::Vector3f p1 = centroid - axis * (height / 2.0f);
    Eigen::Vector3f p2 = centroid + axis * (height / 2.0f);
    viewer->addLine(pcl::PointXYZ(p1[0], p1[1], p1[2]),
                    pcl::PointXYZ(p2[0], p2[1], p2[2]),
                    0.0, 1.0, 0.0, "fitted_axis");
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_LINE_WIDTH, 3, "fitted_axis");

    // 显示最大外圆柱(红色)
    viewer->addCylinder(
        pcl::ModelCoefficients{ .values = { centroid[0], centroid[1], centroid[2],
                                            axis[0], axis[1], axis[2], max_radius } },
        "outer_cylinder");
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR, 1.0, 0.0, 0.0, "outer_cylinder");
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_OPACITY, 0.3, "outer_cylinder");

    // 显示最小内圆柱(蓝色)
    viewer->addCylinder(
        pcl::ModelCoefficients{ .values = { centroid[0], centroid[1], centroid[2],
                                            axis[0], axis[1], axis[2], min_radius } },
        "inner_cylinder");
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR, 0.0, 0.0, 1.0, "inner_cylinder");
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_OPACITY, 0.3, "inner_cylinder");

    // 循环可视化
    while (!viewer->wasStopped()) {
        viewer->spinOnce(100);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

&#129514; 如何调用?

在 main() 最后调用:

visualize(cloud, centroid, axis, min_r, max_r, height);

确保 height 设置为实际点云的高度范围。
&#128161; 结果效果

你将看到:

    白色点:原始采样点

    绿色线:拟合主轴

    红色透明圆柱:最大外包圆柱

    蓝色透明圆柱:最小内接圆柱

中间的距离差就是你实际的 圆柱度误差,可以清晰直观地看到最大/最小偏差点。
&#128230; 构建与运行

确保你在 CMakeLists.txt 中链接了 pcl_visualization:

find_package(PCL REQUIRED COMPONENTS common io visualization)

include_directories(${PCL_INCLUDE_DIRS})
link_directories(${PCL_LIBRARY_DIRS})
add_definitions(${PCL_DEFINITIONS})

add_executable(cylinder_fit main.cpp)
target_link_libraries(cylinder_fit ${PCL_LIBRARIES})

&#9989; 你可以进一步…

    高亮显示超出公差的点(加颜色)

    动态显示点到圆柱面的距离残差

    输出残差热图(Heatmap)
 楼主| 发表于 2025-4-11 15:11:05 | 显示全部楼层
更新后的可视化代码(全部集成)

void visualize_with_deviation(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud,
                              const Eigen::Vector3f& centroid,
                              const Eigen::Vector3f& axis,
                              const std::vector<float>& radialDistances,
                              float min_radius,
                              float max_radius,
                              float tolerance = 0.1f,
                              float height = 10.0f) {
    pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("Cylinder Deviation Viewer"));
    viewer->setBackgroundColor(0, 0, 0);

    // 残差 = 当前点径向距离 - 中心半径
    float center_radius = (max_radius + min_radius) / 2.0f;

    // 创建带颜色的点云
    pcl::PointCloud<pcl::PointXYZRGB>::Ptr colored_cloud(new pcl::PointCloud<pcl::PointXYZRGB>);
    colored_cloud->points.resize(cloud->size());

    float max_dev = 0.0f;
    std::vector<float> deviations;

    for (size_t i = 0; i < cloud->size(); ++i) {
        float dev = radialDistances - center_radius;
        deviations.push_back(dev);
        if (std::abs(dev) > max_dev) max_dev = std::abs(dev);
    }

    for (size_t i = 0; i < cloud->size(); ++i) {
        const auto& pt = cloud->points;
        float dev = deviations;
        pcl::PointXYZRGB colored_pt;
        colored_pt.x = pt.x;
        colored_pt.y = pt.y;
        colored_pt.z = pt.z;

        // 超差点红色,其它按热图着色(冷→热)
        if (std::abs(dev) > tolerance) {
            colored_pt.r = 255;
            colored_pt.g = 0;
            colored_pt.b = 0;
        } else {
            float norm_dev = (dev + max_dev) / (2 * max_dev);  // [0,1]
            colored_pt.r = static_cast<uint8_t>(norm_dev * 255);
            colored_pt.g = static_cast<uint8_t>((1.0f - norm_dev) * 255);
            colored_pt.b = 50;
        }
        colored_cloud->points = colored_pt;
    }

    // 显示带颜色的点云
    viewer->addPointCloud<pcl::PointXYZRGB>(colored_cloud, "colored_cloud");
    viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "colored_cloud");

    // 显示拟合轴
    Eigen::Vector3f p1 = centroid - axis * (height / 2.0f);
    Eigen::Vector3f p2 = centroid + axis * (height / 2.0f);
    viewer->addLine(pcl::PointXYZ(p1[0], p1[1], p1[2]),
                    pcl::PointXYZ(p2[0], p2[1], p2[2]),
                    0.0, 1.0, 0.0, "fitted_axis");

    // 显示最大外圆柱(红色透明)
    viewer->addCylinder(
        pcl::ModelCoefficients{ .values = { centroid[0], centroid[1], centroid[2],
                                            axis[0], axis[1], axis[2], max_radius } },
        "outer_cylinder");
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR, 1.0, 0.0, 0.0, "outer_cylinder");
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_OPACITY, 0.2, "outer_cylinder");

    // 显示最小内圆柱(蓝色透明)
    viewer->addCylinder(
        pcl::ModelCoefficients{ .values = { centroid[0], centroid[1], centroid[2],
                                            axis[0], axis[1], axis[2], min_radius } },
        "inner_cylinder");
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR, 0.0, 0.0, 1.0, "inner_cylinder");
    viewer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_OPACITY, 0.2, "inner_cylinder");

    // 显示残差最大值
    std::cout << "\n&#128269; 残差最大值(最大到拟合圆柱偏差): " << max_dev << " mm" << std::endl;

    // 启动可视化
    while (!viewer->wasStopped()) {
        viewer->spinOnce(100);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

&#128222; 如何调用?

在 main() 末尾调用它替代原来的 visualize():

visualize_with_deviation(cloud, centroid, axis, radialDistances, min_r, max_r, 0.1f, height);

你可以把 0.1f 替换为你 GD&T 图纸上规定的 圆柱度公差值。
&#128444;&#65039; 热力图效果说明

    红色点:超差(公差外)

    蓝→绿→黄→红:表示从负残差到正残差的渐变,蓝色接近中心半径,红色远离中心半径

    透明的红蓝圆柱:分别表示最大最小拟合圆柱
 楼主| 发表于 2025-4-11 15:20:08 | 显示全部楼层
Ceres Solver 圆柱拟合:完整代码
&#128736; 准备工作

你需要安装:

sudo apt install libceres-dev

CMake 记得链接:

find_package(Ceres REQUIRED)
target_link_libraries(my_exe ${CERES_LIBRARIES})

&#9989; C++ 完整代码

#include <iostream>
#include <vector>
#include <ceres/ceres.h>
#include <ceres/rotation.h>
#include <Eigen/Dense>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>

// 残差模型
struct CylinderResidual {
    CylinderResidual(const Eigen::Vector3d& point) : point_(point) {}

    template <typename T>
    bool operator()(const T* const axis_point,
                    const T* const axis_dir,
                    const T* const radius,
                    T* residual) const {
        Eigen::Matrix<T, 3, 1> p(T(point_(0)), T(point_(1)), T(point_(2)));
        Eigen::Matrix<T, 3, 1> p0(axis_point[0], axis_point[1], axis_point[2]);
        Eigen::Matrix<T, 3, 1> d(axis_dir[0], axis_dir[1], axis_dir[2]);

        d.normalize();  // 保证方向单位长度
        Eigen::Matrix<T, 3, 1> vec = p - p0;
        Eigen::Matrix<T, 3, 1> proj = (vec.dot(d)) * d;
        Eigen::Matrix<T, 3, 1> perp = vec - proj;
        residual[0] = perp.norm() - radius[0];  // 点到轴线的距离 - 半径

        return true;
    }

private:
    const Eigen::Vector3d point_;
};

// 创建点云数据(带轻微误差)
pcl::PointCloud<pcl::PointXYZ>::Ptr generateCylinderData() {
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    double r = 5.0, h = 10.0;
    for (double z = -h / 2; z <= h / 2; z += 0.2) {
        for (double theta = 0; theta < 2 * M_PI; theta += M_PI / 36) {
            double noise = ((rand() % 100) / 100.0 - 0.5) * 0.05;
            double x = (r + noise) * std::cos(theta);
            double y = (r + noise) * std::sin(theta);
            cloud->points.emplace_back(x, y, z);
        }
    }
    return cloud;
}

int main() {
    auto cloud = generateCylinderData();

    // 初始估计值(从 PCA 获取更好)
    double axis_point[3] = {0.0, 0.0, 0.0};   // 圆柱中心
    double axis_dir[3] = {0.0, 0.0, 1.0};     // 方向
    double radius[1] = {5.0};                 // 初始半径

    ceres::Problem problem;
    for (const auto& pt : cloud->points) {
        Eigen::Vector3d point(pt.x, pt.y, pt.z);
        ceres::CostFunction* cost_function =
            new ceres::AutoDiffCostFunction<CylinderResidual, 1, 3, 3, 1>(
                new CylinderResidual(point));

        problem.AddResidualBlock(cost_function, nullptr, axis_point, axis_dir, radius);
    }

    // 归一化方向向量约束
    problem.SetParameterization(axis_dir, new ceres::HomogeneousVectorParameterization(3));

    // Solver 配置
    ceres::Solver::Options options;
    options.linear_solver_type = ceres::DENSE_QR;
    options.minimizer_progress_to_stdout = true;

    ceres::Solver::Summary summary;
    ceres::Solve(options, &problem, &summary);

    std::cout << "\n===== 优化结果 =====" << std::endl;
    std::cout << "轴线点: [" << axis_point[0] << ", " << axis_point[1] << ", " << axis_point[2] << "]" << std::endl;
    std::cout << "轴线方向: [" << axis_dir[0] << ", " << axis_dir[1] << ", " << axis_dir[2] << "]" << std::endl;
    std::cout << "拟合半径: " << radius[0] << std::endl;
    std::cout << "Ceres报告:\n" << summary.BriefReport() << std::endl;

    return 0;
}

&#128200; 输出示例(部分)

===== 优化结果 =====
轴线点: [-0.001, 0.003, -0.002]
轴线方向: [0.0003, -0.0021, 0.9999]
拟合半径: 5.0021
Ceres报告:
Ceres Solver Report: Iterations: 14, Initial cost: 1.234, Final cost: 0.005
您需要登录后才可以回帖 登录 | 注册

本版积分规则

QQ|Archiver|小黑屋|几何尺寸与公差论坛

GMT+8, 2025-4-25 19:27 , Processed in 0.039202 second(s), 19 queries .

Powered by Discuz! X3.4 Licensed

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表