tensorflow.js 使用 opencv.js 将人脸特征点网格绘制与姿态估计线绘制结合起来,以获得更高的帧数

系列文章目录

  1. 如何在前端项目中使用opencv.js | opencv.js入门
  2. 如何使用tensorflow.js实现面部特征点检测
  3. tensorflow.js 如何从 public 路径加载人脸特征点检测模型
  4. tensorflow.js 如何使用opencv.js通过面部特征点估算脸部姿态并绘制示意图

文章目录

  • 系列文章目录
  • 前言
  • 一、实现步骤
    • 1. 修改drawMesh.js文件为drawCanvas.js文件
    • 2. 获取帧数信息并显示出来
    • 3. 运行代码查看效果
  • 总结


前言

本文将基于前文的工程进度,将人脸特征点网格(使用原生的canvas方法绘制)和姿态估计线(使用opencv.js+canvas绘制)统一起来,使用opencv.js绘制两者以达到更高的帧数。由于一部分工作基于前文,如果有一些跳跃或者不连贯的地方的疑问请参考前文,或者在评论区提出问题。


一、实现步骤

1. 修改drawMesh.js文件为drawCanvas.js文件

详细代码见drawCanvas.js
drawCanvas函数包含了drawTriangle和drawPoseLine函数,前者是绘制人脸特征点网格的关键函数后者是绘制姿态估计示意线的关键函数,在该文件中将原本的人脸特征点网格的实现修改为opencv.js实现,最终的帧数为60左右,与本机相机的默认帧数相同。

import { TRIANGULATION } from "./triangulation";

export const drawCanvas = (prediction, canvas) => {
  if (!prediction) return;
  const keyPoints = prediction.keypoints;
  if (!keyPoints) return;
  const canvasMat = new window.cv.Mat.zeros(
    canvas.height,
    canvas.width,
    window.cv.CV_8UC4
  );
  for (let i = 0; i < TRIANGULATION.length / 3; i++) {
    const points = [
      TRIANGULATION[i * 3],
      TRIANGULATION[i * 3 + 1],
      TRIANGULATION[i * 3 + 2],
    ].map(
      (index) =>
        new window.cv.Point(
          Math.round(keyPoints[index].x),
          Math.round(keyPoints[index].y)
        )
    );
    drawTriangle(canvasMat, points);
  }

  const circleColor = new window.cv.Scalar(0, 0, 255, 255);
  for (let i = 0; i < keyPoints.length; i++) {
    let center = new window.cv.Point(
      Math.round(keyPoints[i].x),
      Math.round(keyPoints[i].y)
    );
    window.cv.circle(canvasMat, center, 2, circleColor);
  }

  drawPoseLine(canvasMat, keyPoints);
  window.cv.imshow(canvas.id, canvasMat);
  canvasMat.delete();
};

const drawTriangle = (canvasMat, points) => {
  window.cv.line(
    canvasMat,
    points[0],
    points[1],
    new window.cv.Scalar(0, 0, 0, 255),
    1
  );
  window.cv.line(
    canvasMat,
    points[1],
    points[2],
    new window.cv.Scalar(0, 0, 0, 255),
    1
  );
  window.cv.line(
    canvasMat,
    points[2],
    points[0],
    new window.cv.Scalar(0, 0, 0, 255),
    1
  );
};

function drawPoseLine(canvasMat, keyPoints) {
  // 右下眼 145 右上眼 159 左下眼 374 左上眼 386 下嘴唇14 上嘴唇13 鼻梁5 鼻头4
  // 面部上顶点 10 下顶点 152 左顶点 454 右顶点 234
  // 左嘴角 308 右嘴角 78
  // 左眼角 263 右眼角 33

  // 左眼开合距离
  const lEyeValue = Math.pow(
    Math.pow(keyPoints[374].x - keyPoints[386].x, 2) +
      Math.pow(keyPoints[374].y - keyPoints[386].y, 2),
    0.5
  );
  // 右眼开合距离
  const rEyeValue = Math.pow(
    Math.pow(keyPoints[145].x - keyPoints[159].x, 2) +
      Math.pow(keyPoints[145].y - keyPoints[159].y, 2),
    0.5
  );
  // 嘴巴开合距离
  const mouthValue = Math.pow(
    Math.pow(keyPoints[14].x - keyPoints[13].x, 2) +
      Math.pow(keyPoints[14].y - keyPoints[13].y, 2),
    0.5
  );
  // 左眼位置
  const lEyeX = (keyPoints[374].x + keyPoints[386].x) / 2;
  const lEyeY = (keyPoints[374].y + keyPoints[386].y) / 2;

  // 右眼位置
  const rEyeX = (keyPoints[145].x - keyPoints[159].x) / 2;
  const rEyeY = (keyPoints[145].y - keyPoints[159].y) / 2;

  // 脸中心
  const faceCenterX = ((lEyeX + rEyeX) / 2 + keyPoints[4].x) / 2;
  const faceCenterY = ((lEyeY + rEyeY) / 2 + keyPoints[4].y) / 2;
  //
  var modelPoints = window.cv.matFromArray(6, 3, window.cv.CV_32F, [
    0.0,
    0.0,
    0.0, // Nose tip
    0.0,
    -330.0,
    -65.0, // Chin
    -225.0,
    170.0,
    -135.0, // Left eye left corner
    225.0,
    170.0,
    -135.0, // Right eye right corne
    -150.0,
    -150.0,
    -125.0, // Left Mouth corner
    150.0,
    -150.0,
    -125.0, // Right mouth corner
  ]);

  var imagePoints = window.cv.matFromArray(6, 2, window.cv.CV_32F, [
    keyPoints[4].x,
    keyPoints[4].y, // Nose tip
    keyPoints[152].x,
    keyPoints[152].y, // Chin
    keyPoints[263].x,
    keyPoints[263].y, // Left eye left corner
    keyPoints[33].x,
    keyPoints[33].y, // Right eye right corne
    keyPoints[308].x,
    keyPoints[308].y, // Left Mouth corner
    keyPoints[78].x,
    keyPoints[78].y, // Right mouth corner
  ]);

  var focal_length = canvasMat.cols;
  var center = [canvasMat.cols / 2, canvasMat.rows / 2];
  var cameraMatrix = window.cv.matFromArray(3, 3, window.cv.CV_64F, [
    focal_length,
    0,
    center[0],
    0,
    focal_length,
    center[1],
    0,
    0,
    1,
  ]);

  // console.log("Camera Matrix", cameraMatrix.data64F);

  var distCoeffs = window.cv.matFromArray(4, 1, window.cv.CV_64F, [0, 0, 0, 0]); // Assuming no lens distortion

  var rvec = new window.cv.Mat(3, 1, window.cv.CV_64F);
  var tvec = new window.cv.Mat(3, 1, window.cv.CV_64F);

  let ret_val = window.cv.solvePnP(
    modelPoints,
    imagePoints,
    cameraMatrix,
    distCoeffs,
    rvec,
    tvec,
    false,
    window.cv.SOLVEPNP_ITERATIVE // flags
  );

  if (!ret_val) return false;

  var rtn = getEulerAngle(rvec);

  var pitch = rtn[0]; // 俯仰角
  var yaw = rtn[1]; // 水平角
  var roll = rtn[2]; // 翻滚角
  // console.log("pitch:", pitch, "yaw:", yaw, "roll:", roll);

  var noseEndPoint2D = new window.cv.Mat(1, 2, window.cv.CV_64F);
  var jacobian = new window.cv.Mat(imagePoints.rows * 2, 13, window.cv.CV_64F);
  window.cv.projectPoints(
    window.cv.matFromArray(1, 3, window.cv.CV_64F, [0.0, 0.0, 700.0]),
    rvec,
    tvec,
    cameraMatrix,
    distCoeffs,
    noseEndPoint2D,
    jacobian
  );

  // 绘制线段,连接鼻尖和其它点
  var p1 = new window.cv.Point(
    Math.round(imagePoints.data32F[0]),
    Math.round(imagePoints.data32F[1])
  );
  var p2 = new window.cv.Point(
    Math.round(noseEndPoint2D.data64F[0]),
    Math.round(noseEndPoint2D.data64F[1])
  );

  window.cv.line(canvasMat, p1, p2, new window.cv.Scalar(255, 0, 0, 255), 2);
  modelPoints.delete();
  imagePoints.delete();
  cameraMatrix.delete();
  distCoeffs.delete();
  rvec.delete();
  tvec.delete();
  noseEndPoint2D.delete();
  jacobian.delete();
  return true;
}

function getEulerAngle(rotationVector) {
  // calculate rotation angles
  let theta = window.cv.norm(rotationVector, window.cv.NORM_L2);

  // transformed to quaternion
  let w = Math.cos(theta / 2);
  let x = (Math.sin(theta / 2) * rotationVector.data64F[0]) / theta;
  let y = (Math.sin(theta / 2) * rotationVector.data64F[1]) / theta;
  let z = (Math.sin(theta / 2) * rotationVector.data64F[2]) / theta;

  let ysqr = y * y;
  // pitch (x-axis rotation)
  let t0 = 2.0 * (w * x + y * z);
  let t1 = 1.0 - 2.0 * (x * x + ysqr);
  // console.log("t0:", t0, "t1:", t1);
  let pitch = Math.atan2(t0, t1);

  // yaw (y-axis rotation)
  let t2 = 2.0 * (w * y - z * x);
  if (t2 > 1.0) {
    t2 = 1.0;
  }
  if (t2 < -1.0) {
    t2 = -1.0;
  }
  let yaw = Math.asin(t2);

  // roll (z-axis rotation)
  let t3 = 2.0 * (w * z + x * y);
  let t4 = 1.0 - 2.0 * (ysqr + z * z);
  let roll = Math.atan2(t3, t4);

  // console.log("pitch:", pitch, "yaw:", yaw, "roll:", roll);

  // 单位转换:将弧度转换为度
  let Y = parseInt((pitch / Math.PI) * 180);
  let X = parseInt((yaw / Math.PI) * 180);
  let Z = parseInt((roll / Math.PI) * 180);

  return [Y, X, Z];
}

2. 获取帧数信息并显示出来

设计一个1秒间隔的定时器,和一个frameCount,定时器格一秒传出参数到frameRate并清零frameCount,frameCount在detector的callback函数中被增加,这样frameRate每个一秒就会获得当前的帧数,并触发组件更新,代码如下,详细代码见 index.js:
请添加图片描述

3. 运行代码查看效果

npm i -g yarn && yarn 安装依赖
npm start 运行项目,预览结果如下
请添加图片描述


总结

本文介绍了使用 opencv.js 将人脸特征点网格绘制与姿态估计线绘制结合起来,以获得更高的帧数,希望对您有所帮助,如果文章中存在任何问题、疏漏,或者您对文章有任何建议,请在评论区提出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/529080.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

实验:基于Red Hat Enterprise Linux系统建立逻辑卷并进行划分

目录 一. 实验目的 二. 实验内容 三. 实验设计描述及实验结果 1. 为虚拟机添加三块大小为5GB的磁盘nvme0n2 nvme0n3 nvme0n4 2. 将三块硬盘转换为物理卷&#xff0c;并将nvme0n2 nvme0n3两pv建立成名为"自己名字_vg“的卷组&#xff0c;并将nvme0n4扩展进该卷组。 LVM管…

基于单片机四路继电器温湿度控制

**单片机设计介绍&#xff0c; 基于单片机四路继电器温湿度控制 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机四路继电器温湿度控制的设计是一种能够实现精确环境调控的智能化系统。它利用单片机作为核心控制器&…

渗透测试面试题汇总(全)

思路流程 信息收集漏洞挖掘漏洞利用&权限提升清除测试数据&输出报告复测 问题深信服一面:SQL注入防护为什么参数化查询可以防止sql注入SQL头注入点盲注是什么&#xff1f;怎么盲注&#xff1f;宽字节注入产生原理以及根本原因 产生原理在哪里编码根本原因解决办法sql里…

力扣刷题Days33-274. H 指数(js)

目录 1&#xff0c;题目 2&#xff0c;代码 2.1排序 2.2计数排序 3&#xff0c;学习与总结 3.1排序实现的学习总结 3.2计数排序的学习总结 1&#xff0c;题目 给你一个整数数组 citations &#xff0c;其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返…

Java 线程池 参数

1、为什么要使用线程池 线程池能有效管控线程&#xff0c;统一分配任务&#xff0c;优化资源使用。 2、线程池的参数 创建线程池&#xff0c;在构造一个新的线程池时&#xff0c;必须满足下面的条件&#xff1a; corePoolSize&#xff08;线程池基本大小&#xff09;必须大于…

1.Spring的核心思想 —— IOC和DI

1. Spring是什么&#xff1f; 简单的说&#xff0c;Spring其实指的是Spring Framework&#xff08;Spring框架&#xff09;&#xff0c;是一个开源框架。 如果要用一句话概括&#xff1a;它是包含众多工具方法的IOC&#xff08;Inverse of Control控制反转&#xff09;容器。…

【THM】Net Sec Challenge(网络安全挑战)-初级渗透测试

介绍 使用此挑战来测试您对网络安全模块中获得的技能的掌握程度。此挑战中的所有问题都可以仅使用nmap、telnet和来解决hydra。 挑战问题 您可以使用Nmap、 Telnet 和Hydra回答以下问题。 2.1小于10000的最大开放端口号是多少? 8080 nmap -p- -T4 10.10.234.218 2.2普通…

Java入门-java的方法

java方法 java的方法是用来完成某种功能的代码块。使用方法封装代码块&#xff0c;可以提高代码的可复用性&#xff0c;模块化&#xff0c;使用者无需知道代码的具体实现也能通过方法调用使用其提供的功能&#xff0c;简化了应用过程。 方法结构 一般一个方法的构成有如图几部…

【C++】vector问题解决(非法的间接寻址,迭代器失效 , memcpy拷贝问题)

送给大家一句话&#xff1a; 世界在旋转&#xff0c;我们跌跌撞撞前进&#xff0c;这就够了 —— 阿贝尔 加缪 vector问题解决 1 前言2 迭代器区间拷贝3 迭代器失效问题4 memcpy拷贝问题 1 前言 我们之前实现了手搓vector&#xff0c;但是当时依然有些问题没有解决&#xff…

HarmonyOS 开发-多模态页面转场动效实现案例

介绍 本示例介绍多模态页面转场动效实现&#xff1a;通过半模态转场实现半模态登录界面&#xff0c;通过配置NavDestinationMode类型为DIALOG&#xff0c;实现半模态的背景为透明&#xff0c;再与 全屏模态和组件转场结合实现多模态组合登录场景&#xff0c;其中手机验证码登录…

基于springboot+vue+Mysql的学习平台

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

【Web】CTFSHOW-2023CISCN国赛初赛刷题记录(全)

目录 Unzip BackendService go_session deserbug 主打一个精简 Unzip 进来先是一个文件上传界面 右键查看源码&#xff0c;actionupload.php 直接访问/upload.php&#xff0c;看到后端的源码 就是上传一个压缩包&#xff0c;对其进行解包处理 因为其是在/tmp下执行…

ip地址切换器安卓版,保护隐私,自由上网

在移动互联网时代&#xff0c;随着智能手机和平板电脑的普及&#xff0c;移动设备的网络连接变得愈发重要。为了满足用户在不同网络环境下的需求&#xff0c;IP地址切换器安卓版应运而生。本文将以虎观代理为例&#xff0c;为您详细解析IP地址切换器安卓版的功能、应用以及其所…

机器学习 基础 笔记 1

train阶段就是正常的学习 validation是知道正确答案是啥&#xff0c;检查正确率 test是不知道正确答案是啥&#xff0c;看看有啥结果 训练的时候记得model.train 测试&#xff08;后面两种都是&#xff09;的时候要model.eval &#xff08;有些模型两种阶段做的事不一样&a…

不要抱怨,不如抱 Java 运算符吧 (下篇)

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人能接…

2024.4.9-day12-CSS 常用样式属性和字体图标

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 作业 作业 <!DOCTYPE html> <html lang"zh-CN"><he…

P4552 IncDec Sequence(差分)

题目描述 给定一个长度为 n n n 的数列 a 1 , a 2 , ⋯ , a n {a_1,a_2,\cdots,a_n} a1​,a2​,⋯,an​&#xff0c;每次可以选择一个区间 [ l , r ] [l,r] [l,r]&#xff0c;使这个区间内的数都加 1 1 1 或者都减 1 1 1。 请问至少需要多少次操作才能使数列中的所有数都…

rapidssl通配符证书760元

RapidSSL是Geotrust旗下的子品牌&#xff0c;旗下有两款基础型的数字证书产品——基础型单域名SSL证书和基础型通配符SSL证书。RapidSSL旗下的数字证书产品可以为个人或者企事业单位网站提供先进的加密算法和技术&#xff0c;确保网站数据在传输过程中不被窃取或篡改。今天就随…

Matlab|电价型负荷需求响应(考虑电价变化)

程序复现来源于《计及需求响应消纳风电的电-热综合能源系统经济调度 》第四章内容。 一、原理 需求响应的基本原理是需求侧根据电力市场价格和电网要求改变其负荷需求以 获取一定的利益回报。其中 PDR 可通过直观的电价变化信号引导用户调节用电方式&#xff0c; 从而达到优…

android11 如何修改状态栏的背景

修改status_bar.xml &#xff1a; <LinearLayout android:id"id/status_bar_contents"android:background"#1ABC9C"android:layout_width"match_parent"android:layout_height"match_parent"android:paddingStart"dimen/statu…