《点云处理》 点云刚体变换

本文介绍了点云的刚体变换,包括旋转操作,如使用Eigen库进行标准和OpenMP加速的点云旋转,以及如何通过求解旋转轴和角度实现逆向变换。此外,还展示了如何利用CUDA和Thrust进行点云变换,尽管CUDA性能受数据拷贝影响,但提供了另一种并行计算方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

点云刚体变换是点云常见操作之一,像点云配准就需要使用刚体变换。点云刚体变换的表现形式就是在某一个指定空间坐标系下,点云形状不发生改变的情况下,位置发生了变换,一般是指平移,旋转。而点云刚体变换的实现本质就是点云每个点的坐标都发生了改变,即乘以变换矩阵,得到了新的坐标值。这是从点云变换来理解的,另一个角度就是原始点云的坐标系做了变换,表现来看点云就有所变化。

1.点云旋转变换

点云旋转一般都需要指定其绕某个轴(或者说是向量)旋转了某个角度。这种情况是需要已知轴和角度值的。

代码:

/// <summary>
/// 点云绕某个轴(向量)旋转某个角度
/// </summary>
/// <param name="cloud">输入点云</param>
/// <param name="cloudout">输出旋转后的点云</param>
/// <param name="axis">轴(向量)</param>
/// <param name="angle">旋转角度</param>
void AxisAngleTransformEigen(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud, pcl::PointCloud<pcl::PointXYZ>::Ptr& cloudout, 
	const Eigen::Vector3d& axis, const double& angle)
{
	Eigen::Affine3d transform = Eigen::Affine3d::Identity();
	transform.rotate(Eigen::AngleAxisd(angle, axis));
	pcl::transformPointCloud(*cloud, *cloudout, transform);
}

对于数量级较大的点云,上述方法进行点云变换是非常慢。

2.点云旋转变换(OPENMP加速)

使用OPENMP对点云变换加速,会省时一些。

/// <summary>
/// 使用OPENMP加速,点云绕某个轴选择指定角度
/// </summary>
/// <param name="cloud">输入点云</param>
/// <param name="cloudout">输出旋转后的点云</param>
/// <param name="axis">轴(向量)</param>
/// <param name="angle">旋转角度</param>
void AxisAngleTransformEigenOMP(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud, pcl::PointCloud<pcl::PointXYZ>::Ptr& cloudout,
	const Eigen::Vector3d& axis, const double& angle)
{
	Eigen::Affine3d transform = Eigen::Affine3d::Identity();
	transform.rotate(Eigen::AngleAxisd(angle, axis));
	double* matrixPtr = transform.matrix().data();

	cloudout->points.resize(cloud->size());
	omp_set_num_threads(12);
#pragma omp parallel for
	for (int i = 0; i < cloud->size(); ++i)
	{
		(*cloudout)[i].x = matrixPtr[0] * (*cloud)[i].x + matrixPtr[1] * (*cloud)[i].y +
			matrixPtr[2] * (*cloud)[i].z + matrixPtr[3];
		(*cloudout)[i].y = matrixPtr[4] * (*cloud)[i].x + matrixPtr[5] * (*cloud)[i].y +
			matrixPtr[6] * (*cloud)[i].z + matrixPtr[7];
		(*cloudout)[i].z = matrixPtr[8] * (*cloud)[i].x + matrixPtr[9] * (*cloud)[i].y +
			matrixPtr[10] * (*cloud)[i].z + matrixPtr[11];
	}
}

3.求解旋转轴和旋转角度

上述两种方案都是已知旋转轴和旋转角度来求解旋转之后的点云。现在假如已知旋转之后和旋转之前的点云法向都是已知的,反向求解点云需要绕哪个轴旋转多少角度才能得到。应用场景:点云是个平面,但不平行于XOY平面或者其他。现在想要将其旋转至与XOY平面平行,即与Z轴垂直,那么可以使用该方法把旋转轴和旋转角度求出来,然后使用上述的1或者2将点云进行变换。这种方法的原理和实现来自这里

/// <summary>
/// 求解出轴axis和角度theta,使得点云绕轴axis旋转theta之后,其法向由CloudNormal转变成Vec
/// </summary>
/// <param name="Vec">点云旋转之后的法向</param>
/// <param name="CloudNormal">点云旋转之前的法向</param>
/// <param name="theta">旋转角度</param>
/// <param name="axis">旋转轴</param>
void CaculateAxisAngle(const Eigen::Vector3d& Vec, const Eigen::Vector3d& CloudNormal, double& theta, Eigen::Vector3d& axis)
{
	Eigen::Vector3d CloudNormalTemp = CloudNormal;
	CloudNormalTemp.normalize();
	double dotmul = CloudNormalTemp.dot(Vec);
	double NormalNorm = CloudNormalTemp.norm();
	double VecNorm = Vec.norm();
	theta = std::acos(dotmul / (std::sqrt(NormalNorm) * std::sqrt(VecNorm)));
	axis = Vec.cross(CloudNormalTemp);
	axis.normalize();
}

测试效果如下,白色为原始点云,红色为旋转后的点云:
在这里插入图片描述
在这里插入图片描述

4.使用CUDA进行变换

之前提到,点云刚体变换其实就是向量与矩阵的乘法。向量自然就是x,y,z。而矩阵就是刚体变换矩阵。所以很容易就可以写一个计算核函数

核函数写在kernel.cu文件下,核函数代码如下:

/// <summary>
/// 点云刚体变换核函数
/// </summary>
/// <param name="A">存放点云所有点x坐标值的指针</param>
/// <param name="B">存放点云所有点y坐标值的指针</param>
/// <param name="C">存放点云所有点z坐标值的指针</param>
/// <param name="D">存放刚体变换矩阵的指针</param>
/// <param name="N">点云的梳理</param>
/// <returns></returns>
__global__ void cloudTransformKernel(float* A, float* B, float* C, double* D, int N) 
{
    int tid = blockIdx.x * blockDim.x + threadIdx.x;

    if (tid < N) {
        float x = A[tid];
        float y = B[tid];
        float z = C[tid];

        // 矩阵乘法计算
        A[tid] = D[0] * x + D[1] * y + D[2] * z + D[3];
        B[tid] = D[4] * x + D[5] * y + D[6] * z + D[7];
        C[tid] = D[8] * x + D[9] * y + D[10] * z + D[11];
    }
}

自然还需要一个调用核函数的函数,也是在kernel.cu中实现,该函数是供在其他需要使用cuda的cpp代码中使用调用的。代码如下:

/// <summary>
/// 完成线程资源数量分配,调用核函数完成点云刚体变换
/// </summary>
/// <param name="A">存放点云所有点x坐标值的指针</param>
/// <param name="B">存放点云所有点y坐标值的指针</param>
/// <param name="C">存放点云所有点z坐标值的指针</param>
/// <param name="D">存放刚体变换矩阵的指针</param>
/// <param name="N">点云的梳理</param>
/// <returns></returns>
void doCloudTransform(float* A, float* B, float* C, double* D, int N)
{
    int threadsPerBlock = 512;  // 每个块中的线程数量
    int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;  // 网格中的块数量

    cloudTransformKernel << <blocksPerGrid, threadsPerBlock >> > (A, B, C, D, N);
}

这个函数的声明需要在kernel.h文件中。

#pragma once
#ifndef KERNEL_CUH_
#define KERNEL_CUH_

void matrixMultiplication(float* A, float* B, float* C, int N);

void doCloudTransform(float* A, float* B, float* C, double* D, int N);

#endif

在CPP文件中的函数中,当然要进一步封装,方便调用了,代码如下:

#include "dev_array.h"
#include "kernel.h"
/// <summary>
/// 使用CUDA进行点云刚体变换
/// </summary>
/// <param name="cloud">输入点云</param>
/// <param name="cloudout">输出点云</param>
/// <param name="axis">旋转轴</param>
/// <param name="angle">旋转角度</param>
void AxisAngleTransformCUDA(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud, pcl::PointCloud<pcl::PointXYZ>::Ptr& cloudout,
	const Eigen::Vector3d& axis, const double& angle)
{
	auto startTime = std::chrono::high_resolution_clock::now();  // 开始计时
	Eigen::Affine3d transform = Eigen::Affine3d::Identity();
	transform.rotate(Eigen::AngleAxisd(angle, axis));
	double* matrixPtr = transform.matrix().data();
	int size = cloud->points.size();
	dev_array<float> d_A(size);
	dev_array<float> d_B(size);
	dev_array<float> d_C(size);
	std::vector<float> h_A(size);
	std::vector<float> h_B(size);
	std::vector<float> h_C(size);
	omp_set_num_threads(24);
#pragma omp parallel for
	for (int i = 0; i < cloud->points.size(); ++i)
	{
		h_A[i] = cloud->points[i].x;
		h_B[i] = cloud->points[i].y;
		h_C[i] = cloud->points[i].z;
	}
	d_A.set(&h_A[0], size);
	d_B.set(&h_B[0], size);
	d_C.set(&h_C[0], size);
	dev_array<double> d_D(transform.matrix().size());
	d_D.set(matrixPtr, transform.matrix().size());

	auto endTime = std::chrono::high_resolution_clock::now();     // 结束计时
	std::chrono::duration<double> duration = endTime - startTime; // 计算运行时间
	double seconds = duration.count();
	std::cout << "CUDA CPU2GPU 运行时间: " << seconds << " 秒" << std::endl;
	
	startTime = std::chrono::high_resolution_clock::now();  // 开始计时
	doCloudTransform(d_A.getData(), d_B.getData(), d_C.getData(), d_D.getData(), size);
	cudaDeviceSynchronize();
	endTime = std::chrono::high_resolution_clock::now();     // 结束计时
	duration = endTime - startTime; // 计算运行时间
	seconds = duration.count();
	std::cout << "CUDA Caculate 运行时间: " << seconds << " 秒" << std::endl;

	startTime = std::chrono::high_resolution_clock::now();  // 开始计时
	d_A.get(&h_A[0], size);
	d_B.get(&h_B[0], size);
	d_C.get(&h_C[0], size);
	cudaDeviceSynchronize();
	cloudout->points.resize(size);
#pragma omp parallel for
	for (int i = 0; i < cloud->points.size(); ++i)
	{
		cloudout->points[i].x = h_A[i];
		cloudout->points[i].y = h_B[i];
		cloudout->points[i].z = h_C[i];
	}
	endTime = std::chrono::high_resolution_clock::now();     // 结束计时
	duration = endTime - startTime; // 计算运行时间
	seconds = duration.count();
	std::cout << "CUDA GPU2CPU 运行时间: " << seconds << " 秒" << std::endl;
}

上述代码用到的dev_array.h的实现如下:

#pragma once

#ifndef _DEV_ARRAY_H_
#define _DEV_ARRAY_H_

#include <stdexcept>
#include <algorithm>
#include <cuda_runtime.h>

template <class T>
class dev_array
{
    // public functions
public:
    explicit dev_array()
        : start_(0),
        end_(0)
    {}

    // constructor
    explicit dev_array(size_t size)
    {
        allocate(size);
    }
    // destructor
    ~dev_array()
    {
        free();
    }

    // resize the vector
    void resize(size_t size)
    {
        free();
        allocate(size);
    }

    // get the size of the array
    size_t getSize() const
    {
        return end_ - start_;
    }

    // get data
    const T* getData() const
    {
        return start_;
    }

    T* getData()
    {
        return start_;
    }

    // set
    void set(const T* src, size_t size)
    {
        size_t min = std::min(size, getSize());
        cudaError_t result = cudaMemcpy(start_, src, min * sizeof(T), cudaMemcpyHostToDevice);
        if (result != cudaSuccess)
        {
            throw std::runtime_error("failed to copy to device memory");
        }
    }
    // get
    void get(T* dest, size_t size)
    {
        size_t min = std::min(size, getSize());
        cudaError_t result = cudaMemcpy(dest, start_, min * sizeof(T), cudaMemcpyDeviceToHost);
        if (result != cudaSuccess)
        {
            throw std::runtime_error("failed to copy to host memory");
        }
    }


    // private functions
private:
    // allocate memory on the device
    void allocate(size_t size)
    {
        cudaError_t result = cudaMalloc((void**)&start_, size * sizeof(T));
        if (result != cudaSuccess)
        {
            start_ = end_ = 0;
            throw std::runtime_error("failed to allocate device memory");
        }
        end_ = start_ + size;
    }

    // free memory on the device
    void free()
    {
        if (start_ != 0)
        {
            cudaFree(start_);
            start_ = end_ = 0;
        }
    }

    T* start_;
    T* end_;
};

#endif

所以想要使用cuda进行点云处理,需要好几个步骤。首先需要在.cu实现一个核函数,这个核函数中完成点云计算。随后,需要在h文件中声明一个调用函数(主要是方便cpp文件中include),并且可以在实现核函数的.cu文件中实现这个调用函数。最后,在cpp文件中实现一个函数(可以在,h中声明),该函数完成数据从CPU到GPU的拷贝,调用函数的调用,数据从GPU到CPU的拷贝。

上述三种方法的运行时间测试结果如下,openmp版本的最快,其次是pcl中实现的transform,cuda版本的最慢。cuda之所以这么慢,是因为数据拷贝占用了太多时间,cuda的实际计算时间只有0.004s,还是非常快的。测试结果表明这类计算直接还是CPU处理吧。
在这里插入图片描述

5. 使用Thrust进行点云变换

CUDA中还有类似于STL vector的实现——Thrust。正常情况下可以将其当作vector那样使用,用法区别不大。Thrust中有thrust::device_vector和thrust::host_vector两种容器,分别存储GPU和CPU上的数据。可以使用transform来实现点云变换,这就需要实现一个仿函数。这种仿函数除了点云刚体变换之外,当然也可以进行其他的数学运算。想要进行任何形式的运算只需要在仿函数进行实现,然后使用transform调用仿函数即可。

仿函数如下:

struct PointCloudTransformFunctor
{
    double* transform;
   
    __host__ __device__ PointCloudTransformFunctor(double* transform)
        : transform(transform) {}

    __host__ __device__ float3 operator()(const float3& pt) const

    {
        float3 transformed_pt;
        
        transformed_pt.x = transform[0] * pt.x + transform[1] * pt.y +
            transform[2] * pt.z + transform[3];
        transformed_pt.y = transform[4] * pt.x + transform[5] * pt.y +
            transform[6] * pt.z + transform[7];
        transformed_pt.z = transform[8] * pt.x + transform[9] * pt.y +
            transform[10] * pt.z + transform[11];
        

        return transformed_pt;
    }
};

使用transform和仿函数完成点云刚体变换,这里使用transform,作用在thrust::device_vector,不是thrust::host_vector。所以一般在host上的数据都需要拷贝到device上,然后再进行想要实现的运算:

void CloudTransformThrustCore(thrust::device_vector<float3>& cloudin, thrust::device_vector<float3>& cloudout, double* transform)
{
    thrust::transform(cloudin.begin(), cloudin.end(), cloudout.begin(),
        PointCloudTransformFunctor(transform)
    );
}


void CloudTransformThrust(std::vector<float3>& cloudin, std::vector<float3>& cloudout, double* transform)
{
    thrust::device_vector<float3> orig_points(cloudin.begin(), cloudin.end());
    float3 temp;
    temp.x = temp.y = temp.z = 0.0;
    thrust::device_vector<float3> transformed_points(cloudin.size(), temp);
    CloudTransformThrustCore(orig_points, transformed_points, transform);
    thrust::host_vector<float3> temp_vector = transformed_points;
    cloudout.assign(temp_vector.begin(), temp_vector.end());
}

上述仿函数和transform函数都在.cu文件中实现。那么在CPP文件中调用还可以完成一层封装,输入输出都是PCL数据结构的点云。

封装的代码如下:

/// <summary>
/// 使用Thrust对点云进行刚体变换
/// </summary>
/// <param name="cloud">输入点云</param>
/// <param name="cloudout">输出点云</param>
/// <param name="axis">旋转轴</param>
/// <param name="angle">旋转角度</param>
/// <returns></returns>
bool TransformPointCloud(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud, pcl::PointCloud<pcl::PointXYZ>::Ptr& cloudout, const Eigen::Vector3d& axis, const double& angle)
{
	Eigen::Affine3d transform = Eigen::Affine3d::Identity();
	transform.rotate(Eigen::AngleAxisd(angle, axis));
	double* matrixPtr = transform.matrix().data();

	std::vector<float3> hostWrapper(cloud->points.size());
	for (int i = 0; i < cloud->points.size(); ++i)
	{
		hostWrapper[i].x = cloud->points[i].x;
		hostWrapper[i].y = cloud->points[i].y;
		hostWrapper[i].z = cloud->points[i].z;
	}
	
	std::vector<float3> hostWrapperOut;
	dev_array<double> d_D(transform.matrix().size());
	d_D.set(matrixPtr, transform.matrix().size());
	CloudTransformThrust(hostWrapper, hostWrapperOut, d_D.getData());
	
	cloudout->points.resize(hostWrapperOut.size());

	for (int i = 0; i < cloudout->points.size(); ++i)
	{
		cloudout->points[i].x = hostWrapperOut[i].x;
		cloudout->points[i].y = hostWrapperOut[i].y;
		cloudout->points[i].z = hostWrapperOut[i].z;
	}

	return true;
};

运行时间如下,该方法也没有比自己手动写的核函数实现方法快很多,条条大路通罗马,这只是一条路:
在这里插入图片描述
有关于Thrust的官方教程网址,非常详细:
教程1
教程2

<think>好的,用户想了解或实现刚体变换点云数据中的应用。我需要先回顾刚体变换的基本概念,然后结合点云处理的具体步骤来组织回答。 首先,刚体变换包括旋转和平移,保持形状和大小不变。点云应用中,这常用于配准,将不同坐标系下的点云对齐。用户提供的引用中有几个关键点,比如引用[1]提到刚体变换由旋转和平移组成,引用[2]和[3]展示了具体的代码实现,而引用[4]提到了SVD方法求解变换矩阵。这些都需要整合到回答中。 接下来,我需要分步骤解释实现方法。第一步是数学基础,包括旋转矩阵和平移向量的定义。这里可以引用旋转矩阵的推导(引用[1][4])。然后是坐标变换公式,如引用[3]中的仿函数结构,展示如何应用变换矩阵到每个点。 第二步是具体实现方法,包括手动实现和使用库函数。引用[2]中的PCL代码和引用[3]的仿函数可以作为例子,说明如何应用变换矩阵。同时,引用[4]提到的SVD求解方法需要解释,说明如何通过对应点计算最优变换,这部分可能需要数学推导,但用户可能更关注实现步骤,所以需要简化。 第三步是应用场景,比如点云配准,引用[5]提到配准的目的和参数表达方式,这里可以强调刚体变换在配准中的重要性,以及迭代优化的必要性。 最后,代码示例部分,可以结合引用[2][3][4]中的不同实现方式,给出一个Python或C++的例子,展示如何手动应用变换矩阵,或者使用库函数如PCL。需要注意用户可能对编程语言有偏好,但问题中没有明确说明,所以提供两种语言的示例可能更全面。 生成相关问题时要覆盖数学原理、优化方法、应用场景和库的使用,确保问题能帮助用户深入理解。例如,SVD求解的原理、PCL的具体函数、性能优化等。 需要确保回答结构清晰,步骤明确,引用正确,并且代码示例正确无误。同时检查LaTeX格式是否正确,数学表达式使用$...$和$$...$$,避免语法错误。</think>### 刚体变换点云数据中的实现方法 #### 1. 数学基础 刚体变换的核心是通过旋转矩阵$R$和平移向量$t$组合实现坐标变换变换公式为: $$ P' = R \cdot P + t $$ 其中$P$为原始点坐标,$P'$为变换后坐标。旋转矩阵$R$需满足正交性($R^T R = I$)且行列式为1,平移向量$t$为三维偏移量[^1][^5]。 #### 2. 实现步骤 **(1) 手动实现坐标变换** 通过遍历点云中每个点应用变换矩阵: ```cpp // 基于引用[3]仿函数改进 struct TransformPoint { Eigen::Matrix3f R; Eigen::Vector3f t; Eigen::Vector3f operator()(const Eigen::Vector3f& p) { return R * p + t; } }; ``` **(2) 使用点云库实现** PCL库提供`pcl::transformPointCloud`函数直接处理: ```cpp // 引用[2]代码改进 pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>); Eigen::Matrix4f transform = Eigen::Matrix4f::Identity(); transform.block<3,3>(0,0) = rotation_matrix; // 设置旋转 transform.block<3,1>(0,3) = translation_vector; // 设置平移 pcl::transformPointCloud(*cloud, *transformed_cloud, transform); ``` **(3) SVD求解最优变换** 当存在对应点时,可通过以下步骤求解刚体变换: 1. 计算两组点云的质心$\mu_p, \mu_q$ 2. 计算去中心化坐标$X = P - \mu_p$, $Y = Q - \mu_q$ 3. 计算协方差矩阵$H = X^T Y$ 4. 对$H$进行SVD分解:$H = U \Sigma V^T$ 5. 旋转矩阵$R = V U^T$,平移向量$t = \mu_q - R \mu_p$[^4] #### 3. 应用场景 | 场景类型 | 实现要点 | 相关引用 | |----------------|------------------------------|----------| | 多视角点云配准 | 迭代最近点(ICP)优化变换参数 | | | 传感器标定 | 计算标靶坐标系转换关系 | [^2] | | 物体位姿估计 | 匹配模型与场景点云 | [^4] | #### 4. 代码示例(Python实现SVD方法) ```python import numpy as np def compute_rigid_transform(points_A, points_B): centroid_A = np.mean(points_A, axis=0) centroid_B = np.mean(points_B, axis=0) H = (points_A - centroid_A).T @ (points_B - centroid_B) U, _, Vt = np.linalg.svd(H) R = Vt.T @ U.T if np.linalg.det(R) < 0: Vt[-1,:] *= -1 R = Vt.T @ U.T t = centroid_B - R @ centroid_A return R, t ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值