将3D平面转换为2D

3
我有一组三维平面点云数据,这些数据是通过 RANSAC 平面拟合得到的。为了进行某种特定类型的数据分析,我需要将其转换为二维问题,以便所有 z 值几乎相同。假设平面方程为 ax+by+cz+1=0。我的问题是:
  1. 如何从原始点云数据中获取 a、b、c 的值?最小二乘法是否是最佳方法,还是可以通过 RANSAC 拟合获得这些值的其他方法?
  2. 我从一些教程中得到了以下步骤的想法:先将点云数据平移到质心处,然后绕 x 轴旋转,再绕 y 轴旋转,最后再平移回去。这个想法可行吗?
  3. 如何用 a、b、c 表示旋转角度?
我需要一些关于将三维平面转换为二维平面的一般想法,以使所有 z 坐标值相同。

所以你基本上想要在另一个3D平面上建立3D平面的投影? - JAre
是的,你可以这样说。我想要一个点的表示,它们具有几乎相同的z值。虽然在问题中我提到了3D到2D的转换,但你指出了正确的问题,它是另一种投影到不同的3D平面。因此,我需要知道变换步骤。 - ayan.c
3个回答

9

您需要进行基底变换。

让我们创建由三个向量XYZ组成的新正交标准基底。

您的新Z将是平面(a, b, c)的法向量。通过将所有三个分量除以Sqrt(a^2+b^2+c^2)来进行归一化。

然后取一个不与第一个向量平行的任意向量,记为U。计算点积U.Z并从U中减去(U.Z).Z。将结果向量归一化为X

最后,计算叉积Y = Z /\ X

然后,通过计算点积(Pi.X, Pi.Y)将每个点Pi转换为二维。


1

有两个问题需要解决。

问题1:给定一组3D点,描述它们所在的平面。

问题2:给定一组3D点,将它们投影到该平面上。

其他答案已经解决了问题2; 我采用基变换的方法来解决。

对于问题1,您需要找到平面的法向量。为此,请从点云中选择一些随机三元组,并找到每个三元组形成的三角形的法向量。取这些法向量的平均值以估计平面的法向量。使用它来形成新正交基的第一个元素。

下面是一些用于生成测试集的代码,Point3D不是生产质量的代码; 无论如何,您可能可以重新使用其他内容。

#include <iostream>
#include <random>
#include <vector>
#include <algorithm>    // std::random_shuffle
#include <numeric>    // std::random_shuffle
#include <math.h>


struct Point3D
{
    Point3D(double a = 0, double b  = 0, double c = 0)
    {
        x_[0] = a;
        x_[1] = b;
        x_[2] = c;
    }
    int maxIndex() const
    {
        int maxIndex = 0;
        double maxVal = 0;
        for (int i = 0; i != 3;  ++i)
        {
            if (fabs(x_[i]) > maxVal)
            {
                maxIndex = i;
                maxVal = fabs(x_[i]);
            }
        }
        return maxIndex;
    }
    double lengthSquared() const
    {
        return std::accumulate(std::begin(x_), std::end(x_), 0.0, [](double sum, double x) { return sum + x*x; });
    }
    double length() const { return sqrt(lengthSquared()); }
    double dot(const Point3D& other) const
    {
        double d = 0;
        for (int i = 0; i != 3; ++i)
            d += x_[i] * other.x_[i];
        return d;
    }
    const Point3D& add(const Point3D& other)
    {
        for (int i = 0; i != 3; ++i)
            x_[i] += other.x_[i];
        return *this;
    }
    const Point3D&  subtract(const Point3D& other)
    {
        for (int i = 0; i != 3; ++i)
            x_[i] -= other.x_[i];
        return *this;
    }
    const Point3D&  multiply(double scalar)
    {
        for (int i = 0; i != 3; ++i)
            x_[i] *= scalar;
        return *this;
    }
    const Point3D&  divide(double scalar)
    {
        return multiply(1 / scalar);
    }
    const Point3D&  unitise()
    {
        return divide(length());
    }
    // Returns the component of other parallel to this.
    Point3D  projectionOfOther(const Point3D& other) const
    {
        double factor = dot(other) / lengthSquared();
        Point3D projection(*this);
        projection.multiply(factor);
        return projection;
    }

    double x_[3];
};

void print(const Point3D& p)
{
    std::cout << p.x_[0] << ", " << p.x_[1] << ", " << p.x_[2] << ", " << std::endl;
}

typedef Point3D Point3D;

Point3D difference(const Point3D& a, const Point3D& b)
{
    return  Point3D(a).subtract(b);
}
Point3D sum(const Point3D& a, const Point3D& b)
{
    return  Point3D(a).add(b);
}

Point3D crossProduct(const Point3D& v1 ,const Point3D& v2)
{
    return Point3D(v1.x_[1] * v2.x_[2] - v1.x_[2] * v2.x_[1],
                   v1.x_[2] * v2.x_[0] - v1.x_[0] * v2.x_[2],
                   v1.x_[0] * v2.x_[1] - v1.x_[1] * v2.x_[0]
        );
}

Point3D crossProductFromThreePoints(std::vector<Point3D>::iterator first)
{
    std::vector<Point3D>::iterator second = first + 1;
    std::vector<Point3D>::iterator third = first + 2;
    return crossProduct(difference(*third, *first), difference(*third, *second));
}


std::vector<Point3D> makeCloud(int numPoints);

Point3D determinePlaneFromCloud(const std::vector<Point3D>& cloud)
{
    std::vector<Point3D> randomised(cloud);
    int extra = 3- randomised.size() % 3;
    // Might not need this shuffle; but should reduce problems should neighbouring points in the list tend to be close in 3D space 
    // giving small triangles that might have normals a long way from the main plane.
    random_shuffle(begin(randomised), end(randomised));
    randomised.insert(end(randomised), begin(randomised), begin(randomised) + extra);
    int numTriangles = randomised.size() / 3;
    Point3D normals;
    int primaryDir = -1;
    for (int tri = 0; tri != numTriangles; ++tri )
    {
        Point3D cp = crossProductFromThreePoints(begin(randomised) + tri * 3);
        cp.unitise();
        // The first triangle defines the component we'll look at to determine orientation.
        if (primaryDir < 0)
            primaryDir = cp.maxIndex();
        // Flip the orientation if needed.
        if (cp.x_[primaryDir] < 0)
            cp.multiply(-1);
        normals.add(cp);
    }
    // Now we have a candidate normal to the plane.
    // Scale it so that we have normal.p = 1  for each p in the cloud. This is only an average in the case where the p are not completely planar.
    double sum = std::accumulate(std::begin(cloud), std::end(cloud), 0.0, [normals](double sum, const Point3D& p) { return sum + p.dot(normals); });
    double meanC = sum / cloud.size();
    normals.divide(meanC);
    return normals;

}
// Return an orthonormal basis whose first direction is aligned with v1
std::vector<Point3D> createOrthonormalBasis(const Point3D& v1)
{
    // Need a complete basis as a starting point.
    std::vector<Point3D> basis(3);
    basis[0] = v1;

    // This gives the direction in the current basis with which v1 is closest aligned.
    int mi = v1.maxIndex();
    // start with the other vectors being the other principle directions from mi; this ensures that the basis is complete.
    basis[1].x_[(mi + 1) % 3 ] = 1;
    basis[2].x_[(mi + 2) % 3] = 1;

    // Now perform a Gram–Schmidt process to make that an Orthonormal basis.

    for (int i = 0; i != 3; ++i)
    {
        for (int j = 0; j != i; ++j)
        {
            basis[i].subtract(basis[j].projectionOfOther(basis[i]));
        }
        basis[i].unitise();
    }

    return basis;

}

Point3D convertBasis(const Point3D& p, const std::vector<Point3D>& newBasis)
{
    Point3D newCoords;
    for (int i = 0; i != 3; ++i)
    {
        newCoords.x_[i] = p.dot(newBasis[i]);
    }
    return newCoords;
}

int main()
{
    std::vector<Point3D> cloud = makeCloud(99);
    Point3D normal = determinePlaneFromCloud(cloud);

    print(normal);

    // Now we want to express each point p in the form:
    // p = n + au + bv
    // where n.u = n.v = u.v = 0

    std::vector<Point3D> basis = createOrthonormalBasis(normal);
    // The [1] and [2] components are now the 2D locations in the desired plane.
    for each (Point3D p in cloud)
    {
        Point3D mapped = convertBasis(p, basis);
        print(mapped);
    }

    return 0;
}


std::vector<Point3D> makeCloud(int numPoints)
{
    std::default_random_engine generator;
    std::uniform_real_distribution<double> distribution(0.0, 1.0);
    std::vector<Point3D> cloud(numPoints);
    Point3D planeNormal;
    for (int i = 0; i != 2; ++i)
        planeNormal.x_[i] = distribution(generator);
    // Dodgy!
    while (planeNormal.x_[2] < 1e-2)
        planeNormal.x_[2] = distribution(generator);

    for (int i = 0; i != numPoints; ++i)
    {
        for (int j = 0; j != 2; ++j)
            cloud[i].x_[j] = distribution(generator);
        cloud[i].x_[2] = (1 - cloud[i].x_[0] * planeNormal.x_[0] - cloud[i].x_[1] * planeNormal.x_[1]) / planeNormal.x_[2];
        // Add in some noise.
        //        for (int j = 0; j != 3; ++j) cloud[i].x_[j] += distribution(generator) * 0.05;
    }
    planeNormal.unitise();
    return cloud;
}

使用RANSAC从点云中获取平面似乎比取平均法线更好,因为数据可能存在噪声。此外,从中获取基础信息也很容易。 - JAre
部分同意;所提出的解决方案非常简单,但根据数据中的噪声可能过于简单。我的方法是定义一个质量指标来评估给定的方法,并使用它来确定是否值得采用更复杂的方法。 - Keith

1
  1. 构建协方差矩阵:
    • 找到所有点的平均值
    • 从每个点减去平均值
    • matrix(i,j)=sum(p[i]*p[j]) (p[1],p[2],p[3] 是点 p 的坐标)
  2. 找到矩阵的特征向量和特征值
  3. 对应于最小特征值的向量是 Z,其余的是 X 和 Y

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接