csgo-2018-source/mathlib/planefit.cpp

328 lines
11 KiB
C++
Raw Permalink Normal View History

2021-07-25 12:11:47 +08:00
//============ Copyright (c) Valve Corporation, All rights reserved. ============
//
// Code to compute the equation of a plane with a least-squares residual fit.
//
//===============================================================================
#include "vplane.h"
#include "mathlib.h"
#include <algorithm>
using namespace std;
//////////////////////////////////////////////////////////////////////////
// Forward Declarations
//////////////////////////////////////////////////////////////////////////
static const float DETERMINANT_EPSILON = 1e-6f;
template< int PRIMARY_AXIS >
bool ComputeLeastSquaresPlaneFit( const Vector *pPoints, int nNumPoints, VPlane *pFitPlane );
//////////////////////////////////////////////////////////////////////////
// Public Implementation
//////////////////////////////////////////////////////////////////////////
bool ComputeLeastSquaresPlaneFitX( const Vector *pPoints, int nNumPoints, VPlane *pFitPlane )
{
return ComputeLeastSquaresPlaneFit<0>( pPoints, nNumPoints, pFitPlane );
}
bool ComputeLeastSquaresPlaneFitY( const Vector *pPoints, int nNumPoints, VPlane *pFitPlane )
{
return ComputeLeastSquaresPlaneFit<1>( pPoints, nNumPoints, pFitPlane );
}
bool ComputeLeastSquaresPlaneFitZ( const Vector *pPoints, int nNumPoints, VPlane *pFitPlane )
{
return ComputeLeastSquaresPlaneFit<2>( pPoints, nNumPoints, pFitPlane );
}
float ComputeSquaredError( const Vector *pPoints, int nNumPoints, const VPlane *pFitPlane )
{
float flSqrError = 0.0f;
float flError = 0.0f;
for ( int i = 0; i < nNumPoints; ++ i )
{
float flDist = pFitPlane->DistTo( pPoints[i] );
flError += flDist;
flSqrError += flDist * flDist;
}
return flSqrError;
}
//////////////////////////////////////////////////////////////////////////
// Private Implementation
//////////////////////////////////////////////////////////////////////////
// Because this is not a least-squares orthogonal distance fit, an axis must be specified along which residuals are computed.
// A traditional least-squares linear regression computes residuals along the y-axis and fits to a function of x, meaning that vertical lines cannot be properly fit.
// Similarly, this algorithm cannot properly fit planes which lie along a plane parallel to the primary axis
//
// PRIMARY_AXIS
// X = 0
// Y = 1
// Z = 2
template< int PRIMARY_AXIS >
bool ComputeLeastSquaresPlaneFit( const Vector *pPoints, int nNumPoints, VPlane *pFitPlane )
{
memset( pFitPlane, 0, sizeof( VPlane ) );
if ( nNumPoints < 3 )
{
// We must have at least 3 points to fit a plane
return false;
}
Vector vCentroid( 0, 0, 0 ); // averages: x-bar, y-bar, z-bar
Vector vSquaredSums( 0, 0, 0 ); // x => x*x, y => y*y, z => z*z
Vector vCrossSums( 0, 0, 0 ); // x => y*z, y => x*z, z >= x*y
float flNumPoints = ( float )nNumPoints;
for ( int i = 0; i < nNumPoints; ++ i )
{
vCentroid += pPoints[i];
vSquaredSums += pPoints[i] * pPoints[i];
vCrossSums.x += pPoints[i].y * pPoints[i].z;
vCrossSums.y += pPoints[i].x * pPoints[i].z;
vCrossSums.z += pPoints[i].x * pPoints[i].y;
}
vCentroid /= ( float ) nNumPoints;
if ( PRIMARY_AXIS == 0 )
{
// swap X and Z
swap( vCentroid.x, vCentroid.z );
swap( vSquaredSums.x, vSquaredSums.z );
swap( vCrossSums.x, vCrossSums.z );
}
else if ( PRIMARY_AXIS == 1 )
{
// Swap Y and Z
swap( vCentroid.y, vCentroid.z );
swap( vSquaredSums.y, vSquaredSums.z );
swap( vCrossSums.y, vCrossSums.z );
}
// Solve system of equations:
// (example assumes primary axis is Z)
//
// A * ( sum( xi * xi ) - n * vCentroid.x^2 ) + B * ( sum( xi * yi ) - n * vCentroid.x * vCentroid.y ) - sum( xi * zi ) + n * vCentroid.x * vCentroid.z = 0
// A * ( sum( xi * yi ) - n * vCentroid.x * vCentroid.y ) + B * ( sum( yi * yi ) - n * vCentroid.y ^ 2 ) - sum( yi * zi ) + n * vCentroid.y * vCentroid.z = 0
// C = vCentroid.z - A * vCentroid.x - B * vCentroid.y
//
// where z = Ax + By + C
//
// Transform to:
// [ m11 m12 ] [ A ] = [ c1 ]
// [ m21 m22 ] [ B ] = [ c2 ]
//
// M * x = C
// Take the inverse of M, post-multiply by C:
// x = M_inverse * C
float flM11 = vSquaredSums.x - flNumPoints * vCentroid.x * vCentroid.x;
float flM12 = vCrossSums.z - flNumPoints * vCentroid.x * vCentroid.y;
float flC1 = vCrossSums.y - flNumPoints * vCentroid.x * vCentroid.z;
float flM21 = vCrossSums.z - flNumPoints * vCentroid.x * vCentroid.y;
float flM22 = vSquaredSums.y - flNumPoints * vCentroid.y * vCentroid.y;
float flC2 = vCrossSums.x - flNumPoints * vCentroid.y * vCentroid.z;
float flDeterminant = flM11 * flM22 - flM12 * flM21;
if ( fabsf( flDeterminant ) > DETERMINANT_EPSILON )
{
float flInvDeterminant = 1.0f / flDeterminant;
float flA = flInvDeterminant * ( flM22 * flC1 - flM12 * flC2 );
float flB = flInvDeterminant * ( -flM21 * flC1 + flM11 * flC2 );
float flC = vCentroid.z - flA * vCentroid.x - flB * vCentroid.y;
pFitPlane->m_Normal = Vector( -flA, -flB, 1.0f );
float flScale = pFitPlane->m_Normal.NormalizeInPlace();
pFitPlane->m_Dist = flC * 1.0f / flScale;
if ( PRIMARY_AXIS == 0 )
{
// swap X and Z
swap( pFitPlane->m_Normal.x, pFitPlane->m_Normal.z );
}
else if ( PRIMARY_AXIS == 1 )
{
// Swap Y and Z
swap( pFitPlane->m_Normal.y, pFitPlane->m_Normal.z );
}
return true;
}
// Bad determinant
return false;
}
struct Complex_t
{
float r;
float i;
Complex_t() { }
Complex_t( float flR, float flI ) : r( flR ), i( flI ) { }
static Complex_t FromPolar( float flRadius, float flTheta )
{
return Complex_t( flRadius * cosf( flTheta ), flRadius * sinf( flTheta ) );
}
static Complex_t SquareRoot( float flValue )
{
if ( flValue < 0.0f )
{
return Complex_t( 0.0f, sqrtf( -flValue ) );
}
else
{
return Complex_t( sqrtf( flValue ), 0.0f );
}
}
Complex_t operator+( const Complex_t &rhs ) const
{
return Complex_t( r + rhs.r, i + rhs.i );
}
Complex_t operator-( const Complex_t &rhs ) const
{
return Complex_t( r - rhs.r, i - rhs.i );
}
Complex_t operator*( const Complex_t &rhs ) const
{
return Complex_t( r * rhs.r - i * rhs.i, r * rhs.i + i * rhs.r );
}
Complex_t operator*( float rhs ) const
{
return Complex_t( r * rhs, i * rhs );
}
Complex_t operator/( float rhs ) const
{
return Complex_t( r / rhs, i / rhs );
}
Complex_t CubeRoot() const
{
float flRadius = sqrtf( r * r + i * i );
float flTheta = atan2f( i, r );
//if ( flTheta < 0.0f ) flTheta += 2.0f * 3.14159f;
// Demoivre's theorem for principal root
return FromPolar( powf( flRadius, 1.0f / 3.0f ), flTheta / 3.0f );
}
};
// [kutta]
// This code is a work-in-progress; need to write code to robustly find an eigenvector given its eigenvalue.
#if USE_ORTHOGONAL_LEAST_SQUARES
template< int PRIMARY_AXIS = 0 >
bool TryFindEigenvector( float flEigenvalue, const Vector *pMatrix, Vector *pEigenvector )
{
const float flCoefficientEpsilon = 1e-3;
const int nOtherRow1 = ( PRIMARY_AXIS + 1 ) % 3;
const int nOtherRow2 = ( PRIMARY_AXIS + 2 ) % 3;
bool bUseRow1 = fabsf( pMatrix[0][nOtherRow1] / flEigenvalue ) > flCoefficientEpsilon && fabsf( pMatrix[0][nOtherRow2] / flEigenvalue ) > flCoefficientEpsilon );
bool bUseRow2 = fabsf( pMatrix[1][nOtherRow1] / flEigenvalue ) > flCoefficientEpsilon && fabsf( pMatrix[1][nOtherRow2] / flEigenvalue ) > flCoefficientEpsilon );
bool bUseRow3 = fabsf( pMatrix[2][nOtherRow1] / flEigenvalue ) > flCoefficientEpsilon && fabsf( pMatrix[2][nOtherRow2] / flEigenvalue ) > flCoefficientEpsilon );
// ...
}
bool ComputeLeastSquaresOrthogonalPlaneFit( const Vector *pPoints, int nNumPoints, VPlane *pFitPlane )
{
memset( pFitPlane, 0, sizeof( VPlane ) );
if ( nNumPoints < 3 )
{
// We must have at least 3 points to fit a plane
return false;
}
Vector vCentroid( 0, 0, 0 ); // averages: x-bar, y-bar, z-bar
Vector vSquaredSums( 0, 0, 0 ); // x => x*x, y => y*y, z => z*z
Vector vCrossSums( 0, 0, 0 ); // x => y*z, y => x*z, z >= x*y
float flNumPoints = ( float )nNumPoints;
for ( int i = 0; i < nNumPoints; ++ i )
{
vCentroid += pPoints[i];
vSquaredSums += pPoints[i] * pPoints[i];
vCrossSums.x += pPoints[i].y * pPoints[i].z;
vCrossSums.y += pPoints[i].x * pPoints[i].z;
vCrossSums.z += pPoints[i].x * pPoints[i].y;
}
vCentroid /= ( float ) nNumPoints;
// Re-center the squared and cross sums
vSquaredSums.x -= flNumPoints * vCentroid.x * vCentroid.x;
vSquaredSums.y -= flNumPoints * vCentroid.y * vCentroid.y;
vSquaredSums.z -= flNumPoints * vCentroid.z * vCentroid.z;
vCrossSums.x -= flNumPoints * vCentroid.y * vCentroid.z;
vCrossSums.y -= flNumPoints * vCentroid.x * vCentroid.z;
vCrossSums.z -= flNumPoints * vCentroid.x * vCentroid.y;
// Best fit normal occurs at the minimum of the Rayleigh quotient:
//
// n' * M * n
// ----------
// n' * n
//
// Where M is the covariance matrix.
// M is computed from ( A' * A ) where A is a 3xN matrix of x/y/z residuals for each point in the data set.
// Solve for eigenvalues & eigenvectors of 3x3 real symmetric covariance matrix.
// The resulting characteristic polynormial equation is a cubic of the form:
// x^3 + Ax^2 + Bx + C = 0
//
// All roots of the equation and eigenvalues are positive real values; the lowest one corresponds to the eigenvalue which is the normal to the best fit plane.
float flA = -( vSquaredSums.x + vSquaredSums.y + vSquaredSums.z );
float flB = vSquaredSums.x * vSquaredSums.z + vSquaredSums.x * vSquaredSums.y + vSquaredSums.y * vSquaredSums.z - vCrossSums.x * vCrossSums.x - vCrossSums.y * vCrossSums.y - vCrossSums.z * vCrossSums.z;
float flC = -( vSquaredSums.x * vSquaredSums.y * vSquaredSums.z + 2.0f * vCrossSums.x * vCrossSums.y * vCrossSums.z ) + ( vSquaredSums.x * vCrossSums.x * vCrossSums.x + vSquaredSums.y * vCrossSums.y * vCrossSums.y + vSquaredSums.z * vCrossSums.z * vCrossSums.z );
// Using formula for roots of cubic polynomial, see http://en.wikipedia.org/wiki/Cubic_function
float flM = 2.0f * flA * flA * flA - 9.0f * flA * flB + 27.0f * flC;
float flK = flA * flA - 3.0f * flB;
float flN = flM * flM - 4.0f * flK * flK * flK;
Complex_t flSolutions[3];
const Complex_t omega1( -0.5f, 0.5f * sqrtf( 3.0f ) );
const Complex_t omega2( -0.5f, -0.5f * sqrtf( 3.0f ) );
Complex_t complexA = Complex_t( flA, 0.0f );
Complex_t complexM = Complex_t( flM, 0.0f );
Complex_t intermediateA = ( ( complexM + Complex_t::SquareRoot( flN ) ) / 2.0f );
Complex_t intermediateB = ( ( complexM - Complex_t::SquareRoot( flN ) ) / 2.0f );
Complex_t cubeA = intermediateA.CubeRoot();
Complex_t cubeB = intermediateB.CubeRoot();
Complex_t tempA = cubeA * cubeA * cubeA;
Complex_t tempB = cubeB * cubeB * cubeB;
flSolutions[0] = ( complexA + cubeA + cubeB ) * -1.0f / 3.0f;
flSolutions[1] = ( complexA + ( omega2 * cubeA ) + ( omega1 * cubeB ) ) * -1.0f / 3.0f;
flSolutions[2] = ( complexA + ( omega1 * cubeA ) + ( omega2 * cubeB ) ) * -1.0f / 3.0f;
float flMinEigenvalue = MIN( flSolutions[0].r, flSolutions[1].r );
flMinEigenvalue = MIN( flMinEigenvalue, flSolutions[2].r );
// Subtract eigenvalue from the diagonal of the matrix to get a 3x3, real-symmetric, non-invertible matrix.
// Pick 2 non-zero rows from this matrix and construct a system of 2 equations.
return true;
}
#endif // USE_ORTHOGONAL_LEAST_SQUARES