MyoLab-infra commited on
Commit
4f1c726
Β·
verified Β·
1 Parent(s): 07cd008

Upload 2 files

Browse files
Files changed (2) hide show
  1. app_c3d_retarget.py +647 -0
  2. requirements.txt +5 -0
app_c3d_retarget.py ADDED
@@ -0,0 +1,647 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Copyright (c) 2025 MyoLab, Inc. All Rights Reserved.
3
+
4
+ This software and associated documentation files (the "Software") are the intellectual property of MyoLab, Inc. Unauthorized copying, modification, distribution, or use of this code, in whole or in part, without express written permission from the copyright owner is strictly prohibited.
5
+
6
+
7
+ MyoSDK Retargeting App
8
+ """
9
+
10
+ import os
11
+ import tempfile
12
+ import time
13
+
14
+ import cv2
15
+ import gradio as gr
16
+ import numpy as np
17
+ import pandas as pd
18
+ import plotly.graph_objs as go
19
+ import torch
20
+ from metrabs_pytorch.scripts.run_video import run_metrabs_video
21
+ from myo_tools.mjs.marker.marker_api import get_marker_names
22
+ from myo_tools.utils.file_ops.dataframe_utils import from_array_to_dataframe
23
+ from myosdk import Client
24
+
25
+ PLOT_CONFIG = {
26
+ "plot_bgcolor": "#0f172a",
27
+ "paper_bgcolor": "#0f172a",
28
+ "font": {"color": "#e2e8f0", "family": "Inter, system-ui, sans-serif"},
29
+ "xaxis": {"gridcolor": "#1e293b", "linecolor": "#334155"},
30
+ "yaxis": {"gridcolor": "#1e293b", "linecolor": "#334155"},
31
+ }
32
+
33
+ custom_css = """
34
+ .upload-box {
35
+ border: 2px dashed #ccc;
36
+ border-radius: 8px;
37
+ padding: 30px;
38
+ text-align: center;
39
+ cursor: pointer;
40
+ }
41
+ .upload-box:hover {
42
+ border-color: #666;
43
+ }
44
+ #file-upload {
45
+ position: absolute !important;
46
+ opacity: 0 !important;
47
+ pointer-events: none !important;
48
+ height: 0 !important;
49
+ }
50
+ """
51
+
52
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
53
+
54
+
55
+ def draw_keypoints(frame, poses2d, radius=10):
56
+ """
57
+ frame: HxWx3 uint8
58
+ poses2d: NxJx2 (N people, J joints)
59
+ """
60
+ for person in poses2d:
61
+ for x, y in person:
62
+ cv2.circle(frame, (int(x), int(y)), radius, (0, 255, 0), -1)
63
+ return frame
64
+
65
+
66
+ def save_video_with_keypoints(results, output_video):
67
+ fourcc = cv2.VideoWriter_fourcc(*"avc1")
68
+ out = cv2.VideoWriter(
69
+ output_video,
70
+ fourcc,
71
+ results[0]["fps"],
72
+ (results[0]["frame_bgr"].shape[1], results[0]["frame_bgr"].shape[0]),
73
+ )
74
+ for res in results:
75
+ frame = res["frame_bgr"]
76
+ fps = res["fps"]
77
+ poses2d = res["poses2d"] # NxJx2
78
+ frame = draw_keypoints(frame, poses2d)
79
+ out.write(frame)
80
+ out.release()
81
+ return output_video
82
+
83
+
84
+ def update_display(files, str_msg="πŸ“ Click to select XML files"):
85
+ if files is None:
86
+ return f'<div class="upload-box" onclick="document.querySelector(\'input[type=file]\').click()">{str_msg}</div>'
87
+
88
+ # Handle single file (not a list) or empty list
89
+ if not isinstance(files, list):
90
+ files = [files]
91
+
92
+ if len(files) == 0:
93
+ return f'<div class="upload-box" onclick="document.querySelector(\'input[type=file]\').click()">{str_msg}</div>'
94
+
95
+ # Handle both file objects (with .name attribute) and strings (file paths)
96
+ filenames = []
97
+ for f in files:
98
+ if isinstance(f, str):
99
+ # If it's a string, extract filename from path
100
+ filenames.append(f.split("/")[-1])
101
+ elif hasattr(f, "name"):
102
+ # If it's a file object with .name attribute
103
+ filenames.append(f.name.split("/")[-1])
104
+ else:
105
+ # Fallback: convert to string and extract filename
106
+ filenames.append(str(f).split("/")[-1])
107
+
108
+ file_list = "<br>".join([f"βœ“ {name}" for name in filenames])
109
+ return f'<div class="upload-box" onclick="document.querySelector(\'input[type=file]\').click()">{file_list}<br><br>Click to reselect</div>'
110
+
111
+
112
+ def load_all_videos():
113
+ video_dir = os.path.join(os.path.dirname(__file__), "./data")
114
+ return [
115
+ os.path.abspath(os.path.join(video_dir, f))
116
+ for f in os.listdir(video_dir)
117
+ if f.lower().endswith((".mp4", ".avi", ".mov", ".mkv"))
118
+ ]
119
+
120
+
121
+ # ------------------------------------------------------------
122
+ # Retargeting
123
+ # ------------------------------------------------------------
124
+ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
125
+ status = []
126
+ output_files = []
127
+
128
+ # Initial validation
129
+ if not api_key:
130
+ api_key = os.getenv("MYOSDK_API_KEY")
131
+ if not api_key:
132
+ yield (
133
+ "❌ Error: API key missing",
134
+ None,
135
+ None,
136
+ gr.update(value=[], visible=True),
137
+ gr.update(visible=False),
138
+ )
139
+
140
+ if markerset_file is None:
141
+ yield (
142
+ "❌ Error: Markerset XML file is required",
143
+ None,
144
+ None,
145
+ gr.update(value=[], visible=True),
146
+ gr.update(visible=False),
147
+ )
148
+
149
+ try:
150
+ # Initialize client
151
+ status.append("πŸ”Ή Initializing MyoSDK client...")
152
+ init_time = time.time()
153
+ yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
154
+ visible=False
155
+ )
156
+ client = Client(api_key=api_key)
157
+
158
+ status.append(
159
+ f"πŸ”Ή MyoSDK client initialized in { time.time() - init_time:.2f} seconds"
160
+ )
161
+ init_time = time.time()
162
+ # Upload markerset
163
+ status.append("πŸ”Ή Uploading markerset file...")
164
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
165
+ visible=False
166
+ )
167
+ mk_asset = client.assets.upload_file(markerset_file.name)
168
+
169
+ status.append(
170
+ f"πŸ”Ή Markerset file uploaded in {time.time() - init_time:.2f} seconds"
171
+ )
172
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
173
+ visible=False
174
+ )
175
+
176
+ mk_id = mk_asset["asset_id"]
177
+
178
+ # Process each C3D file
179
+ total_files = len(c3d_files)
180
+ for idx, f in enumerate(c3d_files):
181
+ status.append(
182
+ f"➑ Processing file {idx + 1}/{total_files}: {os.path.basename(f)}"
183
+ )
184
+
185
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
186
+ visible=False
187
+ )
188
+ init_time = time.time()
189
+ c3d_asset = client.assets.upload_file(f)
190
+ status.append(
191
+ f"\tπŸ”Ή C3D file uploaded in {time.time() - init_time:.2f} seconds"
192
+ )
193
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
194
+ visible=False
195
+ )
196
+ init_time = time.time()
197
+ job = client.jobs.start_retarget(
198
+ c3d_asset_id=c3d_asset["asset_id"],
199
+ markerset_asset_id=mk_id,
200
+ )
201
+
202
+ status.append(
203
+ f"\tπŸ”Ή Retargeting job started in {time.time() - init_time:.2f} seconds"
204
+ )
205
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
206
+ visible=False
207
+ )
208
+ init_time = time.time()
209
+ result = client.jobs.wait(job["job_id"])
210
+
211
+ status.append(
212
+ f"\tπŸ”Ή Retargeting job completed in {time.time() - init_time:.2f} seconds"
213
+ )
214
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
215
+ visible=False
216
+ )
217
+ if result["status"] != "SUCCEEDED":
218
+ status.append(f"\t❌ Failed retarget for {os.path.basename(f)}")
219
+ continue
220
+
221
+ status.append(f"\tβœ… Retargeting completed for {os.path.basename(f)}")
222
+ base = os.path.splitext(os.path.basename(f))[0]
223
+ out_path = os.path.join(tempfile.gettempdir(), base + ".npy")
224
+ client.assets.download(
225
+ result["output"]["retarget_output_asset_id"], out_path
226
+ )
227
+ output_files.append(out_path)
228
+
229
+ if not output_files:
230
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
231
+ visible=False
232
+ )
233
+
234
+ # Load angles from first output file
235
+ status.append("πŸ”Ή Loading angle data...")
236
+ yield "\n".join(status), None, None, gr.update(
237
+ interactive=True, visible=True
238
+ ), gr.update(visible=True)
239
+
240
+ data = np.load(output_files[0])
241
+ joint_angles = data["joint_angles_degrees"].squeeze()
242
+ joint_names = data["joint_names"]
243
+
244
+ df = pd.DataFrame(joint_angles, columns=[jn for jn in joint_names])
245
+ df.insert(0, "frame", df.index)
246
+
247
+ angle_list = list(df.columns[1:])
248
+ initial_value = [angle_list[0]] if angle_list else []
249
+
250
+ status.append("βœ… Complete!")
251
+ yield (
252
+ "\n".join(status),
253
+ gr.update(value=output_files, visible=True),
254
+ df,
255
+ gr.update(choices=angle_list, value=initial_value, visible=True),
256
+ gr.update(visible=True),
257
+ )
258
+
259
+ except Exception as e:
260
+ yield (
261
+ f"❌ {e}",
262
+ None,
263
+ None,
264
+ gr.update(visible=False),
265
+ gr.update(visible=False),
266
+ )
267
+
268
+
269
+ def run_retargeting_video(
270
+ api_key,
271
+ video_file="",
272
+ model="metrabs",
273
+ ):
274
+ status = []
275
+
276
+ # Initial validation
277
+ if not api_key:
278
+ api_key = os.getenv("MYOSDK_API_KEY")
279
+ if not api_key: # covers None, "", or other falsy values
280
+ yield (
281
+ "❌ Error: API key is missing or invalid",
282
+ None,
283
+ None,
284
+ gr.update(visible=False),
285
+ gr.update(visible=False),
286
+ None,
287
+ )
288
+ raise ValueError("❌ Error: API key is missing or invalid")
289
+
290
+ # Extract path from list if it's a list, otherwise use directly
291
+ if isinstance(video_file, list):
292
+ video_path = video_file[0] if len(video_file) > 0 else None
293
+ else:
294
+ video_path = video_file
295
+
296
+ if (
297
+ video_file is None
298
+ or (isinstance(video_file, list) and len(video_file) == 0)
299
+ or video_path is None
300
+ ):
301
+ yield (
302
+ "❌ Error: No video file selected",
303
+ None,
304
+ None,
305
+ gr.update(visible=False),
306
+ gr.update(visible=False),
307
+ video_file,
308
+ )
309
+ return
310
+
311
+ try:
312
+
313
+ print("πŸ”Ή Pose Extraction from Video Started")
314
+ status.append(
315
+ "πŸ”Ή Pose Extraction from Video Started ... this may take a while depending on the video length."
316
+ )
317
+ init_time = time.time()
318
+ yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
319
+ visible=False
320
+ ), video_path
321
+
322
+ results = list(
323
+ run_metrabs_video(video_path=video_path, device=DEVICE, visualize=False)
324
+ )
325
+
326
+ markers = np.array([res["poses3d"] for res in results]).squeeze()
327
+ fps = (
328
+ results[0]["fps"] if results else 25.0
329
+ ) # Default to 25 fps if not available
330
+
331
+ video_with_keypoints = os.path.join(
332
+ tempfile.gettempdir(), "video_with_keypoints.mp4"
333
+ )
334
+ save_video_with_keypoints(results, video_with_keypoints)
335
+
336
+ yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
337
+ visible=True
338
+ ), video_with_keypoints,
339
+ status.append(
340
+ f"πŸ”Ή Pose Extraction from Video Completed in {time.time() - init_time:.2f} seconds with {len(markers)} frames extracted ({((time.time() - init_time)/len(markers)):.2f} seconds per frame)"
341
+ )
342
+ print("πŸ”Ή Pose Extraction from Video Completed")
343
+ yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
344
+ visible=False
345
+ ), video_with_keypoints
346
+ # Initialize client
347
+ status.append("πŸ”Ή Initializing MyoSDK client...")
348
+ init_time = time.time()
349
+ yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
350
+ visible=False,
351
+ ), video_with_keypoints
352
+ client = Client(api_key=api_key)
353
+
354
+ status.append(
355
+ f"πŸ”Ή MyoSDK client initialized in { time.time() - init_time:.2f} seconds"
356
+ )
357
+ init_time = time.time()
358
+ # Upload markerset
359
+ status.append("πŸ”Ή Uploading markerset file...")
360
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
361
+ visible=False,
362
+ ), video_with_keypoints
363
+
364
+ markerset_file_name = "markersets/movi_metrabs_markerset.xml"
365
+
366
+ mk_asset = client.assets.upload_file(markerset_file_name)
367
+
368
+ status.append(
369
+ f"πŸ”Ή Markerset file uploaded in {time.time() - init_time:.2f} seconds"
370
+ )
371
+
372
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
373
+ visible=False
374
+ ), video_with_keypoints
375
+ init_time = time.time()
376
+
377
+ marker_names = get_marker_names(markerset_file_name)
378
+ fn_parquet = os.path.join(tempfile.gettempdir(), "video_trackers.parquet")
379
+ from_array_to_dataframe(markers, marker_names, fps, fn_parquet)
380
+ markers_asset = client.assets.upload_file(fn_parquet, purpose="retarget")
381
+
382
+ print("fn_parquet: ", fn_parquet)
383
+
384
+ init_time = time.time()
385
+ job = client.jobs.start_retarget(
386
+ c3d_asset_id=markers_asset["asset_id"],
387
+ markerset_asset_id=mk_asset["asset_id"],
388
+ )
389
+
390
+ status.append(
391
+ f"\tπŸ”Ή Retargeting job started in {time.time() - init_time:.2f} seconds"
392
+ )
393
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
394
+ visible=False
395
+ ), video_with_keypoints
396
+ init_time = time.time()
397
+ result = client.jobs.wait(job["job_id"])
398
+
399
+ status.append(
400
+ f"\tπŸ”Ή Retargeting job completed in {time.time() - init_time:.2f} seconds"
401
+ )
402
+ yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
403
+ visible=False
404
+ ), video_with_keypoints
405
+ print("STATUS: ", result["status"])
406
+ assert (
407
+ result["status"] == "SUCCEEDED"
408
+ ), f"Failed retarget for {os.path.basename(video_path)}"
409
+
410
+ base = os.path.splitext(os.path.basename(video_path))[0]
411
+ out_path = os.path.join(tempfile.gettempdir(), base + ".npy")
412
+ client.assets.download(result["output"]["retarget_output_asset_id"], out_path)
413
+
414
+ assert os.path.exists(
415
+ out_path
416
+ ), f"Failed to download retargeted data for {os.path.basename(video_path)}"
417
+
418
+ # Load angles from first output file
419
+ status.append("πŸ”Ή Loading angle data...")
420
+ yield "\n".join(status), None, None, gr.update(
421
+ interactive=True, visible=True
422
+ ), gr.update(visible=True), video_with_keypoints
423
+
424
+ data = np.load(out_path)
425
+ joint_angles = data["joint_angles_degrees"].squeeze()
426
+ joint_names = data["joint_names"]
427
+
428
+ df = pd.DataFrame(joint_angles, columns=[jn for jn in joint_names])
429
+ df.insert(0, "frame", df.index)
430
+
431
+ angle_list = list(df.columns[1:])
432
+ initial_value = [angle_list[0]] if angle_list else []
433
+
434
+ status.append("βœ… Complete!")
435
+ yield (
436
+ "\n".join(status),
437
+ gr.update(value=out_path, visible=True),
438
+ df,
439
+ gr.update(choices=angle_list, value=initial_value, visible=True),
440
+ gr.update(visible=True),
441
+ video_with_keypoints,
442
+ )
443
+
444
+ except Exception as e:
445
+ # Use video_path if defined, otherwise use video_file or None
446
+ error_video = (
447
+ video_path
448
+ if "video_path" in locals()
449
+ else (video_file if video_file else None)
450
+ )
451
+ yield (
452
+ "\n".join(status + ["\n❌ Error: " + str(e)]),
453
+ None,
454
+ None,
455
+ gr.update(visible=False),
456
+ gr.update(visible=False),
457
+ error_video,
458
+ )
459
+
460
+
461
+ # ------------------------------------------------------------
462
+ # Plotting
463
+ # ------------------------------------------------------------
464
+ def update_plot(df, joints):
465
+ if df is None or df.empty:
466
+ return go.Figure()
467
+ if not joints:
468
+ return go.Figure()
469
+ if not isinstance(joints, list):
470
+ joints = [joints]
471
+
472
+ fig = go.Figure()
473
+ for j in joints:
474
+ if j in df.columns:
475
+ fig.add_trace(go.Scatter(x=df["frame"], y=df[j], mode="lines", name=j))
476
+
477
+ fig.update_layout(
478
+ title="Joint Angles",
479
+ xaxis_title="Frame",
480
+ yaxis_title="Angle Value",
481
+ plot_bgcolor="#1E1E1E",
482
+ paper_bgcolor="#1E1E1E",
483
+ font=dict(color="#F0F0F0", family="Arial"),
484
+ xaxis=dict(gridcolor="#444444", linecolor="#F0F0F0", tickcolor="#F0F0F0"),
485
+ yaxis=dict(gridcolor="#444444", linecolor="#F0F0F0", tickcolor="#F0F0F0"),
486
+ legend=dict(font=dict(color="#F0F0F0")),
487
+ )
488
+ return fig
489
+
490
+
491
+ with gr.Blocks(css=custom_css) as app:
492
+
493
+ with gr.Row():
494
+ with gr.Column(scale=3):
495
+ gr.Markdown(
496
+ """
497
+ ## MyoSDK Retargeting
498
+ <span style="color:#6b7280">Joint visualization & motion retargeting pipelines</span>
499
+
500
+ This application allows you to retarget motion capture data to biomechanical models using MyoSDK's Kinesis engine.
501
+ Upload C3D files or videos to extract joint angles using [Kinesis](https://myolab.ai/blog/myokinesis) and visualize motion data.
502
+ """
503
+ )
504
+ with gr.Column(scale=1):
505
+ api_key = gr.Textbox(
506
+ label="πŸ”‘ API Key",
507
+ placeholder="Enter your MyoLab API key",
508
+ type="password",
509
+ info="Get your API key from https://dev.myolab.ai",
510
+ )
511
+ with gr.Tab("πŸ“Š Motion Capture Retargeting"):
512
+
513
+ gr.Markdown(
514
+ """
515
+ Upload motion capture data in C3D format along with a markerset XML file to retarget the motion to a biomechanical model.
516
+ The process will extract joint angles and generate visualizations of the motion data.
517
+ """
518
+ )
519
+ with gr.Row(equal_height=True):
520
+ with gr.Column(scale=2):
521
+ gr.Markdown(
522
+ """
523
+ **1. Upload a Markerset File**
524
+ <br>
525
+ <span style="color:#6b7280; font-size: 0.9em">
526
+ Upload an XML file that defines the marker set configuration.
527
+ This file specifies which markers are used and their anatomical locations.
528
+ See [Markerset Editor](https://markerset-editor.myolab.ai) for more details.
529
+ </span>
530
+ """
531
+ )
532
+ upload_markerset_display = gr.HTML(
533
+ '<div class="upload-box" onclick="document.querySelector(\'input[type=file]\').click()">πŸ“ Click to select XML file</div>'
534
+ )
535
+
536
+ markerset = gr.File(
537
+ label=None,
538
+ file_types=[".xml"],
539
+ elem_id="file-upload",
540
+ )
541
+
542
+ markerset.change(
543
+ lambda files: update_display(files, "πŸ“ Click to select XML file"),
544
+ [markerset],
545
+ upload_markerset_display,
546
+ )
547
+
548
+ with gr.Column(scale=2):
549
+ gr.Markdown(
550
+ """
551
+ **2. Upload C3D Motion Capture File(s)**
552
+ <br>
553
+ <span style="color:#6b7280; font-size: 0.9em">
554
+ Upload one or more C3D files containing 3D marker trajectories from motion capture systems.
555
+ Multiple files can be processed in batch. Each file will be retargeted using the same markerset.
556
+ </span>
557
+ """
558
+ )
559
+ upload_c3d_display = gr.HTML(
560
+ '<div class="upload-box" onclick="document.querySelector(\'input[type=file]\').click()">πŸ“ Click to select C3D files</div>'
561
+ )
562
+
563
+ c3d_files = gr.File(
564
+ label=None,
565
+ file_types=[".c3d"],
566
+ elem_id="file-upload",
567
+ file_count="multiple",
568
+ )
569
+
570
+ c3d_files.change(
571
+ lambda files: update_display(files, "πŸ“ Click to select C3D files"),
572
+ [c3d_files],
573
+ upload_c3d_display,
574
+ )
575
+ run_btn_c3d = gr.Button("3. πŸš€ Run Retargeting", variant="primary")
576
+
577
+ with gr.Tab("πŸŽ₯ Video-Based Motion Retargeting"):
578
+ gr.Markdown(
579
+ """
580
+ Extract 3D pose from video and retarget it to a biomechanical model using [Kinesis](https://myolab.ai/blog/myokinesis).
581
+
582
+ ⚠️ **Important:** Using Metrabs for video-based motion retargeting which is **ONLY FOR RESEARCH/ACADEMIC USE**.
583
+ Please cite the [paper](https://arxiv.org/abs/2409.06042) if you use this feature.
584
+ For commercial applications, please contact MyoLab.
585
+ """
586
+ )
587
+ video_file = gr.Video(
588
+ label="1. Upload a Video File (Supported formats: MP4, AVI, MOV, MKV)",
589
+ height=400,
590
+ value=os.path.join(
591
+ os.path.dirname(__file__), "./data/13710671_1080_1920_25fps.mp4"
592
+ ),
593
+ )
594
+ run_v2m_btn_video = gr.Button(
595
+ "2. πŸš€ Run Retargeting from Video", variant="primary"
596
+ )
597
+
598
+ output_file = gr.File(
599
+ label="πŸ“₯ Download Results - Download the retargeted motion data as a NumPy (.npy) file containing joint angles and metadata.",
600
+ visible=False,
601
+ )
602
+ df_state = gr.State()
603
+ joint_dropdown = gr.Dropdown(
604
+ label="Select Joint Angle(s) to Visualize",
605
+ interactive=False,
606
+ multiselect=True,
607
+ visible=True,
608
+ info="After processing completes, select one or more joint angles to plot. The dropdown will be populated with available joints from the retargeted data.",
609
+ )
610
+ plot_area = gr.Plot(
611
+ label="Joint Angle Visualization - Interactive plot showing the selected joint angles over time. Use the legend to toggle individual joints on/off.",
612
+ visible=False,
613
+ )
614
+ status_box = gr.Textbox(
615
+ label="Processing Status",
616
+ lines=12,
617
+ info="Real-time status updates showing the progress of file uploads, retargeting jobs, and data processing.",
618
+ )
619
+
620
+ joint_dropdown.change(
621
+ fn=update_plot,
622
+ inputs=[df_state, joint_dropdown],
623
+ outputs=[plot_area],
624
+ )
625
+ run_btn_c3d.click(
626
+ fn=run_retargeting_c3d,
627
+ inputs=[api_key, c3d_files, markerset],
628
+ outputs=[status_box, output_file, df_state, joint_dropdown, plot_area],
629
+ )
630
+ run_v2m_btn_video.click(
631
+ fn=run_retargeting_video,
632
+ inputs=[api_key, video_file],
633
+ outputs=[
634
+ status_box,
635
+ output_file,
636
+ df_state,
637
+ joint_dropdown,
638
+ plot_area,
639
+ video_file,
640
+ ],
641
+ )
642
+
643
+ if __name__ == "__main__":
644
+ app.launch(
645
+ share=True,
646
+ # server_port=7860,
647
+ )
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>6
2
+ plotly
3
+ myosdk
4
+ myo_tools
5
+ git+https://github.com/Vittorio-Caggiano/metrabs.git