161 lines
6.5 KiB
TypeScript
161 lines
6.5 KiB
TypeScript
import type { Point } from './usePostureAnalysis';
|
|
|
|
// Constants
|
|
export const THRESHOLDS = {
|
|
TILT_WARN: 1.5, // degrees
|
|
CVA_NORM: 50, // degrees
|
|
TRUNK_LEAN_WARN: 2.0 // degrees (estimate)
|
|
};
|
|
|
|
// --- Helper Math ---
|
|
const dist = (a: Point, b: Point) => Math.hypot(b.x - a.x, b.y - a.y);
|
|
|
|
// vector removed
|
|
|
|
const degrees = (radians: number) => radians * (180 / Math.PI);
|
|
|
|
// Angle of vector relative to horizontal (0 degrees = Right, 90 = Down in image coords usually, -90 Up)
|
|
// We want standard math: 0=Right, 90=Up?
|
|
// Image coords: Y grows down.
|
|
// Math.atan2(dy, dx).
|
|
// If we want "Tilt" from horizontal: abs(atan2(dy, dx))
|
|
export const calculateTilt = (p1: Point, p2: Point): number => {
|
|
if (!p1 || !p2) return 0;
|
|
const dy = p2.y - p1.y; // Y is down
|
|
const dx = p2.x - p1.x;
|
|
return degrees(Math.atan2(dy, dx));
|
|
};
|
|
|
|
// CVA: Angle between Horizontal and C7->Tragus
|
|
// C7 is pivot.
|
|
// Vector V = Tragus - C7.
|
|
// Angle = angle of V with Horizontal (-1, 0) (Posterior horizontal line)
|
|
export const calculateCVA = (c7: Point, tragus: Point): number => {
|
|
if (!c7 || !tragus) return 0;
|
|
// In image coords (Y down), Horizontal Posterior line from C7 (assuming facing Right) is (-1, 0).
|
|
// Wait, usually CVA is measured on Side view.
|
|
// If facing Right: Ear is to right of C7? No, Ear is above and slightly forward/back.
|
|
// CVA is formed by horizontal line at C7 and the line to the ear.
|
|
// Ideally it's ~50 degrees upwards from horizontal.
|
|
|
|
// Vector C7 -> Tragus
|
|
const dx = tragus.x - c7.x;
|
|
const dy = tragus.y - c7.y; // negative if ear is above C7
|
|
|
|
// We want angle against Horizontal.
|
|
// atan2(-dy, dx) gives angle in standard math coords (Y increasing up).
|
|
// Let's use simple atan2 of vector.
|
|
// If facing RIGHT: vector is (+x, -y). Angle ~ 45-60 deg.
|
|
// If facing LEFT: vector is (-x, -y). Angle ~ 120-135 deg?
|
|
// We want the inner angle relative to the horizontal line pointing BACKWARDS from the neck?
|
|
// Standard definition: Angle formed by horizontal line at C7 and line extending to Tragus.
|
|
// Usually measured from the horizontal looking forward? Or horizontal looking back?
|
|
// CVA < 50 is bad (head forward).
|
|
// If neck is perfectly vertical (impossible), angle is 90.
|
|
// If neck is horizontal (turtle), angle is 0.
|
|
// So it's angle from horizontal.
|
|
// We assume we want absolute angle from horizontal plane.
|
|
return Math.abs(degrees(Math.atan2(-dy, Math.abs(dx))));
|
|
};
|
|
|
|
// --- Landmark Estimations ---
|
|
|
|
export const estimateC7 = (shoulderL: Point, shoulderR: Point, nose: Point): Point => {
|
|
if (!shoulderL || !shoulderR || !nose) return { x: 0, y: 0 };
|
|
|
|
const midShoulder = {
|
|
x: (shoulderL.x + shoulderR.x) / 2,
|
|
y: (shoulderL.y + shoulderR.y) / 2
|
|
};
|
|
|
|
// Distance Nose to MidShoulder
|
|
const neckLen = dist(nose, midShoulder);
|
|
|
|
// Shift Up (negative Y) by 15%
|
|
// "przesunąć w górę o ok. 15% długości szyi"
|
|
// We assume C7 is roughly where the neck starts, MP shoulders are joint centers (lower).
|
|
// Actually C7 is slightly *above* the line connecting shoulder joints in 2D projection?
|
|
// Or is it? Acromion is high. C7 is often level or slightly above.
|
|
// Heuristic: Move UP.
|
|
return {
|
|
x: midShoulder.x,
|
|
y: midShoulder.y - (neckLen * 0.20) // 20% feels safer for C7 vs shoulder center line
|
|
};
|
|
};
|
|
|
|
export const correctASIS = (hipL: Point, hipR: Point, shoulderL: Point, shoulderR: Point): { l: Point, r: Point } => {
|
|
if (!hipL || !hipR || !shoulderL || !shoulderR) return { l: hipL, r: hipR };
|
|
|
|
const shoulderWidth = dist(shoulderL, shoulderR);
|
|
const offset = shoulderWidth * 0.15;
|
|
|
|
// Move Outwards
|
|
// Left Hip (on screen left usually) -> Move Left (-x)
|
|
// Right Hip -> Move Right (+x)
|
|
// Assuming Front view where Left Hip is on right side of image?
|
|
// MP: LEFT_HIP is person's left.
|
|
// If person faces cam: LEFT_HIP is on Image Right.
|
|
// So we need to check x coords to determine "Outwards".
|
|
|
|
// We assume MP Hips (23, 24) are internal.
|
|
// ASIS is lateral.
|
|
// Shift Outwards by 15% of Shoulder Width.
|
|
// Direction depend on which hip is left/right on SCREEN.
|
|
|
|
// Determine center of hips
|
|
const hipCenter = (hipL.x + hipR.x) / 2;
|
|
|
|
// HipL is usually on the Right side of the screen if Front View (Subject's Left).
|
|
// But MP documentation says:
|
|
// 23 - left hip
|
|
// 24 - right hip
|
|
// If user faces camera: 23 is on Screen Right (larger X), 24 is on Screen Left (smaller X).
|
|
|
|
// Let's just use the vector HipR -> HipL (Lateral direction).
|
|
// If Front View: 24(R) -> 23(L) is vector pointing RIGHT (positive X).
|
|
// If Back View: 23(L) -> 24(R) is vector pointing RIGHT.
|
|
|
|
// Safer: Move away from center.
|
|
const moveFromCenter = (pt: Point, centerX: number, amount: number) => {
|
|
if (pt.x < centerX) return { ...pt, x: pt.x - amount }; // Move Left
|
|
else return { ...pt, x: pt.x + amount }; // Move Right
|
|
};
|
|
|
|
const newL = moveFromCenter(hipL, hipCenter, offset);
|
|
const newR = moveFromCenter(hipR, hipCenter, offset);
|
|
|
|
return { l: newL, r: newR };
|
|
};
|
|
|
|
export const calculateTrunkLean = (midShoulder: Point, midHip: Point): number => {
|
|
// Angle against Vertical
|
|
if (!midShoulder || !midHip) return 0;
|
|
const dx = midShoulder.x - midHip.x;
|
|
const dy = midShoulder.y - midHip.y; // usually negative
|
|
// Vertical is 90.
|
|
const angle = degrees(Math.atan2(Math.abs(dy), Math.abs(dx))); // 90 is vertical
|
|
return Math.abs(90 - angle);
|
|
};
|
|
|
|
export const calculateATSI = (c7: Point, s1: Point, shoulderL: Point, shoulderR: Point, _hipL: Point, _hipR: Point): number => {
|
|
/*
|
|
ATSI (Anterior Trunk Symmetry Index) simplified:
|
|
Asymmetry of areas/distances.
|
|
Let's check lateral deviation of C7-S1 axis vs center of body blocks.
|
|
Or simpler: (Dist(C7, AcromionL) - Dist(C7, AcromionR)) / Mean...
|
|
User req: "różnice odległościowe między lewą a prawą stroną tułowia względem linii pośrodkowej"
|
|
*/
|
|
if (!c7 || !s1) return 0;
|
|
|
|
// Only feasible if we have solid Axis.
|
|
// Let's implement simple "Trunk Asymmetry":
|
|
// |( ShoulderL.x - Axis )| - |( ShoulderR.x - Axis )|
|
|
// Axis defined by S1 vertical? Or C7-S1 line.
|
|
|
|
// Let's approximate by Waist Triangle diff provided earlier, that's standard POTSI component.
|
|
// Here: return Asymmetry of Shoulders relative to C7 X-coord.
|
|
const distL = Math.abs(shoulderL.x - c7.x);
|
|
const distR = Math.abs(shoulderR.x - c7.x);
|
|
return Math.abs(distL - distR);
|
|
};
|