Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import open3d as o3d | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import tempfile | |
| import traceback | |
| import os | |
| from types import SimpleNamespace | |
| DEBUG = False # True: skip gradio and execute in main function | |
| def process_point_cloud(file_obj): | |
| # Save uploaded file temporarily | |
| if DEBUG: | |
| temp_path = os.path.join(tempfile.gettempdir(), file_obj.name) | |
| with open(temp_path, "wb") as f: | |
| f.write(file_obj.read()) | |
| else: | |
| temp_path = file_obj.name | |
| # Load point cloud using Open3D | |
| pcd = o3d.io.read_point_cloud(temp_path) | |
| # Process: voxel downsampling | |
| downsampled = pcd.voxel_down_sample(voxel_size=0.01) | |
| # Estimate normals for visualization | |
| downsampled.estimate_normals() | |
| points = np.asarray(downsampled.points) | |
| fig = plt.figure(figsize=(6, 6)) | |
| ax = fig.add_subplot(111, projection='3d') | |
| ax.scatter(points[:, 0], points[:, 1], points[:, 2], s=0.5, c='b') | |
| ax.axis('off') | |
| plt.tight_layout() | |
| temp_img_path = os.path.join(tempfile.gettempdir(), "rendered.png") | |
| plt.savefig(temp_img_path, dpi=150) | |
| plt.close() | |
| img_np = plt.imread(temp_img_path) | |
| return img_np | |
| iface = gr.Interface( | |
| fn=process_point_cloud, | |
| inputs=gr.File(file_types=[".ply", ".pcd", ".xyz"], label="Upload Point Cloud"), | |
| outputs=gr.Image(type="numpy", label="Rendered Point Cloud"), | |
| title="Point Cloud Viewer", | |
| description="Upload a .ply, .pcd, or .xyz file. The app will downsample and render it." | |
| ) | |
| def check_delimiter(file_path): | |
| with open(file_path, "r") as f: | |
| line = f.readline() | |
| if "," in line: | |
| return "," | |
| elif "\t" in line: | |
| return "\t" | |
| else: | |
| return " " | |
| def meshing(file_obj, normal_rad_coef, normal_max_nn, orient_k, bpa_min_coef, bpa_max_coef): | |
| try: | |
| if DEBUG: | |
| temp_path = os.path.join(file_obj.dir, file_obj.name) | |
| ext = os.path.splitext(temp_path)[1].lower() | |
| else: | |
| temp_path = file_obj.name | |
| ext = os.path.splitext(temp_path)[1].lower() | |
| # Load the input point cloud | |
| if ext == ".txt": | |
| delimiter = check_delimiter(temp_path) | |
| points = np.loadtxt(temp_path, delimiter=delimiter) | |
| if points.shape[1] != 3: | |
| raise ValueError("Text file must contain 3 columns (X Y Z)") | |
| pcd = o3d.geometry.PointCloud() | |
| pcd.points = o3d.utility.Vector3dVector(points) | |
| else: | |
| pcd = o3d.io.read_point_cloud(temp_path) | |
| dists = pcd.compute_nearest_neighbor_distance() | |
| avg_spacing = np.mean(dists) | |
| normal_radius = avg_spacing*normal_rad_coef # start with 2–3x spacing | |
| pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=normal_radius, max_nn=normal_max_nn)) | |
| pcd.orient_normals_consistent_tangent_plane(k=orient_k) | |
| # Mesh using Ball Pivoting | |
| coeffs = np.linspace(bpa_min_coef, bpa_max_coef, int(4)) | |
| mesh_radii = [avg_spacing * x for x in coeffs] | |
| mesh = o3d.geometry.TriangleMesh.create_from_point_cloud_ball_pivoting(pcd, o3d.utility.DoubleVector(mesh_radii)) | |
| # Mesh using Poisson | |
| # mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(pcd, depth=9) | |
| # Export as OBJ | |
| mesh_output_path = os.path.join(tempfile.gettempdir(), "mesh_output.obj") | |
| o3d.io.write_triangle_mesh(mesh_output_path, mesh, write_ascii=True) | |
| # Render image preview | |
| verts = np.asarray(mesh.vertices) | |
| tris = np.asarray(mesh.triangles) | |
| fig = plt.figure(figsize=(6, 6)) | |
| ax = fig.add_subplot(111, projection='3d') | |
| ax.plot_trisurf(verts[:, 0], verts[:, 1], verts[:, 2], triangles=tris, color='lightblue', edgecolor='gray', linewidth=0.1) | |
| ax.axis('off') | |
| plt.tight_layout() | |
| image_path = os.path.join(tempfile.gettempdir(), "mesh_render.png") | |
| plt.savefig(image_path, dpi=150) | |
| plt.close() | |
| img_np = plt.imread(image_path) | |
| return img_np, mesh_output_path | |
| except Exception as e: | |
| traceback.print_exc() | |
| return np.zeros((200, 400, 3), dtype=np.uint8), None | |
| iface_mesh = gr.Interface( | |
| fn=meshing, | |
| inputs=[ | |
| gr.File(file_types=[".txt", ".ply", ".pcd"], label="Upload Point Cloud"), | |
| gr.Slider(1.0, 10.0, value=2.0, step=0.1, label="Normal Radius (Hybrid Search)"), | |
| gr.Slider(1, 100, value=30, step=1, label="Max NN for Normals"), | |
| gr.Slider(1, 100, value=30, step=1, label="K for Consistent Normal Orientation"), | |
| gr.Slider(0.1, 10.0, value=1.5, step=0.1, label="Ball Pivoting Radius Min"), | |
| gr.Slider(0.1, 10.0, value=3.5, step=0.1, label="Ball Pivoting Radius Max"), | |
| ], | |
| outputs=[ | |
| gr.Image(type="numpy", label="Mesh Preview"), | |
| gr.File(label="Download Mesh (STL)") | |
| ], | |
| title="Point Cloud Meshing to STL", | |
| description="Upload a point cloud (.txt, .ply, .pcd). The app creates a triangle mesh and exports it as an STL file." | |
| ) | |
| if __name__ == "__main__": | |
| if DEBUG: | |
| test_file_path = "./samples/Panel_pca_by20_space.txt" | |
| with open(test_file_path, "rb") as f: | |
| dummy = SimpleNamespace() | |
| dummy.name = os.path.basename(test_file_path) | |
| dummy.dir = os.path.dirname(test_file_path) | |
| dummy.read = lambda: f.read() | |
| # result_img = process_point_cloud(dummy) | |
| result_img, result_mesh = meshing(dummy) | |
| out_path = os.path.join("temporal", "debug_render.png") | |
| plt.imsave(out_path, result_img) | |
| print(f"[DEBUG] Rendered image saved at: {out_path}") | |
| else: | |
| # iface.launch() | |
| iface_mesh.launch() | |