Reem commited on
Commit
eb2d44b
·
1 Parent(s): 3eafa11

3D Skeleton Visualization and Animation

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. A10/OneStepModel.ipynb +2 -2
  2. A10/one_step_results/Conv1D_adam.weights.h5 +3 -0
  3. A10/one_step_results/Conv1D_adam_training.png +3 -0
  4. A10/one_step_results/Conv1D_rmsprop.weights.h5 +3 -0
  5. A10/one_step_results/Conv1D_rmsprop_training.png +3 -0
  6. A10/one_step_results/Conv1D_sgd.weights.h5 +3 -0
  7. A10/one_step_results/Conv1D_sgd_training.png +3 -0
  8. A10/one_step_results/Dense_deep_adam.weights.h5 +3 -0
  9. A10/one_step_results/Dense_deep_adam_training.png +3 -0
  10. A10/one_step_results/Dense_deep_rmsprop.weights.h5 +3 -0
  11. A10/one_step_results/Dense_deep_rmsprop_training.png +3 -0
  12. A10/one_step_results/Dense_deep_sgd.weights.h5 +3 -0
  13. A10/one_step_results/Dense_deep_sgd_training.png +3 -0
  14. A10/one_step_results/Dense_shallow_adam.weights.h5 +3 -0
  15. A10/one_step_results/Dense_shallow_adam_training.png +3 -0
  16. A10/one_step_results/Dense_shallow_rmsprop.weights.h5 +3 -0
  17. A10/one_step_results/Dense_shallow_rmsprop_training.png +3 -0
  18. A10/one_step_results/Dense_shallow_sgd.weights.h5 +3 -0
  19. A10/one_step_results/Dense_shallow_sgd_training.png +3 -0
  20. A10/one_step_results/Dense_wide_adam.weights.h5 +3 -0
  21. A10/one_step_results/Dense_wide_adam_training.png +3 -0
  22. A10/one_step_results/Dense_wide_rmsprop.weights.h5 +3 -0
  23. A10/one_step_results/Dense_wide_rmsprop_training.png +3 -0
  24. A10/one_step_results/Dense_wide_sgd.weights.h5 +3 -0
  25. A10/one_step_results/Dense_wide_sgd_training.png +3 -0
  26. A10/one_step_results/GRU_adam.weights.h5 +3 -0
  27. A10/one_step_results/GRU_adam_training.png +3 -0
  28. A10/one_step_results/GRU_rmsprop.weights.h5 +3 -0
  29. A10/one_step_results/GRU_rmsprop_training.png +3 -0
  30. A10/one_step_results/GRU_sgd.weights.h5 +3 -0
  31. A10/one_step_results/GRU_sgd_training.png +3 -0
  32. A10/one_step_results/LSTM_adam.weights.h5 +3 -0
  33. A10/one_step_results/LSTM_adam_training.png +3 -0
  34. A10/one_step_results/LSTM_rmsprop.weights.h5 +3 -0
  35. A10/one_step_results/LSTM_rmsprop_training.png +3 -0
  36. A10/one_step_results/LSTM_sgd.weights.h5 +3 -0
  37. A10/one_step_results/LSTM_sgd_training.png +3 -0
  38. A10/one_step_results/best_model_visuals/animations/interactive_viewer.html +0 -0
  39. A10/one_step_results/best_model_visuals/ground_truth_xyz.npy +0 -0
  40. A10/one_step_results/best_model_visuals/metadata.json +78 -0
  41. A10/one_step_results/best_model_visuals/predicted_xyz.npy +0 -0
  42. A10/one_step_results/best_model_visuals/skeleton_plots/comparison_frame_0000.png +3 -0
  43. A10/one_step_results/best_model_visuals/skeleton_plots/comparison_frame_0062.png +3 -0
  44. A10/one_step_results/best_model_visuals/skeleton_plots/comparison_frame_0124.png +3 -0
  45. A10/one_step_results/best_model_visuals/skeleton_plots/multiview_frame_0000.png +3 -0
  46. A10/one_step_results/best_model_visuals/skeleton_plots/multiview_frame_0062.png +3 -0
  47. A10/one_step_results/best_model_visuals/skeleton_plots/multiview_frame_0124.png +3 -0
  48. A10/one_step_results/best_model_visuals/visualization_summary.json +21 -0
  49. A10/one_step_results/results_summary.csv +19 -0
  50. A10/visualizer.py +762 -0
A10/OneStepModel.ipynb CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:74c5227f3c26f34af849c7e452a7d824e376d6326dd11f660bebea38aba4c25c
3
- size 288394
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:020038eacba1b25c02ae1ba5bd98169d8d330f52c6a458cf22ef7775f1f55319
3
+ size 269460
A10/one_step_results/Conv1D_adam.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3746b925fa0d08aa3a2e66f33aee0e46314ff05b49d15b04beb133726198bb43
3
+ size 527600
A10/one_step_results/Conv1D_adam_training.png ADDED

Git LFS Details

  • SHA256: b2a9c583a346c832dc0fbe20387d05bd97b550aa836739684c1eddb6c40e737c
  • Pointer size: 130 Bytes
  • Size of remote file: 44.8 kB
A10/one_step_results/Conv1D_rmsprop.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a9e2461ba0da94e0884d505b8f2f8ebdd1f1f205147a3839cad33d4f252a5662
3
+ size 362892
A10/one_step_results/Conv1D_rmsprop_training.png ADDED

Git LFS Details

  • SHA256: 8ca532751c34015eaf2ca7c53fabc8f8e397bdd9ab0ed870f8b98a0378dd025a
  • Pointer size: 130 Bytes
  • Size of remote file: 49.3 kB
A10/one_step_results/Conv1D_sgd.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d453c3765e3f38d3879e3d76fdabc6747c2c4ff82b1b4161958263923f854c24
3
+ size 362892
A10/one_step_results/Conv1D_sgd_training.png ADDED

Git LFS Details

  • SHA256: ed5ce5d610fa1f276cedf51551375bd401e5f202f8f27dc711563dd149b07f87
  • Pointer size: 130 Bytes
  • Size of remote file: 45.4 kB
A10/one_step_results/Dense_deep_adam.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ce343312ea0fd8cd311792f40a96f33d1497b5c75ef10133c029c2ce11ab80eb
3
+ size 659248
A10/one_step_results/Dense_deep_adam_training.png ADDED

Git LFS Details

  • SHA256: a49bbbdd8fa0bb7e5183e002b9387f51ad696ef7b9723b9d86e678e26d30f0ab
  • Pointer size: 130 Bytes
  • Size of remote file: 41.3 kB
A10/one_step_results/Dense_deep_rmsprop.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e05951e17ebd252c608337f41b7d9bd37cc1f04a5374071c1af8bd65a68a507f
3
+ size 451192
A10/one_step_results/Dense_deep_rmsprop_training.png ADDED

Git LFS Details

  • SHA256: 0f41214c218d4842658994c05803da1f7784f2e0cfa018297c43188f66e74d7f
  • Pointer size: 130 Bytes
  • Size of remote file: 37.8 kB
A10/one_step_results/Dense_deep_sgd.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b264ff07437ad909c870e08926792df572062574a52683577ed28c84c05905f3
3
+ size 451192
A10/one_step_results/Dense_deep_sgd_training.png ADDED

Git LFS Details

  • SHA256: 9a418a3f8cf65c6aac4096d49624d0f498bf71bc5912a3a939a996f525df7979
  • Pointer size: 130 Bytes
  • Size of remote file: 45.8 kB
A10/one_step_results/Dense_shallow_adam.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9d3a8bb68b151cfb2a9057f2cd73299c3ca5ecdb9a86f845f17ade36d11bebdc
3
+ size 73720
A10/one_step_results/Dense_shallow_adam_training.png ADDED

Git LFS Details

  • SHA256: a585fb9f47c593bcc6089cb0a639c7d203d4534d0696241b31f784f45740e083
  • Pointer size: 130 Bytes
  • Size of remote file: 40.5 kB
A10/one_step_results/Dense_shallow_rmsprop.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6ab4f548ae2841c87403b7b12478eb96cc4142b5ffcae83451d2345ca0a849bb
3
+ size 56480
A10/one_step_results/Dense_shallow_rmsprop_training.png ADDED

Git LFS Details

  • SHA256: 1188af95b98d75183ffdec7868d4c00b4992842113fd20c9ebc6868138da2f98
  • Pointer size: 130 Bytes
  • Size of remote file: 40.6 kB
A10/one_step_results/Dense_shallow_sgd.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:049db88a11b48c0d74e4c3d7d3645430645cb7737e8746e558f5a1b79bf75926
3
+ size 56480
A10/one_step_results/Dense_shallow_sgd_training.png ADDED

Git LFS Details

  • SHA256: b83aa7581cb3b5ebac5f20d1a39fdcc9efc97606037b64dc9541a71420022564
  • Pointer size: 130 Bytes
  • Size of remote file: 40.2 kB
A10/one_step_results/Dense_wide_adam.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:adf808774722c0d1bbaef46c036ebc8594b57aaef2215439db339e21c1172ca4
3
+ size 1890392
A10/one_step_results/Dense_wide_adam_training.png ADDED

Git LFS Details

  • SHA256: 131e1344ebc40eac2a307cc36397e6724fb0ac2919ba9ca9f3578cc94e2c9ed6
  • Pointer size: 130 Bytes
  • Size of remote file: 65.3 kB
A10/one_step_results/Dense_wide_rmsprop.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4c4aa74df862e022e3dc79534fea9b7b2e5c69d9e370b13344b84e88318b2150
3
+ size 1267800
A10/one_step_results/Dense_wide_rmsprop_training.png ADDED

Git LFS Details

  • SHA256: 6be4a9e1598b3624eeaebfb7c6548e2f6d6a2a10f3b8a37afc4623212a00895d
  • Pointer size: 130 Bytes
  • Size of remote file: 58.9 kB
A10/one_step_results/Dense_wide_sgd.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b3f88d168ebae5cf552be242d87a98def41f295935ad22f92fd63929100bf693
3
+ size 1267800
A10/one_step_results/Dense_wide_sgd_training.png ADDED

Git LFS Details

  • SHA256: a04f5c8545e5a0104e09f01b3440c72711c841408fdf5800034dfb3115e32ee8
  • Pointer size: 130 Bytes
  • Size of remote file: 42.9 kB
A10/one_step_results/GRU_adam.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:843b0ba2564ef2f99982f1160385f91350ab86c8badf92d64c753f84d1392377
3
+ size 388600
A10/one_step_results/GRU_adam_training.png ADDED

Git LFS Details

  • SHA256: d44096671ba8697615f6ff0e8856487cebebbb4a9e43abba244169874ecc8d12
  • Pointer size: 130 Bytes
  • Size of remote file: 46 kB
A10/one_step_results/GRU_rmsprop.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:96f18be2dd7746c11cab5e4d59ffededcb9fca8707981511eda2ba3ee47f1495
3
+ size 268632
A10/one_step_results/GRU_rmsprop_training.png ADDED

Git LFS Details

  • SHA256: 119d8a2777c8aeeea53c3e9b65020edd04fc8b3fbf45047fef1310dd4db1e141
  • Pointer size: 130 Bytes
  • Size of remote file: 41.3 kB
A10/one_step_results/GRU_sgd.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8f27e3c3e6539333ba4f7ed250f886b1dc06d55bb0769709c1f55fa69a013d93
3
+ size 268632
A10/one_step_results/GRU_sgd_training.png ADDED

Git LFS Details

  • SHA256: 077b89f638afec0548cb19e992f1871c8f9454a400b05b8e45f82464a56d2d25
  • Pointer size: 130 Bytes
  • Size of remote file: 47.7 kB
A10/one_step_results/LSTM_adam.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:613e1f1cc92c21b13bb3eaa4e898c8f7ffc4e2da88503fda7e9b458e1c71ea8d
3
+ size 493712
A10/one_step_results/LSTM_adam_training.png ADDED

Git LFS Details

  • SHA256: 13447aa15ca18a7aeb0ee5aa88687a78b97727f0c59fb611fb3721ccf523990d
  • Pointer size: 130 Bytes
  • Size of remote file: 41.8 kB
A10/one_step_results/LSTM_rmsprop.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a31fb497a4c2cecdd0b057401ae6dc5b32c7bbcabb2ad40cb5c61213278726b6
3
+ size 337396
A10/one_step_results/LSTM_rmsprop_training.png ADDED

Git LFS Details

  • SHA256: cce67ef8ed71c8f6bd10406285b9ad9f072d39b7da977a64c69ad45568930982
  • Pointer size: 130 Bytes
  • Size of remote file: 45.2 kB
A10/one_step_results/LSTM_sgd.weights.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a5a3b4316daf6c8b69a2889cdaf99c0c31808c84448de4d021603bbd866ffe8
3
+ size 337396
A10/one_step_results/LSTM_sgd_training.png ADDED

Git LFS Details

  • SHA256: b23c6ef2ea5b661d74bcc7259863908717f559375c665f53c25f424bfc838689
  • Pointer size: 130 Bytes
  • Size of remote file: 40.9 kB
A10/one_step_results/best_model_visuals/animations/interactive_viewer.html ADDED
The diff for this file is too large to render. See raw diff
 
A10/one_step_results/best_model_visuals/ground_truth_xyz.npy ADDED
Binary file (19.6 kB). View file
 
A10/one_step_results/best_model_visuals/metadata.json ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "GRU_sgd",
3
+ "sequence_name": null,
4
+ "n_frames": 125,
5
+ "joints": [
6
+ "head",
7
+ "left_shoulder",
8
+ "left_elbow",
9
+ "right_shoulder",
10
+ "right_elbow",
11
+ "left_hand",
12
+ "right_hand",
13
+ "left_hip",
14
+ "right_hip",
15
+ "left_knee",
16
+ "right_knee",
17
+ "left_foot",
18
+ "right_foot"
19
+ ],
20
+ "bones": [
21
+ [
22
+ "head",
23
+ "left_shoulder"
24
+ ],
25
+ [
26
+ "head",
27
+ "right_shoulder"
28
+ ],
29
+ [
30
+ "left_shoulder",
31
+ "right_shoulder"
32
+ ],
33
+ [
34
+ "left_shoulder",
35
+ "left_elbow"
36
+ ],
37
+ [
38
+ "left_elbow",
39
+ "left_hand"
40
+ ],
41
+ [
42
+ "right_shoulder",
43
+ "right_elbow"
44
+ ],
45
+ [
46
+ "right_elbow",
47
+ "right_hand"
48
+ ],
49
+ [
50
+ "left_shoulder",
51
+ "left_hip"
52
+ ],
53
+ [
54
+ "right_shoulder",
55
+ "right_hip"
56
+ ],
57
+ [
58
+ "left_hip",
59
+ "right_hip"
60
+ ],
61
+ [
62
+ "left_hip",
63
+ "left_knee"
64
+ ],
65
+ [
66
+ "left_knee",
67
+ "left_foot"
68
+ ],
69
+ [
70
+ "right_hip",
71
+ "right_knee"
72
+ ],
73
+ [
74
+ "right_knee",
75
+ "right_foot"
76
+ ]
77
+ ]
78
+ }
A10/one_step_results/best_model_visuals/predicted_xyz.npy ADDED
Binary file (19.6 kB). View file
 
A10/one_step_results/best_model_visuals/skeleton_plots/comparison_frame_0000.png ADDED

Git LFS Details

  • SHA256: 785985697f80e2144863d593d4d7ab98c9a9b4519b47e5ac3d2f39b4e52e8d66
  • Pointer size: 131 Bytes
  • Size of remote file: 439 kB
A10/one_step_results/best_model_visuals/skeleton_plots/comparison_frame_0062.png ADDED

Git LFS Details

  • SHA256: ff07913570ed04cb7309ff50b8a4150b73b1039c38a092d2edd88d48363b19c7
  • Pointer size: 131 Bytes
  • Size of remote file: 409 kB
A10/one_step_results/best_model_visuals/skeleton_plots/comparison_frame_0124.png ADDED

Git LFS Details

  • SHA256: 9f764d4fb2cb2874eeed45888ec54890afc47512e1ec0cb844f2399d79420723
  • Pointer size: 131 Bytes
  • Size of remote file: 442 kB
A10/one_step_results/best_model_visuals/skeleton_plots/multiview_frame_0000.png ADDED

Git LFS Details

  • SHA256: d771a7a6925dbd3a4d81b75ea6116752f45d765455ddcaaa650c4b3f780044d8
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB
A10/one_step_results/best_model_visuals/skeleton_plots/multiview_frame_0062.png ADDED

Git LFS Details

  • SHA256: 591b60a2b3b9e78cd0ab4187baee1818b600f9f1b5ed579f2e3ead5e1f0fe8c5
  • Pointer size: 131 Bytes
  • Size of remote file: 208 kB
A10/one_step_results/best_model_visuals/skeleton_plots/multiview_frame_0124.png ADDED

Git LFS Details

  • SHA256: 8ea89d08eb33a4d2a9762a1bfa3c22bf454e09f63a2b561b3e5cdf55280e24b3
  • Pointer size: 131 Bytes
  • Size of remote file: 208 kB
A10/one_step_results/best_model_visuals/visualization_summary.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bundle_dir": "one_step_results/best_model_visuals",
3
+ "n_frames": 125,
4
+ "has_ground_truth": true,
5
+ "outputs": {
6
+ "plots": [
7
+ "one_step_results/best_model_visuals/skeleton_plots/comparison_frame_0000.png",
8
+ "one_step_results/best_model_visuals/skeleton_plots/multiview_frame_0000.png",
9
+ "one_step_results/best_model_visuals/skeleton_plots/comparison_frame_0062.png",
10
+ "one_step_results/best_model_visuals/skeleton_plots/multiview_frame_0062.png",
11
+ "one_step_results/best_model_visuals/skeleton_plots/comparison_frame_0124.png",
12
+ "one_step_results/best_model_visuals/skeleton_plots/multiview_frame_0124.png"
13
+ ],
14
+ "animations": [
15
+ "one_step_results/best_model_visuals/animations/comparison_animation.gif"
16
+ ],
17
+ "interactive": [
18
+ "one_step_results/best_model_visuals/animations/interactive_viewer.html"
19
+ ]
20
+ }
21
+ }
A10/one_step_results/results_summary.csv ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model,optimizer,mae_x_cm,mae_y_cm,mae_z_cm,mae_overall_cm,epochs
2
+ GRU,adam,5.389063432812691,9.430191665887833,6.671535223722458,7.163596898317337,20
3
+ GRU,rmsprop,5.299752578139305,9.424664080142975,6.957718729972839,7.227378338575363,18
4
+ LSTM,rmsprop,5.213542282581329,9.500730782747269,6.990131735801697,7.234802097082138,42
5
+ Dense_wide,rmsprop,5.343378335237503,9.627385437488556,7.2334036231040955,7.401389628648758,13
6
+ LSTM,adam,5.314872786402702,9.818003326654434,7.153135538101196,7.4286699295043945,20
7
+ Dense_shallow,rmsprop,5.357589945197105,9.719226509332657,7.346736639738083,7.474517822265625,24
8
+ GRU,sgd,5.55296503007412,9.792591631412506,7.209497690200806,7.518351078033447,100
9
+ Dense_shallow,adam,5.409527197480202,9.932142496109009,7.272899895906448,7.538188993930817,16
10
+ Dense_shallow,sgd,5.4327793419361115,10.07804125547409,7.273495942354202,7.594772428274155,66
11
+ Dense_wide,adam,5.493420362472534,10.05532369017601,7.516705244779587,7.688482850790024,15
12
+ Dense_wide,sgd,5.413392558693886,10.271655023097992,7.437676191329956,7.707574963569641,36
13
+ Conv1D,sgd,5.264963582158089,11.2212173640728,7.127776741981506,7.871319353580475,100
14
+ Conv1D,rmsprop,5.114050582051277,11.269953101873398,7.2903648018836975,7.891456037759781,17
15
+ Dense_deep,sgd,5.443181097507477,10.889338701963425,7.420753687620163,7.917757332324982,50
16
+ Conv1D,adam,5.290636420249939,11.23451516032219,7.24792554974556,7.924359291791916,17
17
+ LSTM,sgd,5.349316447973251,12.91179209947586,7.658876478672028,8.639994263648987,100
18
+ Dense_deep,adam,5.5221255868673325,12.439529597759247,8.009973168373108,8.657209575176239,13
19
+ Dense_deep,rmsprop,5.7855673134326935,13.607670366764069,8.328793197870255,9.240677952766418,11
A10/visualizer.py ADDED
@@ -0,0 +1,762 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ """
4
+ A10 visualizer.py
5
+ =================
6
+ 3D skeleton visualization utilities for Issue #42 / Sprint 10.
7
+
8
+ Designed to fit the current A10 sprint codebase where:
9
+ - PoseNet/MoveNet input is 13 joints x 2 coordinates = 26 features
10
+ - Kinect / one-step output is 13 joints x 3 coordinates = 39 features
11
+ - Joint order must match data_loader.KINECT_JOINTS
12
+
13
+ This module supports:
14
+ - Static 3D skeleton plots
15
+ - Side-by-side and overlay comparison plots
16
+ - Per-joint error coloring
17
+ - Multiple camera angles (front / side / top)
18
+ - Joint trajectory trails
19
+ - GIF / MP4 export with matplotlib animations
20
+ - Interactive HTML viewer with play/pause, frame slider, and speed buttons
21
+ - Saving prediction bundles so the visualizer can be called from training code
22
+
23
+ Typical use:
24
+ from visualizer import (
25
+ save_prediction_bundle,
26
+ create_evaluation_visuals,
27
+ )
28
+
29
+ bundle_dir = save_prediction_bundle(
30
+ output_dir='A10/prediction_runs/demo',
31
+ predicted_xyz=pred_xyz,
32
+ ground_truth_xyz=true_xyz,
33
+ sequence_name='A1_kinect.csv',
34
+ metadata={'model': 'Dense_shallow_adam'}
35
+ )
36
+
37
+ create_evaluation_visuals(bundle_dir)
38
+ """
39
+
40
+ from dataclasses import dataclass
41
+ import json
42
+ from pathlib import Path
43
+ from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Union
44
+
45
+ import numpy as np
46
+ import matplotlib.pyplot as plt
47
+ from matplotlib.animation import FuncAnimation, PillowWriter, FFMpegWriter
48
+ from matplotlib.colors import Normalize
49
+ from matplotlib import cm
50
+
51
+ # Plotly is optional but very useful for the interactive viewer.
52
+ try:
53
+ import plotly.graph_objects as go
54
+ PLOTLY_AVAILABLE = True
55
+ except Exception:
56
+ PLOTLY_AVAILABLE = False
57
+
58
+
59
+ # -----------------------------------------------------------------------------
60
+ # Skeleton definition
61
+ # -----------------------------------------------------------------------------
62
+
63
+ JOINTS = [
64
+ 'head', 'left_shoulder', 'left_elbow', 'right_shoulder', 'right_elbow',
65
+ 'left_hand', 'right_hand', 'left_hip', 'right_hip',
66
+ 'left_knee', 'right_knee', 'left_foot', 'right_foot'
67
+ ]
68
+
69
+ JOINT_INDEX = {name: idx for idx, name in enumerate(JOINTS)}
70
+
71
+ # Bone graph for the 13 Kinect joints used in your A10 codebase.
72
+ BONES = [
73
+ ('head', 'left_shoulder'),
74
+ ('head', 'right_shoulder'),
75
+ ('left_shoulder', 'right_shoulder'),
76
+ ('left_shoulder', 'left_elbow'),
77
+ ('left_elbow', 'left_hand'),
78
+ ('right_shoulder', 'right_elbow'),
79
+ ('right_elbow', 'right_hand'),
80
+ ('left_shoulder', 'left_hip'),
81
+ ('right_shoulder', 'right_hip'),
82
+ ('left_hip', 'right_hip'),
83
+ ('left_hip', 'left_knee'),
84
+ ('left_knee', 'left_foot'),
85
+ ('right_hip', 'right_knee'),
86
+ ('right_knee', 'right_foot'),
87
+ ]
88
+
89
+ VIEW_PRESETS = {
90
+ 'front': dict(elev=15, azim=-90),
91
+ 'side': dict(elev=15, azim=0),
92
+ 'top': dict(elev=90, azim=-90),
93
+ 'iso': dict(elev=20, azim=-55),
94
+ }
95
+
96
+
97
+ # -----------------------------------------------------------------------------
98
+ # Helpers
99
+ # -----------------------------------------------------------------------------
100
+
101
+ def _as_path(pathlike: Union[str, Path]) -> Path:
102
+ return pathlike if isinstance(pathlike, Path) else Path(pathlike)
103
+
104
+
105
+ def _ensure_dir(pathlike: Union[str, Path]) -> Path:
106
+ path = _as_path(pathlike)
107
+ path.mkdir(parents=True, exist_ok=True)
108
+ return path
109
+
110
+
111
+ def reshape_xyz(data: np.ndarray) -> np.ndarray:
112
+ """
113
+ Convert xyz data into shape (n_frames, 13, 3).
114
+
115
+ Accepts:
116
+ - (n_frames, 39)
117
+ - (39,)
118
+ - (n_frames, 13, 3)
119
+ - (13, 3)
120
+ """
121
+ arr = np.asarray(data, dtype=np.float32)
122
+
123
+ if arr.ndim == 1:
124
+ if arr.shape[0] != 39:
125
+ raise ValueError(f'1D xyz input must have 39 values, got {arr.shape[0]}')
126
+ arr = arr.reshape(1, 13, 3)
127
+ elif arr.ndim == 2:
128
+ if arr.shape == (13, 3):
129
+ arr = arr.reshape(1, 13, 3)
130
+ elif arr.shape[1] == 39:
131
+ arr = arr.reshape(arr.shape[0], 13, 3)
132
+ else:
133
+ raise ValueError(f'2D xyz input must be (n,39) or (13,3); got {arr.shape}')
134
+ elif arr.ndim == 3:
135
+ if arr.shape[1:] != (13, 3):
136
+ raise ValueError(f'3D xyz input must be (n,13,3); got {arr.shape}')
137
+ else:
138
+ raise ValueError(f'Unsupported xyz input shape: {arr.shape}')
139
+
140
+ return arr
141
+
142
+
143
+ def compute_joint_errors(pred_xyz: np.ndarray, gt_xyz: np.ndarray) -> np.ndarray:
144
+ """Euclidean error per joint, shape (n_frames, 13)."""
145
+ pred = reshape_xyz(pred_xyz)
146
+ gt = reshape_xyz(gt_xyz)
147
+ n = min(len(pred), len(gt))
148
+ pred = pred[:n]
149
+ gt = gt[:n]
150
+ return np.linalg.norm(pred - gt, axis=2)
151
+
152
+
153
+ def compute_frame_errors(pred_xyz: np.ndarray, gt_xyz: np.ndarray) -> np.ndarray:
154
+ """Mean Euclidean joint error per frame, shape (n_frames,)."""
155
+ return compute_joint_errors(pred_xyz, gt_xyz).mean(axis=1)
156
+
157
+
158
+ def infer_axis_limits(*arrays: np.ndarray, pad_ratio: float = 0.08) -> Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]]:
159
+ stacked = np.concatenate([reshape_xyz(a).reshape(-1, 3) for a in arrays], axis=0)
160
+ mins = stacked.min(axis=0)
161
+ maxs = stacked.max(axis=0)
162
+ spans = np.maximum(maxs - mins, 1e-6)
163
+ pads = spans * pad_ratio
164
+ mins -= pads
165
+ maxs += pads
166
+
167
+ # Make a cubic box so the skeleton does not look distorted.
168
+ center = (mins + maxs) / 2.0
169
+ radius = max((maxs - mins).max() / 2.0, 1e-4)
170
+ return (
171
+ (center[0] - radius, center[0] + radius),
172
+ (center[1] - radius, center[1] + radius),
173
+ (center[2] - radius, center[2] + radius),
174
+ )
175
+
176
+
177
+ def _bone_segments(points: np.ndarray) -> Iterable[Tuple[np.ndarray, np.ndarray]]:
178
+ for j1, j2 in BONES:
179
+ yield points[JOINT_INDEX[j1]], points[JOINT_INDEX[j2]]
180
+
181
+
182
+ def save_prediction_bundle(
183
+ output_dir: Union[str, Path],
184
+ predicted_xyz: np.ndarray,
185
+ ground_truth_xyz: Optional[np.ndarray] = None,
186
+ sequence_name: Optional[str] = None,
187
+ metadata: Optional[Dict] = None,
188
+ posenet_xy: Optional[np.ndarray] = None,
189
+ ) -> Path:
190
+ """
191
+ Save model outputs in a simple, reusable format for the visualizer.
192
+ """
193
+ out_dir = _ensure_dir(output_dir)
194
+ pred = reshape_xyz(predicted_xyz)
195
+ np.save(out_dir / 'predicted_xyz.npy', pred)
196
+
197
+ if ground_truth_xyz is not None:
198
+ gt = reshape_xyz(ground_truth_xyz)
199
+ n = min(len(pred), len(gt))
200
+ np.save(out_dir / 'ground_truth_xyz.npy', gt[:n])
201
+ pred = pred[:n]
202
+
203
+ if posenet_xy is not None:
204
+ np.save(out_dir / 'posenet_xy.npy', np.asarray(posenet_xy, dtype=np.float32))
205
+
206
+ meta = dict(metadata or {})
207
+ meta['sequence_name'] = sequence_name
208
+ meta['n_frames'] = int(len(pred))
209
+ meta['joints'] = JOINTS
210
+ meta['bones'] = BONES
211
+ with open(out_dir / 'metadata.json', 'w', encoding='utf-8') as f:
212
+ json.dump(meta, f, indent=2)
213
+
214
+ return out_dir
215
+
216
+
217
+ def load_prediction_bundle(bundle_dir: Union[str, Path]) -> Dict[str, Optional[np.ndarray]]:
218
+ bundle = _as_path(bundle_dir)
219
+ pred = np.load(bundle / 'predicted_xyz.npy')
220
+ gt_path = bundle / 'ground_truth_xyz.npy'
221
+ xy_path = bundle / 'posenet_xy.npy'
222
+ meta_path = bundle / 'metadata.json'
223
+
224
+ gt = np.load(gt_path) if gt_path.exists() else None
225
+ posenet_xy = np.load(xy_path) if xy_path.exists() else None
226
+ metadata = {}
227
+ if meta_path.exists():
228
+ with open(meta_path, 'r', encoding='utf-8') as f:
229
+ metadata = json.load(f)
230
+
231
+ return {
232
+ 'predicted_xyz': reshape_xyz(pred),
233
+ 'ground_truth_xyz': reshape_xyz(gt) if gt is not None else None,
234
+ 'posenet_xy': posenet_xy,
235
+ 'metadata': metadata,
236
+ 'bundle_dir': bundle,
237
+ }
238
+
239
+
240
+ # -----------------------------------------------------------------------------
241
+ # Drawing
242
+ # -----------------------------------------------------------------------------
243
+
244
+ def _draw_skeleton(
245
+ ax,
246
+ points: np.ndarray,
247
+ title: Optional[str] = None,
248
+ joint_errors: Optional[np.ndarray] = None,
249
+ cmap_name: str = 'turbo',
250
+ error_norm: Optional[Normalize] = None,
251
+ bone_color: str = 'black',
252
+ marker_size: int = 36,
253
+ alpha: float = 1.0,
254
+ show_labels: bool = False,
255
+ trails: Optional[np.ndarray] = None,
256
+ ):
257
+ points = np.asarray(points, dtype=np.float32)
258
+ cmap = cm.get_cmap(cmap_name)
259
+
260
+ if joint_errors is None:
261
+ joint_colors = ['tab:blue'] * len(points)
262
+ else:
263
+ if error_norm is None:
264
+ error_norm = Normalize(vmin=float(np.min(joint_errors)), vmax=float(np.max(joint_errors)) + 1e-9)
265
+ joint_colors = [cmap(error_norm(v)) for v in joint_errors]
266
+
267
+ # Trails first
268
+ if trails is not None and len(trails) > 1:
269
+ for j in range(points.shape[0]):
270
+ trail = trails[:, j, :]
271
+ ax.plot(trail[:, 0], trail[:, 1], trail[:, 2], alpha=0.25, linewidth=1.0)
272
+
273
+ # Bones
274
+ for p1, p2 in _bone_segments(points):
275
+ ax.plot(
276
+ [p1[0], p2[0]],
277
+ [p1[1], p2[1]],
278
+ [p1[2], p2[2]],
279
+ color=bone_color,
280
+ linewidth=2,
281
+ alpha=alpha,
282
+ )
283
+
284
+ # Joints
285
+ ax.scatter(points[:, 0], points[:, 1], points[:, 2], c=joint_colors, s=marker_size, alpha=alpha)
286
+
287
+ if show_labels:
288
+ for idx, name in enumerate(JOINTS):
289
+ x, y, z = points[idx]
290
+ ax.text(x, y, z, name, fontsize=8)
291
+
292
+ if title:
293
+ ax.set_title(title)
294
+
295
+
296
+ def _format_axes(ax, axis_limits, view_name='iso'):
297
+ (xlim, ylim, zlim) = axis_limits
298
+ ax.set_xlim(*xlim)
299
+ ax.set_ylim(*ylim)
300
+ ax.set_zlim(*zlim)
301
+ ax.set_xlabel('X')
302
+ ax.set_ylabel('Y')
303
+ ax.set_zlabel('Z')
304
+ view = VIEW_PRESETS.get(view_name, VIEW_PRESETS['iso'])
305
+ ax.view_init(elev=view['elev'], azim=view['azim'])
306
+
307
+
308
+ def plot_frame_comparison(
309
+ predicted_xyz: np.ndarray,
310
+ ground_truth_xyz: Optional[np.ndarray] = None,
311
+ frame_idx: int = 0,
312
+ save_path: Optional[Union[str, Path]] = None,
313
+ show_labels: bool = False,
314
+ overlay: bool = True,
315
+ ) -> plt.Figure:
316
+ """
317
+ Create a report-friendly static figure.
318
+ Layout:
319
+ - GT skeleton
320
+ - Pred skeleton
321
+ - Overlay
322
+ - Error heatmap overlay
323
+ """
324
+ pred = reshape_xyz(predicted_xyz)
325
+ gt = reshape_xyz(ground_truth_xyz) if ground_truth_xyz is not None else None
326
+ frame_idx = int(np.clip(frame_idx, 0, len(pred) - 1))
327
+ pred_f = pred[frame_idx]
328
+ gt_f = gt[frame_idx] if gt is not None else None
329
+
330
+ if gt_f is not None:
331
+ joint_errors = np.linalg.norm(pred_f - gt_f, axis=1)
332
+ error_norm = Normalize(vmin=0.0, vmax=max(float(joint_errors.max()), 1e-6))
333
+ axis_limits = infer_axis_limits(pred_f, gt_f)
334
+ else:
335
+ joint_errors = None
336
+ error_norm = None
337
+ axis_limits = infer_axis_limits(pred_f)
338
+
339
+ if gt_f is None:
340
+ fig = plt.figure(figsize=(6, 6))
341
+ ax = fig.add_subplot(111, projection='3d')
342
+ _draw_skeleton(ax, pred_f, title=f'Predicted skeleton — frame {frame_idx}', show_labels=show_labels)
343
+ _format_axes(ax, axis_limits, 'iso')
344
+ else:
345
+ fig = plt.figure(figsize=(14, 10))
346
+ ax1 = fig.add_subplot(221, projection='3d')
347
+ ax2 = fig.add_subplot(222, projection='3d')
348
+ ax3 = fig.add_subplot(223, projection='3d')
349
+ ax4 = fig.add_subplot(224, projection='3d')
350
+
351
+ _draw_skeleton(ax1, gt_f, title='Ground truth', bone_color='tab:green', show_labels=show_labels)
352
+ _draw_skeleton(ax2, pred_f, title='Prediction', bone_color='tab:blue', show_labels=show_labels)
353
+
354
+ if overlay:
355
+ _draw_skeleton(ax3, gt_f, title='Overlay', bone_color='tab:green', alpha=0.65)
356
+ _draw_skeleton(ax3, pred_f, bone_color='tab:blue', alpha=0.65)
357
+ else:
358
+ _draw_skeleton(ax3, pred_f, title='Prediction')
359
+
360
+ _draw_skeleton(
361
+ ax4,
362
+ pred_f,
363
+ title=f'Error heatmap (mean={joint_errors.mean():.4f})',
364
+ joint_errors=joint_errors,
365
+ error_norm=error_norm,
366
+ bone_color='gray',
367
+ show_labels=show_labels,
368
+ )
369
+
370
+ for ax, view in zip([ax1, ax2, ax3, ax4], ['iso', 'iso', 'front', 'iso']):
371
+ _format_axes(ax, axis_limits, view)
372
+
373
+ fig.suptitle(f'3D Skeleton comparison — frame {frame_idx}', fontsize=14)
374
+ fig.tight_layout()
375
+
376
+ if save_path is not None:
377
+ save_path = _as_path(save_path)
378
+ save_path.parent.mkdir(parents=True, exist_ok=True)
379
+ fig.savefig(save_path, dpi=160, bbox_inches='tight')
380
+
381
+ return fig
382
+
383
+
384
+ def plot_multiview_frame(
385
+ predicted_xyz: np.ndarray,
386
+ ground_truth_xyz: Optional[np.ndarray] = None,
387
+ frame_idx: int = 0,
388
+ save_path: Optional[Union[str, Path]] = None,
389
+ trails: int = 0,
390
+ ):
391
+ pred = reshape_xyz(predicted_xyz)
392
+ gt = reshape_xyz(ground_truth_xyz) if ground_truth_xyz is not None else None
393
+ frame_idx = int(np.clip(frame_idx, 0, len(pred) - 1))
394
+ pred_f = pred[frame_idx]
395
+ gt_f = gt[frame_idx] if gt is not None else None
396
+
397
+ trail_arr = None
398
+ if trails > 0:
399
+ s = max(0, frame_idx - trails)
400
+ trail_arr = pred[s:frame_idx + 1]
401
+
402
+ axis_limits = infer_axis_limits(pred, gt) if gt is not None else infer_axis_limits(pred)
403
+
404
+ fig = plt.figure(figsize=(16, 4.5))
405
+ axes = [fig.add_subplot(1, 4, i + 1, projection='3d') for i in range(4)]
406
+ titles = ['Front', 'Side', 'Top', 'Overlay / iso']
407
+ views = ['front', 'side', 'top', 'iso']
408
+
409
+ if gt_f is not None:
410
+ joint_errors = np.linalg.norm(pred_f - gt_f, axis=1)
411
+ error_norm = Normalize(vmin=0.0, vmax=max(float(joint_errors.max()), 1e-6))
412
+ else:
413
+ joint_errors = None
414
+ error_norm = None
415
+
416
+ for ax, title, view in zip(axes, titles, views):
417
+ if gt_f is not None and view == 'iso':
418
+ _draw_skeleton(ax, gt_f, bone_color='tab:green', alpha=0.6)
419
+ _draw_skeleton(ax, pred_f, joint_errors=joint_errors, error_norm=error_norm, bone_color='gray', alpha=0.85, trails=trail_arr)
420
+ else:
421
+ _draw_skeleton(ax, pred_f, joint_errors=joint_errors, error_norm=error_norm, bone_color='gray', trails=trail_arr)
422
+ ax.set_title(title)
423
+ _format_axes(ax, axis_limits, view)
424
+
425
+ fig.suptitle(f'Multiview 3D skeleton — frame {frame_idx}', fontsize=14)
426
+ fig.tight_layout()
427
+ if save_path is not None:
428
+ save_path = _as_path(save_path)
429
+ save_path.parent.mkdir(parents=True, exist_ok=True)
430
+ fig.savefig(save_path, dpi=160, bbox_inches='tight')
431
+ return fig
432
+
433
+
434
+ # -----------------------------------------------------------------------------
435
+ # Animation export
436
+ # -----------------------------------------------------------------------------
437
+
438
+ def animate_skeletons_matplotlib(
439
+ predicted_xyz: np.ndarray,
440
+ ground_truth_xyz: Optional[np.ndarray] = None,
441
+ save_path: Union[str, Path] = 'animation.gif',
442
+ fps: int = 15,
443
+ dpi: int = 120,
444
+ show_labels: bool = False,
445
+ trail_length: int = 10,
446
+ view_name: str = 'iso',
447
+ ):
448
+ """
449
+ Save a GIF or MP4 animation.
450
+ Uses a 3-panel layout: GT, prediction, overlay/error.
451
+ """
452
+ pred = reshape_xyz(predicted_xyz)
453
+ gt = reshape_xyz(ground_truth_xyz) if ground_truth_xyz is not None else None
454
+ n_frames = len(pred) if gt is None else min(len(pred), len(gt))
455
+ pred = pred[:n_frames]
456
+ if gt is not None:
457
+ gt = gt[:n_frames]
458
+ all_joint_errors = np.linalg.norm(pred - gt, axis=2)
459
+ error_norm = Normalize(vmin=0.0, vmax=max(float(all_joint_errors.max()), 1e-6))
460
+ else:
461
+ all_joint_errors = None
462
+ error_norm = None
463
+
464
+ axis_limits = infer_axis_limits(pred, gt) if gt is not None else infer_axis_limits(pred)
465
+
466
+ fig = plt.figure(figsize=(15, 5))
467
+ axes = [fig.add_subplot(1, 3, i + 1, projection='3d') for i in range(3)]
468
+
469
+ def update(frame_idx):
470
+ for ax in axes:
471
+ ax.cla()
472
+
473
+ pred_f = pred[frame_idx]
474
+ gt_f = gt[frame_idx] if gt is not None else None
475
+ trail = pred[max(0, frame_idx - trail_length):frame_idx + 1] if trail_length > 0 else None
476
+
477
+ if gt_f is not None:
478
+ joint_errors = all_joint_errors[frame_idx]
479
+ _draw_skeleton(axes[0], gt_f, title='Ground truth', bone_color='tab:green', show_labels=show_labels)
480
+ _draw_skeleton(axes[1], pred_f, title='Prediction', joint_errors=joint_errors, error_norm=error_norm, bone_color='gray', show_labels=show_labels)
481
+ _draw_skeleton(axes[2], gt_f, title=f'Overlay — frame {frame_idx}', bone_color='tab:green', alpha=0.5)
482
+ _draw_skeleton(axes[2], pred_f, joint_errors=joint_errors, error_norm=error_norm, bone_color='gray', alpha=0.9, trails=trail)
483
+ else:
484
+ _draw_skeleton(axes[0], pred_f, title=f'Prediction — frame {frame_idx}', bone_color='tab:blue', show_labels=show_labels, trails=trail)
485
+ axes[1].set_visible(False)
486
+ axes[2].set_visible(False)
487
+
488
+ for ax in axes:
489
+ if ax.get_visible():
490
+ _format_axes(ax, axis_limits, view_name)
491
+
492
+ fig.suptitle(f'3D skeleton animation — frame {frame_idx + 1}/{n_frames}', fontsize=14)
493
+ return axes
494
+
495
+ anim = FuncAnimation(fig, update, frames=n_frames, interval=int(1000 / max(fps, 1)), blit=False)
496
+
497
+ save_path = _as_path(save_path)
498
+ save_path.parent.mkdir(parents=True, exist_ok=True)
499
+ suffix = save_path.suffix.lower()
500
+ if suffix == '.gif':
501
+ writer = PillowWriter(fps=fps)
502
+ elif suffix in {'.mp4', '.m4v'}:
503
+ writer = FFMpegWriter(fps=fps)
504
+ else:
505
+ raise ValueError('save_path must end with .gif or .mp4')
506
+
507
+ anim.save(save_path, writer=writer, dpi=dpi)
508
+ plt.close(fig)
509
+ return save_path
510
+
511
+
512
+ # -----------------------------------------------------------------------------
513
+ # Plotly interactive viewer
514
+ # -----------------------------------------------------------------------------
515
+
516
+ def _scatter3d_points(points, name, color, size=5, text=None):
517
+ return go.Scatter3d(
518
+ x=points[:, 0], y=points[:, 1], z=points[:, 2],
519
+ mode='markers+text' if text is not None else 'markers',
520
+ marker=dict(size=size, color=color),
521
+ text=text,
522
+ textposition='top center',
523
+ name=name,
524
+ )
525
+
526
+
527
+ def _scatter3d_bones(points, name, color, width=5):
528
+ xs, ys, zs = [], [], []
529
+ for p1, p2 in _bone_segments(points):
530
+ xs.extend([p1[0], p2[0], None])
531
+ ys.extend([p1[1], p2[1], None])
532
+ zs.extend([p1[2], p2[2], None])
533
+ return go.Scatter3d(x=xs, y=ys, z=zs, mode='lines', line=dict(color=color, width=width), name=name)
534
+
535
+
536
+ def export_interactive_html(
537
+ predicted_xyz: np.ndarray,
538
+ ground_truth_xyz: Optional[np.ndarray] = None,
539
+ html_path: Union[str, Path] = 'viewer.html',
540
+ show_labels: bool = False,
541
+ ):
542
+ """
543
+ Export an interactive Plotly viewer with:
544
+ - play / pause
545
+ - frame stepping via slider
546
+ - speed buttons
547
+ - optional GT overlay toggle via legend
548
+ """
549
+ if not PLOTLY_AVAILABLE:
550
+ raise RuntimeError('Plotly is not installed. Run: pip install plotly')
551
+
552
+ pred = reshape_xyz(predicted_xyz)
553
+ gt = reshape_xyz(ground_truth_xyz) if ground_truth_xyz is not None else None
554
+ n_frames = len(pred) if gt is None else min(len(pred), len(gt))
555
+ pred = pred[:n_frames]
556
+ if gt is not None:
557
+ gt = gt[:n_frames]
558
+ err = np.linalg.norm(pred - gt, axis=2)
559
+ err_mean = err.mean(axis=1)
560
+ else:
561
+ err = None
562
+ err_mean = np.zeros(n_frames)
563
+
564
+ (xlim, ylim, zlim) = infer_axis_limits(pred, gt) if gt is not None else infer_axis_limits(pred)
565
+ text = JOINTS if show_labels else None
566
+
567
+ def frame_data(i, speed_label='normal'):
568
+ pred_f = pred[i]
569
+ traces = [
570
+ _scatter3d_bones(pred_f, 'Prediction bones', 'royalblue'),
571
+ _scatter3d_points(pred_f, 'Prediction joints', 'royalblue', text=text),
572
+ ]
573
+ if gt is not None:
574
+ gt_f = gt[i]
575
+ traces += [
576
+ _scatter3d_bones(gt_f, 'Ground truth bones', 'green'),
577
+ _scatter3d_points(gt_f, 'Ground truth joints', 'green', text=text),
578
+ ]
579
+ return traces
580
+
581
+ frames = [go.Frame(data=frame_data(i), name=str(i), layout=go.Layout(title_text=f'Frame {i} | mean error={err_mean[i]:.4f}' if gt is not None else f'Frame {i}')) for i in range(n_frames)]
582
+
583
+ fig = go.Figure(data=frame_data(0), frames=frames)
584
+ fig.update_layout(
585
+ title='Interactive 3D skeleton viewer',
586
+ scene=dict(
587
+ xaxis=dict(range=list(xlim), title='X'),
588
+ yaxis=dict(range=list(ylim), title='Y'),
589
+ zaxis=dict(range=list(zlim), title='Z'),
590
+ aspectmode='cube',
591
+ camera=dict(eye=dict(x=1.3, y=1.3, z=0.8)),
592
+ ),
593
+ updatemenus=[
594
+ dict(
595
+ type='buttons',
596
+ direction='left',
597
+ x=0.0,
598
+ y=1.15,
599
+ buttons=[
600
+ dict(label='Play', method='animate', args=[None, {'frame': {'duration': 80, 'redraw': True}, 'fromcurrent': True}]),
601
+ dict(label='Pause', method='animate', args=[[None], {'frame': {'duration': 0, 'redraw': False}, 'mode': 'immediate'}]),
602
+ dict(label='Slow', method='animate', args=[None, {'frame': {'duration': 180, 'redraw': True}, 'fromcurrent': True}]),
603
+ dict(label='Normal', method='animate', args=[None, {'frame': {'duration': 80, 'redraw': True}, 'fromcurrent': True}]),
604
+ dict(label='Fast', method='animate', args=[None, {'frame': {'duration': 30, 'redraw': True}, 'fromcurrent': True}]),
605
+ ],
606
+ )
607
+ ],
608
+ sliders=[{
609
+ 'pad': {'b': 10, 't': 35},
610
+ 'len': 0.95,
611
+ 'x': 0.03,
612
+ 'y': 0.0,
613
+ 'steps': [
614
+ {
615
+ 'args': [[str(i)], {'frame': {'duration': 0, 'redraw': True}, 'mode': 'immediate'}],
616
+ 'label': str(i),
617
+ 'method': 'animate',
618
+ }
619
+ for i in range(n_frames)
620
+ ],
621
+ }],
622
+ showlegend=True,
623
+ )
624
+
625
+ html_path = _as_path(html_path)
626
+ html_path.parent.mkdir(parents=True, exist_ok=True)
627
+ fig.write_html(str(html_path), include_plotlyjs='cdn')
628
+ return html_path
629
+
630
+
631
+ # -----------------------------------------------------------------------------
632
+ # High-level workflow helpers
633
+ # -----------------------------------------------------------------------------
634
+
635
+ def create_evaluation_visuals(
636
+ bundle_dir: Union[str, Path],
637
+ frame_indices: Optional[Sequence[int]] = None,
638
+ export_gif: bool = True,
639
+ export_mp4: bool = False,
640
+ export_html: bool = True,
641
+ fps: int = 15,
642
+ trail_length: int = 10,
643
+ ) -> Dict[str, List[str]]:
644
+ """
645
+ Generate all standard outputs into:
646
+ - bundle_dir/skeleton_plots/
647
+ - bundle_dir/animations/
648
+ """
649
+ bundle = load_prediction_bundle(bundle_dir)
650
+ pred = bundle['predicted_xyz']
651
+ gt = bundle['ground_truth_xyz']
652
+ n_frames = len(pred) if gt is None else min(len(pred), len(gt))
653
+
654
+ plot_dir = _ensure_dir(_as_path(bundle_dir) / 'skeleton_plots')
655
+ anim_dir = _ensure_dir(_as_path(bundle_dir) / 'animations')
656
+ outputs = {'plots': [], 'animations': [], 'interactive': []}
657
+
658
+ if frame_indices is None:
659
+ frame_indices = sorted(set([0, max(0, n_frames // 2), max(0, n_frames - 1)]))
660
+
661
+ for frame_idx in frame_indices:
662
+ frame_idx = int(np.clip(frame_idx, 0, n_frames - 1))
663
+ static_path = plot_dir / f'comparison_frame_{frame_idx:04d}.png'
664
+ multiview_path = plot_dir / f'multiview_frame_{frame_idx:04d}.png'
665
+ plot_frame_comparison(pred, gt, frame_idx=frame_idx, save_path=static_path)
666
+ plt.close('all')
667
+ plot_multiview_frame(pred, gt, frame_idx=frame_idx, save_path=multiview_path, trails=trail_length)
668
+ plt.close('all')
669
+ outputs['plots'] += [str(static_path), str(multiview_path)]
670
+
671
+ if export_gif:
672
+ gif_path = anim_dir / 'comparison_animation.gif'
673
+ animate_skeletons_matplotlib(pred, gt, gif_path, fps=fps, trail_length=trail_length)
674
+ outputs['animations'].append(str(gif_path))
675
+
676
+ if export_mp4:
677
+ mp4_path = anim_dir / 'comparison_animation.mp4'
678
+ animate_skeletons_matplotlib(pred, gt, mp4_path, fps=fps, trail_length=trail_length)
679
+ outputs['animations'].append(str(mp4_path))
680
+
681
+ if export_html:
682
+ html_path = anim_dir / 'interactive_viewer.html'
683
+ export_interactive_html(pred, gt, html_path)
684
+ outputs['interactive'].append(str(html_path))
685
+
686
+ summary = {
687
+ 'bundle_dir': str(bundle_dir),
688
+ 'n_frames': int(n_frames),
689
+ 'has_ground_truth': gt is not None,
690
+ 'outputs': outputs,
691
+ }
692
+ with open(_as_path(bundle_dir) / 'visualization_summary.json', 'w', encoding='utf-8') as f:
693
+ json.dump(summary, f, indent=2)
694
+ return outputs
695
+
696
+
697
+ def save_prediction_bundle_from_model(
698
+ model,
699
+ X_input: np.ndarray,
700
+ y_true_xyz: Optional[np.ndarray],
701
+ output_dir: Union[str, Path],
702
+ output_scaler=None,
703
+ sequence_name: Optional[str] = None,
704
+ metadata: Optional[Dict] = None,
705
+ ):
706
+ """
707
+ Convenience helper for training code.
708
+ - model.predict on X_input
709
+ - optional inverse transform using output_scaler
710
+ - save prediction bundle
711
+ """
712
+ pred = model.predict(X_input, verbose=0)
713
+ if output_scaler is not None:
714
+ pred = output_scaler.inverse_transform(pred)
715
+ if y_true_xyz is not None:
716
+ y_true_xyz = output_scaler.inverse_transform(y_true_xyz)
717
+
718
+ return save_prediction_bundle(
719
+ output_dir=output_dir,
720
+ predicted_xyz=pred,
721
+ ground_truth_xyz=y_true_xyz,
722
+ sequence_name=sequence_name,
723
+ metadata=metadata,
724
+ )
725
+
726
+
727
+ if __name__ == '__main__':
728
+ import argparse
729
+
730
+ parser = argparse.ArgumentParser(description='A10 3D skeleton visualizer')
731
+ parser.add_argument('--bundle_dir', type=str, help='Folder containing predicted_xyz.npy and optional ground_truth_xyz.npy')
732
+ parser.add_argument('--pred_npy', type=str, help='Path to predicted xyz .npy file')
733
+ parser.add_argument('--gt_npy', type=str, default=None, help='Path to ground-truth xyz .npy file')
734
+ parser.add_argument('--out_dir', type=str, default='visualizer_outputs', help='Output directory when using --pred_npy/--gt_npy')
735
+ parser.add_argument('--fps', type=int, default=15)
736
+ parser.add_argument('--no_html', action='store_true')
737
+ parser.add_argument('--mp4', action='store_true')
738
+ args = parser.parse_args()
739
+
740
+ if args.bundle_dir:
741
+ create_evaluation_visuals(
742
+ bundle_dir=args.bundle_dir,
743
+ export_gif=True,
744
+ export_mp4=args.mp4,
745
+ export_html=not args.no_html,
746
+ fps=args.fps,
747
+ )
748
+ print(f'Visualization outputs created in {args.bundle_dir}')
749
+ elif args.pred_npy:
750
+ pred = np.load(args.pred_npy)
751
+ gt = np.load(args.gt_npy) if args.gt_npy else None
752
+ bundle = save_prediction_bundle(args.out_dir, pred, gt)
753
+ create_evaluation_visuals(
754
+ bundle_dir=bundle,
755
+ export_gif=True,
756
+ export_mp4=args.mp4,
757
+ export_html=not args.no_html,
758
+ fps=args.fps,
759
+ )
760
+ print(f'Visualization outputs created in {bundle}')
761
+ else:
762
+ parser.error('Provide either --bundle_dir or --pred_npy')