Calculating a best fit plane to vertices - SOLVED

Update: the problem has been solved. The solution can be found further down in this thread.
Original message below the line


For nearly a week now I’ve been stuck on a relatively simple problem: calculating a best fit plane to a number of vertices and then projecting the vertices on it.

What I got so far

First I calculate the center of mass of all vertices. This is simply the average of all locations of the vertices.

After that I calculate a 3x3 matrix (a covariance matrix):
http://www.sciencedirect.com/cache/MiamiImageURL/B6V9D-4G3KBPB-1-6/0?wchp=dGLbVzW-zSkzS
where:
li = X - averageX
mi = Y - averageY
ni = Z - averageZ

Using Cardano’s method I find the 3 eigenvalues that belong to this matrix (lambda1 = lambda2 < lambda3).

A new matrix is calculated: M = T-lambda3*I
where I is an identity matrix. So basically M is just the first matrix where lambda is subtracted from each of the diagonal values.

The eigenvector that belongs to lambda3 is perpendicular to the two linearly independent rows in the matrix M. So it can be calculated by the dotproduct between the two rows.

The final step is projecting the vertices on the plane defined by the eigenvector (which should be the normal of that plane).

The problem

The above works fine, except from one thing. The eigenvector that is calculated isn’t the normal of the plane, but is a vector along that plane. Now everywhere I look says that this method should give you the normal, so I’ve probably made a mistake somewhere, but I really can’t find it.
Any help would be appreciated. Below you will find an image to illustrate the problem and my current code.

http://sites.google.com/site/bartiuscrouch/images/best_fit_plane_problem.png

As you can see, the vector calculated by the script is perfectly located on the best fit plane, but is not the actual normal.

import bpy
import Blender
from Blender import *

# calculates the eigenvalues of a matrix
def eigenValues(mat):
    a = -1*(mat[0][0]+mat[1][1]+mat[2][2])
    b = -1*(mat[0][1]*mat[1][0]+mat[0][2]*mat[2][0]+mat[1][2]*mat[2][1]-mat[0][0]*mat[1][1]-mat[0][0]*mat[2][2]-mat[1][1]*mat[2][2])
    c = -1*(mat.determinant())
    eigenvalues = cubicCardano(a, b, c)
    eigenvalues.sort()
    return eigenvalues

# finds the roots of a cubic function
def cubicCardano(a,b,c):
    p = b-((a**2)/3.0)
    q = c+((2*a**3-9*a*b)/(27.0))
    u = abs(-q/2.0 + abs((q**2)/4.0+(p**3)/27.0)**0.5)**(1/3.0)
    roots = [u, u*complex(-0.5, (3**0.5)/2.0), u*complex(-0.5, -1*((3**0.5)/2.0))]
    solutions = []
    for j in roots:
        x = j - p/(3.0*j) - a/3.0
        try:
            x = x.real
        except:
            pass
        solutions.append(x)
    return solutions

# make certain we're in objectmode, so we have up to date information
editmode = Window.EditMode()
Window.EditMode(0)

# getting the vertex locations
scn = bpy.data.scenes.active
me = scn.objects.active.getData(mesh = True)
locs = [v.co for v in me.verts]

# calculating the center of masss
com = Mathutils.Vector(0.0, 0.0, 0.0)
for loc in locs:
    com += loc
com /= len(locs)
x, y, z = com

# creating the covariance matrix
mat = Mathutils.Matrix([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0])
for loc in locs:
    mat[0][0] += (loc[0]-x)**2
    mat[0][1] += (loc[0]-x)*(loc[1]-y)
    mat[0][2] += (loc[0]-x)*(loc[2]-z)
    mat[1][0] += (loc[1]-y)*(loc[0]-x)
    mat[1][1] += (loc[1]-y)**2
    mat[1][2] += (loc[1]-y)*(loc[2]-z)
    mat[2][0] += (loc[2]-z)*(loc[0]-x)
    mat[2][1] += (loc[2]-z)*(loc[1]-y)
    mat[2][2] += (loc[2]-z)**2

# caculating the normal to the plane
eigenvalues = eigenValues(mat)
eigen = eigenvalues[2]
mat[0][0] -= eigen
mat[1][1] -= eigen
mat[2][2] -= eigen
normal = Mathutils.CrossVecs(mat[0], mat[1])/Mathutils.CrossVecs(mat[0], mat[1]).length

# project vertices onto the plane
for v in me.verts:
    v.co = v.co - Mathutils.DotVecs((v.co-com), normal)*normal

# clean up
Window.Redraw()
Window.EditMode(editmode)

Any help is highly appreciated.
Thanks for your time.

Hi, nothing to say against Cardano’s method… Naturally, the error is first searched in the section

# caculating the normal to the plane
eigenvalues = eigenValues(mat)
eigen = eigenvalues[2]
mat[0][0] -= eigen
mat[1][1] -= eigen
mat[2][2] -= eigen
normal = Mathutils.CrossVecs(mat[0], mat[1])/Mathutils.CrossVecs(mat[0], mat[1]).length

but it can be somewhere above it. Just to check if the error is here, why dont you calculate your normal vector by other math means? --> see this thread for theory and solution: http://blenderartists.org/forum/showthread.php?t=152161

Namely the first part of Dist_V_to_a_Face(v,f) proc. I.e. as soon as you have a given face, then the code:

    v0 = f.v[0]
    v1 = f.v[1]
    v2 = f.v[2]
    vec1 = Mathutils.Vector(v0.co.x-v1.co.x,v0.co.y-v1.co.y,v0.co.z-v1.co.z)
    vec2 = Mathutils.Vector(v2.co.x-v1.co.x,v2.co.y-v1.co.y,v2.co.z-v1.co.z)
    A = vec1[1]*vec2[2]-vec1[2]*vec2[1]
    B = vec1[0]*vec2[2]-vec1[2]*vec2[0]
    C = vec1[0]*vec2[1]-vec1[1]*vec2[0]
    D = -(A*v0.co.x+B*v0.co.y+C*v0.co.z)
    m = 1/math.sqrt(A*A+B*B+C*C)
    if (D&gt;0):
        m = -m
    cos_Alfa = m*A
    cos_Beta = m*B
    cos_Gamma = m*C

will result in constructing the normal vector to this face and it will have coordinates (cos_Alfa,cos_Beta,cos_Gamma). Period!!! Now if this result is the same as the result of your code, then the rror is somewhere else. And if results are different, i.e. the (cos_Alfa,cos_Beta,cos_Gamma) is the correct normal vector. you may think WHY it is so…

Regards,

There is a bit of a problem, since I don’t have a plane. The normal that is calculated is the only information I have along with 1 point on the plane (the center of mass).
But it’s a good idea to check if the problem is above this part. I’ll run some checks to see that nothing goes wrong before this point.

You’re right… my proposed mothod is applicable only for N-verts in one and the same geometric plane. It that case, even w’o a face (plane) you may use the collections of verts to do calculations. BUT the real problem is when the face is non-planar or 1 or more verts does not lie on the same geometric plane as the others (the first 3, for example). There should be some calcs performed in that case but I am not clear which ones (although I have some ideas in mind)…

Regards,

AARRGHH!

This problem was really stumping me.

I couldn’t figure out what was wrong with your code (although I suspect that your cubic equation solver wasn’t producing the right answers).

Eventually I just gave in and figured out that you can do least-squares regression using scipy

If you don’t mind installing an extra python package (http://www.scipy.org/SciPy), this code seems to do the trick:


import bpy
import Blender
from Blender import *
from numpy import *
import scipy.optimize



# make certain we're in objectmode, so we have up to date information
editmode = Window.EditMode()
Window.EditMode(0)

# getting the vertex locations
scn = bpy.data.scenes.active
me = scn.objects.active.getData(mesh = True)
locs = [v.co for v in me.verts]

# calculating the center of masss
com = Mathutils.Vector(0.0, 0.0, 0.0)
for loc in locs:
    com += loc
com /= len(locs)

data=[]
x=[]
y=[]
for i in range(len(locs)):
    data.append(locs[i][2]-com[2])
    x.append(locs[i][0]-com[0])
    y.append(locs[i][1]-com[1])

    
def residiuals(parameter,data,x,y):
    res=[]
    for i in range(len(data)):
            res.append(data[i] - model(parameter,x[i],y[i]))
    return res


def model(parameter, x, y):
    a, b, c = parameter
    return a*x + b*y




params0 = [1., 1.,1.]
result = scipy.optimize.leastsq(residiuals, params0, (data,x,y))
fittedParams = result[0]

normal=Mathutils.Vector(fittedParams[0],fittedParams[1],-1)
normal=normal/normal.length

for v in me.verts:
    v.co = v.co - Mathutils.DotVecs((v.co-com), normal)*normal
    

# clean up
Window.Redraw()
Window.EditMode(editmode)


I think you’re calculating the wrong eigenvector. The one you find is the vector which describes the data best, you want the worst. You want the one with the lowest eigenvalue (I think).

Try using the inverse power series method, that’s what I used when I did this last. I can try and dig out the code later to see exactly what I did.


Edit -
Given a covariance matrix S, choose a vector v with non-zero components (random), and run eq:2 until v’~v. Worked for me.
http://img3.imageshack.us/img3/1259/eqlatex.gif

Let me start by thanking all of you, for the replies.

SciPy works like a charm, but I’m a bit hesitant to use it, as users will have to install another package before they can use the script. I would have chosen for it though, if IanC hadn’t posted his solution.
The funny thing is that I already had a go at the power method before, but somehow didn’t get the right result. In retrospect, what I did wrong was that I applied it on the covariance matrix, without first subtracting the eigenvalue from the diagonal. For the inverse power method this isn’t necessary and so it’s faster. This is what I ended up using.

Looking back on my own code (a few posts above this one), the mistake was in calculating the normal. The cubic solver and eigenvalue calculator were actually correct, but turned out not to be needed.

For anybody who is interested, you can find the working code below:

import bpy
import Blender
from Blender import *

# make certain we're in objectmode, so we have up to date information
editmode = Window.EditMode()
Window.EditMode(0)

# getting the vertex locations
scn = bpy.data.scenes.active
me = scn.objects.active.getData(mesh = True)
locs = [v.co for v in me.verts]

# calculating the center of masss
com = Mathutils.Vector(0.0, 0.0, 0.0)
for loc in locs:
    com += loc
com /= len(locs)
x, y, z = com

# creating the covariance matrix
mat = Mathutils.Matrix([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0])
for loc in locs:
    mat[0][0] += (loc[0]-x)**2
    mat[0][1] += (loc[0]-x)*(loc[1]-y)
    mat[0][2] += (loc[0]-x)*(loc[2]-z)
    mat[1][0] += (loc[1]-y)*(loc[0]-x)
    mat[1][1] += (loc[1]-y)**2
    mat[1][2] += (loc[1]-y)*(loc[2]-z)
    mat[2][0] += (loc[2]-z)*(loc[0]-x)
    mat[2][1] += (loc[2]-z)*(loc[1]-y)
    mat[2][2] += (loc[2]-z)**2

# calculating the normal to the plane
mat.invert()
itermax = 500
iter = 0
vec = Mathutils.Vector(1.0, 1.0, 1.0)
vec2 = (vec*mat)/(vec*mat).length
while vec != vec2 and iter&lt;itermax:
    iter+=1
    vec = vec2
    vec2 = (vec*mat)/(vec*mat).length
normal = vec2

# project vertices onto the plane
for v in me.verts:
    v.co = v.co - Mathutils.DotVecs((v.co-com), normal)*normal

# cleaning up
Window.Redraw()
Window.EditMode(editmode)

Thanks again for all the great help. It’s highly appreciated :slight_smile:

Glad you got it working :slight_smile: As a note, if you need speed, have a look at the output from the inverse power loop. I found that it usually settled down after about 5 iterations for my data, and 20 was more than enough.

sorry for the detail but i’d like to have a better understanding of it

this calculated plane is not the plane between the 4 vertices given

and if there are 2 tri’s plane how do you define the best plane representing theses 2 tri’s plane
is it based on average value or what other parameters is evaluated here ?

note: i cannot put a pic here to show the2 tri’s so difficult to explain it

Thanks

The idea is that you can define a plane which describes the datapoints the best.

The eigenvector of the covariance matrix with the smallest eigenvalue points in the direction of the lowest variance. That is, in that direction, the data doesn’t change very much. Therefore this vector is normal to the plane which describes the data the best (highest variance). The average point of all the data is still going to be the centre (I think), therefore you have a plane defined.

this calculated plane is not the plane between the 4 vertices given

If they are co-planar (exist on the same plane) then it will be, otherwise it’s the best guess.

can this algorythm find all the quads which are not real quad in a mesh and modify theses false quad with the equivalent new plane?

Thanks

Issue with re-working a mesh seems to be very tricky! I am blocked with it in my work under the “Remove or merge vertices in python” thread as it is the only issue left there to achieve a really nice result by using a script. It seems to be very hard to “tell” the computer to do what is very simple in terms of mesh problems and is quite obvious for a viewer (designer) and easily possible to get corrected manually. Perhaps, this included what RickyBlender asks - to convert quads into tris (as soon as qued’s shape is really a triangle) without creating further problems to the mesh. Have I understood you correctly, Ricky? Another type of such mesh problem is to remove faces with area = 0, i.e. when some edges exist to mathematically correctly connect verts in order to comply with BLENDER rule of having faces with 3 or 4 verts only but in fact such hidden faces do create a problem for the designer since he/she is not able to manipulate the surface w/o re-shaping the mesh in advance. A face with area = 0 can also be with ALL verts at one and the same 3D-point so that any edges defined by its verts have lenght = 0. This is easy to correct as removeDubl function with a very small limit deletes the verts non needed, including the zero-area face. But it is an action that should be applied manually any way. Or by a script… A third type of problem is with twisted or crossed faces. Please correct my terminology as I am not Eglish native… What I mean is if you have a simple, absolutely normal flat face with verts 0,1,2 and 3, which means edges (0,1) and (2,3) are not adjacent but opposite… Then edge (2,3) is rotated by 180 degrees and the face remains flat but extremely twisted and if it is in the middle of a mesh => it creates a great problem for that mesh! I think there should be a script developed to correct such problems as no mesh optimisation can be reached w/o this! Shall a new thread be created for that? I’d be happy to contribute to this but I admit that I have no fresh idea on how to proceed with such corrections by using a script… May be not for THIS thread, but I’d be happy to receive info (the code or a link) to script that subdivides the selected part of the object’s mesh!

Regards,

With a refference to my previous posting, I issued a new thread on the correcting mesh problems here

Possibly, but by changing the mesh you may inadvertently change other quads in the mesh. To find the “false” quads you can use a much simpler algorithm (see if the fourth point lies on the same plane as the first three). If they are causing a problem in the mesh, it’s really something to sort out by hand, as it’s probably a sign of poor topology.