| 705 | const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 }); |
| 706 | |
| 707 | const detect = async ts => { |
| 708 | if (stop) return; |
| 709 | |
| 710 | if (ts - lastDetect >= 33) { |
| 711 | lastDetect = ts; |
| 712 | try { |
| 713 | const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true); |
| 714 | if (res && res.detection) { |
| 715 | const det = res.detection; |
| 716 | const box = det.box; |
| 717 | const vw = video.videoWidth || 1; |
| 718 | const vh = video.videoHeight || 1; |
| 719 | |
| 720 | const cx = box.x + box.width * 0.5; |
| 721 | const cy = box.y + box.height * 0.5; |
| 722 | const nx = (cx / vw) * 2 - 1; |
| 723 | const ny = (cy / vh) * 2 - 1; |
| 724 | medianPush(bufX.current, nx, 5); |
| 725 | medianPush(bufY.current, ny, 5); |
| 726 | const nxm = median(bufX.current); |
| 727 | const nym = median(bufY.current); |
| 728 | |
| 729 | const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym)); |
| 730 | |
| 731 | const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh)); |
| 732 | const depthScale = 1 + depthResponse * (faceSize - 0.25); |
| 733 | lookTarget.current.copy(look.multiplyScalar(depthScale)); |
| 734 | |
| 735 | const leftEye = res.landmarks.getLeftEye(); |
| 736 | const rightEye = res.landmarks.getRightEye(); |
| 737 | const lc = centroid(leftEye); |
| 738 | const rc = centroid(rightEye); |
| 739 | const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x); |
| 740 | medianPush(bufT.current, tilt, 5); |
| 741 | tiltTarget.current = median(bufT.current); |
| 742 | |
| 743 | const nose = res.landmarks.getNose(); |
| 744 | const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)]; |
| 745 | const jaw = res.landmarks.getJawOutline(); |
| 746 | const leftCheek = jaw[3] || jaw[2]; |
| 747 | const rightCheek = jaw[13] || jaw[14]; |
| 748 | const dL = dist2(tip, leftCheek); |
| 749 | const dR = dist2(tip, rightCheek); |
| 750 | const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6; |
| 751 | let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1); |
| 752 | yawSignal = Math.tanh(yawSignal); |
| 753 | medianPush(bufYaw.current, yawSignal, 5); |
| 754 | yawTarget.current = median(bufYaw.current); |
| 755 | |
| 756 | setUiFaceActive(true); |
| 757 | } else { |
| 758 | setUiFaceActive(false); |
| 759 | } |
| 760 | } catch { |
| 761 | setUiFaceActive(false); |
| 762 | } |
| 763 | } |
| 764 |
nothing calls this directly
no test coverage detected