Initialize repo
This commit is contained in:
160
src/app/composables/medicalMetrics.ts
Normal file
160
src/app/composables/medicalMetrics.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
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);
|
||||
};
|
||||
Reference in New Issue
Block a user