You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
244 lines
12 KiB
244 lines
12 KiB
import bpy |
|
import sys |
|
import open3d as o3d |
|
import numpy as np |
|
from sklearn.neighbors import NearestNeighbors |
|
import os |
|
import argparse |
|
|
|
def estimate_point_curvatures(point_cloud, k=30): |
|
point_cloud_points = np.asarray(point_cloud.points) |
|
# Initialize the nearest neighbors finder |
|
nn = NearestNeighbors(n_neighbors=k) |
|
nn.fit(point_cloud_points) |
|
curvatures = {i: 0 for i in range(len(point_cloud.points))} |
|
for i, point in enumerate(point_cloud_points): |
|
# Find the k-nearest neighbors (including the point itself) |
|
distances, indices = nn.kneighbors([point]) |
|
neighbors = point_cloud_points[indices[0]] |
|
# Compute the covariance matrix |
|
covariance_matrix = np.cov(neighbors - neighbors.mean(axis=0), rowvar=False) |
|
# Compute the eigenvalues (sorted) |
|
eigenvalues, _ = np.linalg.eigh(covariance_matrix) |
|
eigenvalues.sort() |
|
# Calculate curvature using the smallest eigenvalue |
|
curvature = eigenvalues[0] / sum(eigenvalues) |
|
curvatures[i] = curvature |
|
return curvatures |
|
|
|
def compute_adjacent_normal_angle_variance(pcd, pcd_normal, distance_scale): |
|
kdtree = o3d.geometry.KDTreeFlann(pcd) |
|
distances = [] |
|
for i in range(len(pcd.points)): |
|
[k, idx, dist] = kdtree.search_knn_vector_3d(pcd.points[i], 2) |
|
dist = dist[1:] # distances to the actual neighbors |
|
distances.append(dist) |
|
mean_distance = np.mean(distances) |
|
adjacent_normal_angles_variance = {i: 0 for i in range(len(np.asarray(pcd.points)))} |
|
for i, point in enumerate(pcd.points): |
|
# Search for the k-nearest neighbors of the point, up to 1500 |
|
k, idx, dist = kdtree.search_knn_vector_3d(point, 200) |
|
idx = np.asarray(idx) |
|
indices = idx[dist < mean_distance * distance_scale] |
|
current_normal = pcd_normal[i] |
|
# Calculate dot products between the current normal and its neighbors' normals |
|
dot_products = np.dot(pcd_normal[indices], current_normal) |
|
# Handle any potential NaN values in dot products |
|
valid_dot_products = np.isfinite(dot_products) |
|
dot_products = dot_products[valid_dot_products] |
|
# Clip dot product values to valid range for arccos |
|
dot_products = np.clip(dot_products, -1.0, 1.0) |
|
# Compute angles in degrees and adjust for any NaNs which might be due to numerical errors |
|
angles_array = np.degrees(np.arccos(dot_products)) |
|
# Set the angle with itself to zero since it's not meaningful |
|
angles_array[indices == i] = 0 |
|
# Calculate the mean angle excluding the zero for the current point |
|
valid_angles = angles_array[indices != i] |
|
if valid_angles.size > 0: |
|
adjacent_normal_angles_variance[i] = np.std(valid_angles) |
|
else: |
|
adjacent_normal_angles_variance[i] = 0 # Default to 0 if no valid neighbors exist |
|
return adjacent_normal_angles_variance |
|
|
|
def compute_adjacent_point_vector_degrees_mean(pcd, pcd_normal, distance_scale): |
|
kdtree = o3d.geometry.KDTreeFlann(pcd) |
|
distances = [] |
|
for i in range(len(pcd.points)): |
|
[k, idx, dist] = kdtree.search_knn_vector_3d(pcd.points[i], 2) |
|
dist = dist[1:] # distances to the actual neighbors |
|
distances.append(dist) |
|
mean_distance = np.mean(distances) |
|
adjacent_point_vector_degrees_mean = {i: 0 for i in range(len(np.asarray(pcd.points)))} |
|
for i, point in enumerate(pcd.points): |
|
# Search for the k-nearest neighbors of the point, up to 200 |
|
k, idx, dist = kdtree.search_knn_vector_3d(point, 200) |
|
idx = np.asarray(idx) |
|
indices = idx[dist < mean_distance * distance_scale] |
|
indices = indices[indices != i] |
|
# Retrieve the normal of the current point |
|
current_normal = pcd_normal[i] |
|
adjacent_point_vector = np.asarray(pcd.points)[indices] - point |
|
adjacent_point_vector_cos = np.dot(adjacent_point_vector, current_normal) / np.linalg.norm(adjacent_point_vector, axis = 1) * 1 |
|
valid_adjacent_point_vector_cos = np.isfinite(adjacent_point_vector_cos) |
|
adjacent_point_vector_cos = adjacent_point_vector_cos[valid_adjacent_point_vector_cos] |
|
if len(adjacent_point_vector_cos) > 0: |
|
adjacent_point_vector_degrees = np.degrees(np.arccos(adjacent_point_vector_cos)) |
|
adjacent_point_vector_mean_degrees = np.mean(adjacent_point_vector_degrees) |
|
adjacent_point_vector_degrees_mean[i] = adjacent_point_vector_mean_degrees |
|
else: |
|
adjacent_point_vector_degrees_mean[i] = 0 |
|
return adjacent_point_vector_degrees_mean |
|
|
|
def spread_point(pcd, selected_index, distance_scale, count = False): |
|
kdtree = o3d.geometry.KDTreeFlann(pcd) |
|
distances = [] |
|
for i in range(len(pcd.points)): |
|
[k, idx, dist] = kdtree.search_knn_vector_3d(pcd.points[i], 2) |
|
dist = dist[1:] # distances to the actual neighbors |
|
distances.append(dist) |
|
mean_distance = np.mean(distances) |
|
if count is False: |
|
spread_selected_index = set() |
|
for i in selected_index: |
|
k, idx, dist = kdtree.search_knn_vector_3d(pcd.points[i], 200) |
|
idx = np.asarray(idx) |
|
dist = np.asarray(dist) |
|
indices = idx[dist < mean_distance * distance_scale] |
|
spread_selected_index.update(indices) |
|
spread_selected_index = np.array(list(spread_selected_index)) |
|
else: |
|
spread_selected_index = {i: 0 for i in range(len(np.asarray(pcd.points)))} |
|
for i in selected_index: |
|
k, idx, dist = kdtree.search_knn_vector_3d(pcd.points[i], 200) |
|
idx = np.asarray(idx) |
|
dist = np.asarray(dist) |
|
indices = idx[dist < mean_distance * distance_scale] |
|
for indice in indices: |
|
spread_selected_index[indice] += 1 |
|
return spread_selected_index |
|
|
|
def ApplySmoothEffort(object, selected_vertice_indices = None, factor = 0.5, iterations = 30): |
|
if selected_vertice_indices is not None: |
|
# Assuming 'mesh' is already defined, e.g., mesh = bpy.context.object.data |
|
selected_vertices_index = set() # Initialize an empty set to store unique vertex indices |
|
# Iterate over each index in the list of polygon indices |
|
for idx in selected_vertice_indices: |
|
polygon = object.data.polygons[idx] |
|
for vert_idx in polygon.vertices: |
|
selected_vertices_index.add(vert_idx) |
|
# Switch to Edit mode |
|
bpy.ops.object.mode_set(mode='EDIT') |
|
# Ensure we're dealing with the latest data |
|
bpy.ops.object.mode_set(mode='OBJECT') |
|
for i, vertex in enumerate(object.data.vertices): |
|
vertex.select = False |
|
if i in selected_vertices_index: |
|
vertex.select = True |
|
# Update the object to reflect changes |
|
bpy.ops.object.mode_set(mode='EDIT') |
|
bpy.ops.object.mode_set(mode='OBJECT') |
|
# Add a Smooth modifier and set it to use the vertex group |
|
smooth_mod = object.modifiers.new(name="SmoothMod", type='SMOOTH') |
|
if selected_vertice_indices is not None: |
|
# Create a new vertex group |
|
group = object.vertex_groups.new(name='Smooth Group') |
|
# Add selected vertices to the vertex group |
|
group.add([v.index for v in object.data.vertices if v.select], weight=1.0, type='ADD') |
|
smooth_mod.vertex_group = 'Smooth Group' |
|
smooth_mod.factor = factor |
|
smooth_mod.iterations = iterations |
|
# bpy.ops.object.modifier_apply(modifier="SmoothMod") |
|
# if selected_vertice_indices is not None: |
|
# object.vertex_groups.clear() |
|
|
|
def process(input_mesh_path, output_mesh_path): |
|
# Select and delete all objects in the current scene to start fresh |
|
bpy.ops.object.select_all(action='SELECT') |
|
bpy.ops.object.delete(confirm=False) |
|
# Path to the OBJ file to be imported |
|
bpy.ops.wm.obj_import(filepath=input_mesh_path) |
|
# Set the current object to the newly imported mesh |
|
current_obj = bpy.context.object |
|
# Retrieve the mesh data from the current object |
|
mesh_data = current_obj.data |
|
bpy.context.view_layer.update() |
|
# Convert Blender mesh vertices to a NumPy array |
|
vertices_array = np.array([vertex.co[:] for vertex in mesh_data.vertices]) |
|
# Convert Blender mesh polygons to a NumPy array of triangles |
|
triangles_array = np.array([polygon.vertices[:] for polygon in mesh_data.polygons]) |
|
|
|
# Create an Open3D mesh object from the arrays |
|
open3d_mesh = o3d.geometry.TriangleMesh() |
|
open3d_mesh.vertices = o3d.utility.Vector3dVector(vertices_array) |
|
open3d_mesh.triangles = o3d.utility.Vector3iVector(triangles_array) |
|
open3d_mesh.compute_triangle_normals() |
|
# Retrieve and store the normals as a NumPy array |
|
triangle_normals_array = np.asarray(open3d_mesh.triangle_normals) |
|
|
|
# Compute centroids for each face of the mesh |
|
triangle_centroids_array = np.array([ |
|
np.mean(vertices_array[triangle], axis=0) |
|
for triangle in open3d_mesh.triangles |
|
]) |
|
# Create a point cloud of face centroids |
|
pcd_triangle_centroid = o3d.geometry.PointCloud() |
|
pcd_triangle_centroid.points = o3d.utility.Vector3dVector(triangle_centroids_array) |
|
|
|
point_curvatures = estimate_point_curvatures(pcd_triangle_centroid, 30) |
|
np_point_curvatures = np.array(list(point_curvatures.values())) |
|
point_curvatures_index = np.arange(len(np_point_curvatures))[np_point_curvatures > 0.25] |
|
|
|
spread_point_curvatures_index = spread_point(pcd_triangle_centroid, point_curvatures_index, 50) |
|
|
|
ApplySmoothEffort(current_obj, spread_point_curvatures_index, 0.5, 30) |
|
|
|
adjacent_normal_angle_variance = compute_adjacent_normal_angle_variance(pcd_triangle_centroid, triangle_normals_array, 40) |
|
adjacent_point_vector_degrees_mean = compute_adjacent_point_vector_degrees_mean(pcd_triangle_centroid, triangle_normals_array, 40) |
|
|
|
array_adjacent_point_vector_degrees_mean = np.array(list(adjacent_point_vector_degrees_mean.values())) |
|
array_adjacent_normal_angle_variance = np.array(list(adjacent_normal_angle_variance.values())) |
|
|
|
use_data = np.where(array_adjacent_point_vector_degrees_mean < 90, array_adjacent_normal_angle_variance, 0) |
|
# Calculate the minimum and maximum values in the angle array, ignoring NaNs. |
|
min_angle = np.nanmin(use_data) |
|
max_angle = np.nanmax(use_data) |
|
# Perform min-max normalization on the angles to scale them between 0 and 1. |
|
# This normalization helps in comparing values on a common scale. |
|
normalized_angles = (use_data - min_angle) / (max_angle - min_angle) |
|
first_select_thread = normalized_angles > 0.2 |
|
first_select_thread_number = first_select_thread.astype(np.float64) |
|
thread_normalized_angles_index = np.arange(len(first_select_thread))[first_select_thread] |
|
|
|
spread_point_count = spread_point(pcd_triangle_centroid, thread_normalized_angles_index, 3, count=True) |
|
|
|
overlapp_trigular_points = np.array(list(spread_point_count.values())) > 3 |
|
overlapp_trigular_points_index = np.arange(len(overlapp_trigular_points))[overlapp_trigular_points] |
|
|
|
ApplySmoothEffort(current_obj, overlapp_trigular_points_index, 0.5, 30) |
|
|
|
ApplySmoothEffort(current_obj, factor=0.5, iterations=30) |
|
|
|
bpy.ops.wm.obj_export(filepath=output_mesh_path) |
|
|
|
def process_meshes(input_path, output_path): |
|
""" |
|
Processes each mesh file in the input directory and saves the cleaned meshes in the output directory. |
|
|
|
Args: |
|
input_path (str): Directory containing the input meshes or a single mesh file. |
|
output_path (str): Directory where the cleaned meshes will be saved. |
|
""" |
|
if os.path.isdir(input_path): |
|
for filename in sorted(os.listdir(input_path)): |
|
full_input_path = os.path.join(input_path, filename) |
|
full_output_path = os.path.join(output_path, filename) |
|
process(full_input_path, full_output_path) |
|
else: |
|
process(input_path, output_path) |
|
|
|
if __name__ == "__main__": |
|
parser = argparse.ArgumentParser(description="Processes mesh files.") |
|
parser.add_argument("-ip", "--input_path", type=str, required=True, help="Input path for the mesh file or directory.") |
|
parser.add_argument("-op", "--output_path", type=str, required=True, help="Output path for the cleaned mesh file or directory.") |
|
args = parser.parse_args() |
|
process_meshes(args.input_path, args.output_path)
|
|
|