TensorFlow.js和clmtrackr.js:在浏览器中跟踪用户凝视的方向

这篇文章的作者(我们正在翻译的译本)提供了专门讨论通过网络浏览器解决计算机视觉领域的问题的文章。 借助TensorFlow JavaScript库,解决此类问题并不难。 与其训练我们自己的模型并将其作为成品的一部分提供给用户,我们不如给他们机会独立收集数据并直接在我们自己的计算机上的浏览器中训练模型。 使用这种方法,完全不需要服务器端数据处理。


您可以在此处体验该材料专用于创建的内容。 为此,您将需要现代的浏览器,网络摄像头和鼠标。 这是项目源代码。 该材料的作者说,他并不是为在移动设备上工作而设计的,他没有时间进行适当的改进。 此外,他指出,如果必须处理来自移动摄像机的视频流,此处考虑的任务将变得更加复杂。

主意


让我们使用机器学习技术来找出用户在浏览网页时所处的确切位置。 我们通过使用网络摄像头观察他的眼睛来做到这一点。

在浏览器中访问网络摄像头非常容易。 如果我们假设来自摄像机的整个图像都将用作神经网络的输入,那么对于这些​​目的,我们可以说它太大了。 该系统将必须做很多工作才能确定图像中眼睛的位置。 如果我们正在谈论开发人员自己训练并在服务器上部署的模型,那么这种方法可以很好地展示自己,但是如果我们正在谈论训练和在浏览器中使用该模型,那么这太多了。

为了简化网络的工作,我们只能为它提供一部分图像-包含用户的眼睛和眼睛周围的一小块区域。 该区域是围绕眼睛的矩形,可以使用第三方库进行识别。 因此,我们工作的第一部分如下所示:


网络摄像头输入,面部识别,眼睛检测,裁剪图像

为了检测图像中的人脸,我使用了一个名为clmtrackr的库。 它不是完美的,但是体积小,性能好,并且通常足以应付其任务。

如果将小的但智能选择的图像用作简单卷积神经网络的输入,则该网络可以毫无问题地学习。 这是这个过程的样子:


输入图像(模型)是一个卷积神经网络,坐标是网络在用户正在查看的页面上预测的位置。

这里将描述本节中讨论的想法的最低限度完全实现。 该项目的代码位于存储库中,该项目具有许多其他功能。

准备工作


首先, clmtrackr.js从相应的存储库 clmtrackr.js 。 我们将从一个空的HTML文件开始工作,该文件将使用我们的代码导入jQuery,TensorFlow.js,clmtrackr.js和main.js文件,稍后我们将进行处理:

 <!doctype html> <html> <body>   <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>   <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.12.0"></script>   <script src="clmtrackr.js"></script>   <script src="main.js"></script> </body> </html> 

接收来自网络摄像头的视频流


为了激活网络摄像头并在页面上显示视频流,我们需要获得用户许可。 在这里,我没有提供解决项目与各种浏览器的兼容性问题的代码。 我们将假设用户使用最新版本的Google Chrome浏览器在互联网上工作。

将以下代码添加到HTML文件。 它应该位于<body> ,但位于<script>标记上方:

 <video id="webcam" width="400" height="300" autoplay></video> 

现在让我们使用main.js文件:

 $(document).ready(function() { const video = $('#webcam')[0]; function onStreaming(stream) {   video.srcObject = stream; } navigator.mediaDevices.getUserMedia({ video: true }).then(onStreaming); }); 

自己尝试此代码。 当您打开页面时,浏览器应征得许可,然后网络摄像头中的图片将出现在屏幕上。

稍后,我们将扩展onStreaming()函数的代码。

脸部搜寻


现在,让我们使用clmtrackr.js库在视频中搜索人脸。 首先,通过在const video = ...之后添加以下代码来初始化人脸跟踪系统:

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

现在,在onStreaming()函数中,我们通过在其中添加以下命令来连接人脸搜索系统:

 ctrack.start(video); 

这就是我们所需要的。 现在,系统将能够识别视频流中的面部。

不信? 让我们在您的脸上画一个“面具”,以确保这是真的。
为此,我们需要在负责显示视频的元素上方显示图像。 您可以使用<canvas>在HTML页面上绘制内容。 因此,我们通过将其叠加在显示视频的元素上来创建这样的元素。 以下代码将帮助我们解决这个问题,必须将其添加到HTML文件中的<video>元素下:

 <canvas id="overlay" width="400" height="300"></canvas> <style>   #webcam, #overlay {       position: absolute;       top: 0;       left: 0;   } </style> 

如果需要,可以将内联样式移动到单独的CSS文件中。

在这里,我们在页面中添加了与<video>元素相同大小的<canvas> <video>元素。 此处使用的样式确保了元素将位于相同位置的事实。

现在,每次浏览器显示视频的下一帧时,我们将在<canvas>上绘制一些<canvas> 。 使用requestAnimationLoop()机制执行每帧输出期间任何代码的执行。 在将任何内容输出到<canvas> ,我们需要从中删除之前的内容,并将其清除。 然后,我们建议clmtrackr将图形直接输出到<canvas>

这是实现我们刚才讨论的代码。 将其添加到ctrack.init()命令下面:

 const overlay = $('#overlay')[0]; const overlayCC = overlay.getContext('2d'); function trackingLoop() { // ,     , //     -   . requestAnimationFrame(trackingLoop); let currentPosition = ctrack.getCurrentPosition(); overlayCC.clearRect(0, 0, 400, 300); if (currentPosition) {   ctrack.draw(overlay); } } 

现在,在onStreaming()之后立即在ctrack.start()函数中调用trackingLoop()函数。 此函数本身将计划在每个帧中重新启动。

刷新页面,然后查看网络摄像头。 您应该在视频窗口中的脸部周围看到一个绿色的“蒙版”。 有时,为了使系统正确识别面部,您需要在框架中稍微移动头部。


人脸识别结果

确定包含眼睛的图像区域


现在我们需要找到眼睛所在的矩形区域,并将其放置在单独的<canvas>

幸运的是,cmltracker不仅为我们提供了有关脸部位置的信息,还为我们提供了70个控制点。 如果查看cmltracker的文档 ,则可以精确选择我们需要的控制点。


控制点

我们确定眼睛是图像的矩形部分,其边界接触点23、28、24和26,在每个方向上扩大了5个像素。 该矩形应该包含对我们来说很重要的所有内容,除非用户将头部倾斜太多。

现在,在可以使用该图像片段之前,我们需要再添加一个<canvas>作为其输出。 其尺寸为50x25像素。 带有眼睛的矩形将适合此元素。 轻微的图像变形不是问题。

将此代码添加到描述<canvas>的HTML文件中,该文件将包括图像中带有眼睛的部分:

 <canvas id="eyes" width="50" height="25"></canvas> <style>   #eyes {       position: absolute;       top: 0;       right: 0;   } </style> 

以下函数将返回xy坐标以及眼睛周围矩形的宽度和高度。 作为输入,它接收从clmtrackr接收到的positions数组。 请注意,从clmtrackr接收到的每个坐标都有分量xy 。 此函数必须添加到main.js

 function getEyesRectangle(positions) { const minX = positions[23][0] - 5; const maxX = positions[28][0] + 5; const minY = positions[24][1] - 5; const maxY = positions[26][1] + 5; const width = maxX - minX; const height = maxY - minY; return [minX, minY, width, height]; } 

现在,在每一帧中,我们将从视频流中提取一个带有眼睛的矩形,在叠加在<video>元素上的<canvas>上用红线圈出它,然后将其复制到新的<canvas> 。 请注意,为了正确识别我们需要的区域,我们将计算指标resizeFactorXresizeFactorY

用以下代码替换trackingLoop()函数中的if块:

 if (currentPosition) { //  ,     //   <canvas>,    <video> ctrack.draw(overlay); //  ,  ,    //   const eyesRect = getEyesRectangle(currentPosition); overlayCC.strokeStyle = 'red'; overlayCC.strokeRect(eyesRect[0], eyesRect[1], eyesRect[2], eyesRect[3]); //      , //        //      const resizeFactorX = video.videoWidth / video.width; const resizeFactorY = video.videoHeight / video.height; //          //    <canvas> const eyesCanvas = $('#eyes')[0]; const eyesCC = eyesCanvas.getContext('2d'); eyesCC.drawImage(   video,   eyesRect[0] * resizeFactorX, eyesRect[1] * resizeFactorY,   eyesRect[2] * resizeFactorX, eyesRect[3] * resizeFactorY,   0, 0, eyesCanvas.width, eyesCanvas.height ); } 

现在重新加载页面后,您应该在眼睛周围看到一个红色矩形,并且该矩形包含的内容在相应的<canvas> 。 如果您的眼睛大于我的眼睛,请尝试使用getEyeRectangle函数。


<canvas>元素,该元素绘制一个包含用户眼睛图像的矩形

资料收集


有很多收集数据的方法。 我决定使用可以从鼠标和键盘获得的信息。 在我们的项目中,数据收集看起来像这样。

用户在页面上移动光标并用眼睛注视他,每次程序需要记录另一个样本时按下键盘上的 。 通过这种方法,很容易快速收集大量数据以训练模型。

▍鼠标追踪


为了找出鼠标指针在网页上的确切位置,我们需要一个document.onmousemove事件处理程序。 此外,我们的函数还可以对坐标进行归一化,使其适合[-1,1]范围:

 //   : const mouse = { x: 0, y: 0, handleMouseMove: function(event) {   //      ,    [-1, 1]   mouse.x = (event.clientX / $(window).width()) * 2 - 1;   mouse.y = (event.clientY / $(window).height()) * 2 - 1; }, } document.onmousemove = mouse.handleMouseMove; 

▍影像撷取


为了捕获<canvas>显示的图像并将其保存为张量,TensorFlow.js提供了辅助函数tf.fromPixels() 。 我们使用它来保存然后标准化<canvas>中的图像,该<canvas>显示一个包含用户眼睛的矩形:

 function getImage() { //       return tf.tidy(function() {   const image = tf.fromPixels($('#eyes')[0]);   //  <i><font color="#999999"></font></i>:   const batchedImage = image.expandDims(0);   //    :   return batchedImage.toFloat().div(tf.scalar(127)).sub(tf.scalar(1)); }); } 

请注意, tf.tidy()函数用于在完成后进行清理。

我们可以将所有样本保存在一个大型训练集中,但是在机器学习中,检查模型训练的质量很重要。 因此,我们需要将一些样本保存在单独的对照样本中。 之后,我们可以根据新数据检查模型的行为,并确定模型是否受到过度训练。 为此,对照样品中包括样品总数的20%。

这是用于收集数据和样本的代码:

 const dataset = { train: {   n: 0,   x: null,   y: null, }, val: {   n: 0,   x: null,   y: null, }, } function captureExample() { //            tf.tidy(function() {   const image = getImage();   const mousePos = tf.tensor1d([mouse.x, mouse.y]).expandDims(0);   // ,    (  )     const subset = dataset[Math.random() > 0.2 ? 'train' : 'val'];   if (subset.x == null) {     //        subset.x = tf.keep(image);     subset.y = tf.keep(mousePos);   } else {     //          const oldX = subset.x;     const oldY = subset.y;     subset.x = tf.keep(oldX.concat(image, 0));     subset.y = tf.keep(oldY.concat(mousePos, 0));   }   //     subset.n += 1; }); } 

最后,我们需要将此函数绑定到

 $('body').keyup(function(event) { //         if (event.keyCode == 32) {   captureExample();   event.preventDefault();   return false; } }); 

现在,每次按 ,眼图和鼠标指针的坐标都会添加到其中一个数据集中。

模型训练


创建一个简单的卷积神经网络。 TensorFlow.js为此目的提供了让人想起Keras的API。 网络应具有一个conv2d层,一个maxPooling2d层以及最后一个具有两个输出值的dense层(它们代表屏幕坐标)。 在此过程中,我为网络添加了一个dropout层和一个flatten层,作为正则化器,以将二维数据转换为一维数据。 网络培训是使用Adam优化器完成的。

请注意,在尝试MacBook Air之后,我决定使用这里使用的网络设置。 您可以选择自己的模型配置。

这是模型代码:

 let currentModel; function createModel() { const model = tf.sequential(); model.add(tf.layers.conv2d({   kernelSize: 5,   filters: 20,   strides: 1,   activation: 'relu',   inputShape: [$('#eyes').height(), $('#eyes').width(), 3], })); model.add(tf.layers.maxPooling2d({   poolSize: [2, 2],   strides: [2, 2], })); model.add(tf.layers.flatten()); model.add(tf.layers.dropout(0.2)); //    x  y model.add(tf.layers.dense({   units: 2,   activation: 'tanh', })); //   Adam     0.0005     MSE model.compile({   optimizer: tf.train.adam(0.0005),   loss: 'meanSquaredError', }); return model; } 

在开始训练网络之前,我们设置了固定数量的时代和可变的数据包大小(因为我们可能会使用非常小的数据集)。

 function fitModel() { let batchSize = Math.floor(dataset.train.n * 0.1); if (batchSize < 4) {   batchSize = 4; } else if (batchSize > 64) {   batchSize = 64; } if (currentModel == null) {   currentModel = createModel(); } currentModel.fit(dataset.train.x, dataset.train.y, {   batchSize: batchSize,   epochs: 20,   shuffle: true,   validationData: [dataset.val.x, dataset.val.y], }); } 

现在,在页面上添加一个按钮以开始学习。 此代码转到HTML文件:

 <button id="train">Train!</button> <style>   #train {       position: absolute;       top: 50%;       left: 50%;       transform: translate(-50%, -50%);       font-size: 24pt;   } </style> 

此代码必须添加到JS文件中:

 $('#train').click(function() { fitModel(); }); 

用户在哪里看?


现在我们可以收集数据并准备模型,我们可以开始预测页面上用户正在寻找的位置。 我们在屏幕周围移动的绿色圆圈的帮助下指向这个地方。

首先,在页面上添加一个圆圈:

 <div id="target"></div> <style>   #target {       background-color: lightgreen;       position: absolute;       border-radius: 50%;       height: 40px;       width: 40px;       transition: all 0.1s ease;       box-shadow: 0 0 20px 10px white;       border: 4px solid rgba(0,0,0,0.5);   } </style> 

为了在页面上移动它,我们会定期传输神经网络眼睛的当前图像,并向她询问有关用户在哪里看的问题。 响应的模型产生两个坐标,圆应沿着该坐标移动:

 function moveTarget() { if (currentModel == null) {   return; } tf.tidy(function() {   const image = getImage();   const prediction = currentModel.predict(image);   //          const targetWidth = $('#target').outerWidth();   const targetHeight = $('#target').outerHeight();   const x = (prediction.get(0, 0) + 1) / 2 * ($(window).width() - targetWidth);   const y = (prediction.get(0, 1) + 1) / 2 * ($(window).height() - targetHeight);   //     :   const $target = $('#target');   $target.css('left', x + 'px');   $target.css('top', y + 'px'); }); } setInterval(moveTarget, 100); 

我将间隔设置为100毫秒。 如果您的计算机不如我的计算机强大,则可以决定将其放大。

总结


现在,我们已经拥有实现本材料开头提出的想法所需的一切。 体验我们所做的事情。 跟随他的眼睛移动鼠标光标,然后按空格键。 然后单击开始训练按钮。

收集更多数据,再次单击按钮。 稍后,注视后,绿色圆圈将开始在屏幕上移动。 首先,到达您要看的地方并不是特别好,但是从大约50个收集的样本开始,经过几个阶段的培训,如果幸运的话,它将很准确地移至要查看的页面上。 在此材料中解析的示例的完整代码可以在这里找到。

尽管我们所做的工作看起来很有趣,但是仍然可以进行许多改进。 如果用户移动其头部或改变其在相机前的位置怎么办? 我们的项目不会影响选择限制眼睛所在图像区域的矩形的大小,位置和角度的可能性。 实际上,在这里讨论的示例的完整版本中实现了许多其他功能。 以下是其中一些:

  • 上述用于自定义矩形框的选项。
  • 将图像转换为灰度。
  • 使用CoordConv
  • 一个热图,用于检查模型在哪些地方效果良好以及在哪些地方效果不好。
  • 能够保存和加载数据集。
  • 保存和加载模型的能力。
  • 保持重量的东西在训练后显示出最小的训练损失。
  • 改进的用户界面以及有关使用系统的简要说明。

亲爱的读者们! 您使用TensorFlow吗?

Source: https://habr.com/ru/post/zh-CN426055/


All Articles