duanbotu123 commited on
Commit
059bc71
Β·
verified Β·
1 Parent(s): c278d5c

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +666 -666
app.py CHANGED
@@ -1,666 +1,666 @@
1
- ο»Ώ# app.py
2
- import os
3
- import sys
4
- import glob
5
- import numpy as np
6
- import trimesh
7
- import plotly.graph_objs as go
8
- import gradio as gr
9
- from plyfile import PlyData
10
-
11
- # ------------------ Load pyregister module ------------------
12
- so_candidates = glob.glob("build/python/pyregister*.so")
13
- if not so_candidates:
14
- os.system("bash build.sh")
15
- so_candidates = glob.glob("build/python/pyregister*.so")
16
- if not so_candidates:
17
- raise FileNotFoundError("pyregister build failed, no .so found in build/python")
18
- sys.path.append("build/python")
19
-
20
- import pyregister
21
-
22
- # ------------------ Example data ------------------
23
- EXAMPLES = {
24
- "Rigid": {"target": "./examples/data/fricp/target.ply", "source": "./examples/data/fricp/source.ply"},
25
- "NonRigid": {"target": "./examples/data/spare/target.obj", "source": "./examples/data/spare/source.obj"},
26
- }
27
-
28
- FRICP_PARAS = {
29
- "useinit": False,
30
- "fileinit": "",
31
- "maxiter": 100,
32
- "stop": 1e-5
33
- }
34
-
35
- SPARE_PARAS = {
36
- "iters": 30,
37
- "stopcoarse": 1e-3,
38
- "stopfine": 1e-4,
39
- "use_landmark": False,
40
- "src": [],
41
- "tar": []
42
- }
43
-
44
- def load_example(example_type):
45
- target_path = EXAMPLES[example_type]["target"]
46
- source_path = EXAMPLES[example_type]["source"]
47
- method = example_type
48
- return source_path, target_path, method
49
-
50
- def read_mesh(file_path):
51
- ext = os.path.splitext(file_path)[1].lower()
52
- if ext == ".ply":
53
- plydata = PlyData.read(file_path)
54
- vertex = plydata["vertex"]
55
- vertices = np.stack([vertex["x"], vertex["y"], vertex["z"]], axis=-1)
56
- faces = None
57
- if "face" in plydata and len(plydata["face"].data) > 0:
58
- face_list = [f[0] for f in plydata["face"].data]
59
- if len(face_list) > 0:
60
- faces = np.array(face_list)
61
- if faces.ndim == 1:
62
- faces = faces.reshape(-1, 3)
63
- return vertices, faces
64
- elif ext == ".obj":
65
- mesh = trimesh.load(file_path, process=False)
66
- return np.asarray(mesh.vertices), (np.asarray(mesh.faces) if mesh.faces.size > 0 else None)
67
- else:
68
- raise ValueError(f"Unsupported file type: {ext}")
69
-
70
- def plot_source_target_mesh(target_file, source_file, alpha_target=1.0, alpha_source=1.0, scatter_mode=False):
71
- target_v, target_f = read_mesh(target_file)
72
- source_v, source_f = read_mesh(source_file)
73
-
74
- fig = go.Figure()
75
- lighting_opts = dict(ambient=0.5, diffuse=0.8, specular=0.6, roughness=0.25)
76
- light_pos = dict(x=100, y=200, z=50)
77
-
78
- target_color = "crimson"
79
-
80
- if scatter_mode:
81
-
82
- fig.add_trace(go.Scatter3d(
83
- x=target_v[:, 0], y=target_v[:, 1], z=target_v[:, 2],
84
- mode="markers",
85
-
86
- marker=dict(size=0.6, color=target_color, opacity=1.0),
87
- name="Target Points",
88
- hoverinfo="text",
89
- text=[
90
- f"<b><span style='color:black;'>Target ID:</span></b> {i}"
91
- f"<br><span style='font-size:12px;'>x={x:.3f}, y={y:.3f}, z={z:.3f}</span>"
92
- for i, (x, y, z) in enumerate(target_v)
93
- ],
94
- hoverlabel=dict(
95
- bgcolor="white",
96
- bordercolor="black",
97
- font=dict(color="black", size=13, family="Arial", weight="bold")
98
- )
99
- ))
100
-
101
- fig.add_trace(go.Scatter3d(
102
- x=source_v[:, 0], y=source_v[:, 1], z=source_v[:, 2],
103
- mode="markers",
104
-
105
- marker=dict(size=0.6, color="limegreen", opacity=1.0),
106
- name="Source Points",
107
- hoverinfo="text",
108
- text=[
109
- f"<b><span style='color:red;'>Source ID:</span></b> {i}"
110
- f"<br><span style='font-size:12px;'>x={x:.3f}, y={y:.3f}, z={z:.3f}</span>"
111
- for i, (x, y, z) in enumerate(source_v)
112
- ],
113
- hoverlabel=dict(
114
- bgcolor="white",
115
- bordercolor="limegreen",
116
- font=dict(color="red", size=13, family="Arial", weight="bold")
117
- )
118
- ))
119
-
120
- else:
121
-
122
- if target_f is not None:
123
- fig.add_trace(go.Mesh3d(
124
- x=target_v[:, 0], y=target_v[:, 1], z=target_v[:, 2],
125
- i=target_f[:, 0], j=target_f[:, 1], k=target_f[:, 2],
126
- color="khaki", opacity=alpha_target, name="Target Mesh",
127
- lighting=lighting_opts, lightposition=light_pos,
128
- hovertemplate="<b>Target Surface</b><br>x=%{x:.3f}<br>y=%{y:.3f}<br>z=%{z:.3f}<extra></extra>",
129
- ))
130
- else:
131
- fig.add_trace(go.Scatter3d(
132
- x=target_v[:, 0], y=target_v[:, 1], z=target_v[:, 2],
133
- mode="markers",
134
- marker=dict(size=0.6, color=target_color, opacity=alpha_target),
135
- name="Target Points"
136
- ))
137
-
138
- if source_f is not None:
139
- fig.add_trace(go.Mesh3d(
140
- x=source_v[:, 0], y=source_v[:, 1], z=source_v[:, 2],
141
- i=source_f[:, 0], j=source_f[:, 1], k=source_f[:, 2],
142
- color="darkseagreen", opacity=alpha_source, name="Source Mesh",
143
- lighting=lighting_opts, lightposition=light_pos,
144
- hovertemplate="<b>Source Surface</b><br>x=%{x:.3f}<br>y=%{y:.3f}<br>z=%{z:.3f}<extra></extra>",
145
- ))
146
- else:
147
- fig.add_trace(go.Scatter3d(
148
- x=source_v[:, 0], y=source_v[:, 1], z=source_v[:, 2],
149
- mode="markers",
150
- marker=dict(size=0.6, color="limegreen", opacity=alpha_source),
151
- name="Source Points"
152
- ))
153
-
154
- fig.update_layout(
155
- height=600, width=600, margin=dict(l=0, r=0, t=40, b=0),
156
- scene=dict(aspectmode="data"), showlegend=True
157
- )
158
- return fig
159
-
160
-
161
-
162
- def plot_result_target_mesh(target_file, result_file):
163
- target_v, target_f = read_mesh(target_file)
164
- result_v, result_f = read_mesh(result_file)
165
-
166
- fig = go.Figure()
167
- lighting_opts = dict(ambient=0.5, diffuse=0.8, specular=0.6, roughness=0.25)
168
- light_pos = dict(x=100, y=200, z=50)
169
-
170
- target_mesh_color = "khaki"
171
- result_mesh_color = "lightblue"
172
-
173
- if target_f is not None:
174
- fig.add_trace(go.Mesh3d(
175
- x=target_v[:, 0], y=target_v[:, 1], z=target_v[:, 2],
176
- i=target_f[:, 0], j=target_f[:, 1], k=target_f[:, 2],
177
- color=target_mesh_color, opacity=1.0, name="Target Mesh",
178
- lighting=lighting_opts, lightposition=light_pos
179
- ))
180
- else:
181
- fig.add_trace(go.Scatter3d(
182
- x=target_v[:, 0], y=target_v[:, 1], z=target_v[:, 2],
183
- mode="markers",
184
- marker=dict(size=0.6, color="crimson", opacity=1.0),
185
- name="Target Points (Red)"
186
- ))
187
-
188
- if result_f is not None:
189
- fig.add_trace(go.Mesh3d(
190
- x=result_v[:, 0], y=result_v[:, 1], z=result_v[:, 2],
191
- i=result_f[:, 0], j=result_f[:, 1], k=result_f[:, 2],
192
- color=result_mesh_color, opacity=1.0, name="Result Mesh",
193
- lighting=lighting_opts, lightposition=light_pos
194
- ))
195
- else:
196
- fig.add_trace(go.Scatter3d(
197
- x=result_v[:, 0], y=result_v[:, 1], z=result_v[:, 2],
198
- mode="markers",
199
- marker=dict(size=0.6, color="royalblue", opacity=1.0),
200
- name="Result Points (Blue)"
201
- ))
202
-
203
- fig.update_layout(height=600, width=600, margin=dict(l=0, r=0, t=40, b=0),
204
- scene=dict(aspectmode="data"), showlegend=True)
205
- return fig
206
-
207
- # ------------------ Registration ------------------
208
- def register_and_visualize_with_zip(target_file, source_file, output_dir, method,
209
- useinit=False, fileinit=None, maxiter=100, stop=1e-5,
210
- iters=30, stopcoarse=1e-3, stopfine=1e-4):
211
- if target_file is None or source_file is None:
212
- raise gr.Error("Please upload both target and source point cloud files first!")
213
-
214
- target_path = target_file.name
215
- source_path = source_file.name
216
- os.makedirs(output_dir, exist_ok=True)
217
-
218
- # === Rigid ===
219
- if method == "Rigid":
220
- reg = pyregister.RigidFricpRegistration()
221
- FRICP_PARAS["useinit"] = bool(useinit)
222
- FRICP_PARAS["fileinit"] = fileinit.name if useinit and fileinit else ""
223
- FRICP_PARAS["maxiter"] = int(maxiter)
224
- FRICP_PARAS["stop"] = float(stop)
225
-
226
- reg.Paras_init(FRICP_PARAS["useinit"],
227
- FRICP_PARAS["fileinit"],
228
- FRICP_PARAS["maxiter"],
229
- FRICP_PARAS["stop"])
230
- output_file = "FRICP_res.ply"
231
-
232
- # === NonRigid ===
233
- else:
234
- reg = pyregister.NonrigidSpareRegistration()
235
- SPARE_PARAS["iters"] = int(iters)
236
- SPARE_PARAS["stopcoarse"] = float(stopcoarse)
237
- SPARE_PARAS["stopfine"] = float(stopfine)
238
-
239
- reg.Paras_init(
240
- SPARE_PARAS["iters"],
241
- SPARE_PARAS["stopcoarse"],
242
- SPARE_PARAS["stopfine"],
243
- SPARE_PARAS["use_landmark"],
244
- SPARE_PARAS["src"],
245
- SPARE_PARAS["tar"]
246
- )
247
- output_file = "spare_res.ply"
248
-
249
-
250
- reg.Reg(target_path, source_path, output_dir)
251
-
252
- result_path = os.path.join(output_dir, output_file)
253
- fig_result = plot_result_target_mesh(target_path, result_path)
254
-
255
- import zipfile
256
- zip_path = os.path.join(output_dir, "results.zip")
257
- with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
258
- for f in os.listdir(output_dir):
259
- if f.endswith(".ply"):
260
- zipf.write(os.path.join(output_dir, f), arcname=f)
261
-
262
- return fig_result, zip_path
263
-
264
-
265
- # ------------------ Reregister ------------------
266
- def reuse_last_result_as_source(target_file, output_dir):
267
- if target_file is None:
268
- raise gr.Error("Please upload the target point cloud file first!")
269
- if not os.path.exists(output_dir):
270
- raise gr.Error("Output directory does not exist!")
271
-
272
- result_candidates = []
273
- for fname in ["FRICP_res.ply", "spare_res.ply"]:
274
- fpath = os.path.join(output_dir, fname)
275
- if os.path.exists(fpath):
276
- result_candidates.append(fpath)
277
- if not result_candidates:
278
- raise gr.Error("No previous registration result found. Please run registration first!")
279
-
280
- result_candidates.sort(key=os.path.getmtime, reverse=True)
281
- latest_result = result_candidates[0]
282
-
283
- fig = plot_source_target_mesh(target_file.name, latest_result)
284
- return latest_result, fig
285
-
286
- # ------------------ Utility ------------------
287
- def reset_parameters():
288
- """
289
- Reset all registration parameters (Rigid + NonRigid + Landmarks)
290
- and clear any manual inputs in the UI.
291
- This also resets the 'Use Landmarks' checkbox and hides the landmark UI group.
292
- """
293
-
294
- # --- Reset Rigid (FRICP) parameters ---
295
- FRICP_PARAS.update({
296
- "useinit": False,
297
- "fileinit": "",
298
- "maxiter": 100,
299
- "stop": 1e-5
300
- })
301
-
302
- # --- Reset NonRigid (Spare) parameters ---
303
- SPARE_PARAS.update({
304
- "iters": 30,
305
- "stopcoarse": 1e-3,
306
- "stopfine": 1e-4,
307
- "use_landmark": False,
308
- "src": [],
309
- "tar": []
310
- })
311
-
312
- print("[reset] All parameters reset.")
313
- print("[reset] FRICP_PARAS:", FRICP_PARAS)
314
- print("[reset] SPARE_PARAS:", SPARE_PARAS)
315
-
316
-
317
- return (
318
- FRICP_PARAS["useinit"], # Checkbox
319
- None, # File input reset
320
- FRICP_PARAS["maxiter"],
321
- FRICP_PARAS["stop"],
322
- SPARE_PARAS["iters"],
323
- SPARE_PARAS["stopcoarse"],
324
- SPARE_PARAS["stopfine"],
325
- gr.update(value=""),
326
- gr.update(value=""),
327
- gr.update(value=False),
328
- gr.update(visible=False)
329
- )
330
-
331
-
332
- def clear_all():
333
- """
334
- Full reset for all UI elements:
335
- - Clears uploaded files
336
- - Resets plots
337
- - Resets all parameters (Rigid + NonRigid + Landmark)
338
- - Resets landmark checkbox & hides group
339
- """
340
-
341
- FRICP_PARAS.update({
342
- "useinit": False,
343
- "fileinit": "",
344
- "maxiter": 100,
345
- "stop": 1e-5
346
- })
347
- SPARE_PARAS.update({
348
- "iters": 30,
349
- "stopcoarse": 1e-3,
350
- "stopfine": 1e-4,
351
- "use_landmark": False,
352
- "src": [],
353
- "tar": []
354
- })
355
-
356
- print("[clear] All files, parameters, and landmarks cleared.")
357
- return (
358
- None, # target_input
359
- None, # source_input
360
- None, # upload_plot
361
- None, # result_plot
362
- FRICP_PARAS["useinit"], # fricp_useinit
363
- None, # fricp_fileinit
364
- FRICP_PARAS["maxiter"],
365
- FRICP_PARAS["stop"],
366
- SPARE_PARAS["iters"],
367
- SPARE_PARAS["stopcoarse"],
368
- SPARE_PARAS["stopfine"],
369
- gr.update(value=""),
370
- gr.update(value=""),
371
- gr.update(value=False),
372
- gr.update(visible=False)
373
- )
374
-
375
-
376
- def visualize_and_store(target_file, source_file, scatter_mode=False):
377
- if target_file is None or source_file is None:
378
- return None, None, None
379
- target_v, _ = read_mesh(target_file.name)
380
- source_v, _ = read_mesh(source_file.name)
381
- fig = plot_source_target_mesh(target_file.name, source_file.name, scatter_mode=scatter_mode)
382
-
383
- return fig, source_v, target_v
384
-
385
- def clear_landmarks():
386
- return [], [], "", ""
387
- def highlight_landmarks_on_mesh(target_file, source_file, src_text, tar_text):
388
- src_v, _ = read_mesh(source_file.name)
389
- tar_v, _ = read_mesh(target_file.name)
390
- src_ids = [int(i) for i in src_text.split(",") if i.strip().isdigit()]
391
- tar_ids = [int(i) for i in tar_text.split(",") if i.strip().isdigit()]
392
-
393
-
394
- fig = plot_source_target_mesh(target_file.name, source_file.name)
395
-
396
- # === Source landmarks ===
397
- if len(src_ids) > 0:
398
- pts = src_v[src_ids]
399
- fig.add_trace(go.Scatter3d(
400
- x=pts[:, 0], y=pts[:, 1], z=pts[:, 2],
401
- mode="markers+text",
402
- text=[str(i) for i in src_ids],
403
- textposition="top center",
404
- textfont=dict(color="black", size=10, family="Arial"),
405
- marker=dict(
406
- size=6,
407
- color="limegreen",
408
- opacity=0.9,
409
- line=dict(width=2, color="white"),
410
- symbol="circle"
411
- ),
412
- name="Source Landmarks",
413
- hoverinfo="text",
414
- hovertext=[
415
- f"<b>Source ID:</b> {i}<br>x={x:.3f}, y={y:.3f}, z={z:.3f}"
416
- for i, (x, y, z) in zip(src_ids, pts)
417
- ]
418
- ))
419
-
420
- # === Target landmarks ===
421
- if len(tar_ids) > 0:
422
- pts = tar_v[tar_ids]
423
- fig.add_trace(go.Scatter3d(
424
- x=pts[:, 0], y=pts[:, 1], z=pts[:, 2],
425
- mode="markers+text",
426
- text=[str(i) for i in tar_ids],
427
- textposition="top center",
428
- textfont=dict(color="black", size=10, family="Arial"),
429
- marker=dict(
430
- size=6,
431
- color="crimson",
432
- opacity=0.9,
433
- line=dict(width=2, color="white"),
434
- symbol="circle"
435
- ),
436
- name="Target Landmarks",
437
- hoverinfo="text",
438
- hovertext=[
439
- f"<b>Target ID:</b> {i}<br>x={x:.3f}, y={y:.3f}, z={z:.3f}"
440
- for i, (x, y, z) in zip(tar_ids, pts)
441
- ]
442
- ))
443
-
444
-
445
- fig.update_layout(
446
- scene=dict(
447
- xaxis=dict(visible=False),
448
- yaxis=dict(visible=False),
449
- zaxis=dict(visible=False)
450
- ),
451
- legend=dict(bgcolor="rgba(255,255,255,0.6)"),
452
- margin=dict(l=0, r=0, t=40, b=0),
453
- title="Landmark Highlight View"
454
- )
455
-
456
- return fig
457
-
458
- def start_landmark_selection(target_file, source_file):
459
- if target_file is None or source_file is None:
460
- raise gr.Error("Please upload both Source and Target first!")
461
-
462
- fig = plot_source_target_mesh(target_file.name, source_file.name, scatter_mode=True)
463
- return fig
464
- def update_landmark_ids(src_text, tar_text):
465
- src_ids = [int(i) for i in src_text.split(",") if i.strip().isdigit()]
466
- tar_ids = [int(i) for i in tar_text.split(",") if i.strip().isdigit()]
467
- return src_ids, tar_ids, src_text, tar_text
468
-
469
-
470
- def set_landmarks_to_registration(src_ids, tar_ids):
471
- if not src_ids or not tar_ids:
472
- raise gr.Error("Please select both Source and Target points first!")
473
- if len(src_ids) != len(tar_ids):
474
- raise gr.Error(f"Landmark count mismatch: Source={len(src_ids)} vs Target={len(tar_ids)}")
475
-
476
- SPARE_PARAS["use_landmark"] = True
477
- SPARE_PARAS["src"] = [int(i) for i in src_ids]
478
- SPARE_PARAS["tar"] = [int(i) for i in tar_ids]
479
-
480
- return f"βœ… Landmarks staged: {len(src_ids)} Source ↔ {len(tar_ids)} Target. Will be used in NonRigid registration."
481
-
482
- # ------------------ UI ------------------
483
- BLUE_VIOLET_THEME = gr.themes.Soft(
484
- primary_hue="indigo",
485
- secondary_hue="violet",
486
- neutral_hue="neutral"
487
- )
488
-
489
- SECONDARY_BUTTON_CSS = """
490
- .gr-button-secondary {
491
- background: linear-gradient(135deg, #f8c6ff, #dca4ff) !important;
492
- color: #4a2560 !important;
493
- border: 1px solid rgba(206, 135, 255, 0.6) !important;
494
- box-shadow: 0 8px 20px rgba(206, 135, 255, 0.35);
495
- }
496
-
497
- .gr-button-secondary:hover {
498
- background: linear-gradient(135deg, #f2a9ff, #c589ff) !important;
499
- box-shadow: 0 12px 26px rgba(197, 137, 255, 0.45);
500
- }
501
- """
502
-
503
- with gr.Blocks(theme=BLUE_VIOLET_THEME, css=SECONDARY_BUTTON_CSS) as demo:
504
- gr.Markdown(
505
- """
506
- <div style="
507
- padding: 28px;
508
- border-radius: 22px;
509
- background: linear-gradient(135deg, rgba(79, 96, 255, 0.92), rgba(157, 120, 255, 0.9));
510
- color: #f8f9ff;
511
- box-shadow: 0 18px 48px rgba(64, 64, 180, 0.35);
512
- border: 1px solid rgba(255, 255, 255, 0.25);
513
- ">
514
- <h1 style="margin: 0 0 0.35rem; font-size: 2rem;">🐳 Point Cloud / Mesh Registration Studio πŸš€</h1>
515
- <p style="margin: 0; color: rgba(250, 250, 255, 0.85);">
516
- Align rigid & non-rigid scans, inspect meshes, and fine-tune landmarks with confidence.
517
- </p>
518
- </div>
519
- """
520
- )
521
-
522
- src_ids_state = gr.State([])
523
- tar_ids_state = gr.State([])
524
- src_vertices_state = gr.State(None)
525
- tar_vertices_state = gr.State(None)
526
-
527
- with gr.Row():
528
- with gr.Column(scale=1):
529
-
530
- source_input = gr.File(label="πŸ“₯ Source File", file_types=[".ply", ".obj"])
531
- target_input = gr.File(label="🎯 Target File", file_types=[".ply", ".obj"])
532
- method_dropdown = gr.Dropdown(label="🧠 Registration Method",
533
- choices=["Rigid", "NonRigid"], value="Rigid")
534
-
535
- with gr.Accordion("🧱 Rigid Parameters", open=False):
536
- fricp_useinit = gr.Checkbox(label="✨ Use initial transform?", value=False)
537
- fricp_fileinit = gr.File(label="πŸ“„ Initial transform file", file_types=[".txt"])
538
- fricp_maxiter = gr.Number(label="πŸ” Max iterations", value=100)
539
- fricp_stop = gr.Number(label="🎯 Stop threshold", value=1e-5)
540
-
541
-
542
- with gr.Accordion("πŸŒ€ NonRigid Parameters", open=False):
543
- spare_iters = gr.Number(label="πŸ” Iterations", value=30)
544
- spare_stopcoarse = gr.Number(label="🧊 Stop Coarse", value=1e-3)
545
- spare_stopfine = gr.Number(label="🎚️ Stop Fine", value=1e-4)
546
-
547
-
548
- use_landmark_cb = gr.Checkbox(label="πŸ“ Use Landmarks", value=False)
549
-
550
-
551
- with gr.Group(visible=False) as landmark_group:
552
- gr.Markdown("### πŸ“Έ Landmark Point Selection")
553
- selected_src = gr.Textbox(
554
- label="Source IDs πŸ“Œ",
555
- placeholder="e.g. 12,45,89", interactive=True)
556
- selected_tar = gr.Textbox(
557
- label="Target IDs 🎯",
558
- placeholder="e.g. 7,42,105", interactive=True)
559
-
560
- with gr.Row():
561
- start_landmark_btn = gr.Button("🎯 Start Landmark Selection", variant="primary")
562
- highlight_btn = gr.Button("✨ Highlight Landmarks", variant="secondary")
563
- clear_landmark_btn = gr.Button("🧼 Clear Selections", variant="secondary")
564
-
565
- select_button = gr.Button("βœ… Confirm Landmarks for Registration", variant="primary")
566
-
567
-
568
- output_dir = gr.Textbox(label="πŸ’Ύ Output Directory", value="./output/", placeholder="./output/")
569
- with gr.Row():
570
- run_button = gr.Button("πŸš€ Run Registration", variant="primary")
571
- rerun_button = gr.Button("πŸ”„ Reregister (Use Last Result as Source)", variant="secondary")
572
- clear_button = gr.Button("🧽 Clear Workspace", variant="primary")
573
- reset_button = gr.Button("♻️ Reset Parameters", variant="secondary")
574
-
575
- with gr.Row():
576
- example_dropdown = gr.Dropdown(label="✨ Load Example Data",
577
- choices=["Rigid", "NonRigid"], value="Rigid")
578
- example_button = gr.Button("🎲 Load Example", variant="primary")
579
-
580
- example_button.click(fn=load_example,
581
- inputs=[example_dropdown],
582
- outputs=[source_input, target_input, method_dropdown])
583
-
584
- with gr.Column(scale=2):
585
- upload_plot = gr.Plot(label="πŸ›°οΈ Source vs Target Mesh")
586
- result_plot = gr.Plot(label="🏁 Result vs Target Mesh")
587
- download_button = gr.File(label="⬇️ Download Result Directory",
588
- file_types=[".zip"], interactive=False)
589
-
590
- reset_button.click(
591
- fn=reset_parameters,
592
- inputs=None,
593
- outputs=[
594
- fricp_useinit, fricp_fileinit, fricp_maxiter, fricp_stop,
595
- spare_iters, spare_stopcoarse, spare_stopfine,
596
- selected_src, selected_tar,
597
- use_landmark_cb, landmark_group
598
- ]
599
- )
600
-
601
- def toggle_landmark_visibility(use_landmark):
602
- SPARE_PARAS["use_landmark"] = bool(use_landmark)
603
- print(f"[UI] use_landmark set to {SPARE_PARAS['use_landmark']}")
604
- return gr.update(visible=use_landmark)
605
-
606
- use_landmark_cb.change(fn=toggle_landmark_visibility,
607
- inputs=[use_landmark_cb],
608
- outputs=[landmark_group])
609
-
610
- clear_button.click(
611
- fn=clear_all,
612
- inputs=None,
613
- outputs=[
614
- target_input, source_input, upload_plot, result_plot,
615
- fricp_useinit, fricp_fileinit, fricp_maxiter, fricp_stop,
616
- spare_iters, spare_stopcoarse, spare_stopfine,
617
- selected_src, selected_tar,
618
- use_landmark_cb, landmark_group
619
- ]
620
- )
621
-
622
-
623
- target_input.change(fn=visualize_and_store,
624
- inputs=[target_input, source_input],
625
- outputs=[upload_plot, src_vertices_state, tar_vertices_state])
626
- source_input.change(fn=visualize_and_store,
627
- inputs=[target_input, source_input],
628
- outputs=[upload_plot, src_vertices_state, tar_vertices_state])
629
-
630
- start_landmark_btn.click(fn=start_landmark_selection,
631
- inputs=[target_input, source_input],
632
- outputs=[upload_plot])
633
-
634
- highlight_btn.click(fn=highlight_landmarks_on_mesh,
635
- inputs=[target_input, source_input, selected_src, selected_tar],
636
- outputs=[upload_plot])
637
-
638
- clear_landmark_btn.click(fn=clear_landmarks,
639
- inputs=None,
640
- outputs=[src_ids_state, tar_ids_state, selected_src, selected_tar])
641
-
642
- select_button.click(fn=update_landmark_ids,
643
- inputs=[selected_src, selected_tar],
644
- outputs=[src_ids_state, tar_ids_state, selected_src, selected_tar]
645
- ).then(fn=set_landmarks_to_registration,
646
- inputs=[src_ids_state, tar_ids_state],
647
- outputs=[gr.Textbox(label="Landmark Setup Info")])
648
-
649
- run_button.click(fn=register_and_visualize_with_zip,
650
- inputs=[target_input, source_input, output_dir, method_dropdown,
651
- fricp_useinit, fricp_fileinit, fricp_maxiter, fricp_stop,
652
- spare_iters, spare_stopcoarse, spare_stopfine],
653
- outputs=[result_plot, download_button])
654
-
655
- rerun_button.click(fn=reuse_last_result_as_source,
656
- inputs=[target_input, output_dir],
657
- outputs=[source_input, upload_plot])
658
-
659
- if __name__ == "__main__":
660
- demo.launch(server_name="0.0.0.0", server_port=7860)
661
-
662
-
663
-
664
-
665
-
666
-
 
1
+ ο»Ώ# app.py
2
+ import os
3
+ import sys
4
+ import glob
5
+ import numpy as np
6
+ import trimesh
7
+ import plotly.graph_objs as go
8
+ import gradio as gr
9
+ from plyfile import PlyData
10
+
11
+ # ------------------ Load pyregister module ------------------
12
+ so_candidates = glob.glob("build/python/pyregister*.so")
13
+ if not so_candidates:
14
+ os.system("bash build.sh")
15
+ so_candidates = glob.glob("build/python/pyregister*.so")
16
+ if not so_candidates:
17
+ raise FileNotFoundError("pyregister build failed, no .so found in build/python")
18
+ sys.path.append("build/python")
19
+
20
+ import pyregister
21
+
22
+ # ------------------ Example data ------------------
23
+ EXAMPLES = {
24
+ "Rigid": {"target": "./examples/data/fricp/target.ply", "source": "./examples/data/fricp/source.ply"},
25
+ "NonRigid": {"target": "./examples/data/spare/target.obj", "source": "./examples/data/spare/source.obj"},
26
+ }
27
+
28
+ FRICP_PARAS = {
29
+ "useinit": False,
30
+ "fileinit": "",
31
+ "maxiter": 100,
32
+ "stop": 1e-5
33
+ }
34
+
35
+ SPARE_PARAS = {
36
+ "iters": 30,
37
+ "stopcoarse": 1e-3,
38
+ "stopfine": 1e-4,
39
+ "use_landmark": False,
40
+ "src": [],
41
+ "tar": []
42
+ }
43
+
44
+ def load_example(example_type):
45
+ target_path = EXAMPLES[example_type]["target"]
46
+ source_path = EXAMPLES[example_type]["source"]
47
+ method = example_type
48
+ return source_path, target_path, method
49
+
50
+ def read_mesh(file_path):
51
+ ext = os.path.splitext(file_path)[1].lower()
52
+ if ext == ".ply":
53
+ plydata = PlyData.read(file_path)
54
+ vertex = plydata["vertex"]
55
+ vertices = np.stack([vertex["x"], vertex["y"], vertex["z"]], axis=-1)
56
+ faces = None
57
+ if "face" in plydata and len(plydata["face"].data) > 0:
58
+ face_list = [f[0] for f in plydata["face"].data]
59
+ if len(face_list) > 0:
60
+ faces = np.array(face_list)
61
+ if faces.ndim == 1:
62
+ faces = faces.reshape(-1, 3)
63
+ return vertices, faces
64
+ elif ext == ".obj":
65
+ mesh = trimesh.load(file_path, process=False)
66
+ return np.asarray(mesh.vertices), (np.asarray(mesh.faces) if mesh.faces.size > 0 else None)
67
+ else:
68
+ raise ValueError(f"Unsupported file type: {ext}")
69
+
70
+ def plot_source_target_mesh(target_file, source_file, alpha_target=1.0, alpha_source=1.0, scatter_mode=False):
71
+ target_v, target_f = read_mesh(target_file)
72
+ source_v, source_f = read_mesh(source_file)
73
+
74
+ fig = go.Figure()
75
+ lighting_opts = dict(ambient=0.5, diffuse=0.8, specular=0.6, roughness=0.25)
76
+ light_pos = dict(x=100, y=200, z=50)
77
+
78
+ target_color = "crimson"
79
+
80
+ if scatter_mode:
81
+
82
+ fig.add_trace(go.Scatter3d(
83
+ x=target_v[:, 0], y=target_v[:, 1], z=target_v[:, 2],
84
+ mode="markers",
85
+
86
+ marker=dict(size=0.6, color=target_color, opacity=1.0),
87
+ name="Target Points",
88
+ hoverinfo="text",
89
+ text=[
90
+ f"<b><span style='color:black;'>Target ID:</span></b> {i}"
91
+ f"<br><span style='font-size:12px;'>x={x:.3f}, y={y:.3f}, z={z:.3f}</span>"
92
+ for i, (x, y, z) in enumerate(target_v)
93
+ ],
94
+ hoverlabel=dict(
95
+ bgcolor="white",
96
+ bordercolor="black",
97
+ font=dict(color="black", size=13, family="Arial", weight="bold")
98
+ )
99
+ ))
100
+
101
+ fig.add_trace(go.Scatter3d(
102
+ x=source_v[:, 0], y=source_v[:, 1], z=source_v[:, 2],
103
+ mode="markers",
104
+
105
+ marker=dict(size=0.6, color="limegreen", opacity=1.0),
106
+ name="Source Points",
107
+ hoverinfo="text",
108
+ text=[
109
+ f"<b><span style='color:red;'>Source ID:</span></b> {i}"
110
+ f"<br><span style='font-size:12px;'>x={x:.3f}, y={y:.3f}, z={z:.3f}</span>"
111
+ for i, (x, y, z) in enumerate(source_v)
112
+ ],
113
+ hoverlabel=dict(
114
+ bgcolor="white",
115
+ bordercolor="limegreen",
116
+ font=dict(color="red", size=13, family="Arial", weight="bold")
117
+ )
118
+ ))
119
+
120
+ else:
121
+
122
+ if target_f is not None:
123
+ fig.add_trace(go.Mesh3d(
124
+ x=target_v[:, 0], y=target_v[:, 1], z=target_v[:, 2],
125
+ i=target_f[:, 0], j=target_f[:, 1], k=target_f[:, 2],
126
+ color="khaki", opacity=alpha_target, name="Target Mesh",
127
+ lighting=lighting_opts, lightposition=light_pos,
128
+ hovertemplate="<b>Target Surface</b><br>x=%{x:.3f}<br>y=%{y:.3f}<br>z=%{z:.3f}<extra></extra>",
129
+ ))
130
+ else:
131
+ fig.add_trace(go.Scatter3d(
132
+ x=target_v[:, 0], y=target_v[:, 1], z=target_v[:, 2],
133
+ mode="markers",
134
+ marker=dict(size=0.6, color=target_color, opacity=alpha_target),
135
+ name="Target Points"
136
+ ))
137
+
138
+ if source_f is not None:
139
+ fig.add_trace(go.Mesh3d(
140
+ x=source_v[:, 0], y=source_v[:, 1], z=source_v[:, 2],
141
+ i=source_f[:, 0], j=source_f[:, 1], k=source_f[:, 2],
142
+ color="darkseagreen", opacity=alpha_source, name="Source Mesh",
143
+ lighting=lighting_opts, lightposition=light_pos,
144
+ hovertemplate="<b>Source Surface</b><br>x=%{x:.3f}<br>y=%{y:.3f}<br>z=%{z:.3f}<extra></extra>",
145
+ ))
146
+ else:
147
+ fig.add_trace(go.Scatter3d(
148
+ x=source_v[:, 0], y=source_v[:, 1], z=source_v[:, 2],
149
+ mode="markers",
150
+ marker=dict(size=0.6, color="limegreen", opacity=alpha_source),
151
+ name="Source Points"
152
+ ))
153
+
154
+ fig.update_layout(
155
+ height=600, width=600, margin=dict(l=0, r=0, t=40, b=0),
156
+ scene=dict(aspectmode="data"), showlegend=True
157
+ )
158
+ return fig
159
+
160
+
161
+
162
+ def plot_result_target_mesh(target_file, result_file):
163
+ target_v, target_f = read_mesh(target_file)
164
+ result_v, result_f = read_mesh(result_file)
165
+
166
+ fig = go.Figure()
167
+ lighting_opts = dict(ambient=0.5, diffuse=0.8, specular=0.6, roughness=0.25)
168
+ light_pos = dict(x=100, y=200, z=50)
169
+
170
+ target_mesh_color = "khaki"
171
+ result_mesh_color = "lightblue"
172
+
173
+ if target_f is not None:
174
+ fig.add_trace(go.Mesh3d(
175
+ x=target_v[:, 0], y=target_v[:, 1], z=target_v[:, 2],
176
+ i=target_f[:, 0], j=target_f[:, 1], k=target_f[:, 2],
177
+ color=target_mesh_color, opacity=1.0, name="Target Mesh",
178
+ lighting=lighting_opts, lightposition=light_pos
179
+ ))
180
+ else:
181
+ fig.add_trace(go.Scatter3d(
182
+ x=target_v[:, 0], y=target_v[:, 1], z=target_v[:, 2],
183
+ mode="markers",
184
+ marker=dict(size=0.6, color="crimson", opacity=1.0),
185
+ name="Target Points (Red)"
186
+ ))
187
+
188
+ if result_f is not None:
189
+ fig.add_trace(go.Mesh3d(
190
+ x=result_v[:, 0], y=result_v[:, 1], z=result_v[:, 2],
191
+ i=result_f[:, 0], j=result_f[:, 1], k=result_f[:, 2],
192
+ color=result_mesh_color, opacity=1.0, name="Result Mesh",
193
+ lighting=lighting_opts, lightposition=light_pos
194
+ ))
195
+ else:
196
+ fig.add_trace(go.Scatter3d(
197
+ x=result_v[:, 0], y=result_v[:, 1], z=result_v[:, 2],
198
+ mode="markers",
199
+ marker=dict(size=0.6, color="royalblue", opacity=1.0),
200
+ name="Result Points (Blue)"
201
+ ))
202
+
203
+ fig.update_layout(height=600, width=600, margin=dict(l=0, r=0, t=40, b=0),
204
+ scene=dict(aspectmode="data"), showlegend=True)
205
+ return fig
206
+
207
+ # ------------------ Registration ------------------
208
+ def register_and_visualize_with_zip(target_file, source_file, output_dir, method,
209
+ useinit=False, fileinit=None, maxiter=100, stop=1e-5,
210
+ iters=30, stopcoarse=1e-3, stopfine=1e-4):
211
+ if target_file is None or source_file is None:
212
+ raise gr.Error("Please upload both target and source point cloud files first!")
213
+
214
+ target_path = target_file.name
215
+ source_path = source_file.name
216
+ os.makedirs(output_dir, exist_ok=True)
217
+
218
+ # === Rigid ===
219
+ if method == "Rigid":
220
+ reg = pyregister.RigidFricpRegistration()
221
+ FRICP_PARAS["useinit"] = bool(useinit)
222
+ FRICP_PARAS["fileinit"] = fileinit.name if useinit and fileinit else ""
223
+ FRICP_PARAS["maxiter"] = int(maxiter)
224
+ FRICP_PARAS["stop"] = float(stop)
225
+
226
+ reg.Paras_init(FRICP_PARAS["useinit"],
227
+ FRICP_PARAS["fileinit"],
228
+ FRICP_PARAS["maxiter"],
229
+ FRICP_PARAS["stop"])
230
+ output_file = "FRICP_res.ply"
231
+
232
+ # === NonRigid ===
233
+ else:
234
+ reg = pyregister.NonrigidSpareRegistration()
235
+ SPARE_PARAS["iters"] = int(iters)
236
+ SPARE_PARAS["stopcoarse"] = float(stopcoarse)
237
+ SPARE_PARAS["stopfine"] = float(stopfine)
238
+
239
+ reg.Paras_init(
240
+ SPARE_PARAS["iters"],
241
+ SPARE_PARAS["stopcoarse"],
242
+ SPARE_PARAS["stopfine"],
243
+ SPARE_PARAS["use_landmark"],
244
+ SPARE_PARAS["src"],
245
+ SPARE_PARAS["tar"]
246
+ )
247
+ output_file = "spare_res.ply"
248
+
249
+
250
+ reg.Reg(target_path, source_path, output_dir)
251
+
252
+ result_path = os.path.join(output_dir, output_file)
253
+ fig_result = plot_result_target_mesh(target_path, result_path)
254
+
255
+ import zipfile
256
+ zip_path = os.path.join(output_dir, "results.zip")
257
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
258
+ for f in os.listdir(output_dir):
259
+ if f.endswith(".ply"):
260
+ zipf.write(os.path.join(output_dir, f), arcname=f)
261
+
262
+ return fig_result, zip_path
263
+
264
+
265
+ # ------------------ Reregister ------------------
266
+ def reuse_last_result_as_source(target_file, output_dir):
267
+ if target_file is None:
268
+ raise gr.Error("Please upload the target point cloud file first!")
269
+ if not os.path.exists(output_dir):
270
+ raise gr.Error("Output directory does not exist!")
271
+
272
+ result_candidates = []
273
+ for fname in ["FRICP_res.ply", "spare_res.ply"]:
274
+ fpath = os.path.join(output_dir, fname)
275
+ if os.path.exists(fpath):
276
+ result_candidates.append(fpath)
277
+ if not result_candidates:
278
+ raise gr.Error("No previous registration result found. Please run registration first!")
279
+
280
+ result_candidates.sort(key=os.path.getmtime, reverse=True)
281
+ latest_result = result_candidates[0]
282
+
283
+ fig = plot_source_target_mesh(target_file.name, latest_result)
284
+ return latest_result, fig
285
+
286
+ # ------------------ Utility ------------------
287
+ def reset_parameters():
288
+ """
289
+ Reset all registration parameters (Rigid + NonRigid + Landmarks)
290
+ and clear any manual inputs in the UI.
291
+ This also resets the 'Use Landmarks' checkbox and hides the landmark UI group.
292
+ """
293
+
294
+ # --- Reset Rigid (FRICP) parameters ---
295
+ FRICP_PARAS.update({
296
+ "useinit": False,
297
+ "fileinit": "",
298
+ "maxiter": 100,
299
+ "stop": 1e-5
300
+ })
301
+
302
+ # --- Reset NonRigid (Spare) parameters ---
303
+ SPARE_PARAS.update({
304
+ "iters": 30,
305
+ "stopcoarse": 1e-3,
306
+ "stopfine": 1e-4,
307
+ "use_landmark": False,
308
+ "src": [],
309
+ "tar": []
310
+ })
311
+
312
+ print("[reset] All parameters reset.")
313
+ print("[reset] FRICP_PARAS:", FRICP_PARAS)
314
+ print("[reset] SPARE_PARAS:", SPARE_PARAS)
315
+
316
+
317
+ return (
318
+ FRICP_PARAS["useinit"], # Checkbox
319
+ None, # File input reset
320
+ FRICP_PARAS["maxiter"],
321
+ FRICP_PARAS["stop"],
322
+ SPARE_PARAS["iters"],
323
+ SPARE_PARAS["stopcoarse"],
324
+ SPARE_PARAS["stopfine"],
325
+ gr.update(value=""),
326
+ gr.update(value=""),
327
+ gr.update(value=False),
328
+ gr.update(visible=False)
329
+ )
330
+
331
+
332
+ def clear_all():
333
+ """
334
+ Full reset for all UI elements:
335
+ - Clears uploaded files
336
+ - Resets plots
337
+ - Resets all parameters (Rigid + NonRigid + Landmark)
338
+ - Resets landmark checkbox & hides group
339
+ """
340
+
341
+ FRICP_PARAS.update({
342
+ "useinit": False,
343
+ "fileinit": "",
344
+ "maxiter": 100,
345
+ "stop": 1e-5
346
+ })
347
+ SPARE_PARAS.update({
348
+ "iters": 30,
349
+ "stopcoarse": 1e-3,
350
+ "stopfine": 1e-4,
351
+ "use_landmark": False,
352
+ "src": [],
353
+ "tar": []
354
+ })
355
+
356
+ print("[clear] All files, parameters, and landmarks cleared.")
357
+ return (
358
+ None, # target_input
359
+ None, # source_input
360
+ None, # upload_plot
361
+ None, # result_plot
362
+ FRICP_PARAS["useinit"], # fricp_useinit
363
+ None, # fricp_fileinit
364
+ FRICP_PARAS["maxiter"],
365
+ FRICP_PARAS["stop"],
366
+ SPARE_PARAS["iters"],
367
+ SPARE_PARAS["stopcoarse"],
368
+ SPARE_PARAS["stopfine"],
369
+ gr.update(value=""),
370
+ gr.update(value=""),
371
+ gr.update(value=False),
372
+ gr.update(visible=False)
373
+ )
374
+
375
+
376
+ def visualize_and_store(target_file, source_file, scatter_mode=False):
377
+ if target_file is None or source_file is None:
378
+ return None, None, None
379
+ target_v, _ = read_mesh(target_file.name)
380
+ source_v, _ = read_mesh(source_file.name)
381
+ fig = plot_source_target_mesh(target_file.name, source_file.name, scatter_mode=scatter_mode)
382
+
383
+ return fig, source_v, target_v
384
+
385
+ def clear_landmarks():
386
+ return [], [], "", ""
387
+ def highlight_landmarks_on_mesh(target_file, source_file, src_text, tar_text):
388
+ src_v, _ = read_mesh(source_file.name)
389
+ tar_v, _ = read_mesh(target_file.name)
390
+ src_ids = [int(i) for i in src_text.split(",") if i.strip().isdigit()]
391
+ tar_ids = [int(i) for i in tar_text.split(",") if i.strip().isdigit()]
392
+
393
+
394
+ fig = plot_source_target_mesh(target_file.name, source_file.name)
395
+
396
+ # === Source landmarks ===
397
+ if len(src_ids) > 0:
398
+ pts = src_v[src_ids]
399
+ fig.add_trace(go.Scatter3d(
400
+ x=pts[:, 0], y=pts[:, 1], z=pts[:, 2],
401
+ mode="markers+text",
402
+ text=[str(i) for i in src_ids],
403
+ textposition="top center",
404
+ textfont=dict(color="black", size=10, family="Arial"),
405
+ marker=dict(
406
+ size=6,
407
+ color="limegreen",
408
+ opacity=0.9,
409
+ line=dict(width=2, color="white"),
410
+ symbol="circle"
411
+ ),
412
+ name="Source Landmarks",
413
+ hoverinfo="text",
414
+ hovertext=[
415
+ f"<b>Source ID:</b> {i}<br>x={x:.3f}, y={y:.3f}, z={z:.3f}"
416
+ for i, (x, y, z) in zip(src_ids, pts)
417
+ ]
418
+ ))
419
+
420
+ # === Target landmarks ===
421
+ if len(tar_ids) > 0:
422
+ pts = tar_v[tar_ids]
423
+ fig.add_trace(go.Scatter3d(
424
+ x=pts[:, 0], y=pts[:, 1], z=pts[:, 2],
425
+ mode="markers+text",
426
+ text=[str(i) for i in tar_ids],
427
+ textposition="top center",
428
+ textfont=dict(color="black", size=10, family="Arial"),
429
+ marker=dict(
430
+ size=6,
431
+ color="crimson",
432
+ opacity=0.9,
433
+ line=dict(width=2, color="white"),
434
+ symbol="circle"
435
+ ),
436
+ name="Target Landmarks",
437
+ hoverinfo="text",
438
+ hovertext=[
439
+ f"<b>Target ID:</b> {i}<br>x={x:.3f}, y={y:.3f}, z={z:.3f}"
440
+ for i, (x, y, z) in zip(tar_ids, pts)
441
+ ]
442
+ ))
443
+
444
+
445
+ fig.update_layout(
446
+ scene=dict(
447
+ xaxis=dict(visible=False),
448
+ yaxis=dict(visible=False),
449
+ zaxis=dict(visible=False)
450
+ ),
451
+ legend=dict(bgcolor="rgba(255,255,255,0.6)"),
452
+ margin=dict(l=0, r=0, t=40, b=0),
453
+ title="Landmark Highlight View"
454
+ )
455
+
456
+ return fig
457
+
458
+ def start_landmark_selection(target_file, source_file):
459
+ if target_file is None or source_file is None:
460
+ raise gr.Error("Please upload both Source and Target first!")
461
+
462
+ fig = plot_source_target_mesh(target_file.name, source_file.name, scatter_mode=True)
463
+ return fig
464
+ def update_landmark_ids(src_text, tar_text):
465
+ src_ids = [int(i) for i in src_text.split(",") if i.strip().isdigit()]
466
+ tar_ids = [int(i) for i in tar_text.split(",") if i.strip().isdigit()]
467
+ return src_ids, tar_ids, src_text, tar_text
468
+
469
+
470
+ def set_landmarks_to_registration(src_ids, tar_ids):
471
+ if not src_ids or not tar_ids:
472
+ raise gr.Error("Please select both Source and Target points first!")
473
+ if len(src_ids) != len(tar_ids):
474
+ raise gr.Error(f"Landmark count mismatch: Source={len(src_ids)} vs Target={len(tar_ids)}")
475
+
476
+ SPARE_PARAS["use_landmark"] = True
477
+ SPARE_PARAS["src"] = [int(i) for i in src_ids]
478
+ SPARE_PARAS["tar"] = [int(i) for i in tar_ids]
479
+
480
+ return f"βœ… Landmarks staged: {len(src_ids)} Source ↔ {len(tar_ids)} Target. Will be used in NonRigid registration."
481
+
482
+ # ------------------ UI ------------------
483
+ BLUE_VIOLET_THEME = gr.themes.Soft(
484
+ primary_hue="indigo",
485
+ secondary_hue="violet",
486
+ neutral_hue="neutral"
487
+ )
488
+
489
+ SECONDARY_BUTTON_CSS = """
490
+ .gr-button-secondary {
491
+ background: linear-gradient(135deg, #f8c6ff, #dca4ff) !important;
492
+ color: #4a2560 !important;
493
+ border: 1px solid rgba(206, 135, 255, 0.6) !important;
494
+ box-shadow: 0 8px 20px rgba(206, 135, 255, 0.35);
495
+ }
496
+
497
+ .gr-button-secondary:hover {
498
+ background: linear-gradient(135deg, #f2a9ff, #c589ff) !important;
499
+ box-shadow: 0 12px 26px rgba(197, 137, 255, 0.45);
500
+ }
501
+ """
502
+
503
+ with gr.Blocks(theme=BLUE_VIOLET_THEME, css=SECONDARY_BUTTON_CSS) as demo:
504
+ gr.Markdown(
505
+ """
506
+ <div style="
507
+ padding: 28px;
508
+ border-radius: 22px;
509
+ background: linear-gradient(135deg, rgba(79, 96, 255, 0.92), rgba(157, 120, 255, 0.9));
510
+ color: #f8f9ff;
511
+ box-shadow: 0 18px 48px rgba(64, 64, 180, 0.35);
512
+ border: 1px solid rgba(255, 255, 255, 0.25);
513
+ ">
514
+ <h1 style="margin: 0 0 0.35rem; font-size: 2rem;">🐳 Point Cloud / Mesh Registration Studio πŸš€</h1>
515
+ <p style="margin: 0; color: rgba(250, 250, 255, 0.85);">
516
+ Align rigid & non-rigid scans, inspect meshes, and fine-tune landmarks with confidence.
517
+ </p>
518
+ </div>
519
+ """
520
+ )
521
+
522
+ src_ids_state = gr.State([])
523
+ tar_ids_state = gr.State([])
524
+ src_vertices_state = gr.State(None)
525
+ tar_vertices_state = gr.State(None)
526
+
527
+ with gr.Row():
528
+ with gr.Column(scale=1):
529
+
530
+ source_input = gr.File(label="πŸ“₯ Source File", file_types=[".ply", ".obj"])
531
+ target_input = gr.File(label="🎯 Target File", file_types=[".ply", ".obj"])
532
+ method_dropdown = gr.Dropdown(label="🧠 Registration Method",
533
+ choices=["Rigid", "NonRigid"], value="Rigid")
534
+
535
+ with gr.Accordion("🧱 Rigid Parameters", open=False):
536
+ fricp_useinit = gr.Checkbox(label="✨ Use initial transform?", value=False)
537
+ fricp_fileinit = gr.File(label="πŸ“„ Initial transform file", file_types=[".txt"])
538
+ fricp_maxiter = gr.Number(label="πŸ” Max iterations", value=100)
539
+ fricp_stop = gr.Number(label="🎯 Stop threshold", value=1e-5)
540
+
541
+
542
+ with gr.Accordion("πŸŒ€ NonRigid Parameters", open=False):
543
+ spare_iters = gr.Number(label="πŸ” Iterations", value=30)
544
+ spare_stopcoarse = gr.Number(label="🧊 Stop Coarse", value=1e-3)
545
+ spare_stopfine = gr.Number(label="🎚️ Stop Fine", value=1e-4)
546
+
547
+
548
+ use_landmark_cb = gr.Checkbox(label="πŸ“ Use Landmarks", value=False)
549
+
550
+
551
+ with gr.Group(visible=False) as landmark_group:
552
+ gr.Markdown("### πŸ“Έ Landmark Point Selection")
553
+ selected_src = gr.Textbox(
554
+ label="Source IDs πŸ“Œ",
555
+ placeholder="e.g. 12,45,89", interactive=True)
556
+ selected_tar = gr.Textbox(
557
+ label="Target IDs 🎯",
558
+ placeholder="e.g. 7,42,105", interactive=True)
559
+
560
+ with gr.Row():
561
+ start_landmark_btn = gr.Button("🎯 Start Landmark Selection", variant="primary")
562
+ highlight_btn = gr.Button("✨ Highlight Landmarks", variant="secondary")
563
+ clear_landmark_btn = gr.Button("🧼 Clear Selections", variant="secondary")
564
+
565
+ select_button = gr.Button("βœ… Confirm Landmarks for Registration", variant="primary")
566
+
567
+
568
+ output_dir = gr.Textbox(label="πŸ’Ύ Output Directory", value="./output/", placeholder="./output/")
569
+ with gr.Row():
570
+ run_button = gr.Button("πŸš€ Run Registration", variant="primary")
571
+ rerun_button = gr.Button("πŸ”„ Reregister (Use Last Result as Source)", variant="secondary")
572
+ clear_button = gr.Button("🧽 Clear Workspace", variant="primary")
573
+ reset_button = gr.Button("♻️ Reset Parameters", variant="secondary")
574
+
575
+ with gr.Row():
576
+ example_dropdown = gr.Dropdown(label="✨ Load Example Data",
577
+ choices=["Rigid", "NonRigid"], value="Rigid")
578
+ example_button = gr.Button("🎲 Load Example", variant="primary")
579
+
580
+ example_button.click(fn=load_example,
581
+ inputs=[example_dropdown],
582
+ outputs=[source_input, target_input, method_dropdown])
583
+
584
+ with gr.Column(scale=2):
585
+ upload_plot = gr.Plot(label="πŸ›°οΈ Source vs Target Mesh")
586
+ result_plot = gr.Plot(label="🏁 Result vs Target Mesh")
587
+ download_button = gr.File(label="⬇️ Download Result Directory",
588
+ file_types=[".zip"], interactive=False)
589
+
590
+ reset_button.click(
591
+ fn=reset_parameters,
592
+ inputs=None,
593
+ outputs=[
594
+ fricp_useinit, fricp_fileinit, fricp_maxiter, fricp_stop,
595
+ spare_iters, spare_stopcoarse, spare_stopfine,
596
+ selected_src, selected_tar,
597
+ use_landmark_cb, landmark_group
598
+ ]
599
+ )
600
+
601
+ def toggle_landmark_visibility(use_landmark):
602
+ SPARE_PARAS["use_landmark"] = bool(use_landmark)
603
+ print(f"[UI] use_landmark set to {SPARE_PARAS['use_landmark']}")
604
+ return gr.update(visible=use_landmark)
605
+
606
+ use_landmark_cb.change(fn=toggle_landmark_visibility,
607
+ inputs=[use_landmark_cb],
608
+ outputs=[landmark_group])
609
+
610
+ clear_button.click(
611
+ fn=clear_all,
612
+ inputs=None,
613
+ outputs=[
614
+ target_input, source_input, upload_plot, result_plot,
615
+ fricp_useinit, fricp_fileinit, fricp_maxiter, fricp_stop,
616
+ spare_iters, spare_stopcoarse, spare_stopfine,
617
+ selected_src, selected_tar,
618
+ use_landmark_cb, landmark_group
619
+ ]
620
+ )
621
+
622
+
623
+ target_input.change(fn=visualize_and_store,
624
+ inputs=[target_input, source_input],
625
+ outputs=[upload_plot, src_vertices_state, tar_vertices_state])
626
+ source_input.change(fn=visualize_and_store,
627
+ inputs=[target_input, source_input],
628
+ outputs=[upload_plot, src_vertices_state, tar_vertices_state])
629
+
630
+ start_landmark_btn.click(fn=start_landmark_selection,
631
+ inputs=[target_input, source_input],
632
+ outputs=[upload_plot])
633
+
634
+ highlight_btn.click(fn=highlight_landmarks_on_mesh,
635
+ inputs=[target_input, source_input, selected_src, selected_tar],
636
+ outputs=[upload_plot])
637
+
638
+ clear_landmark_btn.click(fn=clear_landmarks,
639
+ inputs=None,
640
+ outputs=[src_ids_state, tar_ids_state, selected_src, selected_tar])
641
+
642
+ select_button.click(fn=update_landmark_ids,
643
+ inputs=[selected_src, selected_tar],
644
+ outputs=[src_ids_state, tar_ids_state, selected_src, selected_tar]
645
+ ).then(fn=set_landmarks_to_registration,
646
+ inputs=[src_ids_state, tar_ids_state],
647
+ outputs=[gr.Textbox(label="Landmark Setup Info")])
648
+
649
+ run_button.click(fn=register_and_visualize_with_zip,
650
+ inputs=[target_input, source_input, output_dir, method_dropdown,
651
+ fricp_useinit, fricp_fileinit, fricp_maxiter, fricp_stop,
652
+ spare_iters, spare_stopcoarse, spare_stopfine],
653
+ outputs=[result_plot, download_button])
654
+
655
+ rerun_button.click(fn=reuse_last_result_as_source,
656
+ inputs=[target_input, output_dir],
657
+ outputs=[source_input, upload_plot])
658
+
659
+ if __name__ == "__main__":
660
+ demo.launch(server_name="0.0.0.0", server_port=7860)
661
+
662
+
663
+
664
+
665
+
666
+