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();
|
});
|
};
|
}
|