Python implementation of the ball-pivoting algorithm (BPA), which was published in 1999 by Bernardini [1]. Some ideas in this implementation were inspired by the implementation of Digne, which was published in 2014 [2].
This algorithm solves the problem of reconstructing mesh surface from a 3D point cloud. The main assumption this algorithm is
based on is the following: Given three vertices, and a ball of radius r
, the three vertices form a triangle if the ball is getting "caught" and settle
between the points, without containing any other point.
The algorithm stimulates a virtual ball of radius r
. Each iteration consists of two steps:
- Seed triangle - The ball rolls over the point cloud until it gets "caught" between three vertices and settles between in them.
Choosing the right
r
promises no other point is contained in the formed triangle. This triangle is called "Seed triangle". - Expanding triangle - The ball pivots from each edge in the seed triangle, looking for a third point. It pivots until it gets "caught" in the triangle formed by the edge and the third point. A new triangle is formed, and the algorithm tries to expand from it. This process continues until the ball can't find any point to expand to.
At this point, the algorithm looks for a new seed triangle, and the process described above starts all over.
The following figures demonstrates those two steps.
2D view of the finding seed triangle step
2D view of the expanding triangle step
Two vague assumptions that are necessary for the algorithm are that the point cloud is "dense enough", and that the
chosen r
size is "slightly" larger than the average space between points. I couldn't find a metric method to evaluate
those two variables at the moment, and more work needs to be done on this.
I used a virtual 3D grid in which in each cell of the grid, all points are at distance of maximum 2r
from all
other points. With this method, i am able to limit the number of points i need to search. Since we are looking to fit a
ball of radius r
between three points, we can be assured that if the distance be two points is larger than 2r
, the ball
won't get caught between them. Therefore, while checking all possible points to pivot from a point p
when generating
seed triangle or expanding triangle, i need to check p
's cell, and all 8 cells that touches this cell. Note that there
might get points that are 4r
apart, and i have to check that when iterating through these neighbor points. Example for that
is shown in the following figure.
2D view on part of the grid. The orange cells are the neighbors of the green cell
Each cell is represented by a single point. For example, all cells are represented by the point marked by p
:
Each cells saves the entire points contained in it, but i also needed each point to save which cell it belongs to, to avoid iterating all the grid when searching for a point. I decided that each point will save the cell in belongs to. In order to decrease the memory required for this cyclic-design, each point save encoded unique version of the cell it belongs to. The encoding was done by shifting and concatenating the coordinates of the point that defines the cell.
Encodeing the cell define by (1, 2, 3) to 197121
Consists of 3 coordinates, the normal in this point, the cell node it's sitting in, according to the grid's initiation.
Consists of 2 Point objects.
Initially i implemented a multi-threaded version of the algorithm. I knew it won't improve the algorithm's running time due to Python's GIL, that prevents making use of more than one CPU core to run threads in parallel, but i thought it might improve the algorithm's mesh quality if i run simultaneously multiple threads in different parts of the object. It didn't improved the algorithm's quality. I think a better idea would be the implement the algorithm using CUDA and Numba's JIT, but it's not trivial and requires some work.
Finding a seed costs time. We iterate through all points.
For each point p1
, i check in time it's neighbor cells
in order to find all points that are at maximum distance of 2r
from p1
. I sort all neighbor points by distance from
p1
in to make sure the formed triangles will be as small as possible to reduce the number of cases where a
point in contained inside the formed triangle. For each point p2
in p1
's neighbors, the same sorting process occurs.
In order to check if a ball with radius of r
can fit into the triangle defined by p1
, p2
and p3
, i calculate the
radius of the inner-circle of that triangle. This calculation is in
. If the normal of this triangle is in the same direction as the point's normal, the triangle is added to the mesh.
This check is also in .
Expanding a single triangle costs .
For each of the triangle's edges e=(p1,p2)
, i check in
time the neighbor cells of p1
and p2
in order to find all points that are at maximum distance of 2r
from p1
and p2
.
I sort the points as before, in .
I then check if the ball can fit into the formed triangle, and that it's normal vector is in the same
direction as the points.
At the worst case, the algorithm fails to expand the triangle everytime and has to find a new seed, making the total run time complexity . This scenario is unlikely. I ignore in this section the time required for visualization.
I've created a visualizer for the algorithm using the open-source library Open3D [3]. The visualizer updates it's rendering after a seed triangle is found, or an expanding triangle is found. An example of the visualizer is shown in the examples section. I've also added normal visualization for debugging the data generation. An example for that is shown in the following figure.
Stansford's bunny with normals
This algorithm expects to get as input .txt
file in the following pattern:
x y z nx ny nz
Where x
, y
and z
are the point's coordinates, and nx
, ny
and nz
are the point's normal vector's coordinates.
In order to generate the data to test the algorithm, i've downloaded 3D objects in .obj format [4], extracted the points, and
extracted each point's normal based on one of the facets it belongs to. Examples of the data are in the data
folder. Code for generating new data is in data_generator.py
.
- Python>=3.7
- open3d>=0.7.0.0
- numpy>=1.20.1
-
create_mesh(): Takes an optional argument
limit_iterations
, that limits the number of iterations of the algorithm. Generates a mesh. Must be called after initializing the BPA object.Example:
from bpa import BPA bpa = BPA(path='bunny_with_normals.txt', radius=0.005, visualizer=True) bpa.create_mesh(limit_iterations=1000)
-
visualizer.draw_with_normals(): Takes an optional argument
percentage
that limits the number of points that their normal will be drawn. If set to 100, all normals will be drawn. Default value is set to 10. Another argument isnormals_size
that defines the drawn normal's size. Default value is set to 1.Example:
from bpa import BPA bpa = BPA(path='bunny_with_normals.txt', radius=0.005, visualizer=True) bpa.visualizer.draw_with_normals(percentage=20, normals_size=2)
An example of Stansford's bunny is in here:
Red triangles are seed triangles, and green triangles are expanding triangles.
-
Holes! - As you can see in the example, the algorithm fails to reach a full mesh, and many "holes" are left.
-
Overlapping triangles - I have reduced the number of overlapping expanding triangles by checking that the third point we expand to is not in same side of the edge as the third point of the triangle we expand. For example, suppose we are expanding the orange triangle:
e
is the edge we pivot on. Suppose these are the algorithm sees the pointp'
. I make sure the it won't pick it sincep'
in the same side ofe
asp
. This does not apply to the seed triangles. I couldn't think of a way to identify those overlapping cases at the moment. -
Triangles vertices are not saved in clock-wise order, which makes it difficult later in exporting the mesh as
.obj
file. -
Generate
.obj
file out of the result mesh. -
Find a criteria for choosing the initial radius.
-
Pre-check if the point cloud is "dense enough".
-
Find a metric to evaluate how well the constructed mesh is.
-
Implement cuda support.
[1] The Ball-Pivoting Algorithm for Surface Reconstruction, by F. Bernardini, J. Mittleman and H. Rushmeier, 1999.
[2] An Analysis and Implementation of a Parallel Ball Pivoting Algorithm, by J. Digne, 2014.