一、技术选型
公司要研究人脸识别、表情分析需求, 不想花钱调用各家api ... 遂前端来实现这个事, 研究后使用 tensorflow.js + face-api.js 可以完美实现, 所有操作都在前端浏览器端完成, 不需要传输数据到后端处理.
实现效果:
二、前期准备
我是基于vue项目做的 demo, 理论上可以兼容所有前端框架, 首先安装依赖:
npm i @tensorflow-models/body-pix
npm i @tensorflow/tfjs
npm i face-api.js
然后去 face-api 的官方将模型文件下载下来
https://github.com/justadudewhohacks/face-api.js
将 weights 文件夹下的所有文件 全部下载到 项目中 public/models 文件夹下
三、具体使用
在使用的页面引入 face-api.js
import * as faceapi from 'face-api.js';
mounted 周期内 加载模型
async mounted() {
try {
// 加载模型并赋值给 net
await Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri('/models'), // 人脸检测器
faceapi.nets.faceExpressionNet.loadFromUri('/models'), // 表情识别模型
faceapi.nets.ageGenderNet.loadFromUri('/models') // 加载年龄和性别模型
]);
console.log('Face-api.js 模型加载成功');
} catch (error) {
console.error('加载模型失败:', error);
}
},
如果打印出模型加载成功, 那么说明前期准备一切正常,直接复制完整代码去使用吧
<template>
<div class="container">
<h1>面部表情识别</h1>
<input type="file" id="image-upload" accept="image/*" @change="handleImageUpload" />
<div class="content">
<!-- 绘制上传的图片 -->
<div class="image-container">
<canvas id="result-canvas" class="result-canvas"></canvas>
</div>
<!-- 用于显示表情结果的容器 -->
<div id="expression-results" class="expression-results"></div>
</div>
</div>
</template>
<script>
import * as faceapi from 'face-api.js';
export default {
data() {
return {
net: null, // 用于存储加载的模型
imageSrc: null // 用于存储上传的图片 URL
};
},
async mounted() {
try {
// 加载模型并赋值给 net
this.net = await Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri('/models'), // 人脸检测器
faceapi.nets.faceExpressionNet.loadFromUri('/models'), // 表情识别模型(中性、开心、悲伤、生气、害怕、厌恶、惊讶)
faceapi.nets.ageGenderNet.loadFromUri('/models') // 加载年龄和性别模型
]);
console.log('Face-api.js 模型加载成功');
} catch (error) {
console.error('加载模型失败:', error);
}
},
methods: {
async detectFaces(imageElement) {
if (!this.net) {
console.error('模型未加载');
return;
}
console.log('图片尺寸:', imageElement.width, imageElement.height); // 调试日志
// 检测面部、表情、年龄和性别
const detections = await faceapi
.detectAllFaces(
imageElement,
new faceapi.TinyFaceDetectorOptions({
inputSize: 512, // 调整输入尺寸
scoreThreshold: 0.5 // 调整置信度阈值
})
)
.withFaceExpressions()
.withAgeAndGender(); // 检测年龄和性别
console.log('检测结果:', detections);
// 在页面上显示结果
this.displayResults(imageElement, detections);
},
displayResults(imageElement, detections) {
const canvas = document.getElementById('result-canvas');
const ctx = canvas.getContext('2d');
// 调整 Canvas 尺寸
canvas.width = imageElement.width;
canvas.height = imageElement.height;
ctx.drawImage(imageElement, 0, 0, canvas.width, canvas.height);
// 绘制检测结果
const resizedDetections = faceapi.resizeResults(detections, {
width: canvas.width,
height: canvas.height
});
faceapi.draw.drawDetections(canvas, resizedDetections);
faceapi.draw.drawFaceExpressions(canvas, resizedDetections);
// 显示表情、年龄和性别结果
this.displayExpressions(detections);
},
displayExpressions(detections) {
const expressionContainer = document.getElementById('expression-results');
if (!expressionContainer) {
console.error('表情结果容器未找到');
return;
}
expressionContainer.innerHTML = ''; // 清空之前的结果
// 遍历每个检测到的人脸
detections.forEach((detection, index) => {
const expressions = detection.expressions;
const age = detection.age; // 年龄
const gender = detection.gender; // 性别
// 获取表情、年龄和性别信息
const expressionText = this.getExpressionText(expressions);
const ageText = this.getAgeText(age);
const genderText = this.getGenderText(gender);
// 创建结果元素
const resultElement = document.createElement('div');
resultElement.className = 'result-item';
resultElement.innerHTML = `
<strong>人脸 ${index + 1}:</strong><br>
${expressionText}<br>
${ageText}<br>
${genderText}
`;
expressionContainer.appendChild(resultElement);
});
},
getExpressionText(expressions) {
// 将表情概率转换为中文
const expressionMap = {
neutral: '中性',
happy: '开心',
sad: '悲伤',
angry: '生气',
fearful: '害怕',
disgusted: '厌恶',
surprised: '惊讶'
};
// 找到概率最高的表情
let maxExpression = '';
let maxProbability = 0;
for (const [expression, probability] of Object.entries(expressions)) {
if (probability > maxProbability) {
maxExpression = expression;
maxProbability = probability;
}
}
// 返回中文表情
return `表情: ${expressionMap[maxExpression]} (${(maxProbability * 100).toFixed(2)}%)`;
},
getAgeText(age) {
// 返回年龄信息
return `年龄: ${Math.round(age)} 岁`;
},
getGenderText(gender) {
// 返回性别信息
return `性别: ${gender === 'male' ? '男' : '女'}`;
},
handleImageUpload(event) {
const file = event.target.files[0];
if (file) {
const imageElement = document.createElement('img');
imageElement.src = URL.createObjectURL(file);
imageElement.onload = () => {
console.log('图片加载成功'); // 调试日志
this.imageSrc = imageElement.src; // 保存图片 URL
this.detectFaces(imageElement);
};
imageElement.onerror = (error) => {
console.error('图片加载失败:', error); // 错误日志
};
}
}
}
};
</script>
<style scoped>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
text-align: center;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
#image-upload {
margin-bottom: 20px;
}
.content {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
}
.image-container {
flex: 1;
}
.result-canvas {
max-width: 100%;
height: auto;
border: 1px solid #ccc;
}
.expression-results {
flex: 1;
padding: 10px;
border: 1px;
}
</style>