Showing posts with label fitting a plane. Show all posts
Showing posts with label fitting a plane. Show all posts

Plane fit add-on tested with Blender 4.4


In an ongoing effort to check if any of my old add-ons still work on Blender 4.4, I just tested planefit.py.


It works without any change, so I simply committed it with a comment.

The add-on is available from my GitHub repository and you can read more on it in this article.


Add-on to fit a cylinder to vertices

I previously updated two small add-ons that can fit a line or a plane to a collection of vertices and was asked if it was possible to create an add-on that fits a cylinder.

That is a bit more challenging though, but luckily there are people who spend some serious time on designing an algorithm and even providing code (see references below).

Based on that I created a small add-on that can indeed fit a cylinder to a collection of selected vertices. Note that it fits a cylinder where the vertices lie as closely as possible on the surface of the cylinder (see image). If you want to fit a cylinder that encloses all vertices, so more of a solid rod, simply use the linefit add-on and align a cylinder to the best fit line.


Usage

Simply download cyclinderfit.zip from the repo and install and enable the add-on from this zip-file.

Then select the mesh with the vertices you want to fit the cylinder to and select Fit cylinder from the Add menu (in edit mode). The new cylinder object will be added as a separate object that will be in edit mode.

Code availability

The code is available in this GitHub repository.

References

The cylinder fitting code was adapted from code in Xing Jiepan's repo, which in turn was based on the algorithms described in this paper by David Eberly.

To remove dependencies on external packages (except numpy, which is included with Blender), we replaced calls to the scikit.optimize.minimize function with a different implementation of Powells' minimization function from the Sherpa code-base of the Chandra project. 














Planefit.py and linefit.py updated for Blender 4.x

Always happy to see any of my add-ons being used, even if it's a really old one, so based on a BlenderArtists request I updated planefit.py and linefit.py.





Planefit adds a plane (=face) to your mesh that fits any selected vertices as well as possible (details in this post), and linefit is similar and adds a line (=edge) (details here). The articles also explain the math involved a bit if you are interested.

The updated add-ons can be found on GitHub:


Both add-ons can be downloaded in the same manner: Near the top right of the linked pages is a download button; click it to save the .py file, then (re)install the add-on in the usual manner.

Technical details

For any nerds out there: even though the commits seem large, that's mainly because I now use the Black formatter in all my Python projects so lots of whitespace was changed 😁. The actual change to planefit.py was only 1 line (the current line 116): the loop_total property of a polygon is read only nowadays so we cannot (and need not) set it: It is automatically updated when we add the vertex indices.

The change to linefit.py was a bit more involved, mainly because it was even older: Properties in an Operator are now annotated class variables (and have been since version 2.8 I think), so we had to change line 53 from

size = bpy.props.FloatProperty( ....

to

size : bpy.props.FloatProperty(...

A small but necessary change that does not even give you a warning anymore, so something to look out for if you revisit very old add-ons.

Likewise, the function bpy.utils.register_module() doesn't exist anymore and had to be replaced by bpy.utils.register_class()


Planefit add-on: ported to 2.83

My PlaneFit add-on is now compatible with version 2.83.


The addon is available for download from my GitHub repository.
More info on the add-on in this previous article.

PlaneFit add-on: tiny enhancement

My PlaneFit add-on now features a 2nd menu entry that lets you add the plane as a separate object instead of a part of the mesh.


The addon is available for download from my GitHub repository.
More info on the add-on in this previous article.

Linefit add-on

As a companion add-on to PlaneFit I put together a small add-on that can add a single edge (two connected vertices) that best fits a collection of selected vertices.
After installtion it will be available in the 3d-view menu Add → Fit line to selected and the result of applying it to a vaguely cylindrical point cloud is shown below:

Availability

As usual the add-on is available from my GitHub repository (right-click on the link and Save As ...)

Extra information

There are many ways to fit a line to a collection of points but here we use the same eigen decomposition we use for fitting a plane. Instead of selecting the eigen vector with the smallest eigen value as the normal of a plane, we now select the eigen vector with the largest eigen value. This vector accounts for most of the variance in the positions of all the vertices, and is therefore the best fit. (There are other metrics we could use and this explanation is may be a bit too much hand-waiving for real mathematicians but it works :-)
The relevant code is shown below:
import numpy as np

def lineFit(points):
    ctr = points.mean(axis=0)
    x = points - ctr
    M = np.cov(x.T)
    eigenvalues,eigenvectors = np.linalg.eig(M)
    direction = eigenvectors[:,eigenvalues.argmax()]
    return ctr,direction

PlaneFit: Blender add-on, tiny improvement

The PlaneFit add-on wasn't very robust yet so I added a check for less than 3 selected vertices so a plane will only be fitted when it is sensible to do so. Gives a proper warning as well and the menu (In the 3d view, Add->Mesh in edit mode) now has a more readable label.

Availability

I have updated the add-on in my GitHub repository. (right-click on the link and select Save As .. to store the .py file somewhere where you can find it again )

PlaneFit: Blender add-on to fit a plane through a set of vertices

After all the updates to my BlenderMarket add-ons, it is time to spend some time on other add-ons again.

Fitting a plane

Fitting a plane through a collection of selected vertices might be useful in all sort of scenarios. Currently you can snap a plane to a face with Blender's snap tools but if we want to fit a plane to a large set of vertices, we need to resort to basic linear algebra to do this in a fast and robust manner. And because linear algebra is not everybody's favorite subject I created a small add-on.

After installation the add-on is available from the Add menu if you have a mesh in edit mode. Clicking on it will create a square plane that is fitted through all selected vertices. A size option lets you scale this new plane interactively. The result might look something like this:
Note that currently we not check if the minimum of 3 vertices are selected, you get a an error if you try.

Availability

As usual the add-on is available from my GitHub repository. (right-click on the link and select Save As .. to store the .py file somewhere where you can find it again )

Source code

Both the linear algebra and the code to add a plane to an exiting mesh isn't completely trivial, so let me highlight the interesting bits:

Fitting a plane

The fitting code is quite short and straight forward:
import numpy as np

def planeFit(points):
    ctr = points.mean(axis=0)
    x = points - ctr
    M = np.cov(x.T)
    eigenvalues,eigenvectors = np.linalg.eig(M)
    normal = eigenvectors[:,eigenvalues.argmin()]
    return ctr,normal
Any book on linear algebra can give you a better explanation but with a fair bit of hand-waving it can be explained as follows: points is a list of 3d-vectors. ctr will be the midpoint of the plane which is the mean of each of the x, y and z-components.

In line 5 - 7 we calculate the eigen vectors of the point cloud. It is a 3d cloud so we will get 3 eigen vectors and 3 corresponding eigenvalues. Each combination of eigen value and eigen vector can be interpreted as a direction vector and a measure of how well it explains the spread of the points. This means that if the points lie roughly in a plane, the two biggest eigen vectors lie in the best fit plane while the smallest one will be the normal (all eigen vectors are perpendicular). And indeed this smallest one is the one we get in line 8.

Adding a plane an existing mesh

Now adding a plane consisting of four vertices in a square would be quite simple, yes? Ehh, no: the Mesh object has a from_pydata() function but it only works correctly when adding to an initially empty mesh. So lines 15 - 27 essentially replicate what that function does: create 4 vertices, and 4 loops, compose a polygon out of it and add it to the mesh as well. We could have worked with a BMesh representation but then we would not have an efficient way to retrieve all vertex coordinates, something we really need when working with thousands of vertices.

The code in lines 7 - 12 is a very efficient way to get the coordinates of selected vertices into a Numpy array: we create Numpy arrays to hold all the vertex coordinates and the selected status, then get all of them with the fast built-in function foreach_get(). verts[selected] then leaves us with an array of just the coordinates of selected vertices, which we pass to our planeFit() function we saw earlier.

We then create two vectors perpendicular to our normal and use them to construct four vertex coordinates.

         def execute(self, context):
  bpy.ops.object.editmode_toggle()
  me = context.active_object.data
  count = len(me.vertices)
  if count > 0:  # degenerate mesh, but better safe than sorry
   shape = (count, 3)
   verts = np.empty(count*3, dtype=np.float32)
   selected = np.empty(count, dtype=np.bool)
   me.vertices.foreach_get('co', verts)
   me.vertices.foreach_get('select', selected)
   verts.shape = shape
   ctr, normal = planeFit(verts[selected])
   dx, dy = orthopoints(normal)  # definition of orthopoints() not shown
   # can't use mesh.from_pydata here because that won't let us ADD to a mesh
   me.vertices.add(4)
   me.vertices[count  ].co = ctr+dx*self.size
   me.vertices[count+1].co = ctr+dy*self.size
   me.vertices[count+2].co = ctr-dx*self.size
   me.vertices[count+3].co = ctr-dy*self.size
   lcount = len(me.loops)
   me.loops.add(4)
   pcount = len(me.polygons)
   me.polygons.add(1)
   me.polygons[pcount].loop_total = 4
   me.polygons[pcount].loop_start = lcount
   me.polygons[pcount].vertices = [count,count+1,count+2,count+3]
   me.update(calc_edges=True)

  bpy.ops.object.editmode_toggle()
  return {'FINISHED'}