caggianov commited on
Commit
657f2fb
Β·
1 Parent(s): a2c76e0

update layout with embed4d viewer

Browse files
.gitattributes CHANGED
@@ -6,6 +6,7 @@
6
  *.c3d filter=lfs diff=lfs merge=lfs -text
7
  *.ftz filter=lfs diff=lfs merge=lfs -text
8
  *.gz filter=lfs diff=lfs merge=lfs -text
 
9
  *.h5 filter=lfs diff=lfs merge=lfs -text
10
  *.joblib filter=lfs diff=lfs merge=lfs -text
11
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
 
6
  *.c3d filter=lfs diff=lfs merge=lfs -text
7
  *.ftz filter=lfs diff=lfs merge=lfs -text
8
  *.gz filter=lfs diff=lfs merge=lfs -text
9
+ *.glb filter=lfs diff=lfs merge=lfs -text
10
  *.h5 filter=lfs diff=lfs merge=lfs -text
11
  *.joblib filter=lfs diff=lfs merge=lfs -text
12
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: Myosdk App
3
- emoji: 😻
4
  colorFrom: green
5
  colorTo: gray
6
  sdk: gradio
 
1
  ---
2
  title: Myosdk App
3
+ emoji: πŸƒ
4
  colorFrom: green
5
  colorTo: gray
6
  sdk: gradio
app.py CHANGED
@@ -17,10 +17,13 @@ import time
17
 
18
  import cv2
19
  import gradio as gr
 
20
  import numpy as np
 
21
  import plotly.graph_objs as go
22
  import spaces
23
  import torch
 
24
  from metrabs_pytorch.scripts.run_video import run_metrabs_video
25
  from myo_tools.mjs.marker.marker_api import get_marker_names
26
  from myo_tools.utils.file_ops.dataframe_utils import from_array_to_dataframe
@@ -38,6 +41,9 @@ PLOT_CONFIG = {
38
 
39
 
40
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
 
 
 
41
 
42
 
43
  def draw_keypoints(frame, poses2d, radius=10):
@@ -69,15 +75,6 @@ def save_video_with_keypoints(results, output_video):
69
  return output_video
70
 
71
 
72
- def load_all_videos():
73
- video_dir = os.path.join(os.path.dirname(__file__), "./data")
74
- return [
75
- os.path.abspath(os.path.join(video_dir, f))
76
- for f in os.listdir(video_dir)
77
- if f.lower().endswith((".mp4", ".avi", ".mov", ".mkv"))
78
- ]
79
-
80
-
81
  def create_info_text(title, info_text):
82
  return f"""
83
  <div>
@@ -157,7 +154,7 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
157
  None,
158
  gr.update(value=[], visible=True),
159
  gr.update(visible=False),
160
- gr.update(visible=False),
161
  )
162
  return
163
 
@@ -168,7 +165,7 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
168
  None,
169
  gr.update(value=[], visible=True),
170
  gr.update(visible=False),
171
- gr.update(visible=False),
172
  )
173
 
174
  try:
@@ -177,7 +174,7 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
177
  init_time = time.time()
178
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
179
  visible=False
180
- ), gr.update(visible=False)
181
  client = Client(api_key=api_key)
182
 
183
  status.append(
@@ -188,7 +185,7 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
188
  status.append("πŸ”Ή Uploading markerset file...")
189
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
190
  visible=False
191
- ), gr.update(visible=False)
192
  mk_asset = client.assets.upload_file(markerset_file.name)
193
 
194
  status.append(
@@ -196,7 +193,7 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
196
  )
197
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
198
  visible=False
199
- ), gr.update(visible=False)
200
 
201
  mk_id = mk_asset["asset_id"]
202
 
@@ -209,7 +206,7 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
209
 
210
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
211
  visible=False
212
- ), gr.update(visible=False)
213
 
214
  init_time = time.time()
215
  tracker_asset = client.assets.upload_file(f)
@@ -218,7 +215,7 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
218
  )
219
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
220
  visible=False
221
- ), gr.update(visible=False)
222
  init_time = time.time()
223
  job = client.jobs.start_retarget(
224
  tracker_asset_id=tracker_asset["asset_id"],
@@ -231,16 +228,16 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
231
  )
232
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
233
  visible=False
234
- ), gr.update(visible=False)
235
  init_time = time.time()
236
  result = client.jobs.wait(job["job_id"])
237
 
238
  status.append(
239
  f"\tπŸ”Ή Retargeting job completed in {time.time() - init_time:.2f} seconds"
240
- ), gr.update(visible=False),
241
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
242
  visible=False
243
- ), gr.update(visible=False)
244
  assert (
245
  result["status"] == "SUCCEEDED"
246
  ), f"Failed retarget for {os.path.basename(f)}"
@@ -271,21 +268,26 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
271
  time.sleep(0.1) # allow filesystem flush
272
  yield "\n".join(status), None, None, gr.update(
273
  interactive=True, visible=True
274
- ), gr.update(visible=True), gr.update(visible=True, value=output_glb_files[0])
275
-
 
 
276
  df = from_qpos_to_joint_angles(output_files[0])
277
 
278
  angle_list = list(df.columns[1:])
279
  initial_value = [angle_list[0]] if angle_list else []
280
-
281
  status.append("βœ… Complete!")
 
282
  yield (
283
  "\n".join(status),
284
  gr.update(value=output_files, visible=True),
285
- df,
286
- gr.update(choices=angle_list, value=initial_value, visible=True),
287
- gr.update(visible=True),
288
- gr.update(visible=True, value=output_glb_files[0]),
 
 
289
  )
290
 
291
  except Exception as e:
@@ -295,7 +297,7 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
295
  None,
296
  gr.update(visible=False),
297
  gr.update(visible=False),
298
- gr.update(visible=False),
299
  )
300
 
301
 
@@ -319,7 +321,7 @@ def run_retargeting_video(
319
  gr.update(visible=False),
320
  gr.update(visible=False),
321
  video_file,
322
- gr.update(visible=False),
323
  )
324
  return
325
 
@@ -341,7 +343,7 @@ def run_retargeting_video(
341
  gr.update(visible=False),
342
  gr.update(visible=False),
343
  video_file,
344
- gr.update(visible=False),
345
  )
346
  return
347
 
@@ -354,7 +356,7 @@ def run_retargeting_video(
354
  init_time = time.time()
355
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
356
  visible=False
357
- ), video_path, gr.update(visible=False)
358
 
359
  results = list(
360
  run_metrabs_video(video_path=video_path, device=DEVICE, visualize=False)
@@ -363,6 +365,8 @@ def run_retargeting_video(
363
  np.array([res["poses3d"] for res in results]).squeeze()
364
  )
365
 
 
 
366
  fps = (
367
  results[0]["fps"] if results else 25.0
368
  ) # Default to 25 fps if not available
@@ -374,20 +378,26 @@ def run_retargeting_video(
374
 
375
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
376
  visible=True
377
- ), video_with_keypoints, gr.update(visible=False)
 
 
378
  status.append(
379
  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)"
380
  )
381
  print("πŸ”Ή Pose Extraction from Video Completed")
382
- yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
383
  visible=False
384
- ), video_with_keypoints, gr.update(visible=False)
 
 
385
  # Initialize client
386
  status.append("πŸ”Ή Initializing MyoSDK client...")
387
  init_time = time.time()
388
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
389
  visible=False,
390
- ), video_with_keypoints, gr.update(visible=False)
 
 
391
  client = Client(api_key=api_key)
392
 
393
  status.append(
@@ -396,9 +406,11 @@ def run_retargeting_video(
396
  init_time = time.time()
397
  # Upload markerset
398
  status.append("πŸ”Ή Uploading markerset file...")
399
- yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
400
  visible=False,
401
- ), video_with_keypoints, gr.update(visible=False)
 
 
402
 
403
  markerset_file_name = "markersets/movi_metrabs_markerset.xml"
404
 
@@ -408,9 +420,11 @@ def run_retargeting_video(
408
  f"πŸ”Ή Markerset file uploaded in {time.time() - init_time:.2f} seconds"
409
  )
410
 
411
- yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
412
  visible=False
413
- ), video_with_keypoints, gr.update(visible=False)
 
 
414
  init_time = time.time()
415
 
416
  marker_names = get_marker_names(markerset_file_name)
@@ -440,16 +454,20 @@ def run_retargeting_video(
440
  )
441
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
442
  visible=False
443
- ), video_with_keypoints, gr.update(visible=False)
 
 
444
  init_time = time.time()
445
  result = client.jobs.wait(job["job_id"])
446
 
447
  status.append(
448
  f"\tπŸ”Ή Retargeting job completed in {time.time() - init_time:.2f} seconds"
449
  )
450
- yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
451
  visible=False
452
- ), video_with_keypoints, gr.update(visible=False)
 
 
453
 
454
  print("STATUS: ", result["status"])
455
  assert (
@@ -471,29 +489,36 @@ def run_retargeting_video(
471
  result["output"]["retarget_output_asset_ids"]["motion"], output_glb_path
472
  )
473
 
 
 
 
474
  # Load angles from first output file
475
  status.append("πŸ”Ή Loading animation and angle data...")
476
- yield "\n".join(status), None, None, gr.update(
477
- interactive=True, visible=True
478
- ), gr.update(visible=True), video_with_keypoints, gr.update(
479
- visible=True, value=output_glb_path
480
  )
481
-
482
  df = from_qpos_to_joint_angles(out_path)
483
 
484
  angle_list = list(df.columns[1:])
485
  initial_value = [angle_list[0]] if angle_list else []
486
-
487
  status.append("βœ… Complete!")
 
488
  yield (
489
  "\n".join(status),
490
  gr.update(value=out_path, visible=True),
491
- df,
492
- gr.update(choices=angle_list, value=initial_value, visible=True),
493
- gr.update(visible=True),
 
 
494
  video_with_keypoints,
495
- gr.update(visible=True, value=output_glb_path),
496
  )
 
497
 
498
  except Exception as e:
499
  # Use video_path if defined, otherwise use video_file or None
@@ -509,17 +534,41 @@ def run_retargeting_video(
509
  gr.update(visible=False),
510
  gr.update(visible=False),
511
  error_video,
 
512
  )
513
 
514
 
515
  # ------------------------------------------------------------
516
  # Plotting
517
  # ------------------------------------------------------------
518
- def update_plot(df, joints):
519
- if df is None or df.empty:
520
- return go.Figure()
 
 
521
  if not joints:
522
  return go.Figure()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  if not isinstance(joints, list):
524
  joints = [joints]
525
 
@@ -534,25 +583,28 @@ def update_plot(df, joints):
534
  yaxis_title="Angle Value (degrees)",
535
  plot_bgcolor="#1E1E1E",
536
  paper_bgcolor="#1E1E1E",
537
- font=dict(color="#F0F0F0", family="Arial"),
538
- xaxis=dict(gridcolor="#444444", linecolor="#F0F0F0", tickcolor="#F0F0F0"),
539
- yaxis=dict(gridcolor="#444444", linecolor="#F0F0F0", tickcolor="#F0F0F0"),
540
- legend=dict(font=dict(color="#F0F0F0")),
541
  )
 
542
  return fig
543
 
544
 
545
- with gr.Blocks() as app:
 
 
 
546
 
547
  with gr.Row():
548
  with gr.Column(scale=3):
549
  gr.Markdown(
550
  """
551
- ## MyoSDK Kinesis
552
- <span style="color:#6b7280">Joint visualization & motion retargeting pipelines</span>
553
 
554
- This application allows you to retarget motion capture data to biomechanical models using MyoSDK's Kinesis engine.
555
- Upload C3D files or videos to extract joint angles using [Kinesis](https://myolab.ai/blog/myokinesis) and visualize motion data.
556
  """
557
  )
558
  with gr.Column(scale=1):
@@ -562,88 +614,91 @@ with gr.Blocks() as app:
562
  type="password",
563
  info="Get your API key from https://dev.myolab.ai",
564
  )
565
- with gr.Tab("πŸ“Š Motion Capture Retargeting"):
566
-
567
- gr.Markdown(
568
- """
569
- Upload motion capture data in C3D format along with a markerset XML file to retarget the motion to a biomechanical model.
570
- The process will extract joint angles and generate visualizations of the motion data.
571
- """
572
- )
573
- with gr.Row(equal_height=True):
574
- with gr.Column(scale=2):
575
- gr.HTML(
576
- create_info_text(
577
- "1. Upload a Markerset File",
578
- "Upload an XML file that defines the marker set configuration. This file specifies which markers are used and their anatomical locations."
579
- + 'See <a href="https://markerset-editor.myolab.ai">Markerset Editor</a> for more details.',
580
- )
581
- )
582
-
583
- markerset = gr.File(
584
- # label=None,
585
- file_types=[".xml"],
586
- elem_id="file-upload-markerset",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
  value=os.path.join(
588
- os.path.dirname(__file__), "./markersets/cmu_markerset.xml"
 
589
  ),
590
  )
591
-
592
- with gr.Column(scale=2):
593
- gr.HTML(
594
- create_info_text(
595
- "2. Upload C3D Motion Capture File(s)",
596
- "Upload one or more C3D files containing 3D marker trajectories from motion capture systems."
597
- + 'Example from CMU dataset: <a href="https://mocap.cs.cmu.edu/subjects/35/35_30.c3d">35_30.c3d</a>',
598
- )
599
  )
600
- c3d_files = gr.File(
601
- label=None,
602
- file_types=[".c3d"],
603
- elem_id="file-upload-c3d",
604
- file_count="multiple",
605
- value=[
606
- os.path.join(os.path.dirname(__file__), "./data/35_30.c3d")
607
- ],
608
  )
609
-
610
- run_btn_c3d = gr.Button("3. πŸš€ Run Retargeting", variant="primary")
611
-
612
- with gr.Tab("πŸŽ₯ Video-Based Motion Retargeting"):
613
- gr.Markdown(
614
- """
615
- Extract 3D pose from video and retarget it to a biomechanical model using [Kinesis](https://myolab.ai/blog/myokinesis).
616
-
617
- ⚠️ **Important:** Using Metrabs for video-based motion retargeting which is **ONLY FOR RESEARCH/ACADEMIC USE**.
618
- Please cite the [paper](https://arxiv.org/abs/2007.07227) if you use this feature.
619
- For commercial applications, please contact MyoLab at contacts@myolab.ai.
620
- """
621
- )
622
- video_file = gr.Video(
623
- label="1. Upload a Video File (Supported formats: MP4, AVI, MOV, MKV)",
624
- height=400,
625
- value=os.path.join(
626
- os.path.dirname(__file__), "./data/13710671_1080_1920_25fps.mp4"
627
- ),
628
- )
629
- run_v2m_btn_video = gr.Button(
630
- "2. πŸš€ Run Retargeting from Video", variant="primary"
631
- )
632
-
633
- output_3d_motion = gr.Model3D(
634
- label="3D Motion Visualization",
635
- visible=False,
636
- )
637
  output_file = gr.File(
638
  label="πŸ“₯ Download Results - Download the retargeted motion data as a Parquet (.parquet) file containing joint angles and metadata.",
639
- visible=False,
640
  )
641
  df_state = gr.State()
642
  joint_dropdown = gr.Dropdown(
643
  label="Select Joint Angle(s) to Visualize",
644
- interactive=False,
 
645
  multiselect=True,
646
- visible=True,
647
  info="After processing completes, select one or more joint angles to plot. The dropdown will be populated with available joints from the retargeted data.",
648
  )
649
  plot_area = gr.Plot(
@@ -655,11 +710,40 @@ with gr.Blocks() as app:
655
  lines=12,
656
  info="Real-time status updates showing the progress of file uploads, retargeting jobs, and data processing.",
657
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
  joint_dropdown.change(
660
- fn=update_plot,
 
 
 
 
 
 
 
661
  inputs=[df_state, joint_dropdown],
662
  outputs=[plot_area],
 
663
  )
664
  run_btn_c3d.click(
665
  fn=run_retargeting_c3d,
 
17
 
18
  import cv2
19
  import gradio as gr
20
+ import myosdk
21
  import numpy as np
22
+ import pandas as pd
23
  import plotly.graph_objs as go
24
  import spaces
25
  import torch
26
+ from embed4d import model3d_viewer
27
  from metrabs_pytorch.scripts.run_video import run_metrabs_video
28
  from myo_tools.mjs.marker.marker_api import get_marker_names
29
  from myo_tools.utils.file_ops.dataframe_utils import from_array_to_dataframe
 
41
 
42
 
43
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
44
+ UPLOAD_GLB = os.path.join(os.path.dirname(__file__), "./info_files/upload_files.glb")
45
+ ERROR_GLB = os.path.join(os.path.dirname(__file__), "./info_files/error.glb")
46
+ PROCESSING_GLB = os.path.join(os.path.dirname(__file__), "./info_files/processing.glb")
47
 
48
 
49
  def draw_keypoints(frame, poses2d, radius=10):
 
75
  return output_video
76
 
77
 
 
 
 
 
 
 
 
 
 
78
  def create_info_text(title, info_text):
79
  return f"""
80
  <div>
 
154
  None,
155
  gr.update(value=[], visible=True),
156
  gr.update(visible=False),
157
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
158
  )
159
  return
160
 
 
165
  None,
166
  gr.update(value=[], visible=True),
167
  gr.update(visible=False),
168
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
169
  )
170
 
171
  try:
 
174
  init_time = time.time()
175
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
176
  visible=False
177
+ ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
178
  client = Client(api_key=api_key)
179
 
180
  status.append(
 
185
  status.append("πŸ”Ή Uploading markerset file...")
186
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
187
  visible=False
188
+ ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
189
  mk_asset = client.assets.upload_file(markerset_file.name)
190
 
191
  status.append(
 
193
  )
194
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
195
  visible=False
196
+ ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
197
 
198
  mk_id = mk_asset["asset_id"]
199
 
 
206
 
207
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
208
  visible=False
209
+ ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
210
 
211
  init_time = time.time()
212
  tracker_asset = client.assets.upload_file(f)
 
215
  )
216
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
217
  visible=False
218
+ ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
219
  init_time = time.time()
220
  job = client.jobs.start_retarget(
221
  tracker_asset_id=tracker_asset["asset_id"],
 
228
  )
229
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
230
  visible=False
231
+ ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
232
  init_time = time.time()
233
  result = client.jobs.wait(job["job_id"])
234
 
235
  status.append(
236
  f"\tπŸ”Ή Retargeting job completed in {time.time() - init_time:.2f} seconds"
237
+ )
238
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
239
  visible=False
240
+ ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
241
  assert (
242
  result["status"] == "SUCCEEDED"
243
  ), f"Failed retarget for {os.path.basename(f)}"
 
268
  time.sleep(0.1) # allow filesystem flush
269
  yield "\n".join(status), None, None, gr.update(
270
  interactive=True, visible=True
271
+ ), gr.update(visible=True), gr.update(
272
+ visible=True, value=model3d_viewer(output_glb_files[0])
273
+ )
274
+ assert os.path.exists(out_path)
275
  df = from_qpos_to_joint_angles(output_files[0])
276
 
277
  angle_list = list(df.columns[1:])
278
  initial_value = [angle_list[0]] if angle_list else []
279
+ initial_plot = update_plot(df, initial_value)
280
  status.append("βœ… Complete!")
281
+ df_state_path = os.path.abspath(output_files[0])
282
  yield (
283
  "\n".join(status),
284
  gr.update(value=output_files, visible=True),
285
+ df_state_path, # Use absolute path for Linux compatibility
286
+ gr.update(
287
+ choices=angle_list, value=initial_value, visible=True, interactive=True
288
+ ),
289
+ gr.update(value=initial_plot, visible=True),
290
+ gr.update(visible=True, value=model3d_viewer(output_glb_files[0])),
291
  )
292
 
293
  except Exception as e:
 
297
  None,
298
  gr.update(visible=False),
299
  gr.update(visible=False),
300
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
301
  )
302
 
303
 
 
321
  gr.update(visible=False),
322
  gr.update(visible=False),
323
  video_file,
324
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
325
  )
326
  return
327
 
 
343
  gr.update(visible=False),
344
  gr.update(visible=False),
345
  video_file,
346
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
347
  )
348
  return
349
 
 
356
  init_time = time.time()
357
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
358
  visible=False
359
+ ), video_path, gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
360
 
361
  results = list(
362
  run_metrabs_video(video_path=video_path, device=DEVICE, visualize=False)
 
365
  np.array([res["poses3d"] for res in results]).squeeze()
366
  )
367
 
368
+ markers[:, :, 2] = markers[:, :, 2] - np.min(markers[:, :, 2])
369
+
370
  fps = (
371
  results[0]["fps"] if results else 25.0
372
  ) # Default to 25 fps if not available
 
378
 
379
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
380
  visible=True
381
+ ), video_with_keypoints, gr.update(
382
+ visible=True, value=model3d_viewer(PROCESSING_GLB)
383
+ ),
384
  status.append(
385
  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)"
386
  )
387
  print("πŸ”Ή Pose Extraction from Video Completed")
388
+ yield "\n".join(status), None, None, None, gr.update(
389
  visible=False
390
+ ), video_with_keypoints, gr.update(
391
+ visible=True, value=model3d_viewer(PROCESSING_GLB)
392
+ ),
393
  # Initialize client
394
  status.append("πŸ”Ή Initializing MyoSDK client...")
395
  init_time = time.time()
396
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
397
  visible=False,
398
+ ), video_with_keypoints, gr.update(
399
+ visible=True, value=model3d_viewer(PROCESSING_GLB)
400
+ ),
401
  client = Client(api_key=api_key)
402
 
403
  status.append(
 
406
  init_time = time.time()
407
  # Upload markerset
408
  status.append("πŸ”Ή Uploading markerset file...")
409
+ yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
410
  visible=False,
411
+ ), video_with_keypoints, gr.update(
412
+ visible=True, value=model3d_viewer(PROCESSING_GLB)
413
+ ),
414
 
415
  markerset_file_name = "markersets/movi_metrabs_markerset.xml"
416
 
 
420
  f"πŸ”Ή Markerset file uploaded in {time.time() - init_time:.2f} seconds"
421
  )
422
 
423
+ yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
424
  visible=False
425
+ ), video_with_keypoints, gr.update(
426
+ visible=True, value=model3d_viewer(PROCESSING_GLB)
427
+ ),
428
  init_time = time.time()
429
 
430
  marker_names = get_marker_names(markerset_file_name)
 
454
  )
455
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
456
  visible=False
457
+ ), video_with_keypoints, gr.update(
458
+ visible=True, value=model3d_viewer(PROCESSING_GLB)
459
+ ),
460
  init_time = time.time()
461
  result = client.jobs.wait(job["job_id"])
462
 
463
  status.append(
464
  f"\tπŸ”Ή Retargeting job completed in {time.time() - init_time:.2f} seconds"
465
  )
466
+ yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
467
  visible=False
468
+ ), video_with_keypoints, gr.update(
469
+ visible=True, value=model3d_viewer(PROCESSING_GLB)
470
+ ),
471
 
472
  print("STATUS: ", result["status"])
473
  assert (
 
489
  result["output"]["retarget_output_asset_ids"]["motion"], output_glb_path
490
  )
491
 
492
+ assert os.path.getsize(output_glb_path) > 0
493
+ time.sleep(0.1) # allow filesystem flush
494
+ print("output_glb_path: ", output_glb_path, os.path.getsize(output_glb_path))
495
  # Load angles from first output file
496
  status.append("πŸ”Ή Loading animation and angle data...")
497
+ yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
498
+ visible=True
499
+ ), video_with_keypoints, gr.update(
500
+ visible=True, value=model3d_viewer(output_glb_path)
501
  )
502
+ assert os.path.exists(out_path)
503
  df = from_qpos_to_joint_angles(out_path)
504
 
505
  angle_list = list(df.columns[1:])
506
  initial_value = [angle_list[0]] if angle_list else []
507
+ initial_plot = update_plot(df, initial_value)
508
  status.append("βœ… Complete!")
509
+ df_state_path = os.path.abspath(out_path)
510
  yield (
511
  "\n".join(status),
512
  gr.update(value=out_path, visible=True),
513
+ df_state_path, # Use absolute path for Linux compatibility
514
+ gr.update(
515
+ choices=angle_list, value=initial_value, visible=True, interactive=True
516
+ ),
517
+ gr.update(value=initial_plot, visible=True),
518
  video_with_keypoints,
519
+ gr.update(visible=True, value=model3d_viewer(output_glb_path)),
520
  )
521
+ return
522
 
523
  except Exception as e:
524
  # Use video_path if defined, otherwise use video_file or None
 
534
  gr.update(visible=False),
535
  gr.update(visible=False),
536
  error_video,
537
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
538
  )
539
 
540
 
541
  # ------------------------------------------------------------
542
  # Plotting
543
  # ------------------------------------------------------------
544
+
545
+
546
+ def update_plot(parquet_path, joints):
547
+ """Safe Linux/macOS plotting from parquet or DataFrame."""
548
+ # Check joints first to avoid unnecessary processing
549
  if not joints:
550
  return go.Figure()
551
+
552
+ # Check if parquet_path is valid
553
+ if parquet_path is None:
554
+ return go.Figure()
555
+
556
+ if isinstance(parquet_path, pd.DataFrame):
557
+ df = parquet_path
558
+ else:
559
+ # Nothing loaded yet - check if it's an empty string
560
+ if parquet_path == "":
561
+ return go.Figure()
562
+
563
+ # File might not exist yet (race-safe)
564
+ if not os.path.exists(parquet_path):
565
+ return go.Figure()
566
+
567
+ df = pd.read_parquet(parquet_path)
568
+
569
+ if df is None or df.empty:
570
+ return go.Figure()
571
+
572
  if not isinstance(joints, list):
573
  joints = [joints]
574
 
 
583
  yaxis_title="Angle Value (degrees)",
584
  plot_bgcolor="#1E1E1E",
585
  paper_bgcolor="#1E1E1E",
586
+ font=dict(color="#F0F0F0"),
587
+ xaxis=dict(gridcolor="#444444"),
588
+ yaxis=dict(gridcolor="#444444"),
589
+ dragmode="pan",
590
  )
591
+
592
  return fig
593
 
594
 
595
+ CSS = """
596
+ .left-panel, .right-panel { background-color: #1E1E1E; border:1px solid var(--border-color, #e9ecef) !important; border-radius:15px !important; box-shadow:0 4px 20px rgba(0,0,0,.08) !important; padding:12px !important; }
597
+ """
598
+ with gr.Blocks(css=CSS) as app:
599
 
600
  with gr.Row():
601
  with gr.Column(scale=3):
602
  gr.Markdown(
603
  """
604
+ ## <a href="https://myolab.ai/blog/myokinesis">MyoKinesis</a>
605
+ <span style="color:#6b7280">High-Fidelity Human Motion Reconstruction for Developers, Researchers, and Builders</span>
606
 
607
+ Reconstruct anatomically accurate human motion from uploaded C3D or video data using MyoKinesis.
 
608
  """
609
  )
610
  with gr.Column(scale=1):
 
614
  type="password",
615
  info="Get your API key from https://dev.myolab.ai",
616
  )
617
+ with gr.Row(height=550):
618
+ with gr.Row(height=530, elem_classes=["left-panel"]):
619
+ with gr.Tab("πŸ“Š MoCap"):
620
+ with gr.Row():
621
+ with gr.Column(scale=2):
622
+ gr.HTML(
623
+ create_info_text(
624
+ "1. Upload a Markerset File",
625
+ "Upload an XML file that defines the marker set configuration. This file specifies which markers are used and their anatomical locations."
626
+ + 'See <a href="https://markerset-editor.myolab.ai">Markerset Editor</a> for more details.',
627
+ )
628
+ )
629
+
630
+ markerset = gr.File(
631
+ # label=None,
632
+ file_types=[".xml"],
633
+ elem_id="file-upload-markerset",
634
+ value=os.path.join(
635
+ os.path.dirname(__file__),
636
+ "../markersets/cmu_markerset.xml",
637
+ ),
638
+ )
639
+
640
+ gr.HTML(
641
+ create_info_text(
642
+ "2. Upload C3D Motion Capture File(s)",
643
+ "Upload one or more C3D files containing 3D marker trajectories from motion capture systems."
644
+ + 'Example from CMU dataset: <a href="https://mocap.cs.cmu.edu/subjects/35/35_30.c3d">35_30.c3d</a>',
645
+ )
646
+ )
647
+ c3d_files = gr.File(
648
+ label=None,
649
+ file_types=[".c3d"],
650
+ elem_id="file-upload-c3d",
651
+ file_count="multiple",
652
+ value=[
653
+ os.path.join(
654
+ os.path.dirname(__file__), "./data/35_30.c3d"
655
+ )
656
+ ],
657
+ )
658
+
659
+ run_btn_c3d = gr.Button(
660
+ "3. πŸš€ Run Retargeting", variant="primary"
661
+ )
662
+
663
+ with gr.Tab("πŸŽ₯ Video"):
664
+ video_file = gr.Video(
665
+ label="1. Upload a Video File (Supported formats: MP4, AVI, MOV, MKV)",
666
+ height=300,
667
  value=os.path.join(
668
+ os.path.dirname(__file__),
669
+ "./data/5385885.mp4",
670
  ),
671
  )
672
+ run_v2m_btn_video = gr.Button(
673
+ "2. πŸš€ Run Retargeting from Video", variant="primary"
 
 
 
 
 
 
674
  )
675
+ gr.Markdown(
676
+ """
677
+ ⚠️ **Important:** Using Metrabs for video motion retargeting **ONLY FOR RESEARCH/ACADEMIC USE**.
678
+ Please cite the [paper](https://arxiv.org/abs/2007.07227).
679
+ For commercial applications, contact `contacts@myolab.ai`
680
+ """
 
 
681
  )
682
+ with gr.Row(height=530, elem_classes=["right-panel"]):
683
+ output_3d_motion = gr.HTML(
684
+ label="3D Motion Visualization",
685
+ visible=True,
686
+ value=model3d_viewer(
687
+ UPLOAD_GLB,
688
+ height=500,
689
+ ),
690
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  output_file = gr.File(
692
  label="πŸ“₯ Download Results - Download the retargeted motion data as a Parquet (.parquet) file containing joint angles and metadata.",
693
+ visible=True,
694
  )
695
  df_state = gr.State()
696
  joint_dropdown = gr.Dropdown(
697
  label="Select Joint Angle(s) to Visualize",
698
+ choices=[],
699
+ interactive=True,
700
  multiselect=True,
701
+ visible=False, # Hidden initially, shown only when data is ready
702
  info="After processing completes, select one or more joint angles to plot. The dropdown will be populated with available joints from the retargeted data.",
703
  )
704
  plot_area = gr.Plot(
 
710
  lines=12,
711
  info="Real-time status updates showing the progress of file uploads, retargeting jobs, and data processing.",
712
  )
713
+ gr.Markdown(f"MyoSDK v{myosdk.__version__}")
714
+
715
+ def update_plot_wrapper(parquet_path, joints):
716
+ """Wrapper to ensure plot is visible and updated on Linux."""
717
+ if not joints:
718
+ return gr.update(value=go.Figure(), visible=False)
719
+
720
+ if parquet_path is None:
721
+ return gr.update(value=go.Figure(), visible=False)
722
+
723
+ # Check if file exists and convert to absolute path for consistency
724
+ if isinstance(parquet_path, str):
725
+ if not os.path.exists(parquet_path):
726
+ return gr.update(value=go.Figure(), visible=False)
727
+ parquet_path = os.path.abspath(parquet_path)
728
+
729
+ try:
730
+ fig = update_plot(parquet_path, joints)
731
+ return gr.update(value=fig, visible=True)
732
+ except Exception:
733
+ return gr.update(value=go.Figure(), visible=False)
734
 
735
  joint_dropdown.change(
736
+ fn=update_plot_wrapper,
737
+ inputs=[df_state, joint_dropdown],
738
+ outputs=[plot_area],
739
+ show_progress=False,
740
+ )
741
+
742
+ joint_dropdown.input(
743
+ fn=update_plot_wrapper,
744
  inputs=[df_state, joint_dropdown],
745
  outputs=[plot_area],
746
+ show_progress=False,
747
  )
748
  run_btn_c3d.click(
749
  fn=run_retargeting_c3d,
data/5385885.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:615a11bd65c7d1e3af4c309f00335e3be464b7ecb9e6b3bbc354ca80c6df338c
3
+ size 690209
info_files/error.glb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dcefaeeee6ee7a94c2a8f582af1466e21460bc5f8e4fb0f47e33670cb8288711
3
+ size 237300
info_files/processing.glb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e3615d2ef57ca201a359d9d66c595783f8921b873d96635c915959d0ff147412
3
+ size 640944
info_files/upload_files.glb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:441bd0d6b6265e38c9686e929f694dc84ca9ebe3810a3446adb0a2ea737299e0
3
+ size 247132
requirements.txt CHANGED
@@ -3,4 +3,5 @@ plotly
3
  myosdk>=0.1.0
4
  myo_tools
5
  git+https://github.com/Vittorio-Caggiano/metrabs.git
6
- spaces
 
 
3
  myosdk>=0.1.0
4
  myo_tools
5
  git+https://github.com/Vittorio-Caggiano/metrabs.git
6
+ spaces
7
+ embed4d