node.js 后端实现完全免费的人脸识别接口, 完整人脸识别项目, 使用AI实现
本文介绍了在微信小程序中实现人脸识别功能的开发过程。由于小程序canvas与浏览器实现方式不同,作者改用Node.js后端方案,结合face-api.js和TensorFlow.js实现人脸对比、性别/年龄分析和情绪识别功能。后端接口接收用户上传的照片和手机号,与数据库中的员工照片进行比对,返回相似度、性别、年龄和情绪等分析结果。文章提供了完整的Node.js代码实现,包括模型加载、图像处理和AP
·
项目背景:
公司自有小程序, 员工考勤打卡 / 审批单据时进行人脸识别操作, 不想花钱, 希望自行实现,公司OA系统保存了员工入职照片, 这里只做对比就可以
开发历程:
可见我前一篇文章, 在 vue项目中实现了 tensorflowJS + faceapi 的人脸对比 + 情绪识别功能, 便认为在微信小程序中可轻松实现, 但事实不然, canvas 方法 微信小程序实现跟浏览器完全不一样, 通过 monkeypatch 也无法兼容, 只能退一步, faceapi 和 tensorflow 都是支持在 node 环境中运行的, 故使用 nodejs 实现, 小程序端调用接口通信
功能介绍:
开启接口, 接收前端上传的照片和用户手机号码. 通过手机号码查询到数据库中人脸图, 将人脸图和本次上传的照片进行相似度对比, 性别分析, 年龄分析, 情绪分析, 将验证结果返回前端
如果能满足您的需求, 下面直接放完整代码:
由于本人是前端, 只是用 node 实现后端功能, 代码写的不好见谅
如果需要完整项目, 或者小程序调用部分可以私聊我
const express = require("express");
const multer = require("multer");
const path = require("path");
const app = express();
// 添加全局编码中间件(必须放在所有路由前)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use((req, res, next) => {
res.header('Content-Type', 'application/json; charset=utf-8');
next();
});
const port = 3000;
const faceapi = require("face-api.js");
const fs = require("fs");
const storage = multer.diskStorage({
destination: "uploads/",
filename: function (req, file, cb) {
const ext = path.extname(file.originalname); // 获取原始后缀
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
cb(null, uniqueSuffix + ext); // 保留文件后缀
},
});
const upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith("image/")) {
cb(null, true);
} else {
cb(new Error("仅支持图片文件"), false);
}
},
});
// const axios = require('axios');
let ResultCache = [];
// 对环境进行 monkey patch
const { Canvas, Image, ImageData, createCanvas, loadImage } = require("canvas");
faceapi.env.monkeyPatch({ Canvas, Image, ImageData });
// 格式化检测结果
const processBasicInfo = (detections) => {
return detections.map((detection, index) => ({
descriptor: detection.descriptor,
}));
};
// 加载模型方法
const loadModels = async () => {
try {
await Promise.all([
faceapi.nets.tinyFaceDetector.loadFromDisk(__dirname + "/models"),
faceapi.nets.faceLandmark68Net.loadFromDisk(__dirname + "/models"),
faceapi.nets.faceRecognitionNet.loadFromDisk(__dirname + "/models"),
faceapi.nets.faceExpressionNet.loadFromDisk(__dirname + "/models"),
faceapi.nets.ageGenderNet.loadFromDisk(__dirname + "/models"),
]);
} catch (error) {
console.error("模型加载失败:", error);
}
};
// 加载员工档案的人脸方法
const initBanseImage = async (phone) => {
return new Promise(async (resolve, reject) => {
try {
if (typeof phone !== "string") {
throw new Error("phone参数类型错误");
}
// 是否有结果已被保存 加载基础图片
let noHaveDescriptor = true;
ResultCache.forEach((item) => {
if (item.phone === phone) {
noHaveDescriptor = false;
}
});
if (noHaveDescriptor) {
// 这是后端返回base64的方法
// let imgRes = await axios.get('https://xxxxxxx/api/xxxxxxxxx/GetPhotoForAI', { // 这里是获取基础照片的链接, 替换为您自己的
// params: {
// phone: phone
// }
// })
// const sharp = require('sharp');
// const buffer = Buffer.from(imgRes.data.replace(/^data:image\/\w+;base64,/, ''), 'base64');
// const processedImage = await sharp(buffer)
// .toFormat('png')
// .toBuffer();
// const img = await loadImage(processedImage);
// 这是后端直接返回图片的方法
const img = await loadImage(
"https://xxxxxxx/api/xxxxxxxxx/GetPhotoForAI" + // 这里是获取基础照片的链接, 替换为您自己的
phone
);
const canvas = createCanvas(img.width, img.height);
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
const detections = await faceapi
.detectAllFaces(
canvas,
new faceapi.TinyFaceDetectorOptions({
inputSize: 256,
scoreThreshold: 0.5,
})
)
.withFaceLandmarks()
.withFaceDescriptors();
const chineseResults = processBasicInfo(detections);
chineseResults.forEach((item) => {
ResultCache.push({
...item,
phone: phone,
});
});
}
resolve(); // 初始化成功
} catch (error) {
reject(new Error(`基准图初始化失败: ${error.message}`));
}
});
// 接口进来 检测图片相似度
// resiveImage("FB5E692B-640B-4928-A9C6-873C5647D1B6.png", phone);
};
// 查询本次进来的人脸
const resiveImage = async (url, phone) => {
return new Promise(async (resolve, reject) => {
try {
// 加载员工档案的人脸方法
await initBanseImage(phone);
const img = await loadImage(url);
if (!img || isNaN(img.width) || isNaN(img.height)) {
throw new Error("员工档案照片无法解析");
}
const canvas = createCanvas(img.width, img.height);
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
const detections = await faceapi
.detectAllFaces(
canvas,
new faceapi.TinyFaceDetectorOptions({
inputSize: 256,
scoreThreshold: 0.5,
})
)
.withFaceLandmarks()
.withFaceDescriptors()
.withFaceExpressions()
.withAgeAndGender();
if (detections.length === 0) {
throw new Error("照片未检测到人脸");
}
// 计算两张图片的相似度
const distance = faceapi.euclideanDistance(
ResultCache.filter((item) => item.phone === phone)[0].descriptor,
detections[0].descriptor
);
let similarity = Math.max(0, 100 - distance * 100).toFixed(1);
const result = {
similarity: similarity,
age: Math.round(detections[0].age),
gender: detections[0].gender === "male" ? "男" : "女",
dominantEmotion: (() => {
const [emotion, confidence] = Object.entries(
detections[0].expressions
).reduce((max, curr) => (curr[1] > max[1] ? curr : max), ["", 0]);
return {
neutral: "中性",
happy: "开心",
sad: "悲伤",
angry: "生气",
fearful: "害怕",
disgusted: "厌恶",
surprised: "惊讶",
}[emotion];
})(),
};
resolve(result);
} catch (error) {
reject(new Error(error.message));
}
});
};
// 初始化模型 项目启动调用
loadModels().then(() => {
console.log("模型加载成功");
});
// 开启接口
app.post("/api/faceDetect", upload.single("file"), async (req, res) => {
try {
const imagePath = req.file.path;
let result = await resiveImage(imagePath, req._parsedUrl.search);
console.log(req._parsedUrl.search, result);
let msg = {
success: true,
msg: '',
detail: result,
}
if(result.similarity && Number(result.similarity) < 60) { // 相似度达到 60, 其实 50 也行
msg.msg = '人脸对比失败';
msg.success = false;
} else {
msg.msg = '人脸对比成功';
msg.success = true;
}
res.status(200).json(msg);
} catch (err) {
res.status(200).json({ success: false, msg: err.message, detail: null });
} finally {
// 删除临时文件
const imagePath = req.file.path;
fs.unlinkSync(imagePath);
}
});
app.listen(port, () => {
console.log(`Server running at http://10.8.1.16:${port}`);
});
更多推荐
所有评论(0)