normalize cdn(你需要训练一只眼球追踪AI吗:老板眼神飘过,即刻开始工作)

博主:xiaoweixiaowei 2022-12-28 条评论

圆栗子 编译整理

量子位 出品 | 公众号 QbitAI

啊,老板的眼神飞过来了,还不快切回工作界面?

从前,我们几乎无从躲避来自身后的目光,但现在不一定了。

如果有个眼球追踪AI,加上人脸识别,或许就能在被老板盯上的瞬间,进入奋力工作模式。

戏是有点多。不过眼球追踪这件事,只要有电脑的前置摄像头,再有个浏览器,真的可以做到。

来自慕尼黑的程序猿Max Schumacher,就用TensorFlow.js做了一个模型,你看向屏幕的某一点,它就知道你在看的是哪一点了。

我来训练一把

这个模型叫Lookie Lookie,不用服务器,打开摄像头就可以在浏览器上训练,不出三分钟就能养成一只小AI。

在下试了一试。

摄像头拍到的画面就显示在屏幕左上角,脸上是绿色的轮廓,眼睛被一个红色方框框住。

收集数据的方式很简单,只要四处移动鼠标,眼睛跟着鼠标走,然后随时按下空格键,每按一次就采集一个数据点。

第一波,只要按20次空格,系统就提示,可以点击训练按钮了。

训练好之后,屏幕上出现一个绿圈圈。这时候,我的眼睛看哪里,绿圈圈都应该跟着我走的。

可它似乎有些犹豫。系统又提示:现在数据不太够,可能还没训练好,再取一些数据吧。

那好,再取个二三十张图,训练第二波。

果然,这次绿圈圈跑得自信了一些,左看右看它都驰骋 (比较) 如风。

相比之下,对于上下移动的目光,AI的反应似乎没有那么敏锐。大概是因为,电脑屏幕上下距离不够宽,眼球转动不充分吧。

不过,在训练数据如此贫乏的前提下,神经网络也算是茁壮成长了。

需要注意的是,收集数据的时候,脸不要离屏幕太远 (也不要倒立) 。

DIY全攻略 (上) :架子搭起来

作为一个不需要任何服务器就能训练的模型,如果要处理整幅整幅的视频截图,负担可能有些重。

所以,还是先检测人脸,再框出眼睛所在的部分。只把这个区域 (上图右一) 交给神经网络的话,任务就轻松了。

德国少年选择了clmtrackr人脸检测模型,它的优点也是跑起来轻快。

那么,先把它下下来:

https://raw.githubusercontent.com/auduno/clmtrackr/dev/build/clmtrackr.js

然后,打开一个空的html文件,导入jQuery, TensorFlow.js,clmtrackr.js,以及main.js。代码如下:

1 <!doctype html>2 <html>3 <body>4 <script src=\\\”https://code.jquery.com/jquery-3.3.1.min.js\\\”></script>5 <script src=\\\”https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.12.0\\\”></script>6 <script src=\\\”clmtrackr.js\\\”></script>7 <script src=\\\”main.js\\\”></script>8 </body>9 </html>

这样,准备活动就做好了。下面正式开始。

导出视频流

第一步,要经过你 (用户) 的同意,才能打开摄像头,渲染视频流,把画面显示在页面上。

先写这行代码 (此处默认用的是最新版本的Chrome) :

1 <video id=\\\”webcam\\\” width=\\\”400\\\” height=\\\”300\\\” autoplay></video>

然后从main.js开始:

1 $(document).ready(function() {2 const video = $(\’#webcam\’)[0];3 4 function onStreaming(stream) {5 video.srcObject = stream;6 }7 8 navigator.mediaDevices.getUserMedia({ video: true }).then(onStreaming);9 });

到这里,浏览器就该问你“要不要打开摄像头”了。

找到你的脸

上文提到的clmtrackr.js人脸追踪器,这里就出场。

先在const video=…下面,初始化追踪器:

1 const ctrack = new clm.tracker();2 ctrack.init();

然后,在onStreaming() 里面,加下面这句话,就能让追踪器检测视频里的人脸了:

1 ctrack.start(video);

写好这几行,它应该已经能看出你的脸。不相信的话,就让它描出来。

这里需要一个绘图工具。用html里面的<canvas>标签,在视频上面重叠一张画布。

在<video>下面,写上这一串代码:

1 <canvas id=\\\”overlay\\\” width=\\\”400\\\” height=\\\”300\\\”></canvas>2 <style>3 #webcam, #overlay {4 position: absolute;5 top: 0;6 left: 0;7 }8 </style>

这样,就有了跟视频尺寸一样的画布。CSS能保证画布和视频的位置完全吻合。

浏览器每做一次渲染,我们就要在画布上画点什么了。画之前,要先把之前画过的内容擦掉。

代码长这样,写在ctrack.init() 下面:

1 const overlay = $(\’#overlay\’)[0]; 2 const overlayCC = overlay.getContext(\’2d\’); 3 4 function trackingLoop() { 5 // Check if a face is detected, and if so, track it. 6 requestAnimationFrame(trackingLoop); 7 8 let currentPosition = ctrack.getCurrentPosition(); 9 overlayCC.clearRect(0, 0, 400, 300);10 11 if (currentPosition) {12 ctrack.draw(overlay);13 }14 }

现在,在onStreaming() 的ctrack.starg() 后面,调用trackingLoop() 。每一帧里,它都会重新运行。

这个时候,刷新一下浏览器,你的脸上应该有一个绿色又诡异的轮廓了。

眼睛截下来

这一步,是要在眼睛周围画个矩形框。

cmltrackr很善良,除了画个轮廓之外,还有70个面部特征,我们可以选择自己需要的部分。

这里,选23、28、24、26就够了,在每个方向上,往外扩大5个像素。

然后,矩形框应该足够覆盖重要面部信息了 (不离太远、不倒立) 。

现在,再拿另外一张画布,来捕捉这个截下来的矩形。这张画布50 x 25像素即可,只要把矩形框的尺寸调一下,就能放进去:

1 <canvas id=\\\”eyes\\\” width=\\\”50\\\” height=\\\”25\\\”></canvas>2 <style>3 #eyes {4 position: absolute;5 top: 0;6 right: 0;7 }8 </style>

下面这个函数,会返回 (x,y) 坐标,以及矩形的长宽。给它输入的是clmtrackr里面的位置阵列 (Position Array) :

1 function getEyesRectangle(positions) { 2 const minX = positions[23][0] – 5; 3 const maxX = positions[28][0] + 5; 4 const minY = positions[24][1] – 5; 5 const maxY = positions[26][1] + 5; 6 7 const width = maxX – minX; 8 const height = maxY – minY; 910 return [minX, minY, width, height];11 }

接下来,要把矩形框提取出来。具体方法是,在第一张画布上把它描成红色,再复制到第二张画布上。

替换trackingLoop() 里面的if块:

1 if (currentPosition) { 2 // Draw facial mask on overlay canvas: 3 ctrack.draw(overlay); 4 5 // Get the eyes rectangle and draw it in red: 6 const eyesRect = getEyesRectangle(currentPosition); 7 overlayCC.strokeStyle = \’red\’; 8 overlayCC.strokeRect(eyesRect[0], eyesRect[1], eyesRect[2], eyesRect[3]); 910 // The video might internally have a different size, so we need these11 // factors to rescale the eyes rectangle before cropping:12 const resizeFactorX = video.videoWidth / video.width;13 const resizeFactorY = video.videoHeight / video.height;1415 // Crop the eyes from the video and paste them in the eyes canvas:16 const eyesCanvas = $(\’#eyes\’)[0];17 const eyesCC = eyesCanvas.getContext(\’2d\’);1819 eyesCC.drawImage(20 video,21 eyesRect[0] * resizeFactorX, eyesRect[1] * resizeFactorY,22 eyesRect[2] * resizeFactorX, eyesRect[3] * resizeFactorY,23 0, 0, eyesCanvas.width, eyesCanvas.height24 );25 }

现在,应该看得到眼睛周围的红色矩形框了。

DIY全攻略 (下) :训练与测试收集数据

眼球追踪,收集数据的方法其实有很多种。不过,让眼睛跟着鼠标走,是最简单的,随时按下空格都可以捕获一幅图像。

1 追踪鼠标

想知道鼠标每时每刻都在什么位置,就给document.onmousemove加上一个EventListener。

这样做还可以把坐标归一化 (转化到 [-1, 1] 的范围里) :

normalize cdn(你需要训练一只眼球追踪AI吗:老板眼神飘过,即刻开始工作)

1 // Track mouse movement: 2 const mouse = { 3 x: 0, 4 y: 0, 5 6 handleMouseMove: function(event) { 7 // Get the mouse position and normalize it to [-1, 1] 8 mouse.x = (event.clientX / $(window).width()) * 2 – 1; 9 mouse.y = (event.clientY / $(window).height()) * 2 – 1;10 },11 }1213 document.onmousemove = mouse.handleMouseMove;

2 捕捉图像

这里要做的是,按下空格键之后的任务:从画布上捕捉图像,储存为张量。

TensorFlow.js提供了一个助手函数,叫tf.fromPixels() ,只要用它来储存第二张画布里走出的图像,然后归一化:

1 function getImage() { 2 // Capture the current image in the eyes canvas as a tensor. 3 return tf.tidy(function() { 4 const image = tf.fromPixels($(\’#eyes\’)[0]); 5 // Add a batch dimension: 6 const batchedImage = image.expandDims(0); 7 // Normalize and return it: 8 return batchedImage.toFloat().div(tf.scalar(127)).sub(tf.scalar(1)); 9 });10 }

注意注意,虽然把所有数据做成一个大训练集也是可以的,但还是留一部分做验证集比较科学,比如20%。

这样,便与检测模型的性能,以及确认它没有过拟合。

以下是添加新数据点用的代码:

1 const dataset = { 2 train: { 3 n: 0, 4 x: null, 5 y: null, 6 }, 7 val: { 8 n: 0, 9 x: null,10 y: null,11 },12 }1314 function captureExample() {15 // Take the latest image from the eyes canvas and add it to our dataset.16 tf.tidy(function() {17 const image = getImage();18 const mousePos = tf.tensor1d([mouse.x, mouse.y]).expandDims(0);1920 // Choose whether to add it to training (80%) or validation (20%) set:21 const subset = dataset[Math.random() > 0.2 ? \’train\’ : \’val\’];2223 if (subset.x == null) {24 // Create new tensors25 subset.x = tf.keep(image);26 subset.y = tf.keep(mousePos);27 } else {28 // Concatenate it to existing tensors29 const oldX = subset.x;30 const oldY = subset.y;3132 subset.x = tf.keep(oldX.concat(image, 0));33 subset.y = tf.keep(oldY.concat(mousePos, 0));34 }3536 // Increase counter37 subset.n += 1;38 });39 }

最后,把空格键关联进来:

1 $(\’body\’).keyup(function(event) {2 // On space key:3 if (event.keyCode == 32) {4 captureExample();56 event.preventDefault();7 return false;8 }9 });

至此,只要你按下空格,数据集里就会增加一个数据点了。

训练模型

就搭个最简单的CNN吧。

TensorFlow.js里面有一个和Keras很相似的API可以用。

这个网络里,要有一个卷积层,一个最大池化,还要有个密集层,带两个输出值 (坐标) 的那种。

中间,加了一个dropout作为正则化器;还有,用flatten把2D数据降成1D。训练用的是Adam优化器。

模型代码长这样:

1 let currentModel; 2 3 function createModel() { 4 const model = tf.sequential(); 5 6 model.add(tf.layers.conv2d({ 7 kernelSize: 5, 8 filters: 20, 9 strides: 1,10 activation: \’relu\’,11 inputShape: [$(\’#eyes\’).height(), $(\’#eyes\’).width(), 3],12 }));1314 model.add(tf.layers.maxPooling2d({15 poolSize: [2, 2],16 strides: [2, 2],17 }));1819 model.add(tf.layers.flatten());2021 model.add(tf.layers.dropout(0.2));2223 // Two output values x and y24 model.add(tf.layers.dense({25 units: 2,26 activation: \’tanh\’,27 }));2829 // Use ADAM optimizer with learning rate of 0.0005 and MSE loss30 model.compile({31 optimizer: tf.train.adam(0.0005),32 loss: \’meanSquaredError\’,33 });3435 return model;36 }

训练开始之前,要先设置一个固定的epoch数,再把批尺寸设成变量 (因为数据集很小) :

1 function fitModel() { 2 let batchSize = Math.floor(dataset.train.n * 0.1); 3 if (batchSize < 4) { 4 batchSize = 4; 5 } else if (batchSize > 64) { 6 batchSize = 64; 7 } 8 9 if (currentModel == null) {10 currentModel = createModel();11 }1213 currentModel.fit(dataset.train.x, dataset.train.y, {14 batchSize: batchSize,15 epochs: 20,16 shuffle: true,17 validationData: [dataset.val.x, dataset.val.y],18 });19 }

然后,在页面上做个训练按钮吧:

1 <button id=\\\”train\\\”>Train!</button> 2 <style> 3 #train { 4 position: absolute; 5 top: 50%; 6 left: 50%; 7 transform: translate(-50%, -50%); 8 font-size: 24pt; 9 }10 </style>

还有JS:

1 <button id=\\\”train\\\”>Train!</button> 2 <style> 3 #train { 4 position: absolute; 5 top: 50%; 6 left: 50%; 7 transform: translate(-50%, -50%); 8 font-size: 24pt; 9 }10 </style>

拉出来遛遛

绿色圈圈终于来了。AI判断你在看哪,它就出现在哪。

先写绿圈圈:

1 <div id=\\\”target\\\”></div> 2 <style> 3 #target { 4 background-color: lightgreen; 5 position: absolute; 6 border-radius: 50%; 7 height: 40px; 8 width: 40px; 9 transition: all 0.1s ease;10 box-shadow: 0 0 20px 10px white;11 border: 4px solid rgba(0,0,0,0.5);12 }13 </style>

然后,想让绿圈圈动起来,就要定期把眼睛图像传给神经网络。问它你在看哪,它就回答一个坐标:

1 function moveTarget() { 2 if (currentModel == null) { 3 return; 4 } 5 tf.tidy(function() { 6 const image = getImage(); 7 const prediction = currentModel.predict(image); 8 9 // Convert normalized position back to screen position:10 const targetWidth = $(\’#target\’).outerWidth();11 const targetHeight = $(\’#target\’).outerHeight();12 const x = (prediction.get(0, 0) + 1) / 2 * ($(window).width() – targetWidth);13 const y = (prediction.get(0, 1) + 1) / 2 * ($(window).height() – targetHeight);1415 // Move target there:16 const $target = $(\’#target\’);17 $target.css(\’left\’, x + \’px\’);18 $target.css(\’top\’, y + \’px\’);19 });20 }2122 setInterval(moveTarget, 100);

间隔设的是100毫秒,不过也可以改的。

总之,大功告成。

鼻孔眼睛分不清?

眼球追踪模型很有意思,不过还是有一些可爱的缺陷。

比如,算法还只能识别正面,脸稍微侧一点AI就会困惑。

比如,有时候会把鼻孔识别成眼睛。

比如,必须整张脸都出现在画面里,才能识别眼睛的所在,捂住嘴也不行。

△ 来自怪异君

Max也说,还有很多可以探索的空间。

自己训练传送门:

https://cpury.github.io/lookie-lookie/

代码实现传送门:

https://github.com/cpury/lookie-lookie

教程原文传送门:

https://cpury.github.io/learning-where-you-are-looking-at/

— 完 —

诚挚招聘

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

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

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

The End

发布于:2022-12-28,除非注明,否则均为 主机评测原创文章,转载请注明出处。