|
@@ -5,6 +5,7 @@ import { isThisMachineOwnByStudent } from "@/utils/utils";
|
|
import { onMounted } from "vue";
|
|
import { onMounted } from "vue";
|
|
import { useTimers } from "@/setups/useTimers";
|
|
import { useTimers } from "@/setups/useTimers";
|
|
import { store } from "@/store/store";
|
|
import { store } from "@/store/store";
|
|
|
|
+import { throttle } from "lodash-es";
|
|
|
|
|
|
const { addTimeout, addInterval } = useTimers();
|
|
const { addTimeout, addInterval } = useTimers();
|
|
// window.faceapi = faceapi;
|
|
// window.faceapi = faceapi;
|
|
@@ -22,6 +23,35 @@ const { addTimeout, addInterval } = useTimers();
|
|
// };
|
|
// };
|
|
// })();
|
|
// })();
|
|
|
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
+ await faceapi.nets.tinyFaceDetector.load(FACE_API_MODEL_PATH);
|
|
|
|
+ // faceapi.nets.faceRecognitionNet.load(modelsPath);
|
|
|
|
+ await faceapi.loadFaceLandmarkModel(FACE_API_MODEL_PATH);
|
|
|
|
+ faceapi.tf.ENV.set("WEBGL_PACK", false);
|
|
|
|
+
|
|
|
|
+ async function trackHead() {
|
|
|
|
+ const video = <HTMLVideoElement>document.getElementById("video");
|
|
|
|
+ if (video?.readyState === 4 && faceapi.nets.tinyFaceDetector.params) {
|
|
|
|
+ clearInterval(trackHeadInterval);
|
|
|
|
+ } else {
|
|
|
|
+ logger({
|
|
|
|
+ cnl: ["server"],
|
|
|
|
+ key: "FaceTracking",
|
|
|
|
+ act: "未达到实时人脸开启条件",
|
|
|
|
+ });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ logger({ cnl: ["server", "console"], act: "start tracking ... " });
|
|
|
|
+ await detectTest();
|
|
|
|
+
|
|
|
|
+ await detectFaces();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 重复启动头部追踪,直到成功启动
|
|
|
|
+ const trackHeadInterval = addInterval(trackHead, 1000);
|
|
|
|
+});
|
|
|
|
+
|
|
|
|
+//#region webgl 参数
|
|
let __cache4WebglAvailable: boolean | null = null;
|
|
let __cache4WebglAvailable: boolean | null = null;
|
|
function webgl_available() {
|
|
function webgl_available() {
|
|
if (__cache4WebglAvailable !== null) return __cache4WebglAvailable;
|
|
if (__cache4WebglAvailable !== null) return __cache4WebglAvailable;
|
|
@@ -46,24 +76,12 @@ function tensorFlowWebPackStatus() {
|
|
}
|
|
}
|
|
return __cache4TensorFlowWebPackStatus;
|
|
return __cache4TensorFlowWebPackStatus;
|
|
}
|
|
}
|
|
|
|
+//#endregion
|
|
|
|
|
|
-// function getCPUModel() {
|
|
|
|
-// if (typeof nodeRequire != "undefined") {
|
|
|
|
-// var os = window.nodeRequire("os");
|
|
|
|
-// const cpus = os.cpus();
|
|
|
|
-// if (cpus.length > 0) {
|
|
|
|
-// return cpus[0].model;
|
|
|
|
-// }
|
|
|
|
-// }
|
|
|
|
-// return "null";
|
|
|
|
-// }
|
|
|
|
-
|
|
|
|
-// if (os.isWin7) alert("是win7");
|
|
|
|
-// if (os.isWin10) alert("是win10");
|
|
|
|
-
|
|
|
|
-let __inputSize = 128;
|
|
|
|
|
|
+let bestInputSize = 128;
|
|
let disableFaceTracking = false;
|
|
let disableFaceTracking = false;
|
|
|
|
|
|
|
|
+/** 测试学生电脑适合的参数 */
|
|
async function detectTest() {
|
|
async function detectTest() {
|
|
const inputSizeList = [128, 160, 224, 320, 416, 512, 608];
|
|
const inputSizeList = [128, 160, 224, 320, 416, 512, 608];
|
|
const succRate = [0, 0, 0, 0, 0, 0, 0];
|
|
const succRate = [0, 0, 0, 0, 0, 0, 0];
|
|
@@ -82,11 +100,7 @@ async function detectTest() {
|
|
new Promise((resolve) => setTimeout(resolve, 10 * 1000)),
|
|
new Promise((resolve) => setTimeout(resolve, 10 * 1000)),
|
|
]);
|
|
]);
|
|
const detectEndTime = performance.now();
|
|
const detectEndTime = performance.now();
|
|
- if (
|
|
|
|
- !result ||
|
|
|
|
- !result.length ||
|
|
|
|
- detectStartTime - detectEndTime > 2 * 1000
|
|
|
|
- ) {
|
|
|
|
|
|
+ if (!result || detectEndTime - detectStartTime > 2 * 1000) {
|
|
disableFaceTracking = true;
|
|
disableFaceTracking = true;
|
|
_hmt.push(["_trackEvent", "答题页面", "启动检测耗时过长:停止实时"]);
|
|
_hmt.push(["_trackEvent", "答题页面", "启动检测耗时过长:停止实时"]);
|
|
logger({
|
|
logger({
|
|
@@ -95,13 +109,12 @@ async function detectTest() {
|
|
dtl: "启动检测耗时过长:停止实时",
|
|
dtl: "启动检测耗时过长:停止实时",
|
|
ext: {
|
|
ext: {
|
|
result: JSON.stringify(result),
|
|
result: JSON.stringify(result),
|
|
- cost: detectStartTime - detectEndTime,
|
|
|
|
|
|
+ cost: detectEndTime - detectStartTime,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
} catch (error) {
|
|
- console.log(error);
|
|
|
|
disableFaceTracking = true;
|
|
disableFaceTracking = true;
|
|
_hmt.push(["_trackEvent", "答题页面", "启动检测错误:停止实时"]);
|
|
_hmt.push(["_trackEvent", "答题页面", "启动检测错误:停止实时"]);
|
|
logger({
|
|
logger({
|
|
@@ -116,7 +129,7 @@ async function detectTest() {
|
|
for (let n = 0; n < detectTimes; n++) {
|
|
for (let n = 0; n < detectTimes; n++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 3 * 1000));
|
|
await new Promise((resolve) => setTimeout(resolve, 3 * 1000));
|
|
if (store.exam.isDoingFaceLiveness) {
|
|
if (store.exam.isDoingFaceLiveness) {
|
|
- console.log("正在活检,暂停实时人脸");
|
|
|
|
|
|
+ logger({ cnl: ["server", "console"], act: "正在活检,暂停实时人脸" });
|
|
await new Promise((resolve) => setTimeout(resolve, 120 * 1000));
|
|
await new Promise((resolve) => setTimeout(resolve, 120 * 1000));
|
|
}
|
|
}
|
|
const inputSize = inputSizeList[idx];
|
|
const inputSize = inputSizeList[idx];
|
|
@@ -149,41 +162,60 @@ async function detectTest() {
|
|
}
|
|
}
|
|
|
|
|
|
if (result && result.length >= 1) {
|
|
if (result && result.length >= 1) {
|
|
- console.log(`inputSize: ${inputSize} ${result.length}`);
|
|
|
|
|
|
+ logger({
|
|
|
|
+ cnl: ["server"],
|
|
|
|
+ key: "FaceTracking",
|
|
|
|
+ dtl: `inputSize: ${inputSize} ${result.length}`,
|
|
|
|
+ });
|
|
succRate[idx]++;
|
|
succRate[idx]++;
|
|
} else {
|
|
} else {
|
|
- console.log(`inputSize: ${inputSize} 检测失败`);
|
|
|
|
|
|
+ logger({
|
|
|
|
+ cnl: ["server"],
|
|
|
|
+ key: "FaceTracking",
|
|
|
|
+ act: "FT检测失败",
|
|
|
|
+ dtl: `inputSize: ${inputSize}`,
|
|
|
|
+ });
|
|
}
|
|
}
|
|
} catch (error) {
|
|
} catch (error) {
|
|
- console.log(error);
|
|
|
|
- console.log(`inputSize: ${inputSize} 检测失败-异常`);
|
|
|
|
|
|
+ logger({
|
|
|
|
+ cnl: ["server"],
|
|
|
|
+ key: "FaceTracking",
|
|
|
|
+ act: "FT检测失败-异常",
|
|
|
|
+ dtl: `inputSize: ${inputSize}`,
|
|
|
|
+ possibleError: error,
|
|
|
|
+ });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (succRate[idx] === detectTimes) {
|
|
if (succRate[idx] === detectTimes) {
|
|
- console.log(`inputSize: ${inputSizeList[idx]} 提前选中`);
|
|
|
|
|
|
+ logger({
|
|
|
|
+ cnl: ["server"],
|
|
|
|
+ key: "FaceTracking",
|
|
|
|
+ act: "FT提前选中",
|
|
|
|
+ dtl: `inputSize: ${inputSizeList[idx]}`,
|
|
|
|
+ });
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- console.log({ succRate });
|
|
|
|
const max = Math.max(...succRate);
|
|
const max = Math.max(...succRate);
|
|
|
|
|
|
const idx = succRate.indexOf(max);
|
|
const idx = succRate.indexOf(max);
|
|
|
|
|
|
- __inputSize = inputSizeList[idx];
|
|
|
|
|
|
+ bestInputSize = inputSizeList[idx];
|
|
logger({
|
|
logger({
|
|
cnl: ["server", "local"],
|
|
cnl: ["server", "local"],
|
|
pgn: "实时人脸检测",
|
|
pgn: "实时人脸检测",
|
|
- act: "最好的 inputSize 为:" + __inputSize,
|
|
|
|
|
|
+ dtl: "最好的 inputSize 为:" + bestInputSize,
|
|
|
|
+ ext: { succRate },
|
|
});
|
|
});
|
|
|
|
|
|
- return __inputSize;
|
|
|
|
|
|
+ return bestInputSize;
|
|
}
|
|
}
|
|
|
|
|
|
function getFaceDetectorOptions() {
|
|
function getFaceDetectorOptions() {
|
|
return new faceapi.TinyFaceDetectorOptions({
|
|
return new faceapi.TinyFaceDetectorOptions({
|
|
- inputSize: __inputSize || 128,
|
|
|
|
|
|
+ inputSize: bestInputSize || 128,
|
|
scoreThreshold: 0.5,
|
|
scoreThreshold: 0.5,
|
|
});
|
|
});
|
|
|
|
|
|
@@ -191,53 +223,23 @@ function getFaceDetectorOptions() {
|
|
// return new faceapi.MtcnnOptions({ minFaceSize: 200, scaleFactor: 0.8 });
|
|
// return new faceapi.MtcnnOptions({ minFaceSize: 200, scaleFactor: 0.8 });
|
|
}
|
|
}
|
|
|
|
|
|
-const detectTimeArray: number[] = [];
|
|
|
|
-
|
|
|
|
-onMounted(async () => {
|
|
|
|
- await faceapi.nets.tinyFaceDetector.load(FACE_API_MODEL_PATH);
|
|
|
|
- // faceapi.nets.faceRecognitionNet.load(modelsPath);
|
|
|
|
- await faceapi.loadFaceLandmarkModel(FACE_API_MODEL_PATH);
|
|
|
|
- faceapi.tf.ENV.set("WEBGL_PACK", false);
|
|
|
|
-
|
|
|
|
- let trackStarted = false;
|
|
|
|
-
|
|
|
|
- async function trackHead() {
|
|
|
|
- const video = <HTMLVideoElement>document.getElementById("video");
|
|
|
|
- if (
|
|
|
|
- video &&
|
|
|
|
- video.readyState === 4 &&
|
|
|
|
- faceapi.nets.tinyFaceDetector.params
|
|
|
|
- ) {
|
|
|
|
- trackStarted = true;
|
|
|
|
- } else {
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- console.log("start tracking ... ");
|
|
|
|
- await detectTest();
|
|
|
|
-
|
|
|
|
- await detectFaces();
|
|
|
|
- }
|
|
|
|
- const trackHeadInterval = addInterval(() => {
|
|
|
|
- if (trackStarted) {
|
|
|
|
- clearInterval(trackHeadInterval);
|
|
|
|
- } else {
|
|
|
|
- void trackHead();
|
|
|
|
- }
|
|
|
|
- }, 1000);
|
|
|
|
-});
|
|
|
|
-// beforeDestroy() {
|
|
|
|
-// clearTimeout(this.warningTimeout);
|
|
|
|
-// clearTimeout(this.detectFacesTimeout);
|
|
|
|
-// },
|
|
|
|
|
|
+const indepentExamingMsg = throttle(
|
|
|
|
+ () => $message.warning("请独立完成考试"),
|
|
|
|
+ 20 * 1000
|
|
|
|
+);
|
|
|
|
+const posureExamingMsg = throttle(
|
|
|
|
+ () => $message.warning("请调整坐姿,诚信考试"),
|
|
|
|
+ 20 * 1000
|
|
|
|
+);
|
|
|
|
|
|
let singleTimeUsage = 0;
|
|
let singleTimeUsage = 0;
|
|
let multipleTimeUsage = 0;
|
|
let multipleTimeUsage = 0;
|
|
-let showWaringTime = Date.now();
|
|
|
|
-
|
|
|
|
|
|
+const detectTimeArray: number[] = [];
|
|
let failTimes = 0;
|
|
let failTimes = 0;
|
|
|
|
|
|
let detectFacesTimeout: number;
|
|
let detectFacesTimeout: number;
|
|
-let warningTimeout: number;
|
|
|
|
|
|
+
|
|
|
|
+/** 定时检测人脸 */
|
|
async function detectFaces() {
|
|
async function detectFaces() {
|
|
if (
|
|
if (
|
|
disableFaceTracking ||
|
|
disableFaceTracking ||
|
|
@@ -252,34 +254,25 @@ async function detectFaces() {
|
|
_hmt.push(["_trackEvent", "答题页面", "关闭实时人脸检测,因为耗时过长"]);
|
|
_hmt.push(["_trackEvent", "答题页面", "关闭实时人脸检测,因为耗时过长"]);
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
- // FIXME: 接收活体进行中的事件
|
|
|
|
- // if (this.isDoingFaceLiveness) {
|
|
|
|
- // logger({
|
|
|
|
- // cnl: ["server"],
|
|
|
|
- // pgn: "实时人脸检测",
|
|
|
|
- // act: "正在活检,暂停实时人脸",
|
|
|
|
- // });
|
|
|
|
- // clearTimeout(detectFacesTimeout);
|
|
|
|
- // detectFacesTimeout = addTimeout(() => void detectFaces(), 10 * 1000);
|
|
|
|
- // return;
|
|
|
|
- // }
|
|
|
|
|
|
+ if (store.exam.isDoingFaceLiveness) {
|
|
|
|
+ logger({
|
|
|
|
+ cnl: ["server"],
|
|
|
|
+ pgn: "实时人脸检测",
|
|
|
|
+ act: "正在活检,暂停实时人脸",
|
|
|
|
+ });
|
|
|
|
+ clearTimeout(detectFacesTimeout);
|
|
|
|
+ detectFacesTimeout = addTimeout(() => void detectFaces(), 10 * 1000);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
|
|
const videoEl = <HTMLVideoElement>document.getElementById("video");
|
|
const videoEl = <HTMLVideoElement>document.getElementById("video");
|
|
- // var canvas = document.createElement("canvas");
|
|
|
|
- // canvas.width = 133;
|
|
|
|
- // canvas.height = 100;
|
|
|
|
-
|
|
|
|
- // var context = canvas.getContext("2d");
|
|
|
|
- // context.drawImage(videoEl, 0, 0, 133, 100);
|
|
|
|
const detectStartTime = performance.now();
|
|
const detectStartTime = performance.now();
|
|
- // this.___vWidth =
|
|
|
|
- // this.___vWidth ||
|
|
|
|
- // document.getElementById("video-container").clientWidth;
|
|
|
|
|
|
|
|
const options = getFaceDetectorOptions();
|
|
const options = getFaceDetectorOptions();
|
|
let result;
|
|
let result;
|
|
|
|
|
|
try {
|
|
try {
|
|
|
|
+ logger({ cnl: ["server"], key: "FaceTracking", act: "开始一次人脸检测" });
|
|
result = await faceapi
|
|
result = await faceapi
|
|
// .detectSingleFace(videoEl, options)
|
|
// .detectSingleFace(videoEl, options)
|
|
.detectAllFaces(videoEl, options);
|
|
.detectAllFaces(videoEl, options);
|
|
@@ -288,17 +281,34 @@ async function detectFaces() {
|
|
logger({ cnl: ["server"], act: "实时人脸检测失败", possibleError: e });
|
|
logger({ cnl: ["server"], act: "实时人脸检测失败", possibleError: e });
|
|
throw e;
|
|
throw e;
|
|
}
|
|
}
|
|
- // console.log(result);
|
|
|
|
|
|
+
|
|
|
|
+ if (!result) {
|
|
|
|
+ try {
|
|
|
|
+ logger({
|
|
|
|
+ cnl: ["server"],
|
|
|
|
+ key: "不可能的事情发生了",
|
|
|
|
+ dtl: "人脸检测结果格式不符合预期",
|
|
|
|
+ stk: JSON.stringify(result || {}),
|
|
|
|
+ });
|
|
|
|
+ } catch (error) {
|
|
|
|
+ logger({
|
|
|
|
+ cnl: ["server"],
|
|
|
|
+ key: "不可能的事情发生了",
|
|
|
|
+ dtl: "人脸检测结果stringify错误",
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
|
|
const detectEndTime = performance.now();
|
|
const detectEndTime = performance.now();
|
|
logger({
|
|
logger({
|
|
cnl: ["server", "console"],
|
|
cnl: ["server", "console"],
|
|
pgn: "实时人脸检测",
|
|
pgn: "实时人脸检测",
|
|
|
|
+ act: "做完一次人脸检测,准备统计...",
|
|
ext: {
|
|
ext: {
|
|
|
|
+ resultLen: result.length,
|
|
WebGL: webgl_available(),
|
|
WebGL: webgl_available(),
|
|
WEBGL_PACK: tensorFlowWebPackStatus(),
|
|
WEBGL_PACK: tensorFlowWebPackStatus(),
|
|
"single detect time": detectEndTime - detectStartTime,
|
|
"single detect time": detectEndTime - detectStartTime,
|
|
- resultLen: result.length,
|
|
|
|
},
|
|
},
|
|
});
|
|
});
|
|
singleTimeUsage = detectEndTime - detectStartTime;
|
|
singleTimeUsage = detectEndTime - detectStartTime;
|
|
@@ -316,11 +326,11 @@ async function detectFaces() {
|
|
cnl: ["server"],
|
|
cnl: ["server"],
|
|
pgn: "实时人脸检测",
|
|
pgn: "实时人脸检测",
|
|
ext: {
|
|
ext: {
|
|
|
|
+ detectTimeArray,
|
|
roundAvg: roundAvg + "ms",
|
|
roundAvg: roundAvg + "ms",
|
|
computer: isThisMachineOwnByStudent() ? "学生电脑" : "学习中心电脑",
|
|
computer: isThisMachineOwnByStudent() ? "学生电脑" : "学习中心电脑",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
- console.log(detectTimeArray);
|
|
|
|
detectTimeArray.push(0, 0); // 避免再次达到push条件和上传条件
|
|
detectTimeArray.push(0, 0); // 避免再次达到push条件和上传条件
|
|
|
|
|
|
// FIXME: 上线初期停止统计此类信息,过于零散
|
|
// FIXME: 上线初期停止统计此类信息,过于零散
|
|
@@ -336,40 +346,31 @@ async function detectFaces() {
|
|
|
|
|
|
multipleTimeUsage = roundAvg;
|
|
multipleTimeUsage = roundAvg;
|
|
}
|
|
}
|
|
- // init this.showWaringTime
|
|
|
|
- showWaringTime = showWaringTime || Date.now();
|
|
|
|
|
|
|
|
- if (result.length >= 2 && Date.now() - showWaringTime > 20 * 1000) {
|
|
|
|
- showWaringTime = Date.now();
|
|
|
|
- $message.warning("请独立完成考试");
|
|
|
|
|
|
+ if (result.length >= 2) {
|
|
|
|
+ indepentExamingMsg();
|
|
}
|
|
}
|
|
|
|
|
|
- if (result.length === 0 && Date.now() - showWaringTime > 20 * 1000) {
|
|
|
|
- showWaringTime = Date.now();
|
|
|
|
- $message.warning("请调整坐姿,诚信考试");
|
|
|
|
- failTimes = failTimes || failTimes++;
|
|
|
|
|
|
+ if (result.length === 0) {
|
|
|
|
+ posureExamingMsg();
|
|
|
|
+ failTimes++;
|
|
}
|
|
}
|
|
|
|
|
|
- if (
|
|
|
|
- (!result || result.length !== 1) &&
|
|
|
|
- !videoEl.classList.contains("video-warning")
|
|
|
|
- ) {
|
|
|
|
|
|
+ if (result.length !== 1 && !videoEl.classList.contains("video-warning")) {
|
|
videoEl.classList.add("video-warning");
|
|
videoEl.classList.add("video-warning");
|
|
- clearTimeout(warningTimeout);
|
|
|
|
- warningTimeout = addTimeout(function () {
|
|
|
|
- videoEl.classList.remove("video-warning");
|
|
|
|
- }, 3000);
|
|
|
|
|
|
+ addTimeout(() => videoEl.classList.remove("video-warning"), 3000);
|
|
}
|
|
}
|
|
|
|
|
|
clearTimeout(detectFacesTimeout);
|
|
clearTimeout(detectFacesTimeout);
|
|
|
|
+ logger({ cnl: ["server"], lvl: "debug", act: "准备下次人脸检测" });
|
|
detectFacesTimeout = addTimeout(async () => {
|
|
detectFacesTimeout = addTimeout(async () => {
|
|
- if (failTimes > 5) {
|
|
|
|
|
|
+ if (failTimes >= 5) {
|
|
$message.warning("请保持正确坐姿,确保脸部在摄像头内,背景无强光。");
|
|
$message.warning("请保持正确坐姿,确保脸部在摄像头内,背景无强光。");
|
|
- failTimes = 1;
|
|
|
|
|
|
+ failTimes = 0;
|
|
await detectTest();
|
|
await detectTest();
|
|
}
|
|
}
|
|
await detectFaces();
|
|
await detectFaces();
|
|
- }, 60 * 1000);
|
|
|
|
|
|
+ }, 20 * 1000);
|
|
}
|
|
}
|
|
</script>
|
|
</script>
|
|
|
|
|