Skeleton-based action recognition using MediaPipe landmarks and a lightweight Graph Convolutional Network
This project classifies 18 gym exercises from short video clips (~1.5s) using skeleton keypoints β no raw pixels, no background, no clothing. The approach mirrors how a physical therapist assesses movement: by observing how the body moves, not what it looks like.
The pipeline extracts 33 body landmarks per frame with MediaPipe PoseLandmarker, computes three complementary skeleton features (J, B, C), and classifies them with TFC-GCN β a lightweight graph convolutional network with only 193K parameters.
Key finding: The split methodology matters critically. An initial split at clip level yielded 87.87% accuracy β later revealed as inflated by data leakage (91.5% of test clips had their source video in train). After correcting to a source-video-grouped split, the TFC-GCN v4 ensemble achieves 71.72% β a rigorous, leakage-free result.
| Model | Params | Test Acc | F1 (w.) | Notes |
|---|---|---|---|---|
| MLP (flat) | 4,935,826 | 73.74% | 73.90% | No structural bias; 25Γ more params |
| BiLSTM | 868,580 | 68.69% | 68.72% | Temporal bias only |
| TFC-GCN v1 (SGD) | 193,664 | 18.86% | 12.66% | SGD collapses on small datasets |
| TFC-GCN v2 (Adam) | 193,664 | 70.03% | 70.49% | Flip aug. destroys deadlift recall |
| TFC-GCN v3 | 193,664 | 69.36% | 69.59% | Label smoothing |
| TFC-GCN v4 | 193,664 | 70.03% | 69.67% | Class-weighted CE + CosineWarmRestart |
| TFC-GCN v4 Ensemble | 3Γ193,664 | 71.72% | 71.71% | 3 seeds, soft voting |
| MLP-Compact | 190,518 | 65.66% | 66.58% | Parameter-matched baseline |
Parameter efficiency: TFC-GCN v4 achieves 36.2% accuracy per 100K params vs. 1.5% for the flat MLP β 24Γ more efficient. When matched at ~190K params, the GCN beats MLP-Compact by +6.06pp, confirming that graph inductive biases provide genuine value.
flowchart LR
A("πΉ Raw Videos\n3,088 MP4\n18 classes") --> B
subgraph B["βοΈ Stage 1 β Curation"]
direction TB
B1["Discover & count"] --> B2["Undersample to 123/class"]
B2 --> B3["Source-video-grouped split\n70/15/15"]
end
B --> C
subgraph C["𦴠Stage 2 β Pose Extraction"]
direction TB
C1["MediaPipe\n33 landmarks / frame"] --> C2["Sample 32 frames uniform"]
C2 --> C3["Compute J, B, C features"]
C3 --> C4["Tensor (9, 32, 33)"]
end
C --> D
subgraph D["π§ Stage 3 β TFC-GCN v4"]
direction TB
D1["3-stream input\nJ | B | C"] --> D2["GCNTFCLayer Γ 4"]
D2 --> D3["Global Avg Pool\n+ Dropout"]
D3 --> D4["FC β 18 classes"]
end
D --> E("β
71.72%\nEnsemble β Clean Split")
style A fill:#2d3748,color:#fff,stroke:#4a5568
style E fill:#276749,color:#fff,stroke:#2f855a
The dataset contains source videos split into multiple short clips. An initial split at clip level allowed clips from the same source video to appear in both train and test β 91.5% of test samples had their source video in train.
| Model | Leaked split | Clean split | Drop |
|---|---|---|---|
| MLP | 86.09% | 73.74% | β12pp |
| BiLSTM | 89.64% | 68.69% | β21pp |
| TFC-GCN v1 | 87.87% | 18.86% | β69pp |
Fix: metadata_clean.json groups all clips of the same source video into the same split. All results in this project use this clean split unless otherwise noted.
Three complementary views of the same movement:
ββ J β Joint Position (relative to hip midpoint) ββββββββββββββββββ
J[t,i] = x[t,i] β (x[t,23] + x[t,24]) / 2
β Invariant to absolute position in frame and body size.
ββ B β Bone Vector (joint-to-parent geometry) βββββββββββββββββββββ
B[t,i] = x[t,i] β x[t, parent(i)]
β Captures pose geometry: joint angles, bone orientations.
ββ C β Relative Displacement (movement dynamics) ββββββββββββββββββ
C[t,i] = x[tβ1,i] β center[t+1]
β Captures speed and direction of each joint's movement.
Concatenated β (9, 32, 33) float32 tensor per video (~37 KB Β· ~77 MB for the full dataset).
flowchart TB
J("x_J β 3Γ32Γ33") --> IC1["Conv 1Γ1 Β· 3β64"]
B("x_B β 3Γ32Γ33") --> IC2["Conv 1Γ1 Β· 3β64"]
C("x_C β 3Γ32Γ33") --> IC3["Conv 1Γ1 Β· 3β64"]
IC1 & IC2 & IC3 --> FU["Concat + Fuse Β· 192β64"]
FU --> L1["GCNTFCLayer 64β64"]
L1 --> L2["GCNTFCLayer 64β64"]
L2 --> L3["GCNTFCLayer 64β48 stride 2"]
L3 --> L4["GCNTFCLayer 48β48 stride 2"]
L4 --> GAP["Global Avg Pool"]
GAP --> DO["Dropout p=0.5"]
DO --> OUT(["FC 48 β 18 Β· class logits"])
style J fill:#1a365d,color:#fff,stroke:#2b6cb0
style B fill:#1a365d,color:#fff,stroke:#2b6cb0
style C fill:#1a365d,color:#fff,stroke:#2b6cb0
style OUT fill:#276749,color:#fff,stroke:#2f855a
Each GCNTFCLayer stacks:
| Component | Role |
|---|---|
GCNLayer β Γ β M |
Aggregates anatomically connected joints; M is a learnable attention mask |
| TFC Block | 4 parallel streams: PointConv Β· PointConv+Pool Β· CrossExtractionConv Β· GatedCNN (GLU) |
| SST-Att | Spatial-temporal attention over joints and frames per class |
Total: 193,664 parameters β under 1 MB.
| Hyperparameter | Value | Rationale |
|---|---|---|
| Optimizer | Adam (Ξ²β=0.9, Ξ²β=0.999) | SGD diverges with ~1,500 training samples |
| Learning rate | 1Γ10β»Β³ | Standard for Adam on small datasets |
| Scheduler | CosineAnnealingWarmRestarts (Tβ=50) | Periodic restarts escape local minima |
| Loss | Class-weighted CE + label smoothing Ξ΅=0.1 | Compensates imbalanced test distribution |
| Weight decay | 1Γ10β»β΄ | L2 regularization |
| Early stopping | patience=30 on val_acc | Best checkpoint at epoch 106, val_acc=78.05% |
| Augmentation | Temporal flip (50%) + Gaussian noise Ο=0.01 + scale Γ[0.95,1.05] | No horizontal flip (destroys deadlift recall) |
| Normalization | Z-score per channel over train set | Critical β without it accuracy drops below 20% |
Ensemble: 3 models (seeds 42, 0, 1) with soft-voting. Individual seeds: 70.03% / 68.69% / 65.66%. Ensemble: 71.72% (+1.68pp over best seed).
- Original: 3,088 videos Β· 18 classes Β· ~1.48s @ 25fps Β· 1280Γ720
- After undersample: 2,214 videos (123/class)
- After pose extraction: 2,126 tensors (96.0% success rate)
- Clean split: Train 1,501 Β· Val 328 Β· Test 297
Highest failure rates: incline bench press (13%) and bench press (12%) β exercises performed lying on a bench, where full-body landmark detection is harder.
.
βββ 01_prepare_dataset.py # Curation, undersample, source-video-grouped splits
βββ 02_extract_pose.py # MediaPipe extraction + J/B/C features (--t-frames flag)
βββ 03_tfc_gcn_model.py # TFC-GCN architecture (193K params)
βββ 04_train.py # TFC-GCN v1 β SGD + Nesterov + warmup + cosine
βββ 04_train_v2.py # TFC-GCN v2 β Adam + horizontal flip
βββ 05_train_v3.py # TFC-GCN v3 β Adam + label smoothing
βββ 06_train_v4.py # TFC-GCN v4 β Adam + class weights + CosineWarmRestart β
βββ eval_v4_ensemble.py # Ensemble evaluation (3 seeds, soft voting)
βββ 05_evaluate.py # Single-model evaluation β metrics, confusion matrix
βββ main.py # End-to-end pipeline orchestrator
β
βββ baselines/
β βββ baseline_a_mlp.py # MLP flat 9504β512β128β18 (4.93M params)
β βββ baseline_b_lstm.py # BiLSTM on (T, 99) sequences (868K params)
β βββ baseline_c_mlp_gap.py # MLP with temporal GAP (47K params)
β βββ baseline_d_mlp_compact.py # MLP parameter-matched to TFC-GCN (190K params)
β βββ COMPARACION.md # Baseline comparison (original split)
β
βββ INFORME/
β βββ INFORME_COMPLETO.md # Full academic report (template-compliant)
β βββ generate_figures.py # Generates all report figures
β βββ figures/ # 16 figures (PNG) for the report
β
βββ processed_data/
β βββ metadata.json # Original split (has data leakage β do not use for eval)
β βββ metadata_clean.json # Clean split grouped by source video β
β βββ features/ # (9,32,33) tensors β T=32 (2,126 files, ~77 MB)
β βββ features_t48/ # (9,48,33) tensors β T=48 (experimental)
β βββ analysis/ # Dataset & feature plots (PNG)
β
βββ results/ # TFC-GCN v1 β original split
βββ results_clean/ # TFC-GCN v1 β clean split
βββ results_v2_clean/ # TFC-GCN v2
βββ results_v3_clean/ # TFC-GCN v3
βββ results_v4_clean/ # TFC-GCN v4 seed=42 β main model
βββ results_v4_s0/ # TFC-GCN v4 seed=0
βββ results_v4_s1/ # TFC-GCN v4 seed=1
β
βββ INFORME_FINAL.md # Development notes & iterative analysis
βββ COMPARACION_CLEAN.md # Full model comparison on clean split
βββ PRESENTACION_FINAL.md # Final presentation slide guide
βββ PRESENTACION_1.md # First presentation (leakage narrative)
βββ TESIS.md # Master's thesis proposal (FormCheck system)
βββ articles/ # Reference PDFs (TFC-GCN, GLU, NTU)
βββ requirements.txt
βββ .gitignore
Not tracked by git:
raw_datasets/(21 GB) Β·processed_data/features/(~77 MB) Β·venv/Β·*.pthmodel weights Β·pose_landmarker_lite.task
# 1. Clone & install
git clone <repo-url> && cd VCP/TP
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# 2. Full pipeline β clean split (recommended)
python 01_prepare_dataset.py
python 02_extract_pose.py --metadata processed_data/metadata_clean.json
python 06_train_v4.py --metadata processed_data/metadata_clean.json --output results_v4_clean
python eval_v4_ensemble.py # ensemble of 3 seeds β 71.72%
# 3. Run all baselines
python baselines/baseline_a_mlp.py --metadata processed_data/metadata_clean.json --output baselines/results_a_clean
python baselines/baseline_b_lstm.py --metadata processed_data/metadata_clean.json --output baselines/results_b_clean
python baselines/baseline_d_mlp_compact.py --metadata processed_data/metadata_clean.json --output baselines/results_d_clean- Wang K, Deng H. TFC-GCN: Lightweight Temporal Feature Cross-Extraction Graph Convolutional Network for Skeleton-Based Action Recognition. Sensors 2023, 23, 5593. https://doi.org/10.3390/s23125593
- Dauphin YN et al. Language Modeling with Gated Convolutional Networks. ICML 2017.
- Kipf TN, Welling M. Semi-Supervised Classification with Graph Convolutional Networks. ICLR 2017.
- MediaPipe Pose Landmark Model. Google LLC, 2023. https://developers.google.com/mediapipe/solutions/vision/pose_landmarker
MIA304 VisiΓ³n y PercepciΓ³n Computarizada Β· Universidad de San AndrΓ©s Β· Mayo 2026
