整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

10分钟内浏览器中,手把手教你构建一个手势识别模型 - 代码开源

沉 发自 宇宙中心

量子位 出品 | 公众号 QbitAI

深度学习技术的不断普及,越来越多的语言可以用来进行深度学习项目的开发,即使是JavaScript这样曾经只是在浏览器中运行的用于处理轻型任务的脚本语言。

TensorFlow.js是谷歌推出的基于JavaScript的深度学习框架,它提供的高级API使得开发可以直接在浏览器中运行的深度学习算法变得轻而易举。

这不,美国的一位老哥Gant Laborde使用TensorFlow.js开发了一款是用深度学习技术在浏览器中识别“石头剪刀布”游戏手势的网页应用,放出了demo并将代码开源在了Github上。

对于JavaScript开发者来说,这是打开深度学习大门的极佳入门教材。只需10分钟,你就可以训练一个准确率可观的手势识别模型,并且调用摄像头对实时视频中的手势进行识别。


使用运行在浏览器中的深度学习模型识别手势

在一切开始之前

在打开新世界的大门之前,我们总是需要做一些准备工作。

在这里,给大家简单地介绍一下典型的深度学习算法的开发步骤,目的是希望读者们在接下来的操作中明确地知道自己在做什么,而不仅仅是点几个按钮罢了。

这里不会涉及任何艰涩的数学公式,请放心食用。


我们平常所说的深度学习算法,更确切地说,应该是基于深度神经网络的算法(或者说模型)。

这里并不需要知道深度神经网络究竟是个什么东西(你可能需要再花百倍于此的时间才有可能搞明白其具体原理),只需要知道,它可以视作是一个函数f,一个很难用简单公式表达出来的函数。

所谓函数,就要有自变量x和因变量y。

自变量x,我们一般称之为输入(input),在这个问题中就是一张做出“石头”、“剪刀”或“布”手势的手的图像。

而因变量y,我们一般称之为输出(output),在这个问题中是三个取值为0-1的数值,分别对应输入手势是“石头”、“剪刀”和“布”的概率。

我们依靠这个函数f得到我们想要的结果,但是f并不是天上掉下来的,它由人为选取的模型和(大量的)模型参数组成。

其中模型参数往往由大量数据学习得到,这个让模型学习参数的过程我们称之为模型训练(train),是深度学习算法开发中最关键的一步。

在这个问题中,我们需要大量(x,y)数据对来进行训练,也就是大量(图像,手势)数据对,如(图像1,剪刀)、(图像2、石头)、(图像3、布)…… 这些数据对往往需要由人为搜集、标注得到。

我们可以通过一些评估指标来衡量模型的好坏程度,比如在这个问题中,手势识别的准确度。通过这些评估指标我们可以验证(validate)模型是否经过了充分的训练、效果有没有达到我们的预期。如果是,我们可以将其部署投入使用,测试其在现实情况中的表现。

总结来说,一个深度学习算法的开发,需要经过数据准备模型选择与训练模型效果评估模型测试这四个阶段。

现在,正式开始!

数据准备

我们之前提到,需要大量的(图像,手势)数据对来进行模型的训练。搜集这样的数据无疑是一个繁琐的工作,拍照、标注……

幸运的是,谷歌工程师Laurence Moroney为我们提供了这样一个数据集,其中包含了白色背景下的三种手势共2892张图像及对应的手势标签,一些例子:


Moroney提供的数据集的一些例子

数据集网址:

http://www.laurencemoroney.com/rock-paper-scissors-dataset/

一切看似都是这么的顺利。等等,我们怎么把这么一坨图像搞进浏览器里去?

在浏览器里执行JavaScript,好像并不能从本地读取文件。

一个显见的想法是,我们把训练数据当做网页中的图片,读进DOM的img元素中。我们先将训练数据中每一张图像“拉直“成1像素高的图像,再将所有图像一行一行堆叠在一起。

比如我们原图大小为64x64,“拉直”之后尺寸为1x4096,训练集的2520张图像堆叠后形成大小为4096x2520的巨大图像(虽然它在视觉上已经失去了意义),像下面这样。

这张巨大图像被称为精灵表单(sprite-sheet),包含了许多小图像。

这个网页应用的作者提供了生成sprite-sheet的Python代码,在github仓库根目录的spritemaker文件夹下。


生成的尺寸为4096x2520的sprite-sheet

在demo页面中,点击“Load and Show Examples(读取数据并展示样例)”按钮,等待一阵,我们可以看到数据被读入了浏览器,并且出现了一个侧边栏,其中展示了42张从数据集中随机选取的图像。

这个侧边栏由TensorFlow Visor提供,可以帮助我们直观地观察模型的训练过程,我们可以随时按下键盘左上方的`键切出或隐藏该面板。


TensorFlow Visor界面中展示的数据样例

模型选择、训练与效果评估

接下来我们将面临抉择。

两个按钮摆在我们的面前,“Create Simple Model(创建简单模型)”和“Create Advance Model(创建高级模型)”。



先从简单的来吧,我们点击“Create Simple Model”。按`键切出TensorFlow Visor面板,可以看到上面出现了刚刚创建的简单模型的网络结构,这是一个5层的卷积神经网络模型(Flatten层不计入层数),你只需要知道它可以看做是一个一个相对简单函数的堆叠,并且这确实是一个非常简单基础的卷积神经网络模型。



TensorFlow Visor界面中展示的网络结构

点击“Check Untrained Model Results(查看未训练模型结果)”,面板中出现了一个Accuracy(准确率)表格,和一个矩阵,它们就是这个问题中我们对于模型的评价指标。

准确率表格中,每一行是一个手势类别的准确率值;矩阵中,手势X的行和手势Y的列确定的单元格代表实际是手势X,被算法认为是手势Y的图像数量,这样的矩阵我们叫做“混淆矩阵”,因为它展现了算法对于两两手势容易搞混的程度。

可以看到,因为我们的模型还没有进行训练,所以算法认为所有输入图像中的手势都是“剪刀”,它还很懵懂。

那么就开始训练它吧!点击“Train Your Simple Model(训练简单模型)”!TensorFlow Visor面板中出现了“Model Training(模型训练)”一栏,展示了训练中实时的准确率(Accuracy)和损失(Loss)值,正常情况下,我们应该可以看到随着训练的进行,准确率不断上升,而损失不断下降。训练在12个epoch(60个batch)后停止。


TensorFlow Visor界面中展示的训练进程

训练结束后,点击“Check Model After Training(查看训练后模型结果)”。在原来的准确率表格和混淆矩阵下方出现了训练后模型的准确率(Trained Accuracy)和混淆矩阵(Trained Confusion Matrix)。

Amazing!训练后,模型在验证数据上对于三种手势的识别准确率都超过了95%,混淆矩阵也是健康的(对角线深,其余浅)。


TensorFlow Visor界面中展示的训练后模型效果

你也许会想,“高级的东西总比简单的东西好吧?高级模型效果一定更好。” 其实这是一个常见的误区。

如果你选择“Create Advance Model(创建高级模型)”,重复上述操作,会发现高级模型不仅训练时间更长,效果也不如简单模型那么好。

更进一步,高级模型如果训练时间过长,会出现过拟合(overfitting)的情况。

过拟合是指,模型太注重完美拟合训练数据,导致其虽然在训练数据上的表现极佳,但是对于训练数据之外它没有见过的数据效果较差,或者我们也会说模型此时的泛化(generalize)能力较差。

模型测试

既然已经有了一个表现很不错的简单模型,那么让我们立刻将它投入使用吧!

点击“Launch Webcam(打开摄像头)”,对准一面白墙,对着摄像头做出不同的手势,应用会定时捕捉视频图像,通过训练好的模型算法,告诉你当前手势属于三种类别的概率,是不是很酷炫呢?


使用已训练模型识别视频中的手势

Done!

至此,你已经在完全在浏览器中训练了一个用于手势分类的深度学习模型,通过一些指标验证了它的有效性,并且在现实情境中对它进行了测试。

尽管这些步骤很简单,但你了解它们在做什么——欢迎来到深度学习的世界!

传送门

源代码仓库:

https://github.com/GantMan/rps_tfjs_demo

Demo页面:

https://rps-tfjs.netlify.com/

— 完 —

诚挚招聘

量子位正在招募编辑/记者,工作地点在北京中关村。期待有才气、有热情的同学加入我们!相关细节,请在量子位公众号(QbitAI)对话界面,回复“招聘”两个字。

量子位 QbitAI · 头条号签约作者

վ'ᴗ' ի 追踪AI技术和产品新动态

先使用jQuery选择器获取到想要绑定click事件的img元素,然后可以直接绑定click方法,也可以通过bind方法绑定。这里详细介绍一下bind方法。jQuery 事件 - bind() 方法 —— 定义和用法

html中如何给图片添加点击事件的详解


bind() 方法为被选元素添加一个或多个事件处理程序,并规定事件发生时运行的函数。

jQuery 事件 - bind() 方法 ——将事件和函数绑定到元素

规定向被选元素添加的一个或多个事件处理程序,以及当事件发生时运行的函数。

jQuery 事件 - bind() 方法——语法

1 $(selector).bind(event,data,function)

jQuery 事件 - bind() 方法——参数描述

event 必需。规定添加到元素的一个或多个事件。由空格分隔多个事件。必须是有效的事件。

data 可选。规定传递到函数的额外数据。

function 必需。规定当事件发生时运行的函数。

实例:


1 //直接给所有img标签绑定click事件

2 $("img").click(function(){

3 alert('你点击了图片');

4 })

5

6 //使用bind方法绑定click事件

7 $("img").bind("click",function(){

8 alert('你点击了图片');

9 })


Html 的img标签添加点击事件


1 package com.topnews;

2

3 import java.util.ArrayList;

4

5 import android.annotation.SuppressLint;

6 import android.app.Activity;

7 import android.app.Fragment;

8 import android.content.Context;

9 import android.content.Intent;

10 import android.graphics.Bitmap;

11 import android.os.AsyncTask;

12 import android.os.Bundle;

13 import android.text.TextUtils;

14 import android.util.Log;

15 import android.view.View;

16 import android.view.ViewGroup.LayoutParams;

17 import android.webkit.WebChromeClient;

18 import android.webkit.WebSettings;

19 import android.webkit.WebView;

20 import android.webkit.WebViewClient;

21 import android.webkit.WebSettings.LayoutAlgorithm;

22 import android.widget.Button;

23 import android.widget.FrameLayout;

24 import android.widget.ProgressBar;

25 import android.widget.TextView;

26

27 import com.topnews.base.BaseActivity;

28 import com.topnews.bean.NewsEntity;

29 import com.topnews.service.NewsDetailsService;

30 import com.topnews.tool.BaseTools;

31 import com.topnews.tool.DataTools;

32 import com.topnews.tool.DateTools;

33

34 @SuppressLint("JavascriptInterface")

35 public class DetailsActivity extends BaseActivity {

36 private TextView title;

37 private ProgressBar progressBar;

38 private FrameLayout customview_layout;

39 private String news_url;

40 private String news_title;

41 private String news_source;

42 private String news_date;

43 private NewsEntity news;

44 private TextView action_comment_count;

45 WebView webView;

46

47 @Override

48 protected void onCreate(Bundle savedInstanceState) {

49 // TODO Auto-generated method stub

50 super.onCreate(savedInstanceState);

51 setContentView(R.layout.details);

52 setNeedBackGesture(true);// 设置需要手势监听

53 getData();

54 initView();

55 initWebView();

56 }

57

58 /* 获取传递过来的数据 */

59 private void getData() {

60 news = (NewsEntity) getIntent().getSerializableExtra("news");

61 news_url = news.getSource_url();

62 news_title = news.getTitle();

63 news_source = news.getSource();

64 news_date = 65DateTools.getNewsDetailsDate(String.valueOf(news.getPublishTime()));

66 }

67

68 private void initWebView() {

69 webView = (WebView) findViewById(R.id.wb_details);

70 LayoutParams params = new 71LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

72 if (!TextUtils.isEmpty(news_url)) {

73 WebSettings settings = webView.getSettings();

74 settings.setJavaScriptEnabled(true);// 设置可以运行JS脚本

75 // settings.setTextZoom(120);//Sets the text zoom of the page in

76 // percent. The default is 100.

77 settings.setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN);

78 // settings.setUseWideViewPort(true); //打开页面时, 自适应屏幕

79 // settings.setLoadWithOverviewMode(true);//打开页面时, 自适应屏幕

80 settings.setSupportZoom(false);// 用于设置webview放大

81 settings.setBuiltInZoomControls(false);

82 webView.setBackgroundResource(R.color.transparent);

83 // 添加js交互接口类,并起别名 imagelistner

84 webView.addJavascriptInterface(new 85JavascriptInterface(getApplicationContext()), "imagelistner");

86 webView.setWebChromeClient(new MyWebChromeClient());

87 webView.setWebViewClient(new MyWebViewClient());

88 Log.i("info", "news_url:" + news_url);

89 Log.i("info", "news_title:" + news_title);

90 Log.i("info", "news_source:" + news_source);

91 Log.i("info", "news_date:" + news_date);

92 new MyAsnycTask().execute(news_url, news_title, news_source + " " + 93news_date);

94 }

95 }

96

97 private void initView() {

98 title = (TextView) findViewById(R.id.title);

99 progressBar = (ProgressBar) findViewById(R.id.ss_htmlprogessbar);

100 customview_layout = (FrameLayout) 101findViewById(R.id.customview_layout);

102 // 底部栏目

103 action_comment_count = (TextView) 104findViewById(R.id.action_comment_count);

105

106 progressBar.setVisibility(View.VISIBLE);

107 title.setTextSize(13);

108 title.setVisibility(View.VISIBLE);

109 title.setText(news_url);

110 action_comment_count.setText(String.valueOf(news.getCommentNum()));

111 }

112

113 @Override

114 public void onBackPressed() {

115 super.onBackPressed();

116 overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);

117 }

118

119 private class MyAsnycTask extends AsyncTask<string, string,="" string=""> {

120

121 @Override

122 protected String doInBackground(String... urls) {

123 String data = NewsDetailsService.getNewsDetails(urls[0], urls[1], urls[2]);

124 Log.i("info", "MyAsnycTask.data:" + data);

125 return data;

126 }

127

128 @Override

129 protected void onPostExecute(String data) {

130 webView.loadDataWithBaseURL(null, data, "text/html", "utf-8", null);

131 }

132 }

133

134 // 注入js函数监听

135 private void addImageClickListner() {

136 // 这段js函数的功能就是,遍历所有的img几点,并添加onclick函数,在还是执137 行的时候调用本地接口传递url过去

138 webView.loadUrl("javascript:(function(){" + "var objs = 139document.getElementsByTagName(\"img\");" + "var imgurl=''; "

140 + "for(var i=0;i<objs.length;i++) "="" +="" "{"="" 141"imgurl+="objs[i].src+',';"" objs[i].onclick="function()" {="" 142window.imagelistner.openimage(imgurl);="" }="" "}"="" "})()");="" js通信接口="" 143public="" class="" javascriptinterface="" private="" context="" context;="" 144javascriptinterface(context="" context)="" this.context="context;" void="" 145openimage(string="" img)="" string[]="" imgs="img.split(",");" 146arraylist<string=""> imgsUrl = new ArrayList<string>();

147 for (String s : imgs) {

148 imgsUrl.add(s);

149 Log.i("图片的URL>>>>>>>>>>>>>>>>>>>>>>>", s);

150 }

151 Intent intent = new Intent();

152 intent.putStringArrayListExtra("infos", imgsUrl);

153 intent.setClass(context, ImageShowActivity.class);

154 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

155 context.startActivity(intent);

156 }

157 }

158

159 // 监听

160 private class MyWebViewClient extends WebViewClient {

161 @Override

162 public boolean shouldOverrideUrlLoading(WebView view, String url) {

163 return super.shouldOverrideUrlLoading(view, url);

164 }

165

166 @Override

167 public void onPageFinished(WebView view, String url) {

168 view.getSettings().setJavaScriptEnabled(true);

169 super.onPageFinished(view, url);

170 // html加载完成之后,添加监听图片的点击js函数

171 addImageClickListner();

172 progressBar.setVisibility(View.GONE);

173 webView.setVisibility(View.VISIBLE);

174 }

175

176 @Override

177 public void onPageStarted(WebView view, String url, Bitmap favicon) {

178 view.getSettings().setJavaScriptEnabled(true);

179 super.onPageStarted(view, url, favicon);

180 }

181

182 @Override

183 public void onReceivedError(WebView view, int errorCode, String description, 184String failingUrl)

185 {

progressBar.setVisibility(View.GONE);

super.onReceivedError(view, errorCode, description, failingUrl);

}

}

private class MyWebChromeClient extends WebChromeClient {

@Override

public void onProgressChanged(WebView view, int newProgress) {

// TODO Auto-generated method stub

if (newProgress != 100) {

progressBar.setProgress(newProgress);

}

super.onProgressChanged(view, newProgress);

}

}

}</string></objs.length;i++)></string,>


// NewsDetailsService.java


1 package com.topnews.service;

2

3 import java.io.IOException;

4 import org.jsoup.Jsoup;

5 import org.jsoup.nodes.Document;

6 import org.jsoup.nodes.Element;

7

8 import android.text.TextUtils;

9

10 public class NewsDetailsService {

11 public static String getNewsDetails(String url, String news_title,

12 String news_date) {

13 Document document = null;

14 String data = "" +

15 "<center><h2 style="'font-size:16px;'">" + news_title + "</h2></center>";

16 data = data + "<p align="'left'" style="'margin-left:10px'">"

17 + "<span style="'font-size:10px;'">"

18 + news_date

19 + "</span>"

20 + "</p>";

21 data = data + "<hr size="'1'">";

22 try {

23 document = Jsoup.connect(url).timeout(9000).get();

24 Element element = null;

25 if (TextUtils.isEmpty(url)) {

26 data = "";

27 element = document.getElementById("memberArea");

28 } else {

29 element = document.getElementById("artibody");

30 }

31 if (element != null) {

32 data = data + element.toString();

33 }

34 data = data + "";

35 } catch (IOException e) {

36 e.printStackTrace();

37 }

38 return data;

39 }

40 }

以上就是html中如何给图片添加点击事件的详解的详细内容,

家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

今天遇到一个非常有意思的库,即 Handsfree.js,特意分享给大家,看看是否可以结合自己的日常研发、业务支持等做一点小小的创新。话不多说,直接开始。

1.什么是 Handsfree

Build handsfree User Experiences and add face, hand, and pose tracking to your projects in a snap ✨

Handsfree 是一个通过计算机视觉集成手势,面部表情和各种姿势识别的前端库。其核心 AI 技术用到了 tensorflow,可在浏览器上触发交互事件,比如:滚动网页,检测人脸并展示相关表情,控制桌面游戏等等,开发者还可以通过 Handsfree 与 websocket 的结合控制任意与电脑连接的设备。

官方提供的以下模型可以实时组合和重新配置:

  • MediaPipe Hands (2D):每只手 21 个 2D 手部标志;同时追踪最多 4 只手;支持捏合状态、手指指针和手势
  • TensorFlow Handpose:支持 21 个 3D 手部标记;一次只能追踪 1 只手;支持额外的助手和插件
  • MediaPipe FaceMesh:支持 468 个 2D 人脸特征点;一次最多追踪 4 张面孔; 支持更多助手和插件
  • MediaPipe Pose:支持具有 33 个 2D 姿势地标的全身模式;具有 25 个 2D 上身姿势标志的上半身模式
  • TensorFlow Handpose:支持 6DOF 头部姿势;11 种面部变形和 16 种辅助状态;附带基于 “Face Pointer” 的插件

2.如何使用 Handsfree

开发者可以选择从 CDN 加载资源然后使用,比如下面的例子:

<head>
  <!-- 加载 Handsfree.js 的官方包,包含 css 和 js -->
  <link rel="stylesheet" href="https://unpkg.com/handsfree@8.5.1/build/lib/assets/handsfree.css" />
  <script src="https://unpkg.com/handsfree@8.5.1/build/lib/handsfree.js"></script>
</head>

<body>
  <!-- 实例化Handsfree同时启用  -->
  <script>
    const handsfree = new Handsfree({hands: true})
    handsfree.enablePlugins('browser')
    handsfree.start()
  </script>
</body>

当然,也可以选择从 NPM 下载然后使用:

// 启用 Mediapipe 的 “手” 模型
const handsfree = new Handsfree({hands: true})
// 启动标签为 "browser" 的插件
handsfree.enablePlugins('browser')
// 开始追踪
handsfree.start()

但是,值得一提的是某些模型体积可能超过 10Mb+,加载可能需要先对较长的时间。因此,如果要自行托管模型并离线使用,可以将模型从 npm 包下载到项目的公共文件夹中,比如 PUBLIC:

// 在 WINDOWS 平台上
xcopy /e node_modules\handsfree\build\lib PUBLIC
// 在其他平台上
cp -r node_modules/handsfree/build/lib/* PUBLIC

然后按照如下方式引用:

import Handsfree from 'handsfree'
const handsfree = new Handsfree({
  hands: true,
  // Set this to your where you moved the models into
  assetsPath: '/PUBLIC/assets',
})
handsfree.enablePlugins('browser')
handsfree.start()

Handsfree.js 还允许开发者通过 handsfree.use(pluginName,callback) 创建各种插件,比如:

// A plugin that console logs your data on every frame
handsfree.use('consoleLogger', (data) => {
  console.log(data.weboji.rotation, data.pose.data.faceLandmarks)
})

以上代码将创建一个新插件,然后可以使用 handsfree.plugin.consoleLogger 访问该插件,并将在每一帧上运行。回调存储在 handsfree.plugin.consoleLogger.onFrame 中,可以使用以下命令切换插件可用状态:

handsfree.plugin.consoleLogger.enable()
handsfree.plugin.consoleLogger.disable()

更多关于 Handsfree.js 的用法和原理可以参考文末资料,本文不再过多展开。

参考资料

https://handsfreejs.netlify.app/#models

https://ko-fi.com/handsfreejs