FaceMotion.vue 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196
  1. <template>
  2. <div class="page-container">
  3. <div id="video-container" style="position: relative" class="page-container">
  4. <div v-if="shouldShowSections" class="instruction-tips above-video">
  5. <div
  6. class="instruction-animation"
  7. :style="{
  8. width: '33.3%',
  9. 'font-size': 16,
  10. padding: '0 10px',
  11. 'margin-left': reverseSection * 33 + '%',
  12. }"
  13. data-intro="停留的时间:每一次停留时长可能不一样。"
  14. >
  15. 请将脸部移入此区域,停留<span style="color: blue">{{
  16. currentStep.stay
  17. }}</span
  18. >秒,并保持<span style="color: blue">{{
  19. shouldDetectExpression ? (currentStep.happy ? "笑容" : "严肃") : ""
  20. }}</span>
  21. </div>
  22. </div>
  23. <div class="instruction-tips above-video">
  24. <div
  25. class="instruction-animation"
  26. :style="{
  27. width: '100%',
  28. 'font-size': '18px',
  29. 'text-align': 'center',
  30. }"
  31. >
  32. <div v-if="currentStep.action === 'FACE_COMPARE'">
  33. 请调整脸部与摄像头的距离
  34. </div>
  35. <div v-else>
  36. 保持<span style="color: blue">{{
  37. shouldDetectExpression
  38. ? currentStep.happy
  39. ? "笑容"
  40. : "严肃"
  41. : ""
  42. }}</span>
  43. <Progress hide-info :percent="stepProgress" />
  44. </div>
  45. </div>
  46. </div>
  47. <div
  48. v-if="isDetecting"
  49. class="instruction-total above-video"
  50. style="z-index: 3"
  51. >
  52. <div class="total-text" data-intro="请在规定的时间内完成。">
  53. {{ instructions.total }}
  54. </div>
  55. </div>
  56. <div v-if="shouldShowSections" class="seperators above-video">
  57. <div class="line"></div>
  58. <div class="line"></div>
  59. </div>
  60. <!-- <div v-if="!behaving" class="blocks above-video">
  61. <div class="block-index">1</div>
  62. <div class="block-index">2</div>
  63. <div class="block-index">3</div>
  64. </div> -->
  65. <div v-if="shouldShowSections" class="blocks above-video">
  66. <div
  67. v-for="item in [3, 2, 1]"
  68. :key="item"
  69. :class="[
  70. 'block-index-size',
  71. currentStep.section !== item && 'block-index-blur',
  72. ]"
  73. ></div>
  74. </div>
  75. <div
  76. v-if="shouldShowSections"
  77. :class="[
  78. 'above-video',
  79. 'instruction-face',
  80. 'instruction-animation',
  81. behaving && 'instruction-face-animation-state',
  82. ]"
  83. :style="{
  84. width: '33.3%',
  85. 'margin-left': reverseSection * 33 + '%',
  86. }"
  87. data-intro="停留的位置:请将脸部停留在头像所处的列。检测成功,头像会停止抖动。"
  88. >
  89. <!-- <el-progress
  90. type="circle"
  91. :stroke-width="12"
  92. :show-text="false"
  93. :percentage="stepProgress"
  94. style="margin-top: -13px;"
  95. class="above-video"
  96. ></el-progress> -->
  97. <!-- <div
  98. style="margin: 0 auto; border-radius: 50%; margin-top: -50px; width: 200px; height: 200px; border: 10px solid black;"
  99. ></div> -->
  100. </div>
  101. <div
  102. v-if="behaving"
  103. :class="[
  104. 'above-video',
  105. 'instruction-face',
  106. 'instruction-animation',
  107. behaving && 'instruction-face-animation-state',
  108. ]"
  109. :style="{
  110. width: '33.3%',
  111. 'margin-left': 1 * 33 + '%',
  112. }"
  113. >
  114. <!-- <el-progress
  115. type="circle"
  116. :stroke-width="12"
  117. :show-text="false"
  118. :percentage="stepProgress"
  119. style="margin-top: -13px;"
  120. class="above-video"
  121. ></el-progress> -->
  122. </div>
  123. <video
  124. id="inputVideo"
  125. class="detect-video"
  126. style="transform: scaleX(-1);"
  127. autoplay
  128. muted
  129. @loadedmetadata="onPlay"
  130. ></video>
  131. <canvas id="overlay" class="above-video" />
  132. </div>
  133. <div style="position: absolute; top: 0; left: 0; display:none;">
  134. <img
  135. id="base-photo"
  136. src="/student_base_photo/0/6/1560392244118.jpg"
  137. style="width: 150px;"
  138. />
  139. </div>
  140. </div>
  141. </template>
  142. <script>
  143. import * as faceapi from "face-api.js";
  144. import MD5 from "js-md5";
  145. // import introJs from "intro.js";
  146. import throttle from "lodash-es/throttle";
  147. // models path
  148. const modelsPath = "/models/20190620/";
  149. window.faceapi = faceapi;
  150. // let withBoxes = true;
  151. const os = (function() {
  152. const ua = navigator.userAgent.toLowerCase();
  153. return {
  154. isWin2K: /windows nt 5.0/.test(ua),
  155. isXP: /windows nt 5.1/.test(ua),
  156. isVista: /windows nt 6.0/.test(ua),
  157. isWin7: /windows nt 6.1/.test(ua),
  158. isWin8: /windows nt 6.2/.test(ua),
  159. isWin81: /windows nt 6.3/.test(ua),
  160. isWin10: /windows nt 10.0/.test(ua),
  161. };
  162. })();
  163. // tiny_face_detector options
  164. function getFaceDetectorOptions() {
  165. let inputSize = 160;
  166. if (os.isWin7) {
  167. inputSize = 256; // 在win7上无bug,速度快,效果较好
  168. } else if (os.isWin10) {
  169. inputSize = 320; // 在win10上,效果较好
  170. }
  171. window.____hideMe =
  172. window.____hideMe ||
  173. new faceapi.TinyFaceDetectorOptions({
  174. inputSize, // 这行是解决Box.constructor - expected box to be IBoundingBox | IRect, instead have 问题的关键
  175. scoreThreshold: 0.5,
  176. });
  177. return window.____hideMe;
  178. // return new faceapi.SsdMobilenetv1Options({ minConfidence: 0.8 });
  179. // return new faceapi.MtcnnOptions({ minFaceSize: 200, scaleFactor: 0.8 });
  180. }
  181. export default {
  182. name: "FaceMotion",
  183. data() {
  184. return {
  185. isDetecting: false,
  186. shoudAdjustDistance: true,
  187. introStarted: false,
  188. asked: false,
  189. instructions: {
  190. total: 60,
  191. steps: [
  192. { section: 2, stay: 3, happy: true, finished: false },
  193. { section: 3, stay: 5, happy: true, finished: false },
  194. { section: 2, stay: 4, happy: true, finished: false },
  195. ],
  196. },
  197. behaving: false,
  198. behavingStartDate: null,
  199. behavingTimestampe: null,
  200. shouldDetectExpression: null,
  201. pauseDetecting: false,
  202. compareResult: null,
  203. finalResult: {
  204. examRecordDataId: 0,
  205. faceBiopsyItemId: 0,
  206. verifySteps: [
  207. {
  208. action: "FACE_COMPARE",
  209. errorMsg: "string",
  210. resourceType: "PIC",
  211. resourceUrl: "string",
  212. result: true,
  213. resultJson: "string",
  214. stay: 0,
  215. stepId: 0,
  216. },
  217. ],
  218. },
  219. };
  220. },
  221. computed: {
  222. shouldShowSections() {
  223. if (this.currentStep.section === 0) return false;
  224. if (this.introStarted) return true;
  225. if (this.isDetecting && this.shoudAdjustDistance) return false;
  226. return this.isDetecting;
  227. },
  228. currentStep() {
  229. return this.instructions.steps.find(v => !v.finished) || {};
  230. },
  231. corFinalResult() {
  232. const idx = this.finalResult.verifySteps.findIndex(
  233. v => v.stepId === this.currentStep.stepId
  234. );
  235. return this.finalResult.verifySteps[idx];
  236. },
  237. instructionsFinished() {
  238. return this.instructions.steps.every(v => v.finished);
  239. },
  240. reverseSection() {
  241. return [3, 2, 1][this.currentStep.section - 1] - 1;
  242. },
  243. stepProgress() {
  244. if (this.instructionsFinished) return 0;
  245. if (!this.behaving) return 100;
  246. let progress =
  247. 100 -
  248. (100 * (this.behavingTimestampe - this.behavingStartDate)) /
  249. (this.currentStep.stay * 1000);
  250. // console.log("progress: ", progress);
  251. if (progress > 100) {
  252. progress = 100;
  253. } else if (progress < 0) {
  254. progress = 0;
  255. }
  256. return progress;
  257. },
  258. },
  259. watch: {
  260. instructionsFinished(finished) {
  261. if (finished) {
  262. clearInterval(this.remainInteval);
  263. this.isDetecting = false;
  264. // this.$message({
  265. // message: "恭喜你,活体检测通过",
  266. // type: "success",
  267. // });
  268. this.$Message.success({
  269. content: "活体检测通过",
  270. duration: 5,
  271. });
  272. // this.resetTest();
  273. this.closeMe();
  274. }
  275. },
  276. "instructions.total"(total) {
  277. if (total <= 0) {
  278. console.log(this.corFinalResult);
  279. this.corFinalResult.timeout = true;
  280. this.corFinalResult.result = false;
  281. this.corFinalResult.errorMsg = "超时!活体检测失败!";
  282. this.failedTest("超时!活体检测失败!");
  283. }
  284. },
  285. },
  286. async created() {
  287. // console.log(faceapi);
  288. // console.log(faceapi.nets.tinyFaceDetector);
  289. this.$Spin.show({});
  290. await this.fetchData();
  291. this.resetTest();
  292. await faceapi.nets.tinyFaceDetector.load(modelsPath);
  293. await faceapi.loadFaceLandmarkModel(modelsPath);
  294. await faceapi.nets.faceExpressionNet.load(modelsPath);
  295. // await faceapi.nets.ssdMobilenetv1.load(modelsPath);
  296. // await faceapi.nets.faceRecognitionNet.load(modelsPath);
  297. faceapi.tf.ENV.set("WEBGL_PACK", false);
  298. // faceapi.nets.mtcnn.load(modelsPath);
  299. },
  300. mounted() {
  301. this.run();
  302. },
  303. methods: {
  304. async fetchData() {
  305. const examRecordDataId = this.$route.params.examRecordDataId;
  306. // FIXME: 失败了再取?
  307. const faceBiopsyInfoData = await this.$http.get(
  308. "/api/ecs_oe_student/faceBiopsy/getFaceBiopsyInfo?examRecordDataId=" +
  309. examRecordDataId
  310. );
  311. const faceBiopsyInfo = faceBiopsyInfoData.data;
  312. console.log(faceBiopsyInfo);
  313. this.faceBiopsyInfo = faceBiopsyInfo;
  314. },
  315. async closeMe() {
  316. const faceLiveResultData = await this.$http.post(
  317. "/api/ecs_oe_student/faceBiopsy/saveFaceBiopsyResult",
  318. this.finalResult
  319. );
  320. const faceLiveResult = faceLiveResultData.data;
  321. console.log(faceLiveResult);
  322. this.$emit("closeFaceMotion", faceLiveResult);
  323. },
  324. resetTest() {
  325. // this.isDetecting = true;
  326. // this.asked = false;
  327. this.shoudAdjustDistance = true;
  328. this.behavingStartDate = null;
  329. this.happyFailedTimes = 0;
  330. this.singleFaceFailedTimes = 0;
  331. const steps = this.faceBiopsyInfo.verifySteps
  332. // .filter(s => ["HAPPY", "SERIOUS"].includes(s.action))
  333. .map(s => {
  334. return {
  335. section: 0,
  336. stay: s.stay,
  337. happy: s.action === "HAPPY",
  338. finished: false,
  339. stepId: s.stepId,
  340. action: s.action,
  341. };
  342. });
  343. this.instructions = {
  344. total: 60,
  345. steps,
  346. };
  347. console.log(this.instructions);
  348. this.shouldDetectExpression = true;
  349. // this.shouldDoFaceRecognition = true;
  350. const examRecordDataId = this.$route.params.examRecordDataId;
  351. this.finalResult = {
  352. examRecordDataId,
  353. faceBiopsyItemId: this.faceBiopsyInfo.faceBiopsyItemId,
  354. verifySteps: this.faceBiopsyInfo.verifySteps,
  355. };
  356. },
  357. async run() {
  358. // load face detection and face landmark models
  359. // await changeFaceDetector(TINY_FACE_DETECTOR)
  360. // changeInputSize(224);
  361. // try to access users webcam and stream the images
  362. // to the video element
  363. const stream = await navigator.mediaDevices.getUserMedia({
  364. audio: false,
  365. // video: {},
  366. video: {
  367. // width: { min: "100vw" },
  368. // height: { min: "100vh" },
  369. width: 640,
  370. height: 480,
  371. frameRate: 15,
  372. // resizeMode: "crop-and-scale",
  373. },
  374. });
  375. // console.log(
  376. // "video stream settings",
  377. // stream.getVideoTracks()[0].getSettings()
  378. // );
  379. // console.log(
  380. // "video stream constraints",
  381. // stream.getVideoTracks()[0].getConstraints()
  382. // );
  383. // console.log(
  384. // "video stream capabilities",
  385. // stream.getVideoTracks()[0].getCapabilities()
  386. // );
  387. const videoEl = document.getElementById("inputVideo");
  388. videoEl.srcObject = stream;
  389. },
  390. async intro() {
  391. // this.$message({
  392. // type: "info",
  393. // message: "开始活体检测",
  394. // duration: 1500,
  395. // });
  396. this.$Message.info({ content: "开始活体检测", duration: 2 });
  397. // let loading;
  398. // loading = this.$Spin.show({});
  399. this.$Spin.show({});
  400. // this.isDetecting = true;
  401. this.behavingStartDate = null;
  402. // this.introStarted = true;
  403. // this.$nextTick(() => {
  404. // const io = introJs()
  405. // .setOptions({
  406. // nextLabel: "下一步",
  407. // prevLabel: "上一步",
  408. // skipLabel: "跳过",
  409. // doneLabel: "完成",
  410. // })
  411. // .start();
  412. // // loading.hide();
  413. // this.$Spin.hide();
  414. // const realStart = () => {
  415. // this.isDetecting = true;
  416. // clearInterval(this.remainInteval);
  417. // this.remainInteval = setInterval(() => {
  418. // this.instructions.total--;
  419. // if (this.instructions.total <= 0) {
  420. // clearInterval(this.remainInteval);
  421. // }
  422. // }, 1000);
  423. // this.onPlay();
  424. // };
  425. // io.onbeforeexit(() => {
  426. // // loading = this.$Spin.show({});
  427. // this.$Spin.show({});
  428. // });
  429. // io.onexit(() => {
  430. // setTimeout(() => {
  431. // realStart();
  432. // this.introStarted = false;
  433. // // console.log("exit intro");
  434. // // loading.hide();
  435. // this.$Spin.hide();
  436. // }, 300);
  437. // });
  438. // });
  439. const realStart = () => {
  440. this.isDetecting = true;
  441. clearInterval(this.remainInteval);
  442. this.remainInteval = setInterval(() => {
  443. this.instructions.total--;
  444. if (this.instructions.total <= 0) {
  445. clearInterval(this.remainInteval);
  446. }
  447. }, 1000);
  448. this.onPlay();
  449. };
  450. realStart();
  451. },
  452. async onPlay() {
  453. if (!this.doneCompare && this.pauseDetecting) {
  454. console.log({ pauseDetecting: this.pauseDetecting });
  455. return;
  456. }
  457. if (!this.asked) {
  458. this.asked = true;
  459. await this.increaseTestSpeed();
  460. // await new Promise(resolve => setTimeout(resolve, 3000));
  461. await this.intro();
  462. // this.$confirm("开始活体检测?", "确认开始")
  463. // .then(async () => {
  464. // })
  465. // .catch(() => {
  466. // this.$message({
  467. // type: "info",
  468. // message: "刷新可重新选择",
  469. // });
  470. // });
  471. }
  472. if (!this.isDetecting) return;
  473. const detectStartTime = performance.now();
  474. const videoEl = document.getElementById("inputVideo");
  475. this.___vWidth =
  476. this.___vWidth ||
  477. document.getElementById("video-container").clientWidth;
  478. const options = getFaceDetectorOptions();
  479. let result;
  480. /**
  481. * tiny
  482. * 无表情,无landmarks,60~70ms
  483. * 有表情,增加20~30ms
  484. * 有landmarks,增加20~30ms
  485. *
  486. * ssdMobilenetv1
  487. * 无表情,无landmarks,130ms
  488. * 有表情,增加30~40ms
  489. * 有landmarks,增加20~30ms
  490. *
  491. * mtcnn
  492. * 无表情,无landmarks,200ms 每次差异很大
  493. * 有表情,增加30~40ms
  494. * 有landmarks,增加20~30ms
  495. *
  496. */
  497. if (this.shouldDetectExpression) {
  498. // const canvas2 = faceapi.createCanvasFromMedia(videoEl);
  499. result = await faceapi
  500. // .detectSingleFace(videoEl, options)
  501. // .detectAllFaces(canvas2, options)
  502. .detectAllFaces(videoEl, options)
  503. .withFaceLandmarks()
  504. .withFaceExpressions();
  505. // if (result.length === 0) {
  506. // document.body.appendChild(canvas2);
  507. // }
  508. } else {
  509. result = await faceapi
  510. // .detectSingleFace(videoEl, options)
  511. .detectAllFaces(videoEl, options)
  512. .withFaceLandmarks();
  513. }
  514. // console.log(result);
  515. if (result && result.length >= 2) {
  516. this.corFinalResult.result = false;
  517. this.corFinalResult.errorMsg = "检测到多张人脸!活体检测失败!";
  518. this.failedTest("检测到多张人脸!活体检测失败!");
  519. }
  520. if (result && result.length === 0) {
  521. if (!this.shoudAdjustDistance) {
  522. // 只有不是调整人脸距离的时候增加失败次数
  523. this.singleFaceFailedTimes++;
  524. }
  525. if (this.singleFaceFailedTimes >= 5) {
  526. this.corFinalResult.result = false;
  527. this.corFinalResult.errorMsg =
  528. "活检过程中没有检测到人脸!活体检测失败!";
  529. this.failedTest("活检过程中没有检测到人脸!活体检测失败!");
  530. }
  531. }
  532. // 人脸比对 - 开始
  533. {
  534. if (
  535. this.shouldDoFaceRecognition &&
  536. faceapi.nets.ssdMobilenetv1.params &&
  537. faceapi.nets.faceRecognitionNet.params
  538. ) {
  539. const personFromVideo = await faceapi
  540. // .detectSingleFace(videoEl, options)
  541. .detectSingleFace(videoEl, options)
  542. .withFaceLandmarks()
  543. .withFaceDescriptor();
  544. const personFromBasePhoto = await faceapi
  545. .detectSingleFace(document.getElementById("base-photo"))
  546. .withFaceLandmarks()
  547. .withFaceDescriptor();
  548. if (personFromVideo && personFromBasePhoto) {
  549. // create FaceMatcher with automatically assigned labels
  550. // from the detection results for the reference image
  551. const faceMatcher = new faceapi.FaceMatcher(personFromBasePhoto);
  552. const bestMatch = faceMatcher.findBestMatch(
  553. personFromVideo.descriptor
  554. );
  555. if (bestMatch.distance > 0.8) {
  556. console.log("%c肯定不是王章军", "color: red");
  557. }
  558. if (bestMatch.distance >= 0.4 && bestMatch.distance <= 0.8) {
  559. console.log("有可能是王章军");
  560. }
  561. if (bestMatch.distance < 0.4) {
  562. console.log("%c肯定是王章军", "color: green");
  563. }
  564. console.log(bestMatch.toString());
  565. }
  566. }
  567. }
  568. // 人脸比对 - 结束
  569. if (result && result[0]) {
  570. result = result[0];
  571. // Object.entries(result.expressions).forEach(([key, value]) => {
  572. // if (value > 0.5) {
  573. // console.log(key, value);
  574. // }
  575. // });
  576. // console.log(Object.entries(result.expressions));
  577. // console.log(".......");
  578. // const canvasStartTime = performance.now();
  579. const canvas = document.getElementById("overlay");
  580. const dims = faceapi.matchDimensions(canvas, videoEl, true);
  581. const resizedResult = faceapi.resizeResults(result, dims);
  582. // const canvasEndTime = performance.now();
  583. // console.log(" canvas time: ", canvasEndTime - canvasStartTime);
  584. // console.log(resizedResult);
  585. // console.log(resizedResult.detection.box.top);
  586. // console.log(resizedResult.detection.box.left);
  587. let box;
  588. if (this.shouldDetectExpression || resizedResult.detection) {
  589. // 在检测表情时有detection属性,没有box属性。没有检测landmarks
  590. box = resizedResult.detection.box;
  591. } else {
  592. box = resizedResult.box;
  593. }
  594. // console.log(box.area);
  595. if (box.area > 60000 || box.area < 20000) {
  596. const message = box.area > 60000 ? "请远离摄像头" : "请靠近摄像头";
  597. this.tipHandler =
  598. this.tipHandler ||
  599. throttle(message => {
  600. // this.$message({
  601. // type: "warning",
  602. // message,
  603. // duration: 1000,
  604. // offset: 300,
  605. // });
  606. this.$Message.warning({ content: message, duration: 1 });
  607. }, 1500);
  608. this.tipHandler(message);
  609. if (this.shoudAdjustDistance) {
  610. setTimeout(() => this.onPlay(), 300);
  611. return;
  612. }
  613. } else {
  614. this.shoudAdjustDistance = false;
  615. if (this.currentStep.action === "FACE_COMPARE") {
  616. console.log("该做同步人脸比对了");
  617. // 同步人脸比对
  618. try {
  619. if (this.pauseDetecting || this.doneCompare) return;
  620. this.pauseDetecting = true;
  621. const cs = await this.snap();
  622. console.log(cs);
  623. if (!cs) return;
  624. this.compareResult = cs;
  625. // 后台的计算需要通过resultJson来判断: isPass isStranger existsSystemError
  626. this.finalResult.verifySteps[0].result = this.compareResult.isPass;
  627. this.finalResult.verifySteps[0].resourceType = "PIC";
  628. this.finalResult.verifySteps[0].resourceUrl = this.compareResult.fileUrl;
  629. this.finalResult.verifySteps[0].resultJson = this.compareResult.faceCompareResult;
  630. this.finalResult.verifySteps[0].errorMsg = this.compareResult.errorMsg;
  631. this.pauseDetecting = false;
  632. this.doneCompare = true;
  633. if (this.compareResult.isPass) {
  634. console.log("人脸同步比对成功");
  635. if (!this.instructionsFinished && this.currentStep)
  636. this.currentStep.finished = true;
  637. } else {
  638. this.failedTest("人脸同步比对失败");
  639. return;
  640. }
  641. } catch (error) {
  642. console.log(error);
  643. } finally {
  644. // this.pauseDetecting = false;
  645. this.videoStartPlay();
  646. }
  647. }
  648. }
  649. // 区域左边的一半
  650. const centerPoint = box.left + (box.right - box.left) / 2;
  651. if (
  652. (centerPoint >
  653. this.___vWidth * ((this.currentStep.section - 1) / 3) &&
  654. centerPoint < this.___vWidth * (this.currentStep.section / 3)) ||
  655. this.currentStep.section === 0
  656. ) {
  657. if (this.behavingStartDate === null) {
  658. // 到指定区块后才开始检测表情
  659. if (this.shouldDetectExpression) {
  660. if (
  661. (result.expressions.happy < 0.5 && this.currentStep.happy) ||
  662. (result.expressions.neutral < 0.5 && !this.currentStep.happy)
  663. ) {
  664. // this.$message({
  665. // type: "warning",
  666. // message: this.currentStep.happy ? "请保持微笑" : "请保持严肃",
  667. // duration: 1000,
  668. // offset: 300,
  669. // });
  670. this.$Message.warning({
  671. content: this.currentStep.happy ? "请保持微笑" : "请保持严肃",
  672. duration: 1,
  673. });
  674. setTimeout(() => this.onPlay(), 1000);
  675. return;
  676. }
  677. }
  678. this.behavingStartDate = new Date();
  679. }
  680. this.behaving = true;
  681. this.behavingTimestampe = Date.now();
  682. } else {
  683. this.behaving = false;
  684. this.behavingStartDate = null;
  685. if (!this.moveFaceMessage) {
  686. // this.moveFaceMessage = this.$message({
  687. // type: "info",
  688. // message: "请将您的脸部移向笑脸所在的区块",
  689. // duration: 3000,
  690. // offset: 300,
  691. // });
  692. this.moveFaceMessage = this.$Message.info({
  693. content: "请将您的脸部移向笑脸所在的区块",
  694. duration: 1,
  695. });
  696. setTimeout(() => {
  697. this.moveFaceMessage = null;
  698. }, 3000);
  699. }
  700. }
  701. const detectEndTime = performance.now();
  702. console.log("single detect time: ", detectEndTime - detectStartTime);
  703. if (this.shouldDetectExpression && this.behavingStartDate) {
  704. // 到指定区块后才开始检测表情
  705. if (result.expressions.happy < 0.5 && this.currentStep.happy) {
  706. this.happyFailedTimes++;
  707. if (this.happyFailedTimes % 2) {
  708. if (Date.now() - (this.showExpresionTipDate || 0) > 1500) {
  709. this.showExpresionTipDate = Date.now();
  710. // this.$message({
  711. // type: "warning",
  712. // message: this.currentStep.happy ? "请保持微笑" : "请保持严肃",
  713. // duration: 1000,
  714. // offset: 300,
  715. // });
  716. this.$Message.warning({
  717. content: this.currentStep.happy ? "请保持微笑" : "请保持严肃",
  718. duration: 1,
  719. });
  720. }
  721. }
  722. }
  723. // if(result.expressions.happy >= 0.5 && this.currentStep.happy) {
  724. // this.happyFailedTimes = 0; // 恢复的话,容易降低恶意用户攻击难度
  725. // }
  726. if (result.expressions.neutral < 0.5 && !this.currentStep.happy) {
  727. this.happyFailedTimes++;
  728. if (this.happyFailedTimes % 2) {
  729. if (Date.now() - (this.showExpresionTipDate || 0) > 1500) {
  730. this.showExpresionTipDate = Date.now();
  731. // this.$message({
  732. // type: "warning",
  733. // message: this.currentStep.happy ? "请保持微笑" : "请保持严肃",
  734. // duration: 1000,
  735. // offset: 300,
  736. // });
  737. this.$Message.warning({
  738. content: this.currentStep.happy ? "请保持微笑" : "请保持严肃",
  739. duration: 1,
  740. });
  741. }
  742. }
  743. }
  744. if (this.happyFailedTimes >= 6) {
  745. this.corFinalResult.result = false;
  746. this.corFinalResult.errorMsg = "指定表情失败!活体检测失败!";
  747. this.failedTest("指定表情失败!活体检测失败!");
  748. }
  749. }
  750. const stayMoreForProgress = 500; // wait for progress reach 0/100
  751. if (
  752. this.behaving &&
  753. Date.now() - this.behavingStartDate - stayMoreForProgress >
  754. this.currentStep.stay * 1000
  755. ) {
  756. console.log("通过section" + this.currentStep.section);
  757. this.behaving = false;
  758. this.happyFailedTimes = 0;
  759. this.behavingStartDate = null;
  760. if (!this.instructionsFinished && this.currentStep) {
  761. this.corFinalResult.result = true;
  762. this.currentStep.finished = true;
  763. }
  764. }
  765. // console.log(resizedResult.alignedRect.relativeBox.y);
  766. // if (true) {
  767. // faceapi.draw.drawDetections(canvas, resizedResult);
  768. // }
  769. // faceapi.draw.drawFaceLandmarks(canvas, resizedResult);
  770. }
  771. setTimeout(() => this.onPlay(), 300);
  772. },
  773. failedTest(msg) {
  774. clearInterval(this.remainInteval);
  775. this.isDetecting = false;
  776. if (!this.instructionsFinished) {
  777. // this.$message({
  778. // message: msg || "活体检测失败",
  779. // type: "error",
  780. // });
  781. this.$Message.error({ content: msg || "活体检测失败", duration: 5 });
  782. }
  783. // this.resetTest();
  784. this.closeMe();
  785. },
  786. async increaseTestSpeed() {
  787. if (!this.__inThisMethodOnce) {
  788. this.__inThisMethodOnce = true;
  789. } else {
  790. return;
  791. }
  792. const videoEl = document.getElementById("inputVideo");
  793. const options = getFaceDetectorOptions();
  794. console.log("increaseTestSpeed ---");
  795. await new Promise(resolve => {
  796. const interval = setInterval(() => {
  797. if (
  798. videoEl.readyState === 4 &&
  799. faceapi.nets.tinyFaceDetector.params &&
  800. faceapi.nets.faceExpressionNet.params
  801. // !faceapi.nets.ssdMobilenetv1.params
  802. // !faceapi.nets.mtcnn.params
  803. ) {
  804. resolve();
  805. clearInterval(interval);
  806. }
  807. }, 300);
  808. });
  809. console.log(videoEl.readyState, faceapi.nets.tinyFaceDetector.params);
  810. console.log("increaseTestSpeed --- doing faceapi");
  811. const result = await faceapi
  812. .detectSingleFace(videoEl, options)
  813. .withFaceLandmarks()
  814. .withFaceExpressions();
  815. console.log("increaseTestSpeed --- result:", result);
  816. if (!result) {
  817. console.log("increaseTestSpeed --- end failed");
  818. } else {
  819. console.log("increaseTestSpeed --- end successfully");
  820. }
  821. this.$Spin.hide();
  822. },
  823. videoStartPlay() {
  824. const video = document.getElementById("inputVideo");
  825. video && video.play();
  826. },
  827. async snap() {
  828. if (this.disableSnap) {
  829. return;
  830. }
  831. try {
  832. this.disableSnap = true;
  833. const captureBlob = await this.getSnapShot();
  834. if (captureBlob.size < 10 * 1024) {
  835. this.$Message.error({
  836. content: "抓拍照片太小!",
  837. duration: 15,
  838. closable: true,
  839. });
  840. // 经查以前记录,不完整图片均为8192大小。此处设置小于10KB的图片为未抓拍成功
  841. window._hmt.push([
  842. "_trackEvent",
  843. "摄像头框",
  844. "抓拍照片较小",
  845. captureBlob.size,
  846. ]);
  847. throw "抓拍照片较小";
  848. }
  849. this.videoStartPlay();
  850. const [captureFilePath, signIdentifier] = await this.uploadToServer(
  851. captureBlob
  852. );
  853. return this.faceCompareSync(captureFilePath, signIdentifier);
  854. } catch (error) {
  855. console.log("同步照片比对流程失败");
  856. throw error;
  857. } finally {
  858. this.videoStartPlay();
  859. // this.disableSnap = false;
  860. }
  861. },
  862. async getSnapShot() {
  863. return new Promise((resolve, reject) => {
  864. const video = document.getElementById("inputVideo");
  865. if (video.readyState !== 4 || !video.srcObject.active) {
  866. this.$Message.error({
  867. content: "摄像头没有正常启用",
  868. duration: 5,
  869. closable: true,
  870. });
  871. window._hmt.push([
  872. "_trackEvent",
  873. "摄像头框",
  874. "摄像头状态",
  875. "摄像头没有正常启用-退出" +
  876. (this.lastSnapTime ? "(非初次抓拍)" : ""),
  877. ]);
  878. reject("摄像头没有正常启用");
  879. this.logout(
  880. "?LogoutReason=" +
  881. "摄像头没有正常启用-退出" +
  882. (this.lastSnapTime ? "(非初次抓拍)" : "")
  883. );
  884. return;
  885. }
  886. video.pause();
  887. var canvas = document.createElement("canvas");
  888. canvas.width = 220;
  889. canvas.height = 165;
  890. var context = canvas.getContext("2d");
  891. context.drawImage(video, 0, 0, 220, 165);
  892. canvas.toBlob(resolve, "image/png", 0.95);
  893. });
  894. },
  895. async uploadToServer(captureBlob) {
  896. async function blobToArray(blob) {
  897. return new Promise(resolve => {
  898. var reader = new FileReader();
  899. reader.addEventListener("loadend", function() {
  900. // reader.result contains the contents of blob as a typed array
  901. resolve(reader.result);
  902. });
  903. reader.readAsArrayBuffer(blob);
  904. });
  905. }
  906. //保存抓拍照片到服务器
  907. let resultUrl, signIdentifier;
  908. try {
  909. const buffer = await blobToArray(captureBlob);
  910. // console.log(buffer);
  911. // var view1 = new Uint8Array(buffer);
  912. // console.log(buffer[0], buffer[1], buffer[429721]);
  913. const fileMd5 = MD5(buffer);
  914. console.log(fileMd5);
  915. const params = new URLSearchParams();
  916. params.append("fileSuffix", "png");
  917. params.append("fileMd5", fileMd5);
  918. const res = await this.$http.get(
  919. "/api/ecs_oe_student/examControl/getCapturePhotoUpYunSign?" + params
  920. );
  921. // console.log(res);
  922. // let myHeaders = new Headers();
  923. // for (let [k, v] of Object.entries(res.data.headers)) {
  924. // // console.log(k, v);
  925. // if (k.includes("tion") || k.includes("Date") || k.includes("MD5")) {
  926. // if (k === "Date") k = "x-date";
  927. // myHeaders.append(k, v);
  928. // }
  929. let myFormData = new FormData();
  930. for (let [k, v] of Object.entries(res.data.formParams)) {
  931. myFormData.append(k, v);
  932. }
  933. myFormData.append("file", captureBlob);
  934. try {
  935. const res2 = await fetch(res.data.formUrl, {
  936. method: "POST",
  937. body: myFormData,
  938. });
  939. if (!res2.ok) {
  940. throw res2.status;
  941. }
  942. } catch (error) {
  943. window._hmt.push([
  944. "_trackEvent",
  945. "摄像头框",
  946. "抓拍照片保存失败--upyun",
  947. error,
  948. ]);
  949. throw error;
  950. }
  951. // console.log(response);
  952. resultUrl = res.data.accessUrl;
  953. signIdentifier = res.data.signIdentifier;
  954. // this.serverLog("debug/S-005001", "抓拍照片保存成功:");
  955. window._hmt.push(["_trackEvent", "摄像头框", "抓拍照片保存成功"]);
  956. } catch (e) {
  957. console.log(e);
  958. // this.serverLog("debug/S-006001", "抓拍照片保存失败");
  959. window._hmt.push([
  960. "_trackEvent",
  961. "摄像头框",
  962. "保存抓拍照片到服务器失败!",
  963. ]);
  964. this.$Message.error({
  965. content: "抓拍照片保存失败!",
  966. duration: 15,
  967. closable: true,
  968. });
  969. throw "抓拍照片保存失败!";
  970. }
  971. return [resultUrl, signIdentifier];
  972. },
  973. async faceCompareSync(captureFilePath, signIdentifier) {
  974. try {
  975. const res = await this.$http.post(
  976. "/api/ecs_oe_student_face/examCaptureQueue/compareFaceSync?signIdentifier=" +
  977. signIdentifier +
  978. "&fileUrl=" +
  979. encodeURIComponent(captureFilePath)
  980. );
  981. // // TODO: 识别成功、失败的通知或跳转
  982. // this.$emit("on-recognize-result", {
  983. // error: null,
  984. // pass: res.data.isPass,
  985. // stranger: res.data.isStranger,
  986. // });
  987. return res.data;
  988. } catch (e) {
  989. console.log(e);
  990. // this.$Message.error(e.message);
  991. throw "同步照片比较失败!";
  992. }
  993. },
  994. },
  995. };
  996. </script>
  997. <style scoped>
  998. .page-container {
  999. /* margin-left: 20px; */
  1000. margin: 0 auto;
  1001. width: 640px;
  1002. height: 480px;
  1003. overflow: hidden;
  1004. }
  1005. .above-video {
  1006. z-index: 2;
  1007. }
  1008. .instruction-animation {
  1009. transition: margin-left 2s ease-in-out 0.5s;
  1010. }
  1011. .instruction-tips {
  1012. position: absolute;
  1013. top: 0;
  1014. left: 0;
  1015. text-align: center;
  1016. width: 100%;
  1017. background-color: rgba(255, 255, 255, 0.6);
  1018. padding: 20px 0;
  1019. }
  1020. .instruction-total {
  1021. position: absolute;
  1022. top: 50px;
  1023. left: 0;
  1024. text-align: center;
  1025. width: 100%;
  1026. background-color: rgba(255, 255, 255, 0);
  1027. }
  1028. .total-text {
  1029. background-color: rgba(255, 255, 255, 0.6);
  1030. width: 80px;
  1031. height: 80px;
  1032. line-height: 80px;
  1033. font-size: 40px;
  1034. border-radius: 50%;
  1035. border: 3px solid gold;
  1036. margin: 20px auto 0 auto;
  1037. }
  1038. .seperators {
  1039. position: absolute;
  1040. top: 0;
  1041. left: 0;
  1042. width: 100%;
  1043. height: 100%;
  1044. background-color: rgba(255, 255, 255, 0.1);
  1045. display: flex;
  1046. justify-content: space-evenly;
  1047. }
  1048. .seperators .line {
  1049. width: 5px;
  1050. height: 100%;
  1051. background-color: red;
  1052. }
  1053. .blocks {
  1054. position: absolute;
  1055. top: 0;
  1056. left: 0;
  1057. width: 100%;
  1058. height: 100%;
  1059. background-color: rgba(255, 255, 255, 0.1);
  1060. display: flex;
  1061. justify-content: space-around;
  1062. align-items: center;
  1063. }
  1064. .blocks .block-index {
  1065. margin-top: 150px;
  1066. width: 100px;
  1067. height: 100px;
  1068. border-radius: 50%;
  1069. background-color: rgba(255, 0, 0, 0.7);
  1070. color: yellow;
  1071. font-size: 50px;
  1072. line-height: 100px;
  1073. text-align: center;
  1074. }
  1075. .blocks .block-index-size {
  1076. margin-top: 175px;
  1077. width: 100%;
  1078. height: 100%;
  1079. transition: all 1s ease-out;
  1080. }
  1081. .blocks .block-index-blur {
  1082. background-color: rgba(100, 100, 100, 0.8);
  1083. }
  1084. .instruction-face {
  1085. position: absolute;
  1086. top: 30%;
  1087. left: 0;
  1088. width: 33%;
  1089. height: 100px;
  1090. background-size: contain;
  1091. background-repeat: no-repeat;
  1092. background-position-x: center;
  1093. text-align: center;
  1094. /* background-color: rgba(255, 255, 255, 0.6); */
  1095. /* background-image: url(./smile-icon.png); */
  1096. animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both infinite;
  1097. transform: translate3d(0, 0, 0);
  1098. backface-visibility: hidden;
  1099. perspective: 1000px;
  1100. }
  1101. .instruction-face-animation-state {
  1102. animation-iteration-count: 1;
  1103. }
  1104. #overlay,
  1105. .overlay {
  1106. position: absolute;
  1107. top: 0;
  1108. left: 0;
  1109. }
  1110. /* .detect-video {
  1111. width: 100vw;
  1112. height: 100vh;
  1113. } */
  1114. @keyframes shake {
  1115. 10%,
  1116. 90% {
  1117. transform: translate3d(-1px, 0, 0);
  1118. }
  1119. 20%,
  1120. 80% {
  1121. transform: translate3d(2px, 0, 0);
  1122. }
  1123. 30%,
  1124. 50%,
  1125. 70% {
  1126. transform: translate3d(-4px, 0, 0);
  1127. }
  1128. 40%,
  1129. 60% {
  1130. transform: translate3d(4px, 0, 0);
  1131. }
  1132. }
  1133. </style>
  1134. <style>
  1135. .el-message__content {
  1136. font-size: 24px !important;
  1137. }
  1138. </style>