There are two problems to be solved.
Problem 1: Given a set of 3D points, characterise the plane that they lie within.
Problem 2: Given a set of 3D points, project them onto that plane.
Problem 2 is addressed by other answers; I go for the change of basis approach.
For problem 1, you need to find the normal to the place. To do this, select some random triplets from the point cloud and find the normal to the triangle each forms. Take the average of those normals to estimate the normal to the plane. Use that to form the first element of the new orthonormal basis.
Here goes, along with some code to generate a test set.
[ Point3D is not production quality; you probably can re-use something else for that anyway.]
#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;
}