Selaa lähdekoodia

上传抓拍到又拍云,人脸检测

Michael Wang 6 vuotta sitten
vanhempi
commit
e7bdbaff7d

+ 1 - 0
.env.development

@@ -1,4 +1,5 @@
 VUE_APP_TK_SERVER_URL=http://ecs-dev.qmth.com.cn:8868
+VUE_APP_UPYUN_URL=https://ecs-test-static.qmth.com.cn
 VUE_APP_UPYUN_UPLOAD_URL=https://v0.api.upyun.com/exam-cloud-test
 VUE_APP_UPYUN_HEADER_AUTH=Basic ZXhhbWNsb3VkOmV4YW1jbG91ZDEyMzQ1Ng==
 VUE_APP_FACEPP_KEY=kEz_MSSjkNuHL3fHhCvv62fXkAo-vobE

+ 1 - 0
.env.production

@@ -1,4 +1,5 @@
 VUE_APP_TK_SERVER_URL=http://ecs.qmth.com.cn:8868
+VUE_APP_UPYUN_URL=https://ecs-static.qmth.com.cn
 VUE_APP_UPYUN_UPLOAD_URL=https://v0.api.upyun.com/exam-cloud
 VUE_APP_UPYUN_HEADER_AUTH=Basic ZXhhbWNsb3VkOmV4YW1jbG91ZDEyMzQ1Ng==
 VUE_APP_FACEPP_KEY=VOlRKNlCSAYIOcSLDKOaZukkqpmi-Pwo

+ 174 - 0
src/components/FaceRecognition/FaceRecognition.vue

@@ -0,0 +1,174 @@
+<template>
+  <div>
+    <video id="video" ref="video" :width="width" :height="height" autoplay>
+    </video>
+    <div style="position: absolute; width: 400px; text-align: center; margin-top: -50px; color: #232323;">
+      <span class="verify-button" @click="snap">开始识别</span>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import {
+  UPYUN_URL,
+  FACEPP_API,
+  FACEPP_KEY,
+  FACEPP_SECRET
+} from "../../constants/constants.js";
+
+export default {
+  name: "FaceRecognition",
+  data() {
+    return { hide: true };
+  },
+  props: {
+    width: String,
+    height: String,
+    closeCamera: Boolean // optional
+  },
+  async mounted() {
+    this.openCamera();
+  },
+  watch: {
+    closeCamera: function(newValue, oldValue) {
+      if (newValue) {
+        console.log("关闭摄像头");
+        this.$refs.video.srcObject.getTracks().forEach(function(track) {
+          track.stop();
+        });
+      } else {
+        this.openCamera();
+      }
+    }
+  },
+  beforeDestroy() {
+    this.$refs.video.srcObject.getTracks().forEach(function(track) {
+      track.stop();
+    });
+  },
+  methods: {
+    async openCamera() {
+      const video = this.$refs.video;
+
+      if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+        try {
+          console.log("启动摄像头");
+          const stream = await navigator.mediaDevices.getUserMedia({
+            video: { facingMode: "user" }
+          });
+
+          video.srcObject = stream;
+          video.play();
+        } catch (error) {
+          console.log(error);
+          this.$Message.error("无法启用摄像头");
+        }
+      } else {
+        this.$Message.error("没有找到可用的摄像头");
+      }
+    },
+    // TODO: 定时抓拍
+    snap() {
+      const video = this.$refs.video;
+      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(this.uploadToServer);
+      video.play();
+    },
+    async uploadToServer(captureBlob) {
+      //保存抓拍照片到又拍云
+      var fileName = new Date().getTime() + ".jpg";
+      var fileUrl =
+        "/capture_photo/" + this.user.identityNumber + "/" + fileName;
+      try {
+        await this.$upyunhttp.put(fileUrl, captureBlob, {
+          headers: {
+            "Content-Type": "image/jpeg"
+          }
+        });
+      } catch (e) {
+        console.log(e);
+        this.$Message.error(e.message);
+        return;
+      }
+
+      const captureFilePath =
+        UPYUN_URL +
+        "/capture_photo/" +
+        this.user.identityNumber +
+        "/" +
+        fileName;
+      await this.faceCompare(captureFilePath);
+      this.$emit("on-recognize-result", "something wrong");
+    },
+    async faceCompare(captureFilePath) {
+      const res = await this.$http.get(
+        "/api/ecs_core/studentFaceInfo/identityNumber",
+        {
+          params: {
+            identityNumber: this.user.identityNumber,
+            orgId: this.user.rootOrgId
+          }
+        }
+      );
+      const faceToken = res.data.faceToken;
+      console.log(faceToken);
+      console.log(captureFilePath);
+
+      // TODO: 研究是否有更合适的位置,只使用了一次. 应该由server进行检测
+      try {
+        const faceCompareRes = await fetch(
+          FACEPP_API +
+            "/compare?" +
+            `api_key=${FACEPP_KEY}&api_secret=${FACEPP_SECRET}&face_token1=${faceToken}&image_url2=${captureFilePath}`,
+          {
+            method: "post",
+            headers: {
+              "Content-Type": "application/x-www-form-urlencoded"
+            }
+          }
+        );
+        const verifyResult = await faceCompareRes.json();
+        console.log("人脸检测结果: " + verifyResult);
+
+        // 告知服务器人脸检测结果
+        const params = new URLSearchParams();
+        params.append("pass", true);
+        params.append("action", "COMPARE");
+        await this.$http.post("/api/face_capture", params);
+
+        // TODO: 识别成功、失败的通知或跳转
+      } catch (e) {
+        console.log(e);
+        this.$Message.error(e.message);
+        return;
+      }
+    }
+  },
+  computed: {
+    ...mapState(["user"])
+  }
+};
+</script>
+
+<style scoped>
+.verify-button {
+  font-size: 16px;
+  background-color: #ffcc00;
+  display: inline-block;
+  padding: 6px 16px;
+  border-radius: 6px;
+}
+
+.verify-button:hover {
+  color: #444444;
+  cursor: pointer;
+}
+</style>

+ 0 - 59
src/components/HelloWorld.vue

@@ -1,59 +0,0 @@
-<template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <p>
-      For guide and recipes on how to configure / customize this project,<br>
-      check out the
-      <a href="https://cli.vuejs.org" target="_blank">vue-cli documentation</a>.
-    </p>
-    <h3>Installed CLI Plugins</h3>
-    <ul>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank">babel</a></li>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank">pwa</a></li>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank">eslint</a></li>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest" target="_blank">unit-jest</a></li>
-    </ul>
-    <h3>Essential Links</h3>
-    <ul>
-      <li><a href="https://vuejs.org" target="_blank">Core Docs</a></li>
-      <li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li>
-      <li><a href="https://chat.vuejs.org" target="_blank">Community Chat</a></li>
-      <li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>
-    </ul>
-    <h3>Ecosystem</h3>
-    <ul>
-      <li><a href="https://router.vuejs.org" target="_blank">vue-router</a></li>
-      <li><a href="https://vuex.vuejs.org" target="_blank">vuex</a></li>
-      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank">vue-devtools</a></li>
-      <li><a href="https://vue-loader.vuejs.org" target="_blank">vue-loader</a></li>
-      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
-    </ul>
-  </div>
-</template>
-
-<script>
-export default {
-  name: "HelloWorld",
-  props: {
-    msg: String
-  }
-};
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped>
-h3 {
-  margin: 40px 0 0;
-}
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-a {
-  color: #42b983;
-}
-</style>

+ 1 - 0
src/constants/constants.js

@@ -2,6 +2,7 @@ export const FACEPP_API = "facepp_api";
 export const FACEID_LINENESS_URL =
   "https://api.megvii.com/faceid/liveness/v2/do?token=";
 export const TK_SERVER_URL = process.env.VUE_APP_TK_SERVER_URL;
+export const UPYUN_URL = process.env.VUE_APP_UPYUN_URL;
 export const UPYUN_UPLOAD_URL = process.env.VUE_APP_UPYUN_UPLOAD_URL;
 export const UPYUN_HEADER_AUTH = process.env.VUE_APP_UPYUN_HEADER_AUTH;
 export const FACEPP_KEY = process.env.VUE_APP_FACEPP_KEY;

+ 46 - 18
src/features/OnlineExam/OnlineExamFaceCheckModal.vue

@@ -1,30 +1,27 @@
 <template>
-  <Modal v-model="faceCheckModalOpen" width="900" :mask-closable="false" :closable="false">
+  <Modal v-model="faceCheckModalOpen" width="900" :mask-closable="false" :closable="false" @on-visible-change="updateCameraState">
     <div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
       <div class="qm-title-text">人脸识别</div>
-      <Icon type="ios-close" class="qm-icon-button" size="24" @click="close" />
+      <Icon type="ios-close" class="qm-icon-button" size="24" @click="closeModal" />
     </div>
     <div style="display: grid; grid-template-columns: 200px 400px 1fr; grid-gap: 5px;">
-      <div class="avatar">
-        <img :src="userPhoto" width="200px" alt="底照" />
-        <div class="avatar-info" style="text-align: center; margin-top: -50px; color: white;">
+      <div class="avatar" :style="{ backgroundImage: `url('${userPhoto}')` }">
+        <!-- <img :src="userPhoto" width="200px" height="300px" alt="底照" /> -->
+        <div class="avatar-info" style="text-align: center; margin-top: 260px; color: white;">
           <!-- FIXME: 没有底照的逻辑 -->
           <span style="background-color: rgba(0, 0, 0, 0.5); display: inline-block;padding: 6px 16px; border-radius: 6px;">我的底照</span>
         </div>
       </div>
       <div class="camera">
-        <video id="video" width="400px" height="100%" autoplay>
-        </video>
-        <div class="avatar-info" style="text-align: center; margin-top: -50px; color: #232323;">
-          <span class="verify-button" style="font-size: 16px; background-color: #ffcc00; display: inline-block;padding: 6px 16px; border-radius: 6px;">开始识别</span>
-        </div>
+        <FaceRecognition width="400" height="300" :close-camera="closeCamera" @on-recognize-result="getFaceRecognitionResult">
+        </FaceRecognition>
       </div>
-      <div class="verify-desc">
-        <h4 class="font-thin m-t-none m-b text-info">操作提示:</h4>
-        <p class="text-sm m-b-xs">1.请先确保摄像头设备已连接并能正常工作;</p>
-        <p class="text-sm m-b-xs">2.请保持光源充足,不要逆光操作;</p>
-        <p class="text-sm m-b-xs">3.请保证脸部正面面向摄像头,并适当调整姿势保证整个脸部能够进入左侧识别画面;</p>
-        <p class="text-sm m-b-xs">4.系统识别通过后,将自动跳转进入考试界面;</p>
+      <div class="verify-desc qm-primary-text">
+        <h4 class="qm-big-text" style="font-weight: bold">操作提示:</h4>
+        <p>1.请先确保摄像头设备已连接并能正常工作;</p>
+        <p>2.请保持光源充足,不要逆光操作;</p>
+        <p>3.请保证脸部正面面向摄像头,并适当调整姿势保证整个脸部能够进入左侧识别画面;</p>
+        <p>4.系统识别通过后,将自动跳转进入考试界面;</p>
       </div>
     </div>
     <div slot="footer">
@@ -33,12 +30,14 @@
 </template>
 
 <script>
+import FaceRecognition from "@/components/FaceRecognition/FaceRecognition.vue";
 import { createNamespacedHelpers } from "vuex";
 const { mapState, mapMutations } = createNamespacedHelpers("examHomeModule");
 
 export default {
+  name: "OnlineExamFaceCheckModal",
   data() {
-    return { userPhoto: null };
+    return { userPhoto: null, closeCamera: false };
   },
   props: {
     open: Boolean
@@ -66,9 +65,38 @@ export default {
   },
   methods: {
     ...mapMutations(["toggleFaceCheckModal"]),
-    close() {
+    closeModal() {
+      this.closeCamera = true;
       this.toggleFaceCheckModal(false);
+    },
+    updateCameraState(modalVisible) {
+      console.log(modalVisible);
+      this.closeCamera = !modalVisible;
+    },
+    getFaceRecognitionResult(err, result) {
+      console.log(err);
+      if (!err) {
+        const { faceCount, verifyResult, fileName } = result;
+      }
     }
+  },
+  components: {
+    FaceRecognition
   }
 };
 </script>
+
+<style scoped>
+.avatar {
+  background: center no-repeat;
+  background-size: cover;
+  width: 200px;
+  height: 300px;
+}
+
+.verify-desc {
+  padding: 0 1em;
+  line-height: 1.8em;
+}
+</style>
+

+ 3 - 3
src/router.js

@@ -14,12 +14,12 @@ let router = new Router({
   routes: [
     {
       path: "/",
-      name: "home",
+      name: "Home",
       component: OnlineExamHome
     },
     {
       path: "/login",
-      name: "login",
+      name: "Login",
       component: Login
     },
     {
@@ -39,7 +39,7 @@ let router = new Router({
     },
     {
       path: "/password",
-      name: "password",
+      name: "Password",
       component: Password
     },
     {

+ 22 - 9
src/utils/axios.js

@@ -1,9 +1,12 @@
 import Vue from "vue";
-import Axios from "axios";
+import axios from "axios";
 import { Message } from "iview";
 import router from "../router";
+import { UPYUN_UPLOAD_URL, UPYUN_HEADER_AUTH } from "@/constants/constants.js";
 //axios配置 start
 
+const qmInstance = axios.create({});
+
 //请求拦截
 
 /**
@@ -15,7 +18,7 @@ import router from "../router";
  * */
 
 let wk_token, wk_key;
-Axios.interceptors.request.use(
+qmInstance.interceptors.request.use(
   config => {
     if (config.url.includes("/login") === false) {
       if (!wk_token) {
@@ -44,7 +47,7 @@ Axios.interceptors.request.use(
 );
 
 //响应拦截
-Axios.interceptors.response.use(
+qmInstance.interceptors.response.use(
   response => {
     return response;
   },
@@ -72,23 +75,33 @@ Axios.interceptors.response.use(
     if (status == 403 || status == 401) {
       wk_token = null;
       router.push({
-        name: "login"
+        name: "Login"
       });
     }
     return Promise.reject(error);
   }
 );
 
-Axios.defaults.withCredentials = true; //允许跨域携带cookie
-Axios.defaults.timeout = 10000; //超时时间
-Axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; //标识这是一个 ajax 请求
+qmInstance.defaults.withCredentials = true; //允许跨域携带cookie
+qmInstance.defaults.timeout = 10000; //超时时间
+qmInstance.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; //标识这是一个 ajax 请求
 
-Vue.prototype.$http = Axios;
+const upyunInstance = axios.create({
+  baseURL: UPYUN_UPLOAD_URL
+});
+// FIXME: axios bug. wait 0.19 release. https://github.com/axios/axios/issues/385
+upyunInstance.defaults.headers.common = {};
+upyunInstance.defaults.headers.common["Authorization"] = UPYUN_HEADER_AUTH;
 
+Vue.prototype.$http = qmInstance;
+Vue.prototype.$upyunhttp = upyunInstance;
 export default {
   install: function(Vue) {
     Object.defineProperty(Vue.prototype, "$http", {
-      value: Axios
+      value: qmInstance
+    });
+    Object.defineProperty(Vue.prototype, "$upyunhttp", {
+      value: upyunInstance
     });
   }
 };