|
|
//============ 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
|