STRAPS_LOCALHOST/lib/pose/HARCore.ts

152 lines
5.8 KiB
TypeScript

import { XGBoostPredictor } from './XGBoostPredictor';
import { Landmark, EXERCISE_CONFIGS } from './ExerciseRules';
import { RehabCore } from './RehabCore';
import { calculateAngle } from './MathUtils';
// Label Encoder mapping from python: Classes: ['berdiri' 'duduk' 'jatuh']
const LABELS = ['Standing', 'Sitting', 'Fall Detected'];
export class HARCore {
private predictor: XGBoostPredictor;
private rehab: RehabCore;
private currentExercise: string | null = null;
constructor() {
this.predictor = new XGBoostPredictor();
this.rehab = new RehabCore();
}
// Set the active exercise to track (from menu)
public setExercise(name: string) {
// Map UI name to internal config name if needed
// For now assume direct match e.g. "Bicep Curl" -> "bicep_curls"
// Simple normalizer
const lowerName = name.toLowerCase();
// Find matching key in EXERCISE_CONFIGS
// EXERCISE_CONFIGS keys are like 'bicep_curl', 'squat'
// UI names might be "Bicep Curl", "Squats"
const key = Object.keys(EXERCISE_CONFIGS).find(k =>
lowerName.includes(k.replace('_', ' ')) ||
k.replace('_', ' ').includes(lowerName.split(' ')[0])
);
this.currentExercise = key || null;
}
public resetParams() {
this.rehab.reset();
// this.currentExercise = null; // Don't nullify exercise, just counters
}
public async process(landmarks: Landmark[], worldLandmarks: Landmark[] = []) {
if (!landmarks || landmarks.length === 0) return null;
// 1. Activity Recognition (HAR) - XGBoost
const features = this.extractFeatures(landmarks);
const probs = this.predictor.predict(features);
const maxIdx = probs.indexOf(Math.max(...probs));
const status = LABELS[maxIdx];
const confidence = probs[maxIdx];
// 2. Exercise Counting (Rehab) - Heuristic
let reps = 0;
let feedback = "";
let debug = {};
if (this.currentExercise) {
const result = this.rehab.process(this.currentExercise, landmarks, worldLandmarks);
if (result) {
// Combine left/right reps for total or max?
// Usually we want total completed reps.
reps = this.rehab.getReps(this.currentExercise);
// Construct feedback
const stateL = result.left.stage;
const stateR = result.right.stage;
feedback = `L: ${stateL || '-'} | R: ${stateR || '-'}`;
if (result.feedback && result.feedback.length > 0) {
feedback += ` | ${result.feedback}`; // Add generic feedback
}
debug = {
angles: { l: result.left.angle, r: result.right.angle },
scores: result.scores
};
}
}
return {
status,
confidence,
exercise: this.currentExercise,
reps,
feedback,
debug
};
}
private extractFeatures(landmarks: Landmark[]): number[] {
// 1. Flatten Raw Keypoints (33 * 4 = 132 features)
const raw: number[] = [];
landmarks.forEach(lm => {
raw.push(lm.x, lm.y, lm.z, lm.visibility || 0);
});
// 2. Derived Features
// Helper to get landmark
const getLm = (idx: number) => landmarks[idx];
// Helper to flatten {x,y} to point for calculateAngle
const pt = (lm: Landmark) => ({x: lm.x, y: lm.y});
const calcAng = (a: Landmark, b: Landmark, c: Landmark) => calculateAngle(pt(a), pt(b), pt(c));
const derived: number[] = [];
// Angles
// 0: Left Elbow (11-13-15)
derived.push(calcAng(getLm(11), getLm(13), getLm(15)));
// 1: Right Elbow (12-14-16)
derived.push(calcAng(getLm(12), getLm(14), getLm(16)));
// 2: Left Hip (11-23-25)
derived.push(calcAng(getLm(11), getLm(23), getLm(25)));
// 3: Right Hip (12-24-26)
derived.push(calcAng(getLm(12), getLm(24), getLm(26)));
// 4: Left Knee (23-25-27)
derived.push(calcAng(getLm(23), getLm(25), getLm(27)));
// 5: Right Knee (24-26-28)
derived.push(calcAng(getLm(24), getLm(26), getLm(28)));
// Distances & Ratios
const dist = (a: Landmark, b: Landmark) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
const shoulderWidth = dist(getLm(11), getLm(12));
const hipWidth = dist(getLm(23), getLm(24));
const midShoulder = { x: (getLm(11).x + getLm(12).x)/2, y: (getLm(11).y + getLm(12).y)/2 };
const midHip = { x: (getLm(23).x + getLm(24).x)/2, y: (getLm(23).y + getLm(24).y)/2 };
const torsoHeight = dist(midShoulder as Landmark, midHip as Landmark);
const eps = 1e-6;
// 6: Shoulder Width Ratio
derived.push(shoulderWidth / (torsoHeight + eps));
// 7: Hip Width Ratio
derived.push(hipWidth / (torsoHeight + eps));
// 8: Torso Vertical Alignment (Cosine Similarity with [0, -1])
// Vector from Hip to Shoulder (Upwards)
const torsoVec = { x: midShoulder.x - midHip.x, y: midShoulder.y - midHip.y };
const verticalVec = { x: 0, y: -1 }; // Up in image coordinates (y is down)?
// Python: vertical_vector = np.array([0, -1])
// In python/opencv y is down. So [0, -1] is UP vector.
const dot = (torsoVec.x * verticalVec.x) + (torsoVec.y * verticalVec.y);
const norm = Math.sqrt(torsoVec.x * torsoVec.x + torsoVec.y * torsoVec.y);
derived.push(norm > 0 ? dot / norm : 0);
return [...raw, ...derived];
}
}