import {ref } from 'vue'; import { loadModels,detectWithTimeout,detectSingleWithTimeout,isFaceFrontal,isFaceCentered } from "@/facerecognition/faceManager"; import { getVideoDevices,stopCamera,stopStream,drawVideoToCanvas,clearCanvas,captureImageFromVideo,captureBlobFromVideo,createCanvasFromVideo} from '@/facerecognition/deviceManager'; import { ElMessage} from "element-plus"; import * as faceapi from "face-api.js"; export class FaceRecognitionVm { constructor(options = { type : 0}){ this.recognitionType = options.type; // 初始化canvas和video控件对象 this.canvasDom = ref(null); this.videoDom = ref(null); this.videoArr = ref([]); //选择的摄像头的id this.constraints = ref(""); this.videoConstraints = ref({}); this.recognitionState = ref(0); this.startDetectFace = ref(false); this.imageDataUrl = ref(null); this.startRecognize = ref(false); this.stopCameraImmidiate = ref(false); this.lastStatus = '' this.statusStartTime = 0 this.progress = ref(0) this.statusMessage = ref('请点击人脸录入按钮开始录入人脸'); this.MIN_STATUS_DURATION = 3000; this.baseEyeOpen = null this.isBlinking = false this.blinkDetected = false this.baseMouthOpen = null this.mouthOpenDetected = false this.mouthCloseAfterOpenDetected = false this.motionDetected = false this.motionHistory = [] this.motionThreshold = 3 this.motionFrames = 10 this.lastLeftEyeHeight = null this.lastRightEyeHeight = null this.eyeHistory = [] // 默认回调函数 const defaultCallbacks = { onDetectionStart: () => {}, onFaceRecognitionComplete: (imageData) => {}, onError: (error) => {} } this.callbacks = { ...defaultCallbacks, ...options.callbacks } } registerHandleClose=()=>{ this.startDetectFace.value = false; this.startRecognize.value = false; this.statusMessage.value= '请点击人脸录入按钮开始录入人脸'; if(this.videoDom.value){ console.log('stop camera'); stopCamera(this.videoDom.value); } this.recognitionState.value = 0; } recognizeFace = () => { if(this.startRecognize.value ) return; this.statusMessage.value = '正在打开摄像头,请稍等...'; this.startRecognize.value = true; this.imageDataUrl.value = null; this.recognitionState.value = 1; let that = this; this.baseEyeOpen = null this.baseMouthOpen = null this.isBlinking = false this.blinkDetected = false this.mouthOpenDetected = false this.mouthCloseAfterOpenDetected = false this.motionDetected = false this.motionHistory = [] this.motionThreshold = 3 this.motionFrames = 10 loadModels().then(()=>{ that.setupCamera(); }).catch((err) => { this.startRecognize.value = false; console.error("Error loading models:", err); ElMessage.error("人脸识别模型加载失败,请检查网络连接或稍后再试。"); }); } // 获取设备列表 setupCamera = async()=>{ try { clearCanvas(this.canvasDom.value); ElMessage({ message: "正在打开摄像头,请稍等...", type: "warning", plain: true, duration: 1000, }); const devices = await getVideoDevices(); this.videoArr.value = []; devices.forEach(device => { //console.log(`Device: ${device.label} (ID: ${device.id})`); this.videoArr.value.push({ label: device.label, id: device.id, }); }); this.constraints.value = this.videoArr.value[0].id; this.videoConstraints.value.deviceId = { exact: this.constraints.value }; navigator.mediaDevices // 开启视频,关闭音频 .getUserMedia({ audio: false, video: { width: 300, // 设置视频宽度 height: 300, // 设置视频高度 facingMode: "user", // 使用前置摄像头 deviceId: this.videoConstraints.value.deviceId, }, }) .then((stream) => { var video = this.videoDom.value; video.onloadeddata = () => { console.log('视频帧已准备好,可开始检测'); this.faceRecognition(); }; video.srcObject = stream; video.play(); }) .catch((err) => { console.log(err); }); } catch (err) { // 处理错误 this.startRecognize.value = false; console.log(err); window.alert("该浏览器不支持开启摄像头,请更换最新版浏览器"); } } faceRecognition = () => { this.startDetectFace.value = true; this.detectFaces(); } detectFaces = async() => { if((!this.startDetectFace.value)){ console.log('自动检测已停用'); return; } try { var video = this.videoDom.value; var canvas = this.canvasDom.value; const displaySize = { width: video.videoWidth, height: video.videoHeight } faceapi.matchDimensions(canvas, displaySize) const detections = await detectWithTimeout(this.videoDom.value); let currentStatus = '' if (detections.length != 1) { // console.log('请确保只有一张人脸') currentStatus = 'not-centered-or-not-frontal'; } else { const detection = detections[0] const box = detection.detection.box const score = detection.detection.score const landmarks = detection.landmarks // 中心点判断 const isCentered = isFaceCentered(video, box) // 正脸判断 const isFrontal = isFaceFrontal(landmarks) if (isCentered && isFrontal) { // console.log(score); const leftEye = landmarks.getLeftEye() const rightEye = landmarks.getRightEye() const mouth = landmarks.getMouth() // 左右眼高度 const leftEyeH = Math.abs(leftEye[1].y - leftEye[5].y) const rightEyeH = Math.abs(rightEye[1].y - rightEye[5].y) const avgEyeH = (leftEyeH + rightEyeH) / 2 // 嘴巴高度 const mouthH = Math.abs(mouth[14].y - mouth[18].y) // 改进后的嘴巴张开检测 const mouthWidth = Math.abs(mouth[0].x - mouth[6].x) // 嘴巴的宽度 const mouthHeight = Math.abs(mouth[14].y - mouth[18].y) // 嘴巴的高度 // 计算宽高比 const mouthAspectRatio = mouthHeight / mouthWidth // 设置一个阈值来判断嘴巴是否张开,通常一个较大的阈值表示张嘴 const mouthOpenThreshold = 0.3 // 你可以根据实际情况调整此值 // 记录baseline if (!this.baseEyeOpen) { this.baseEyeOpen = avgEyeH this.baseMouthOpen = mouthH currentStatus = '请张嘴' }else{ if (mouthAspectRatio > mouthOpenThreshold) { this.mouthOpenDetected = true console.log('✅ 检测到张嘴') currentStatus = '请合上嘴' } else{ if(this.mouthOpenDetected){ if(mouthAspectRatio < 0.1){ this.mouthCloseAfterOpenDetected = true; console.log('✅ 检测到张嘴后闭嘴') }else{ currentStatus = '请合上嘴' } }else{ currentStatus = '请张嘴' } } // 活体判断 if ( this.mouthCloseAfterOpenDetected) { currentStatus = '✅ 检测通过' this.startDetectFace.value = false; // 停止自动检测 this.videoDom.value.pause(); this.takePhoto(); return; } } } else { // console.log('请调整人脸位置或角度') currentStatus = '请张嘴' } } // 状态切换逻辑 const now = Date.now() if (currentStatus == this.lastStatus) { const duration = now - this.statusStartTime // progress.value = Math.min(100, (duration / MIN_STATUS_DURATION) * 100) if (duration > this.MIN_STATUS_DURATION) { // 状态已保持足够时间 this.statusMessage.value = this.getStatusText(currentStatus) // ElMessage.info(this.statusMessage.value); } } else { this.lastStatus = currentStatus this.statusStartTime = now // progress.value = 0 this.statusMessage.value = this.getStatusText(currentStatus, true) // ElMessage.info(this.statusMessage.value); } } catch (err) { console.error("检测失败:", err); } // 继续检测 requestAnimationFrame(this.detectFaces); } getStatusText = (status, immediate = false) => { if (status === 'ok') return immediate ? '检测到正脸,请保持...' : '✅ 识别成功' if (status === 'not-centered-or-not-frontal') return '⚠️ 请保持人脸居中且正视摄像头' return status } // 简单高光检测函数 detectHighlights = (imageData, threshold = 240) => { const pixels = imageData.data let brightPixels = 0 for (let i = 0; i < pixels.length; i += 4) { const brightness = 0.299 * pixels[i] + 0.587 * pixels[i+1] + 0.114 * pixels[i+2] if (brightness > threshold) brightPixels++ } const ratio = brightPixels / (pixels.length / 4) return ratio } toGrayscale = (data, width, height) => { const gray = new Uint8ClampedArray(width * height) for (let i = 0; i < data.length; i += 4) { const r = data[i] const g = data[i+1] const b = data[i+2] gray[i/4] = 0.299*r + 0.587*g + 0.114*b } return gray } // 手写LBP computeLBP = (gray, width, height) => { const lbp = new Uint8Array(width * height) for (let y = 1; y < height -1; y++) { for (let x = 1; x < width -1; x++) { const center = gray[y*width + x] let code = 0 code |= (gray[(y-1)*width + (x-1)] > center) << 7 code |= (gray[(y-1)*width + x] > center) << 6 code |= (gray[(y-1)*width + (x+1)] > center) << 5 code |= (gray[y*width + (x+1)] > center) << 4 code |= (gray[(y+1)*width + (x+1)] > center) << 3 code |= (gray[(y+1)*width + x] > center) << 2 code |= (gray[(y+1)*width + (x-1)] > center) << 1 code |= (gray[y*width + (x-1)] > center) << 0 lbp[y*width + x] = code } } return lbp } // 开始识别 takePhoto = async () => { var curVideo = this.videoDom.value; var curCanvas = this.canvasDom.value; drawVideoToCanvas(curVideo, curCanvas); // 将画布内容转为 Base64 数据 //captureImageFromVideo(curVideo); captureBlobFromVideo(curVideo) .then(imageData=>{ this.imageDataUrl.value = imageData; console.log("捕获视频帧成功!"); // 关闭摄像头 stopCamera(curVideo); this.startRecognize.value = false; this.statusMessage.value = '✅ 识别成功'; if(this.recognitionType === 0) { ElMessage({ message: "识别成功,请点击提交数据按钮进行注册", type: "success", plain: true, }); } this.callbacks.onFaceRecognitionComplete(this.imageDataUrl.value); }) .catch(err=>{ console.error("捕获视频帧失败:", err); this.recognizeFace(); }); }; }