瀏覽代碼

完善新活体逻辑

Michael Wang 5 年之前
父節點
當前提交
d580cb64f1
共有 2 個文件被更改,包括 313 次插入76 次删除
  1. 15 4
      src/features/OnlineExam/Examing/ExamingHome.vue
  2. 298 72
      src/features/OnlineExam/Examing/FaceMotion/FaceMotion.vue

+ 15 - 4
src/features/OnlineExam/Examing/ExamingHome.vue

@@ -21,7 +21,11 @@
       <div :class="['question-nav']">
         <QuestionNavView :paper-struct="paperStruct" />
       </div>
-      <div v-if="faceEnable && startVideoAfterDelay" class="camera">
+      <div
+        v-if="faceEnable && startVideoAfterDelay"
+        class="camera"
+        :style="{ display: showFaceMotion ? 'none' : 'block' }"
+      >
         <FaceRecognition
           v-if="faceEnable"
           width="400"
@@ -551,7 +555,7 @@ export default {
           "/api/ecs_oe_student/faceBiopsy/getFaceBiopsyBaseInfo?examRecordDataId=" +
             examRecordDataId
         );
-        console.log(faceBiopsyBaseInfoData);
+        console.log(faceBiopsyBaseInfoData.data);
 
         faceVerifyMinute = faceBiopsyBaseInfoData.data.faceVerifyMinute;
         identificationOfLivingBodyScheme =
@@ -559,7 +563,8 @@ export default {
       }
 
       // 仅在线上使用活体检测
-      if (process.env.NODE_ENV === "production" && faceVerifyMinute) {
+      // if (process.env.NODE_ENV === "production" && faceVerifyMinute) {
+      if (faceVerifyMinute) {
         const enoughTimeForFaceId = this.remainTime // 如果remainTime取到了的话
           ? this.remainTime / (60 * 1000) - 1 > faceVerifyMinute
           : true;
@@ -595,8 +600,14 @@ export default {
     closeFaceId() {
       this.showFaceId = false;
     },
-    closeFaceMotion() {
+    closeFaceMotion(faceLiveResult) {
       this.showFaceMotion = false;
+      console.log(faceLiveResult);
+      if (faceLiveResult.endExam) {
+        this.realSubmitPaper();
+      } else if (faceLiveResult.needNextVerify) {
+        this.initFaceLiveness();
+      }
     },
     async answerAllQuestions(ignoreDirty) {
       const answers = this.examQuestionList

+ 298 - 72
src/features/OnlineExam/Examing/FaceMotion/FaceMotion.vue

@@ -29,10 +29,19 @@
             'text-align': 'center',
           }"
         >
-          保持<span style="color: blue">{{
-            shouldDetectExpression ? (currentStep.happy ? "笑容" : "严肃") : ""
-          }}</span>
-          <Progress hide-info :percent="stepProgress" />
+          <div v-if="currentStep.action === 'FACE_COMPARE'">
+            请调整脸部与摄像头的距离
+          </div>
+          <div v-else>
+            保持<span style="color: blue">{{
+              shouldDetectExpression
+                ? currentStep.happy
+                  ? "笑容"
+                  : "严肃"
+                : ""
+            }}</span>
+            <Progress hide-info :percent="stepProgress" />
+          </div>
         </div>
       </div>
 
@@ -135,6 +144,7 @@
 
 <script>
 import * as faceapi from "face-api.js";
+import MD5 from "js-md5";
 // import introJs from "intro.js";
 import throttle from "lodash-es/throttle";
 
@@ -198,6 +208,24 @@ export default {
       behavingStartDate: null,
       behavingTimestampe: null,
       shouldDetectExpression: null,
+      pauseDetecting: false,
+      compareResult: null,
+      finalResult: {
+        examRecordDataId: 0,
+        faceBiopsyItemId: 0,
+        verifySteps: [
+          {
+            action: "FACE_COMPARE",
+            errorMsg: "string",
+            resourceType: "PIC",
+            resourceUrl: "string",
+            result: true,
+            resultJson: "string",
+            stay: 0,
+            stepId: 0,
+          },
+        ],
+      },
     };
   },
   computed: {
@@ -210,6 +238,12 @@ export default {
     currentStep() {
       return this.instructions.steps.find(v => !v.finished) || {};
     },
+    corFinalResult() {
+      const idx = this.finalResult.verifySteps.findIndex(
+        v => v.stepId === this.currentStep.stepId
+      );
+      return this.finalResult.verifySteps[idx];
+    },
     instructionsFinished() {
       return this.instructions.steps.every(v => v.finished);
     },
@@ -242,7 +276,7 @@ export default {
         //   type: "success",
         // });
         this.$Message.success({
-          content: "恭喜你,活体检测通过",
+          content: "活体检测通过",
           duration: 5,
         });
         // this.resetTest();
@@ -251,6 +285,10 @@ export default {
     },
     "instructions.total"(total) {
       if (total <= 0) {
+        console.log(this.corFinalResult);
+        this.corFinalResult.timeout = true;
+        this.corFinalResult.result = false;
+        this.corFinalResult.errorMsg = "超时!活体检测失败!";
         this.failedTest("超时!活体检测失败!");
       }
     },
@@ -258,10 +296,9 @@ export default {
   async created() {
     // console.log(faceapi);
     // console.log(faceapi.nets.tinyFaceDetector);
-    this.resetTest();
-
     this.$Spin.show({});
     await this.fetchData();
+    this.resetTest();
 
     await faceapi.nets.tinyFaceDetector.load(modelsPath);
     await faceapi.loadFaceLandmarkModel(modelsPath);
@@ -287,87 +324,50 @@ export default {
       const faceBiopsyInfo = faceBiopsyInfoData.data;
       console.log(faceBiopsyInfo);
       this.faceBiopsyInfo = faceBiopsyInfo;
-
+    },
+    async closeMe() {
       const faceLiveResultData = await this.$http.post(
         "/api/ecs_oe_student/faceBiopsy/saveFaceBiopsyResult",
-        {
-          examRecordDataId,
-          faceBiopsyItemId: faceBiopsyInfo.faceBiopsyItemId,
-          verifySteps: faceBiopsyInfo.verifySteps.map(s => {
-            s.result = true;
-            return s;
-          }),
-        }
+        this.finalResult
       );
-      console.log(faceLiveResultData.data);
-    },
-    closeMe() {
-      this.$emit("closeFaceMotion");
+      const faceLiveResult = faceLiveResultData.data;
+      console.log(faceLiveResult);
+      this.$emit("closeFaceMotion", faceLiveResult);
     },
     resetTest() {
       // this.isDetecting = true;
-      this.asked = false;
+      // this.asked = false;
       this.shoudAdjustDistance = true;
       this.behavingStartDate = null;
       this.happyFailedTimes = 0;
       this.singleFaceFailedTimes = 0;
-      this.instructions = {
-        total: 60,
-        steps: [
-          {
+      const steps = this.faceBiopsyInfo.verifySteps
+        // .filter(s => ["HAPPY", "SERIOUS"].includes(s.action))
+        .map(s => {
+          return {
             section: 0,
-            stay: (Math.round(Math.random() * 10) % 5) + 2,
-            happy: Math.random() > 0.5,
+            stay: s.stay,
+            happy: s.action === "HAPPY",
             finished: false,
-          },
-          {
-            section: 0,
-            stay: (Math.round(Math.random() * 10) % 5) + 2,
-            happy: Math.random() > 0.5,
-            finished: false,
-          },
-          {
-            section: 0,
-            stay: (Math.round(Math.random() * 10) % 5) + 2,
-            happy: Math.random() > 0.5,
-            finished: false,
-          },
-        ],
+            stepId: s.stepId,
+            action: s.action,
+          };
+        });
+      this.instructions = {
+        total: 60,
+        steps,
       };
-      let section = (Math.round(Math.random() * 10) % 3) + 1;
-      this.instructions.steps[0].section = section;
-      let step = 1;
-      let sectionNew;
-      // 每一个section都与上一个不一样
-      // TODO: 有0section的情况
-      while ((sectionNew = (Math.round(Math.random() * 10) % 3) + 1)) {
-        if (section === sectionNew) {
-          continue;
-        }
-        this.instructions.steps[step].section = sectionNew;
-        [section, sectionNew] = [sectionNew, section];
-        // console.log(section, sectionNew);
-        step++;
-        if (step === 3) {
-          break;
-        }
-      }
 
       console.log(this.instructions);
-      // this.instructions.steps.map(v => (v.section = 0));
-      this.instructions.steps[0].section = 0;
-      this.instructions.steps[1].section = 0;
-      this.instructions.steps[2].section = 0;
-
-      const happy = this.instructions.steps[0].happy;
-      this.instructions.steps[1].happy = !happy;
-      this.instructions.steps[2].happy = happy;
 
       this.shouldDetectExpression = true;
       // this.shouldDoFaceRecognition = true;
-
-      // function setSection(index, previousSection) {}
-      // console.log(this.instructions.steps);
+      const examRecordDataId = this.$route.params.examRecordDataId;
+      this.finalResult = {
+        examRecordDataId,
+        faceBiopsyItemId: this.faceBiopsyInfo.faceBiopsyItemId,
+        verifySteps: this.faceBiopsyInfo.verifySteps,
+      };
     },
     async run() {
       // load face detection and face landmark models
@@ -468,6 +468,10 @@ export default {
       realStart();
     },
     async onPlay() {
+      if (!this.doneCompare && this.pauseDetecting) {
+        console.log({ pauseDetecting: this.pauseDetecting });
+        return;
+      }
       if (!this.asked) {
         this.asked = true;
         await this.increaseTestSpeed();
@@ -533,6 +537,8 @@ export default {
 
       // console.log(result);
       if (result && result.length >= 2) {
+        this.corFinalResult.result = false;
+        this.corFinalResult.errorMsg = "检测到多张人脸!活体检测失败!";
         this.failedTest("检测到多张人脸!活体检测失败!");
       }
 
@@ -542,6 +548,9 @@ export default {
           this.singleFaceFailedTimes++;
         }
         if (this.singleFaceFailedTimes >= 5) {
+          this.corFinalResult.result = false;
+          this.corFinalResult.errorMsg =
+            "活检过程中没有检测到人脸!活体检测失败!";
           this.failedTest("活检过程中没有检测到人脸!活体检测失败!");
         }
       }
@@ -624,7 +633,7 @@ export default {
               //   offset: 300,
               // });
               this.$Message.warning({ content: message, duration: 1 });
-            }, 1000);
+            }, 1500);
           this.tipHandler(message);
           if (this.shoudAdjustDistance) {
             setTimeout(() => this.onPlay(), 300);
@@ -632,6 +641,39 @@ export default {
           }
         } else {
           this.shoudAdjustDistance = false;
+          if (this.currentStep.action === "FACE_COMPARE") {
+            console.log("该做同步人脸比对了");
+            // 同步人脸比对
+            try {
+              if (this.pauseDetecting || this.doneCompare) return;
+              this.pauseDetecting = true;
+              const cs = await this.snap();
+              console.log(cs);
+              if (!cs) return;
+              this.compareResult = cs;
+              // 后台的计算需要通过resultJson来判断: isPass isStranger existsSystemError
+              this.finalResult.verifySteps[0].result = this.compareResult.isPass;
+              this.finalResult.verifySteps[0].resourceType = "PIC";
+              this.finalResult.verifySteps[0].resourceUrl = this.compareResult.fileUrl;
+              this.finalResult.verifySteps[0].resultJson = this.compareResult.faceCompareResult;
+              this.finalResult.verifySteps[0].errorMsg = this.compareResult.errorMsg;
+              this.pauseDetecting = false;
+              this.doneCompare = true;
+              if (this.compareResult.isPass) {
+                console.log("人脸同步比对成功");
+                if (!this.instructionsFinished && this.currentStep)
+                  this.currentStep.finished = true;
+              } else {
+                this.failedTest("人脸同步比对失败");
+                return;
+              }
+            } catch (error) {
+              console.log(error);
+            } finally {
+              // this.pauseDetecting = false;
+              this.videoStartPlay();
+            }
+          }
         }
         // 区域左边的一半
         const centerPoint = box.left + (box.right - box.left) / 2;
@@ -730,6 +772,8 @@ export default {
             }
           }
           if (this.happyFailedTimes >= 6) {
+            this.corFinalResult.result = false;
+            this.corFinalResult.errorMsg = "指定表情失败!活体检测失败!";
             this.failedTest("指定表情失败!活体检测失败!");
           }
         }
@@ -744,8 +788,10 @@ export default {
           this.behaving = false;
           this.happyFailedTimes = 0;
           this.behavingStartDate = null;
-          if (!this.instructionsFinished && this.currentStep)
+          if (!this.instructionsFinished && this.currentStep) {
+            this.corFinalResult.result = true;
             this.currentStep.finished = true;
+          }
         }
         // console.log(resizedResult.alignedRect.relativeBox.y);
         // if (true) {
@@ -805,6 +851,186 @@ export default {
       }
       this.$Spin.hide();
     },
+    videoStartPlay() {
+      const video = document.getElementById("inputVideo");
+      video && video.play();
+    },
+    async snap() {
+      if (this.disableSnap) {
+        return;
+      }
+      try {
+        this.disableSnap = true;
+        const captureBlob = await this.getSnapShot();
+        if (captureBlob.size < 10 * 1024) {
+          this.$Message.error({
+            content: "抓拍照片太小!",
+            duration: 15,
+            closable: true,
+          });
+          // 经查以前记录,不完整图片均为8192大小。此处设置小于10KB的图片为未抓拍成功
+          window._hmt.push([
+            "_trackEvent",
+            "摄像头框",
+            "抓拍照片较小",
+            captureBlob.size,
+          ]);
+          throw "抓拍照片较小";
+        }
+        this.videoStartPlay();
+        const [captureFilePath, signIdentifier] = await this.uploadToServer(
+          captureBlob
+        );
+        return this.faceCompareSync(captureFilePath, signIdentifier);
+      } catch (error) {
+        console.log("同步照片比对流程失败");
+        throw error;
+      } finally {
+        this.videoStartPlay();
+        // this.disableSnap = false;
+      }
+    },
+    async getSnapShot() {
+      return new Promise((resolve, reject) => {
+        const video = document.getElementById("inputVideo");
+        if (video.readyState !== 4 || !video.srcObject.active) {
+          this.$Message.error({
+            content: "摄像头没有正常启用",
+            duration: 5,
+            closable: true,
+          });
+          window._hmt.push([
+            "_trackEvent",
+            "摄像头框",
+            "摄像头状态",
+            "摄像头没有正常启用-退出" +
+              (this.lastSnapTime ? "(非初次抓拍)" : ""),
+          ]);
+          reject("摄像头没有正常启用");
+          this.logout(
+            "?LogoutReason=" +
+              "摄像头没有正常启用-退出" +
+              (this.lastSnapTime ? "(非初次抓拍)" : "")
+          );
+          return;
+        }
+        video.pause();
+        var canvas = document.createElement("canvas");
+        canvas.width = 220;
+        canvas.height = 165;
+
+        var context = canvas.getContext("2d");
+        context.drawImage(video, 0, 0, 220, 165);
+
+        canvas.toBlob(resolve, "image/png", 0.95);
+      });
+    },
+    async uploadToServer(captureBlob) {
+      async function blobToArray(blob) {
+        return new Promise(resolve => {
+          var reader = new FileReader();
+          reader.addEventListener("loadend", function() {
+            // reader.result contains the contents of blob as a typed array
+            resolve(reader.result);
+          });
+          reader.readAsArrayBuffer(blob);
+        });
+      }
+      //保存抓拍照片到服务器
+      let resultUrl, signIdentifier;
+      try {
+        const buffer = await blobToArray(captureBlob);
+
+        // console.log(buffer);
+        // var view1 = new Uint8Array(buffer);
+        // console.log(buffer[0], buffer[1], buffer[429721]);
+        const fileMd5 = MD5(buffer);
+        console.log(fileMd5);
+
+        const params = new URLSearchParams();
+        params.append("fileSuffix", "png");
+        params.append("fileMd5", fileMd5);
+        const res = await this.$http.get(
+          "/api/ecs_oe_student/examControl/getCapturePhotoUpYunSign?" + params
+        );
+
+        // console.log(res);
+
+        // let myHeaders = new Headers();
+        // for (let [k, v] of Object.entries(res.data.headers)) {
+        //   // console.log(k, v);
+        //   if (k.includes("tion") || k.includes("Date") || k.includes("MD5")) {
+        //     if (k === "Date") k = "x-date";
+        //     myHeaders.append(k, v);
+        //   }
+        let myFormData = new FormData();
+        for (let [k, v] of Object.entries(res.data.formParams)) {
+          myFormData.append(k, v);
+        }
+        myFormData.append("file", captureBlob);
+
+        try {
+          const res2 = await fetch(res.data.formUrl, {
+            method: "POST",
+            body: myFormData,
+          });
+          if (!res2.ok) {
+            throw res2.status;
+          }
+        } catch (error) {
+          window._hmt.push([
+            "_trackEvent",
+            "摄像头框",
+            "抓拍照片保存失败--upyun",
+            error,
+          ]);
+          throw error;
+        }
+
+        // console.log(response);
+        resultUrl = res.data.accessUrl;
+        signIdentifier = res.data.signIdentifier;
+        // this.serverLog("debug/S-005001", "抓拍照片保存成功:");
+        window._hmt.push(["_trackEvent", "摄像头框", "抓拍照片保存成功"]);
+      } catch (e) {
+        console.log(e);
+        // this.serverLog("debug/S-006001", "抓拍照片保存失败");
+        window._hmt.push([
+          "_trackEvent",
+          "摄像头框",
+          "保存抓拍照片到服务器失败!",
+        ]);
+        this.$Message.error({
+          content: "抓拍照片保存失败!",
+          duration: 15,
+          closable: true,
+        });
+        throw "抓拍照片保存失败!";
+      }
+      return [resultUrl, signIdentifier];
+    },
+    async faceCompareSync(captureFilePath, signIdentifier) {
+      try {
+        const res = await this.$http.post(
+          "/api/ecs_oe_student_face/examCaptureQueue/compareFaceSync?signIdentifier=" +
+            signIdentifier +
+            "&fileUrl=" +
+            encodeURIComponent(captureFilePath)
+        );
+
+        // // TODO: 识别成功、失败的通知或跳转
+        // this.$emit("on-recognize-result", {
+        //   error: null,
+        //   pass: res.data.isPass,
+        //   stranger: res.data.isStranger,
+        // });
+        return res.data;
+      } catch (e) {
+        console.log(e);
+        // this.$Message.error(e.message);
+        throw "同步照片比较失败!";
+      }
+    },
   },
 };
 </script>