ryanhlewis commited on
Commit
268d37c
·
verified ·
1 Parent(s): 9393005

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +196 -0
app.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import open3d as o3d
3
+ import numpy as np
4
+ import time
5
+ import multiprocessing
6
+ import os
7
+ import tempfile
8
+ import trimesh
9
+
10
+ alignment_cache = {}
11
+ current_orientation = "+z"
12
+
13
+ def compute_alignment(files):
14
+ """Align point clouds and cache results."""
15
+ ck = tuple(files)
16
+ if ck in alignment_cache: return alignment_cache[ck]
17
+ if len(files) < 2: return (None, None)
18
+ st = time.time()
19
+ print(f"Aligning {len(files)} point clouds using {multiprocessing.cpu_count()} CPU cores")
20
+ clouds, names = [], []
21
+ for p in files:
22
+ c = o3d.io.read_point_cloud(p)
23
+ if not c.has_points(): return (None, f"Failed to load point cloud from {os.path.basename(p)}")
24
+ clouds.append(c), names.append(os.path.basename(p))
25
+ ref = clouds[0]
26
+ trans = [np.eye(4)]
27
+ for i, src in enumerate(clouds[1:], 1):
28
+ vs = [0.5, 0.3, 0.1]
29
+ t = np.eye(4)
30
+ for v in vs:
31
+ s_down, r_down = src.voxel_down_sample(v), ref.voxel_down_sample(v)
32
+ rn = v * 2
33
+ s_down.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=rn, max_nn=30))
34
+ r_down.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=rn, max_nn=30))
35
+ d = v * 2
36
+ mi = 10 if i == 0 else 5
37
+ r = o3d.pipelines.registration.registration_icp(
38
+ s_down, r_down, d, t,
39
+ o3d.pipelines.registration.TransformationEstimationPointToPlane(),
40
+ o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=mi)
41
+ )
42
+ t = r.transformation
43
+ trans.append(t)
44
+ aligned = []
45
+ for c, t in zip(clouds, trans):
46
+ cc = o3d.geometry.PointCloud(c)
47
+ cc.transform(t)
48
+ aligned.append(cc)
49
+ ap = [np.asarray(x.points) for x in aligned]
50
+ if not ap: return (None, "Failed to process point clouds")
51
+ cat = np.vstack(ap)
52
+ mn, mx = np.min(cat, 0), np.max(cat, 0)
53
+ ctr = (mn + mx) / 2
54
+ scl = np.max(mx - mn)
55
+ combined = o3d.geometry.PointCloud()
56
+ for c in aligned: combined += c
57
+ tm = time.time() - st
58
+ msg = f"Successfully aligned {len(files)} point clouds in {tm:.2f} seconds."
59
+ data = {
60
+ 'aligned_clouds': aligned, 'transformations': trans, 'clouds': clouds,
61
+ 'filenames': names, 'global_center': ctr, 'global_scale': scl,
62
+ 'combined_cloud': combined
63
+ }
64
+ alignment_cache[ck] = (data, msg)
65
+ return data, msg
66
+
67
+ def visualize_alignment(files, show_colors=False, show_outlines=True, point_density=10000,
68
+ orientation="+z", point_size=0.005, is_initial_view=False):
69
+ if not files or len(files) < 2: return (None, "Please upload at least 2 point cloud files (.ply)")
70
+ res, msg = compute_alignment(files)
71
+ if not res: return (None, msg)
72
+ clouds = res['aligned_clouds']
73
+ ctr, scl = res['global_center'], res['global_scale']
74
+ cols = [[1,0,0],[0,1,0],[0,0,1],[1,1,0],[1,0,1],[0,1,1],[1,0.5,0]]
75
+ tdir = tempfile.mkdtemp()
76
+ global current_orientation
77
+ current_orientation = orientation
78
+ normed, boxes = [], []
79
+ for i, c in enumerate(clouds):
80
+ cp = o3d.geometry.PointCloud(c)
81
+ if show_colors: cp.paint_uniform_color(cols[i % len(cols)])
82
+ pts = np.asarray(cp.points)
83
+ pts = (pts - ctr)/(scl/2)
84
+ n = o3d.geometry.PointCloud()
85
+ n.points = o3d.utility.Vector3dVector(pts)
86
+ if cp.has_colors(): n.colors = cp.colors
87
+ normed.append(n)
88
+ if show_outlines:
89
+ b = n.get_axis_aligned_bounding_box()
90
+ b.color = cols[i % len(cols)]
91
+ boxes.append(b)
92
+ comb = o3d.geometry.PointCloud()
93
+ for c in normed: comb += c
94
+ if point_density < len(comb.points):
95
+ bb = comb.get_axis_aligned_bounding_box()
96
+ vol = np.prod(bb.get_extent())
97
+ ppu = point_density / vol if vol > 0 else 1
98
+ vs = max(0.001, (1/ppu)**(1/3))
99
+ comb = comb.voxel_down_sample(vs)
100
+ vs, fs, vc = [], [], []
101
+ pts = np.asarray(comb.points)
102
+ if comb.has_colors(): pcols = np.asarray(comb.colors)
103
+ else: pcols = np.tile([0.8,0.8,0.8], (len(pts),1))
104
+ sph = trimesh.creation.icosphere(subdivisions=1, radius=point_size)
105
+ mxp = min(point_density, len(pts))
106
+ idxs = np.linspace(0, len(pts)-1, mxp, dtype=int)
107
+ for i, idx in enumerate(idxs):
108
+ p = pts[idx]
109
+ c = pcols[idx]
110
+ s = sph.copy()
111
+ s.apply_translation(p)
112
+ s.visual.vertex_colors = np.tile((c*255).astype(np.uint8), (len(s.vertices),1))
113
+ si = len(vs)
114
+ vs.extend(s.vertices)
115
+ fs.extend(s.faces + si)
116
+ vc.extend(s.visual.vertex_colors)
117
+ cm = trimesh.Trimesh(vertices=vs, faces=fs, vertex_colors=vc)
118
+ if show_outlines:
119
+ box_edges = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),
120
+ (0,4),(1,5),(2,6),(3,7)]
121
+ for i,b in enumerate(boxes):
122
+ mb, xb = b.min_bound, b.max_bound
123
+ bv = [[mb[0],mb[1],mb[2]],[xb[0],mb[1],mb[2]],[xb[0],xb[1],mb[2]],[mb[0],xb[1],mb[2]],
124
+ [mb[0],mb[1],xb[2]],[xb[0],mb[1],xb[2]],[xb[0],xb[1],xb[2]],[mb[0],xb[1],xb[2]]]
125
+ bc = cols[i % len(cols)]
126
+ r = point_size*0.8
127
+ for s0,s1 in box_edges:
128
+ p0, p1 = bv[s0], bv[s1]
129
+ d = np.array(p1) - np.array(p0)
130
+ ln = np.linalg.norm(d)
131
+ if ln<1e-6: continue
132
+ d /= ln
133
+ za = np.array([0,0,1])
134
+ if abs(np.dot(d,za))>0.999: ra = np.array([1,0,0])
135
+ else:
136
+ ra = np.cross(za,d)
137
+ ra /= np.linalg.norm(ra)
138
+ ang = np.arccos(np.dot(za,d))
139
+ rot = trimesh.transformations.rotation_matrix(ang,ra)
140
+ trn = trimesh.transformations.translation_matrix(p0)
141
+ cyl = trimesh.creation.cylinder(radius=r,height=ln,sections=8)
142
+ cyl.apply_translation([0,0,ln/2])
143
+ cyl.apply_transform(rot)
144
+ cyl.apply_transform(trn)
145
+ cyl.visual.face_colors = (np.array(bc)*255).astype(np.uint8)
146
+ cm = trimesh.util.concatenate([cm,cyl])
147
+ sc = trimesh.Scene(cm)
148
+ sc.add_geometry(trimesh.creation.axis(origin_size=0.01, axis_radius=0.0025))
149
+ if orientation == "+y":
150
+ sc.apply_transform(trimesh.transformations.rotation_matrix(-np.pi/2,[1,0,0]))
151
+ elif orientation == "-y":
152
+ sc.apply_transform(trimesh.transformations.rotation_matrix(np.pi/2,[1,0,0]))
153
+ elif orientation == "-z":
154
+ sc.apply_transform(trimesh.transformations.rotation_matrix(np.pi,[1,0,0]))
155
+ fo = os.path.join(tdir,"aligned_scene.glb")
156
+ sc.export(fo)
157
+ return (fo, msg)
158
+
159
+ def process_upload(files, sc, so, pd, o, ps):
160
+ d, m = compute_alignment(files)
161
+ if d is None: return (None, m)
162
+ return visualize_alignment(files, sc, so, pd, o, ps, True)
163
+
164
+ def update_visualization_only(files, sc, so, pd, o, ps):
165
+ if not files or len(files)<2: return None
166
+ if tuple(files) not in alignment_cache: return None
167
+ mo, _ = visualize_alignment(files, sc, so, pd, o, ps, False)
168
+ return mo
169
+
170
+ with gr.Blocks(theme=gr.themes.Base()) as app:
171
+ gr.Markdown("# Point Cloud Alignment Tool - 3D ICP")
172
+ with gr.Row():
173
+ with gr.Column(scale=1):
174
+ file_input = gr.File(file_count="multiple", file_types=[".ply"], label="Upload Point Cloud Files (.ply)", type="filepath")
175
+ with gr.Row():
176
+ show_colors = gr.Checkbox(label="Show Colored Models", value=False)
177
+ show_outlines = gr.Checkbox(label="Show Bounding Box Outlines", value=True)
178
+ point_density = gr.Slider(1000, 500000, 10000, step=1000, label="Point Density (fewer points = faster rendering)")
179
+ orientation = gr.Dropdown(["+z","-z","+y","-y"], value="+z", label="Model Orientation (up direction)")
180
+ point_size = gr.Slider(0.001, 0.05, 0.005, step=0.001, label="Point Size")
181
+ submit_btn = gr.Button("Align Point Clouds", variant="primary")
182
+ with gr.Column(scale=2):
183
+ output_model = gr.Model3D(label="Aligned Point Clouds", clear_color=[0.1,0.1,0.1,1.0])
184
+ output_text = gr.Textbox(label="Status")
185
+ submit_btn.click(process_upload,
186
+ [file_input, show_colors, show_outlines, point_density, orientation, point_size],
187
+ [output_model, output_text])
188
+ show_colors.change(update_visualization_only, [file_input, show_colors, show_outlines, point_density, orientation, point_size], [output_model])
189
+ show_outlines.change(update_visualization_only, [file_input, show_colors, show_outlines, point_density, orientation, point_size], [output_model])
190
+ point_density.change(update_visualization_only, [file_input, show_colors, show_outlines, point_density, orientation, point_size], [output_model])
191
+ orientation.change(update_visualization_only, [file_input, show_colors, show_outlines, point_density, orientation, point_size], [output_model])
192
+ point_size.change(update_visualization_only, [file_input, show_colors, show_outlines, point_density, orientation, point_size], [output_model])
193
+
194
+ if __name__ == "__main__":
195
+ print(f"Using {multiprocessing.cpu_count()} CPU cores for parallel processing")
196
+ app.launch(share=True)