import type { Point } from 'react-native-vision-camera'; import type { BoundingBox, Landmarks } from './faceDetection'; type DetectedFace = BoundingBox; export type Scores = { finalScore: number; skewScore: number; motionScore: number; }; const noScore: Scores = { finalScore: 0, skewScore: 0, motionScore: 0, }; function calculateDistance(point1: Point, point2: Point) { 'worklet'; return Math.sqrt( Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2) ); } function getSkewScore(landmarks: Landmarks) { 'worklet'; const { leftEye, rightEye, nose, mouth, leftEar, rightEar } = landmarks; const eyeDistance = calculateDistance(leftEye, rightEye); const earDistance = calculateDistance(leftEar, rightEar); const noseToLeftEyeX = Math.abs(nose.x - leftEye.x); const noseToRightEyeX = Math.abs(nose.x - rightEye.x); const horizontalSymmetry = Math.abs(noseToLeftEyeX - noseToRightEyeX) / eyeDistance; const eyeAvgY = (leftEye.y + rightEye.y) / 2; const noseToEyeAvgY = Math.abs(nose.y - eyeAvgY); const noseToMouthY = Math.abs(nose.y - mouth.y); const verticalSymmetry = Math.abs(noseToEyeAvgY - noseToMouthY) / eyeDistance; const earSymmetry = Math.abs(leftEar.y - rightEar.y) / earDistance; const totalSymmetry = (horizontalSymmetry + verticalSymmetry + earSymmetry) / 3; const sensitivityFactor = 2; const adjustedSymmetry = Math.pow(totalSymmetry, sensitivityFactor); const straightnessScore = Math.max(0, 1 - adjustedSymmetry); return straightnessScore; } function getMotionSpeed( currentDetected: DetectedFace, prevDetected?: DetectedFace | null ): number | undefined { 'worklet'; const currentLandmarks = currentDetected.landmarks; const prevLandmarks = prevDetected?.landmarks; if (!prevDetected) { return 0; } if (!currentLandmarks || !prevLandmarks) { return 0; } const currentKeypoints: Point[] = [ currentLandmarks.rightEye, currentLandmarks.leftEye, currentLandmarks.nose, currentLandmarks.mouth, currentLandmarks.rightEar, currentLandmarks.leftEar, ]; const prevKeypoints: Point[] = [ prevLandmarks.rightEye, prevLandmarks.leftEye, prevLandmarks.nose, prevLandmarks.mouth, prevLandmarks.rightEar, prevLandmarks.leftEar, ]; const timeDiff = currentDetected.timestamp - prevDetected.timestamp; const motionDiff = currentKeypoints.map((keypoint, i) => { const prevKeypoint = prevKeypoints[i] as Point; const xDiff = keypoint.x - prevKeypoint.x; const yDiff = keypoint.y - prevKeypoint.y; const diff = Math.sqrt(xDiff * xDiff + yDiff * yDiff); return diff; }); const avgDiff = motionDiff.reduce((sum, diff) => sum + diff, 0) / motionDiff.length; return avgDiff / timeDiff; } function getMotionScore( currentDetected: DetectedFace, prevDetected?: DetectedFace | null ): number { 'worklet'; const motionSpeed = getMotionSpeed(currentDetected, prevDetected); return Math.max(0, 1 - 10 * (motionSpeed ?? 0)); } function getScore( currentDetected: DetectedFace, prevDetected?: DetectedFace ): Scores { 'worklet'; const currentLandmarks = currentDetected.landmarks; if (!currentLandmarks) return noScore; const skewScore = getSkewScore(currentLandmarks); const motionScore = getMotionScore(currentDetected, prevDetected); const skewWeight = 0.8; const speedWeight = 0.2; const finalScore = skewScore * skewWeight + motionScore * speedWeight; return { finalScore, skewScore, motionScore, }; } export function scoreFrame(detectedFaces: DetectedFace[][]): Scores { 'worklet'; const currentDetected = detectedFaces[detectedFaces.length - 1]?.[0]; const prevDetected = detectedFaces[detectedFaces.length - 2]?.[0]; if (!currentDetected) return noScore; return getScore(currentDetected, prevDetected); }