Laplacian Smoothing
When you 3D scan an object using a laser or an iPhone, the sensor is never perfect. It makes tiny mistakes. The resulting 3D mesh is covered in microscopic spikes and bumps called Noise. We need a way to mathematically iron out the wrinkles without destroying the overall shape.
The Problem with Noise:
1# Adding Artificial Noise2def add_noise_to_mesh(mesh, severity):3 """Simulates a bad 3D scan or rounding errors."""45 for vertex in mesh.vertices:67 # 1. Get the vertex normal (which way is it facing)8 normal = vertex.normal910 # 2. Pick a random number between -severity and +severity11 random_shift = random.uniform(-severity, severity)1213 # 3. Push the vertex along its normal14 vertex.position += normal * random_shift1516 return mesh
To smooth out a spike in the mesh, the algorithm needs to look at the geometry immediately surrounding that spike. In topology, this immediate surrounding area is called the 1-Ring Neighborhood.
The Umbrella:
1# Finding the 1-Ring Neighbors2def get_adjacent_vertices(mesh, target_vertex):3 """Who is directly connected to this point?"""45 neighbors = []67 # Check every edge in the entire mesh8 for edge in mesh.edges:910 # If our target vertex is on one side of the edge...11 if edge.v1 == target_vertex:12 # ...the vertex on the other side is a neighbor!13 neighbors.append(edge.v2)1415 elif edge.v2 == target_vertex:16 neighbors.append(edge.v1)1718 return neighbors
Now that we know who the neighbors are, we can calculate exactly how "spiky" our target vertex is. We do this by calculating a 3D arrow called the Laplacian Vector.
The Pulling Force:
1# Calculating the Laplacian Vector2def calculate_laplacian(vertex, neighbors):3 """How far away is the vertex from its ideal smooth position?"""45 # 1. Find the centroid (average) of all the neighbors6 total_x = sum([n.x for n in neighbors])7 total_y = sum([n.y for n in neighbors])8 total_z = sum([n.z for n in neighbors])910 count = len(neighbors)11 centroid = Point(total_x/count, total_y/count, total_z/count)1213 # 2. Draw a 3D arrow from the target vertex TO that centroid14 # This arrow is the Laplacian Vector!15 laplacian_vector = centroid - vertex.position1617 return laplacian_vector
Once we have the Laplacian Vector for every vertex in the mesh, we simply push the vertices along those arrows.
The Lambda Factor:
0.1 means the vertex only moves 10% of the way down the arrow. 4. By doing this iteratively (running the math 10 or 20 times in a row with a small Lambda), the mesh smoothly relaxes over time without collapsing instantly!1# Smoothing the Vertex2def update_vertex_position(vertex, laplacian_vector, lambda_factor):3 """Moving the vertex along the arrow."""45 # We do NOT move the vertex all the way to the centroid!6 # That would instantly flatten out the entire 3D model.7 # Instead, we only move it a tiny fraction of the way.89 # lambda_factor is usually a tiny number like 0.110 movement = laplacian_vector * lambda_factor1112 # Update the position!13 vertex.position = vertex.position + movement
There is a major flaw with standard Laplacian Smoothing: it shrinks the model! Because every spike is pulled inward, if you run the algorithm 100 times, a majestic 3D dragon will shrink into a tiny, smooth pebble.
Taubin Smoothing:
1# Taubin Smoothing (Preserving Volume)2def taubin_smooth(mesh, iterations):3 """The push-and-pull method."""45 # Positive lambda (pushes inward)6 lambda_factor = 0.3378 # Negative mu (pushes outward!)9 mu_factor = -0.341011 for i in range(iterations):1213 # Step 1: Standard Laplacian Smooth (shrinks the mesh)14 mesh = apply_laplacian(mesh, lambda_factor)1516 # Step 2: Reverse Laplacian Smooth (inflates the mesh!)17 mesh = apply_laplacian(mesh, mu_factor)1819 return mesh
How does the computer actually know if a mesh is "noisy" or "smooth"? We can use the exact same Laplacian math to evaluate and score the quality of the 3D model.
Visualizing the Error:
1# Calculating the Error Metric2def evaluate_mesh_smoothness(mesh):3 """How bumpy is this mesh overall?"""45 total_error = 067 for vertex in mesh.vertices:89 # We calculate the Laplacian Vector (the arrow to the centroid)10 laplacian_vector = calculate_laplacian(vertex)1112 # The length of that arrow is the "Error" for this vertex13 error = length(laplacian_vector)1415 # Add it to the total score16 total_error += error1718 # Average it out19 average_error = total_error / len(mesh.vertices)2021 return average_error
When applied to a complex geometry like a 3D Scan, Laplacian Smoothing acts exactly like a Blur filter in Photoshop, but for 3D coordinates instead of 2D pixels.
The Denoising Pipeline:
1# Full Smoothing Pipeline2def repair_scan_data(mesh, taubin_iterations=10):3 """Clean up a noisy 3D scan for 3D Printing."""45 print("Initial Noise Level:", evaluate_mesh_smoothness(mesh))67 # 1. We run Taubin smoothing to protect the volume8 cleaned_mesh = taubin_smooth(mesh, taubin_iterations)910 print("Final Noise Level:", evaluate_mesh_smoothness(cleaned_mesh))1112 # 2. Re-calculate lighting normals for the new smooth surface13 cleaned_mesh.calculate_smooth_normals()1415 return cleaned_mesh