File size: 5,760 Bytes
0f772ca
 
 
 
 
ac2392f
0f772ca
 
dea5ada
 
 
 
0f772ca
 
dea5ada
 
 
 
 
 
0f772ca
 
 
 
 
 
 
 
 
 
2e34547
 
 
 
 
 
 
 
 
0f772ca
2e34547
0f772ca
 
 
 
 
 
 
 
 
 
ac2392f
 
 
 
 
 
 
 
 
 
087bb16
ac2392f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
087bb16
 
 
ac2392f
 
087bb16
 
ac2392f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
087bb16
 
 
 
 
 
 
 
ac2392f
 
 
 
 
 
 
 
0f772ca
dea5ada
ac2392f
dea5ada
 
 
ac2392f
dea5ada
ac2392f
 
dea5ada
 
 
 
ac2392f
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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()