| name | game-analytics-platform-computer-vision |
| description | Real-time computer vision fitness game platform using YOLO, MediaPipe, Spring Boot orchestration, and React dashboard for webcam-based exercise tracking |
| triggers | ["how do I set up the game analytics platform","create a new computer vision exercise game","integrate YOLO tracking with MediaPipe pose detection","build a fitness tracking game with webcam","manage Python AI processes from Spring Boot","export exercise metrics to CSV from vision data","configure real-time pose estimation game","add text-to-speech coaching to workout tracker"] |
Game Analytics Platform - Computer Vision Fitness Tracker
Skill by ara.so — Data Skills collection.
What This Project Does
Game Analytics Platform is a local-first, real-time computer vision system that tracks user movements across 16 fitness exercises using webcam input. It combines:
- YOLO v8 for object detection and tracking (balls, cones, people)
- MediaPipe for skeletal pose estimation and form validation
- Spring Boot (Java 17) backend for process orchestration
- React + Vite frontend dashboard for game control
- Python AI scripts that export workout metrics to CSV
- pyttsx3 for real-time audio coaching
The architecture runs entirely locally with a 3-tier design: React UI → Spring Boot API → Python AI processes.
Installation
Prerequisites
Install these first:
- Python 3.10+ (ensure "Add to PATH" is checked)
- Java 17 (from Adoptium)
- Node.js LTS
Auto-Install
python install.py
python3 install.py
This creates a Python virtual environment, installs dependencies, downloads YOLO models, and builds the frontend.
Manual Setup (if auto-install fails)
python -m venv venv
venv\Scripts\activate
source venv/bin/activate
pip install ultralytics mediapipe opencv-python pandas pyttsx3
cd frontend
npm install
npm run build
cd ..
cd backend
mvn clean package
cd ..
Starting the Platform
start.bat
./start.sh
Access dashboard at http://localhost:8080
Architecture Components
1. Spring Boot Backend (Java)
The backend orchestrates Python AI processes via REST API.
Key Files:
backend/src/main/java/com/gameanalytics/controller/GameController.java
backend/src/main/java/com/gameanalytics/service/ProcessService.java
REST API Endpoints:
POST /api/games/{id}/start
POST /api/games/{id}/stop
GET /api/games/data
GET /api/games
Process Management Pattern:
public class ProcessService {
private Process currentProcess;
private final Object lock = new Object();
public boolean startGame(int gameId) {
synchronized (lock) {
if (currentProcess != null && currentProcess.isAlive()) {
return false;
}
String pythonPath = System.getProperty("os.name").toLowerCase().contains("win")
? "venv\\Scripts\\python.exe"
: "venv/bin/python";
String scriptPath = "games/exe_" + gameId + ".py";
ProcessBuilder pb = new ProcessBuilder(pythonPath, scriptPath);
pb.directory(new File(System.getProperty("user.dir")));
pb.redirectErrorStream(true);
try {
currentProcess = pb.start();
(() -> {
( (
(currentProcess.getInputStream()))) {
String line;
((line = reader.readLine()) != ) {
System.out.println( + line);
}
} (IOException e) {
e.printStackTrace();
}
}).start();
;
} (IOException e) {
e.printStackTrace();
;
}
}
}
{
(lock) {
(currentProcess != && currentProcess.isAlive()) {
currentProcess.destroy();
{
currentProcess.waitFor(, TimeUnit.SECONDS);
} (InterruptedException e) {
currentProcess.destroyForcibly();
}
currentProcess = ;
;
}
;
}
}
}
2. Python AI Vision Scripts
Each game is a standalone Python script in games/exe_*.py.
Template for New Game:
import cv2
import pandas as pd
import numpy as np
from ultralytics import YOLO
import mediapipe as mp
import pyttsx3
import signal
import sys
from datetime import datetime
import threading
running = True
event_buffer = []
tts_engine = None
def signal_handler(sig, frame):
"""Handle SIGTERM from Java backend"""
global running
print("Received stop signal, cleaning up...")
running = False
def tts_worker(queue):
"""Async text-to-speech thread"""
global tts_engine
tts_engine = pyttsx3.init()
while running:
if not queue.empty():
message = queue.get()
tts_engine.say(message)
tts_engine.runAndWait()
def main():
global running, event_buffer
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
yolo_model = YOLO('models/yolov8n.pt')
mp_pose = mp.solutions.pose
pose = mp_pose.Pose(
min_detection_confidence=0.5,
min_tracking_confidence=0.5
)
queue Queue
tts_queue = Queue()
tts_thread = threading.Thread(target=tts_worker, args=(tts_queue,))
tts_thread.daemon =
tts_thread.start()
cap = cv2.VideoCapture()
cap.isOpened():
()
rep_count =
last_state =
()
running:
ret, frame = cap.read()
ret:
frame = cv2.resize(frame, (, ))
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
yolo_results = yolo_model.track(frame, persist=, verbose=)
pose_results = pose.process(rgb_frame)
pose_results.pose_landmarks:
landmarks = pose_results.pose_landmarks.landmark
left_hip = landmarks[mp_pose.PoseLandmark.LEFT_HIP.value]
left_knee = landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value]
left_ankle = landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value]
hip_y = left_hip.y
knee_y = left_knee.y
angle = (hip_y - knee_y) *
angle < last_state != :
last_state =
angle > last_state == :
rep_count +=
last_state =
tts_queue.put()
event_buffer.append({
: datetime.now().isoformat(),
: ,
: rep_count,
: angle
})
mp.solutions.drawing_utils.draw_landmarks(
frame, pose_results.pose_landmarks, mp_pose.POSE_CONNECTIONS
)
cv2.putText(frame, , (, ),
cv2.FONT_HERSHEY_SIMPLEX, , (, , ), )
cv2.imshow(, frame)
cv2.waitKey() & == ():
running =
cap.release()
cv2.destroyAllWindows()
event_buffer:
df = pd.DataFrame(event_buffer)
output_file =
df.to_csv(output_file, index=)
()
()
__name__ == :
main()
3. React Frontend
API Integration Pattern:
const API_BASE = 'http://localhost:8080/api/games';
export const startGame = async (gameId) => {
const response = await fetch(`${API_BASE}/${gameId}/start`, {
method: 'POST',
});
if (!response.ok) {
const error = await response.text();
throw new Error(error || 'Failed to start game');
}
return response.json();
};
export const stopGame = async (gameId) => {
const response = await fetch(`${API_BASE}/${gameId}/stop`, {
method: 'POST',
});
return response.json();
};
export const getWorkoutData = async () => {
const response = await fetch(`/data`);
response.();
};
= () => {
poller = ( () => {
files = ();
(files);
}, interval);
(poller);
};
Configuration
Each game has a JSON config in configs/game_{id}.json:
{
"game_id": 1,
"name": "YOLO Ball Counter",
"yolo_model": "models/yolov8n.pt",
"confidence_threshold": 0.5,
"tracking_persistence": true,
"audio_coaching": true,
"target_fps": 30,
"resolution": [640, 480],
"coaching_triggers": {
"milestone_reps": [5, 10, 20],
"form_warning_angle": 45
}
}
Loading config in Python:
import json
def load_game_config(game_id):
with open(f'configs/game_{game_id}.json', 'r') as f:
return json.load(f)
config = load_game_config(1)
yolo_model = YOLO(config['yolo_model'])
confidence = config['confidence_threshold']
Common Patterns
1. Adding a New Exercise Game
touch games/exe_17.py
cat > configs/game_17.json << EOF
{
"game_id": 17,
"name": "Jumping Jacks Counter",
"yolo_model": "models/yolov8n-pose.pt",
"confidence_threshold": 0.6
}
EOF
2. Combining YOLO + MediaPipe
yolo_results = yolo_model(frame)
pose_results = pose.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
if pose_results.pose_landmarks and len(yolo_results) > 0:
hand = pose_results.pose_landmarks.landmark[mp_pose.PoseLandmark.LEFT_WRIST.value]
for detection in yolo_results[0].boxes:
if detection.cls == 32:
ball_x, ball_y = detection.xywh[0][:2]
hand_x = hand.x * frame.shape[1]
hand_y = hand.y * frame.shape[0]
distance = np.sqrt((hand_x - ball_x)**2 + (hand_y - ball_y)**2)
if distance < 50:
print("Hand touched ball!")
3. CSV Data Export Pattern
event_buffer = []
event_buffer.append({
'timestamp': datetime.now().isoformat(),
'event_type': 'crossing',
'player_position_x': x,
'player_position_y': y,
'speed_estimate': speed,
'rep_count': reps
})
df = pd.DataFrame(event_buffer)
df['session_id'] = datetime.now().strftime('%Y%m%d_%H%M%S')
df.to_csv(f"data/workout_{df['session_id'].iloc[0]}.csv", index=False)
4. Thread-Safe Audio Coaching
from queue import Queue
import threading
import pyttsx3
def tts_worker(queue):
engine = pyttsx3.init()
while True:
message = queue.get()
if message is None:
break
engine.say(message)
engine.runAndWait()
queue.task_done()
tts_queue = Queue()
tts_thread = threading.Thread(target=tts_worker, args=(tts_queue,))
tts_thread.daemon = True
tts_thread.start()
if rep_count % 5 == 0:
tts_queue.put(f"Great job! {rep_count} reps completed")
Troubleshooting
Python Process Won't Stop
public boolean stopGame() {
synchronized (lock) {
if (currentProcess != null && currentProcess.isAlive()) {
currentProcess.destroy();
try {
if (!currentProcess.waitFor(3, TimeUnit.SECONDS)) {
currentProcess.destroyForcibly();
currentProcess.waitFor(2, TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
currentProcess.destroyForcibly();
}
currentProcess = null;
return true;
}
return false;
}
}
Webcam Not Found
for i in range(5):
cap = cv2.VideoCapture(i)
if cap.isOpened():
print(f"Camera found at index {i}")
cap.release()
break
YOLO Model Loading Fails
import os
from ultralytics import YOLO
model_path = 'models/yolov8n.pt'
if not os.path.exists(model_path):
print("Downloading YOLO model...")
model = YOLO('yolov8n.pt')
os.makedirs('models', exist_ok=True)
else:
model = YOLO(model_path)
CORS Issues (if running frontend separately)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
}
CSV Not Appearing in Frontend
const handleStopGame = async (gameId) => {
await stopGame(gameId);
setTimeout(async () => {
const files = await getWorkoutData();
setWorkoutFiles(files);
}, 2000);
};
Performance Optimization
results = model.track(frame, persist=True, tracker="bytetrack.yaml")
frame_skip = 2
frame_count = 0
while running:
ret, frame = cap.read()
frame_count += 1
if frame_count % frame_skip != 0:
continue
Environment Variables
YOLO_MODEL_PATH=models/yolov8n.pt
MEDIAPIPE_MODEL_COMPLEXITY=1
TTS_RATE=150
WEBCAM_INDEX=0
OUTPUT_DIR=data
import os
from dotenv import load_dotenv
load_dotenv()
model_path = os.getenv('YOLO_MODEL_PATH', 'models/yolov8n.pt')
webcam_index = int(os.getenv('WEBCAM_INDEX', '0'))