Press "Enter" to skip to content

三分钟训练眼球追踪术,AI就知道你在盯着哪个妹子 | TensorFlow.js代码

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

啊,老板的眼神飞过来了,还不快切回工作界面?
从前,我们几乎无从躲避来自身后的目光,但现在不一定了。
如果有个  AI,加上  ,或许就能在被老板盯上的瞬间,进入奋力工作模式。
戏是有点多。不过眼球追踪这件事,只要有电脑的前置摄像头,再有个浏览器,真的可以做到。

来自慕尼黑的程序猿Max Schumacher,就用  做了一个模型,你看向屏幕的 某一点 ,它就知道你在看的是哪一点了。
我来训练一把
这个模型叫  ,不用服务器,打开摄像头就可以在浏览器上训练, 不出三分钟 就能养成一只小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/[email protected]"></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=… 下面, 初始化 追踪器:
1const ctrack = new clm.tracker();

2ctrack.init();

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

1ctrack.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() 下面:
1const overlay = $('#overlay')[0];

2const overlayCC = overlay.getContext('2d');

3

4function 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) :
1function 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;

9

10 return [minX, minY, width, height];

11}

接下来,要把矩形框提取出来。具体方法是,在第一张画布上把它描成红色,再复制到第二张画布上。
替换 trackingLoop() 里面的if块:
1if (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]);

9

10 // The video might internally have a different size, so we need these

11 // factors to rescale the eyes rectangle before cropping:

12 const resizeFactorX = video.videoWidth / video.width;

13 const resizeFactorY = video.videoHeight / video.height;

14

15 // Crop the eyes from the video and paste them in the eyes canvas:

16 const eyesCanvas = $('#eyes')[0];

17 const eyesCC = eyesCanvas.getContext('2d');

18

19 eyesCC.drawImage(

20 video,

21 eyesRect[0] * resizeFactorX, eyesRect[1] * resizeFactorY,

22 eyesRect[2] * resizeFactorX, eyesRect[3] * resizeFactorY,

23 0, 0, eyesCanvas.width, eyesCanvas.height

24 );

25}

现在,应该看得到眼睛周围的 红色矩形框 了。
DIY全攻略 (下) :训练与测试
收集数据
眼球追踪,收集数据的方法其实有很多种。不过,让眼睛跟着鼠标走,是最简单的,随时按下空格都可以捕获一幅图像。

1 追踪鼠标

想知道鼠标每时每刻都在什么位置,就给 document.onmousemove 加上一个 EventListener 。
这样做还可以把坐标归一化 (转化到 [-1, 1] 的范围里) :
1// Track mouse movement:

2const 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}

12

13document.onmousemove = mouse.handleMouseMove;

2 捕捉图像

这里要做的是,按下空格键 之后 的任务:从画布上捕捉图像,储存为张量。
TensorFlow.js提供了一个助手函数,叫 tf.fromPixels() ,只要用它来储存第二张画布里走出的图像,然后归一化:
1function 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%。
这样,便与检测模型的性能,以及确认它没有 过拟合 
以下是添加新数据点用的代码:
1const 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}

13

14function 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);

19

20 // Choose whether to add it to training (80%) or validation (20%) set:

21 const subset = dataset[Math.random() > 0.2 ? 'train' : 'val'];

22

23 if (subset.x == null) {

24 // Create new tensors

25 subset.x = tf.keep(image);

26 subset.y = tf.keep(mousePos);

27 } else {

28 // Concatenate it to existing tensors

29 const oldX = subset.x;

30 const oldY = subset.y;

31

32 subset.x = tf.keep(oldX.concat(image, 0));

33 subset.y = tf.keep(oldY.concat(mousePos, 0));

34 }

35

36 // Increase counter

37 subset.n += 1;

38 });

39}

最后,把 空格键 关联进来:
1$('body').keyup(function(event) {

2 // On space key:

3 if (event.keyCode == 32) {

4 captureExample();

5

6 event.preventDefault();

7 return false;

8 }

9});

至此,只要你按下空格,数据集里就会增加一个数据点了。
训练模型
就搭个最简单的 CNN 吧。

TensorFlow.js里面有一个和Keras很相似的 API 可以用。
这个网络里,要有一个卷积层,一个最大池化,还要有个密集层,带两个输出值 (坐标) 的那种。
中间,加了一个 dropout 作为正则化器;还有,用 flatten 把2D数据降成1D。训练用的是Adam优化器。
模型代码长这样:
1let currentModel;

2

3function 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 }));

13

14 model.add(tf.layers.maxPooling2d({

15 poolSize: [2, 2],

16 strides: [2, 2],

17 }));

18

19 model.add(tf.layers.flatten());

20

21 model.add(tf.layers.dropout(0.2));

22

23 // Two output values x and y

24 model.add(tf.layers.dense({

25 units: 2,

26 activation: 'tanh',

27 }));

28

29 // Use ADAM optimizer with learning rate of 0.0005 and MSE loss

30 model.compile({

31 optimizer: tf.train.adam(0.0005),

32 loss: 'meanSquaredError',

33 });

34

35 return model;

36}

训练开始之前,要先设置一个 固定 的epoch数,再把批尺寸设成 变量 (因为数据集很小) :
1function 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 }

12

13 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>

然后,想让绿圈圈动起来,就要 定期 把眼睛图像传给神经网络。问它你在看哪,它就回答一个坐标:
1function 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);

14

15 // Move target there:

16 const $target = $('#target');

17 $target.css('left', x + 'px');

18 $target.css('top', y + 'px');

19 });

20}

21

22setInterval(moveTarget, 100);

间隔设的是 100毫秒 ,不过也可以改的。
总之,大功告成。
鼻孔眼睛分不清?
眼球追踪模型很有意思,不过还是有一些可爱的缺陷。
比如,算法还只能识别正面,脸稍微 侧一点 AI就会困惑。
比如,有时候会把 鼻孔 识别成眼睛。
比如,必须整张脸都出现在画面里,才能识别眼睛的所在, 捂住嘴 也不行。

Be First to Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注