update
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"element-plus": "^2.11.1",
|
||||
"face-api.js": "^0.22.2",
|
||||
"gsap": "^3.13.0",
|
||||
"howler": "^2.2.4",
|
||||
"mitt": "^3.0.1",
|
||||
|
||||
71
pnpm-lock.yaml
generated
71
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
||||
element-plus:
|
||||
specifier: ^2.11.1
|
||||
version: 2.11.1(vue@3.5.20)
|
||||
face-api.js:
|
||||
specifier: ^0.22.2
|
||||
version: 0.22.2
|
||||
gsap:
|
||||
specifier: ^3.13.0
|
||||
version: 3.13.0
|
||||
@@ -425,67 +428,56 @@ packages:
|
||||
resolution: {integrity: sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.50.0':
|
||||
resolution: {integrity: sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.50.0':
|
||||
resolution: {integrity: sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.50.0':
|
||||
resolution: {integrity: sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.50.0':
|
||||
resolution: {integrity: sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.50.0':
|
||||
resolution: {integrity: sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.50.0':
|
||||
resolution: {integrity: sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.50.0':
|
||||
resolution: {integrity: sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.50.0':
|
||||
resolution: {integrity: sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.50.0':
|
||||
resolution: {integrity: sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.50.0':
|
||||
resolution: {integrity: sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.50.0':
|
||||
resolution: {integrity: sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==}
|
||||
@@ -517,6 +509,10 @@ packages:
|
||||
'@sxzz/popperjs-es@2.11.7':
|
||||
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
|
||||
|
||||
'@tensorflow/tfjs-core@1.7.0':
|
||||
resolution: {integrity: sha512-uwQdiklNjqBnHPeseOdG0sGxrI3+d6lybaKu2+ou3ajVeKdPEwpWbgqA6iHjq1iylnOGkgkbbnQ6r2lwkiIIHw==}
|
||||
engines: {yarn: '>= 1.3.2'}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
@@ -526,9 +522,21 @@ packages:
|
||||
'@types/lodash@4.17.20':
|
||||
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
|
||||
|
||||
'@types/offscreencanvas@2019.3.0':
|
||||
resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==}
|
||||
|
||||
'@types/seedrandom@2.4.27':
|
||||
resolution: {integrity: sha512-YvMLqFak/7rt//lPBtEHv3M4sRNA+HGxrhFZ+DQs9K2IkYJbNwVIb8avtJfhDiuaUBX/AW0jnjv48FV8h3u9bQ==}
|
||||
|
||||
'@types/web-bluetooth@0.0.16':
|
||||
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
|
||||
|
||||
'@types/webgl-ext@0.0.30':
|
||||
resolution: {integrity: sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==}
|
||||
|
||||
'@types/webgl2@0.0.4':
|
||||
resolution: {integrity: sha512-PACt1xdErJbMUOUweSrbVM7gSIYm1vTncW2hF6Os/EeWi6TXYAYMPp+8v6rzHmypE5gHrxaxZNXgMkJVIdZpHw==}
|
||||
|
||||
'@vitejs/plugin-vue-jsx@3.1.0':
|
||||
resolution: {integrity: sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -740,6 +748,9 @@ packages:
|
||||
resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==}
|
||||
engines: {node: ^18.19.0 || >=20.5.0}
|
||||
|
||||
face-api.js@0.22.2:
|
||||
resolution: {integrity: sha512-9Bbv/yaBRTKCXjiDqzryeKhYxmgSjJ7ukvOvEBy6krA0Ah/vNBlsf7iBNfJljWiPA8Tys1/MnB3lyP2Hfmsuyw==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -889,6 +900,10 @@ packages:
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
node-fetch@2.1.2:
|
||||
resolution: {integrity: sha512-IHLHYskTc2arMYsHZH82PVX8CSKT5lzb7AXeyO06QnjGDKtkv+pv3mEki6S7reB/x1QPo+YPxQRNEVgR5V/w3Q==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
|
||||
node-releases@2.0.19:
|
||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||
|
||||
@@ -986,6 +1001,9 @@ packages:
|
||||
resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
seedrandom@2.4.3:
|
||||
resolution: {integrity: sha512-2CkZ9Wn2dS4mMUWQaXLsOAfGD+irMlLEeSP3cMxpGbgyOOzJGFa+MWCOMTOCMyZinHRPxyOj/S/C57li/1to6Q==}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -1041,6 +1059,9 @@ packages:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tslib@1.14.1:
|
||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
|
||||
unicorn-magic@0.3.0:
|
||||
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1579,6 +1600,15 @@ snapshots:
|
||||
|
||||
'@sxzz/popperjs-es@2.11.7': {}
|
||||
|
||||
'@tensorflow/tfjs-core@1.7.0':
|
||||
dependencies:
|
||||
'@types/offscreencanvas': 2019.3.0
|
||||
'@types/seedrandom': 2.4.27
|
||||
'@types/webgl-ext': 0.0.30
|
||||
'@types/webgl2': 0.0.4
|
||||
node-fetch: 2.1.2
|
||||
seedrandom: 2.4.3
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/lodash-es@4.17.12':
|
||||
@@ -1587,8 +1617,16 @@ snapshots:
|
||||
|
||||
'@types/lodash@4.17.20': {}
|
||||
|
||||
'@types/offscreencanvas@2019.3.0': {}
|
||||
|
||||
'@types/seedrandom@2.4.27': {}
|
||||
|
||||
'@types/web-bluetooth@0.0.16': {}
|
||||
|
||||
'@types/webgl-ext@0.0.30': {}
|
||||
|
||||
'@types/webgl2@0.0.4': {}
|
||||
|
||||
'@vitejs/plugin-vue-jsx@3.1.0(vite@7.1.3)(vue@3.5.20)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.3
|
||||
@@ -1888,6 +1926,11 @@ snapshots:
|
||||
strip-final-newline: 4.0.0
|
||||
yoctocolors: 2.1.2
|
||||
|
||||
face-api.js@0.22.2:
|
||||
dependencies:
|
||||
'@tensorflow/tfjs-core': 1.7.0
|
||||
tslib: 1.14.1
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
@@ -1987,6 +2030,8 @@ snapshots:
|
||||
|
||||
nanoid@5.1.5: {}
|
||||
|
||||
node-fetch@2.1.2: {}
|
||||
|
||||
node-releases@2.0.19: {}
|
||||
|
||||
normalize-wheel-es@1.2.0: {}
|
||||
@@ -2086,6 +2131,8 @@ snapshots:
|
||||
|
||||
run-applescript@7.0.0: {}
|
||||
|
||||
seedrandom@2.4.3: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
@@ -2131,6 +2178,8 @@ snapshots:
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tslib@1.14.1: {}
|
||||
|
||||
unicorn-magic@0.3.0: {}
|
||||
|
||||
unplugin-utils@0.3.0:
|
||||
|
||||
BIN
public/models/face_landmark_68_model-shard1
Normal file
BIN
public/models/face_landmark_68_model-shard1
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
public/models/face_recognition_model-shard1
Normal file
BIN
public/models/face_recognition_model-shard1
Normal file
Binary file not shown.
6
public/models/face_recognition_model-shard2
Normal file
6
public/models/face_recognition_model-shard2
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
public/models/tiny_face_detector_model-shard1
Normal file
BIN
public/models/tiny_face_detector_model-shard1
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
[{"weights":[{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}],"paths":["tiny_face_detector_model-shard1"]}]
|
||||
@@ -8,31 +8,75 @@ import { globalStore } from "../globalstore.js";
|
||||
import faceTemplate from "../static/ali_face_template.js";
|
||||
import MyPhoto from './MyPhoto.vue'
|
||||
import PhotoSquare from './PhotoSquare.vue'
|
||||
import { loadFaceApiModels, validateFaceInImage } from "../libs/faceValidator.js"
|
||||
|
||||
defineProps({
|
||||
show: true
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
await loadFaceApiModels()
|
||||
})
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { tr } from "element-plus/es/locales.mjs"
|
||||
const router = useRouter();
|
||||
|
||||
// 上传前的校验
|
||||
const beforeUpload = (file) => {
|
||||
// 基础格式校验
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
||||
if (!isJpgOrPng) {
|
||||
ElMessage.error('只能上传JPG或PNG格式的图片!');
|
||||
return false;
|
||||
}
|
||||
|
||||
const isLt2M = file.size / 1024 / 1024 < 5;
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过5MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
const loadingInstance = ElMessage({
|
||||
message: '正在验证人脸...',
|
||||
type: 'info',
|
||||
duration: 0,
|
||||
showClose: false
|
||||
});
|
||||
|
||||
try {
|
||||
const faceValidationResult = await validateFaceInImage(file);
|
||||
loadingInstance.close();
|
||||
|
||||
if (!faceValidationResult.success) {
|
||||
ElMessage({
|
||||
message: faceValidationResult.message,
|
||||
type: 'error',
|
||||
duration: 4000
|
||||
});
|
||||
resolve(false);
|
||||
} else {
|
||||
ElMessage({
|
||||
message: faceValidationResult.message,
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
loadingInstance.close();
|
||||
console.error('人脸验证出错:', error);
|
||||
ElMessage({
|
||||
message: '人脸验证失败,请重试',
|
||||
type: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getTemplateIdsFromUrl = (response, url, index)=> {
|
||||
|
||||
219
src/libs/faceValidator.js
Normal file
219
src/libs/faceValidator.js
Normal file
@@ -0,0 +1,219 @@
|
||||
import * as faceapi from 'face-api.js'
|
||||
|
||||
// 模型加载状态
|
||||
let modelsLoaded = false
|
||||
|
||||
/**
|
||||
* 加载 face-api.js 模型
|
||||
*/
|
||||
export async function loadFaceApiModels() {
|
||||
if (modelsLoaded) {
|
||||
console.log('Face API 模型已经加载过了')
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('开始加载 Face API 模型...')
|
||||
|
||||
// 获取基础路径,处理 vite 的 base 配置
|
||||
const basePath = import.meta.env.BASE_URL || '/'
|
||||
const modelsPath = basePath.endsWith('/') ? basePath + 'models' : basePath + '/models'
|
||||
|
||||
console.log('模型加载路径:', modelsPath)
|
||||
|
||||
// 加载必需的模型
|
||||
await Promise.all([
|
||||
faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),
|
||||
faceapi.nets.faceLandmark68Net.loadFromUri(modelsPath),
|
||||
faceapi.nets.faceRecognitionNet.loadFromUri(modelsPath)
|
||||
])
|
||||
|
||||
console.log('Face API 模型加载完成')
|
||||
modelsLoaded = true
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Face API 模型加载失败:', error)
|
||||
// 如果本地模型加载失败,尝试从 CDN 加载
|
||||
try {
|
||||
console.log('尝试从 CDN 加载模型...')
|
||||
await Promise.all([
|
||||
faceapi.nets.tinyFaceDetector.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.13/model'),
|
||||
faceapi.nets.faceLandmark68Net.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.13/model'),
|
||||
faceapi.nets.faceRecognitionNet.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.13/model')
|
||||
])
|
||||
console.log('CDN 模型加载成功')
|
||||
modelsLoaded = true
|
||||
return true
|
||||
} catch (cdnError) {
|
||||
console.error('CDN 模型加载也失败:', cdnError)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模型是否已加载
|
||||
*/
|
||||
export function areModelsLoaded() {
|
||||
return modelsLoaded
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证图片中的人脸
|
||||
* @param {File} file - 图片文件
|
||||
* @returns {Promise<Object>} - 验证结果
|
||||
*/
|
||||
export async function validateFaceInImage(file) {
|
||||
// 如果模型未加载,先加载模型
|
||||
if (!modelsLoaded) {
|
||||
const loaded = await loadFaceApiModels()
|
||||
if (!loaded) {
|
||||
return {
|
||||
success: false,
|
||||
message: '人脸识别模型加载失败,请刷新页面重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image()
|
||||
|
||||
img.onload = async () => {
|
||||
try {
|
||||
console.log('开始进行人脸检测...')
|
||||
|
||||
// 使用 TinyFaceDetector 进行人脸检测
|
||||
const detections = await faceapi
|
||||
.detectAllFaces(img, new faceapi.TinyFaceDetectorOptions({
|
||||
inputSize: 416,
|
||||
scoreThreshold: 0.5
|
||||
}))
|
||||
.withFaceLandmarks()
|
||||
|
||||
console.log(`检测到 ${detections.length} 张人脸`)
|
||||
|
||||
// 检查是否检测到人脸
|
||||
if (detections.length === 0) {
|
||||
resolve({
|
||||
success: false,
|
||||
message: '未检测到人脸,请上传包含清晰人脸的图片'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否只有一张人脸
|
||||
if (detections.length > 1) {
|
||||
resolve({
|
||||
success: false,
|
||||
message: '检测到多张人脸,请上传只包含一张人脸的图片'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const detection = detections[0]
|
||||
const confidence = detection.detection.score
|
||||
|
||||
console.log('人脸检测置信度:', confidence)
|
||||
|
||||
// 检查置信度是否足够高
|
||||
const minConfidence = 0.6 // 降低到 0.6 以提高通过率
|
||||
if (confidence < minConfidence) {
|
||||
resolve({
|
||||
success: false,
|
||||
message: `人脸清晰度不够(置信度: ${(confidence * 100).toFixed(1)}%),请上传更清晰的人脸图片`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查人脸大小(相对于图片尺寸)
|
||||
const faceBox = detection.detection.box
|
||||
const imageArea = img.naturalWidth * img.naturalHeight
|
||||
const faceArea = faceBox.width * faceBox.height
|
||||
const faceRatio = faceArea / imageArea
|
||||
|
||||
console.log('人脸占图片比例:', (faceRatio * 100).toFixed(2) + '%')
|
||||
|
||||
const minFaceRatio = 0.015 // 降低到 1.5% 以提高通过率
|
||||
if (faceRatio < minFaceRatio) {
|
||||
resolve({
|
||||
success: false,
|
||||
message: '人脸在图片中占比太小,请上传人脸更大更清晰的图片'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查人脸角度(通过关键点判断)
|
||||
if (detection.landmarks) {
|
||||
const landmarks = detection.landmarks
|
||||
const leftEye = landmarks.getLeftEye()
|
||||
const rightEye = landmarks.getRightEye()
|
||||
|
||||
// 计算眼睛之间的角度来判断人脸是否正面
|
||||
if (leftEye.length > 0 && rightEye.length > 0) {
|
||||
const leftEyeCenter = leftEye[0]
|
||||
const rightEyeCenter = rightEye[3]
|
||||
|
||||
const eyeAngle = Math.abs(
|
||||
Math.atan2(
|
||||
rightEyeCenter.y - leftEyeCenter.y,
|
||||
rightEyeCenter.x - leftEyeCenter.x
|
||||
) * 180 / Math.PI
|
||||
)
|
||||
|
||||
console.log('人脸角度:', eyeAngle.toFixed(1) + '度')
|
||||
|
||||
const maxAngle = 20 // 放宽到 20 度
|
||||
if (eyeAngle > maxAngle) {
|
||||
resolve({
|
||||
success: false,
|
||||
message: '请上传正面人脸照片,避免侧脸或过度倾斜的角度'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有验证通过
|
||||
resolve({
|
||||
success: true,
|
||||
message: `人脸验证通过(置信度: ${(confidence * 100).toFixed(1)}%)`,
|
||||
confidence: confidence,
|
||||
faceRatio: faceRatio,
|
||||
faceBox: {
|
||||
x: faceBox.x,
|
||||
y: faceBox.y,
|
||||
width: faceBox.width,
|
||||
height: faceBox.height
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('人脸检测过程中出错:', error)
|
||||
resolve({
|
||||
success: false,
|
||||
message: '人脸检测失败,请重试或更换图片'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
resolve({
|
||||
success: false,
|
||||
message: '图片加载失败,请检查图片格式'
|
||||
})
|
||||
}
|
||||
|
||||
// 加载图片
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
img.src = e.target.result
|
||||
}
|
||||
reader.onerror = () => {
|
||||
resolve({
|
||||
success: false,
|
||||
message: '图片读取失败,请重试'
|
||||
})
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user