clmtrackr.js
from the corresponding repository . We'll start the work with an empty HTML file in which jQuery, TensorFlow.js, clmtrackr.js and main.js
file are main.js
with our code, which we will work on a little later: <!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>
<body>
, but above the <script>
tags: <video id="webcam" width="400" height="300" autoplay></video>
main.js
file: $(document).ready(function() { const video = $('#webcam')[0]; function onStreaming(stream) { video.srcObject = stream; } navigator.mediaDevices.getUserMedia({ video: true }).then(onStreaming); });
onStreaming()
function code.const video = ...
: const ctrack = new clm.tracker(); ctrack.init();
onStreaming()
function, we connect the face search system by adding the following command: ctrack.start(video);
<canvas>
. Therefore, we will create such an element, putting it on the element that displays the video. This will help us with the following code, which must be added to the HTML file under the <video>
element already existing there: <canvas id="overlay" width="400" height="300"></canvas> <style> #webcam, #overlay { position: absolute; top: 0; left: 0; } </style>
<canvas>
element to the page of the same size as the <video>
element. The fact that the elements will be located in the same position is provided by the styles used here.<canvas>
. The execution of any code during the output of each frame is performed using the requestAnimationLoop()
mechanism. Before we put anything into the <canvas>
, we need to remove from it what was on it before, clearing it. We can then suggest that clmtrackr
perform graphics output directly on the <canvas>
.ctrack.init()
command: 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); } }
trackingLoop()
function in the onStreaming()
function immediately after ctrack.start()
. This function will plan its own restart itself in each frame.<canvas>
.<canvas>
for its output. Its size will be 50x25 pixels. A rectangle with eyes will be inscribed in this element. Small deformations of the image are not a problem.<canvas>
, into which that part of the image that has eyes: <canvas id="eyes" width="50" height="25"></canvas> <style> #eyes { position: absolute; top: 0; right: 0; } </style>
x
and y
coordinates, as well as the width and height of the rectangle surrounding the eye. She, as input, takes an array of positions
, obtained from clmtrackr. Note that each coordinate obtained from clmtrackr has components x
and y
. This function should be added to 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]; }
<canvas>
element that is superimposed on the <video>
element, and then copy it into a new <canvas>
. Please note that in order to correctly identify the area we need, we will calculate the resizeFactorX
and resizeFactorY
.if
block in the trackingLoop()
function: 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>
. If your eyes are bigger than mine, experiment with the getEyeRectangle
function.
on the keyboard each time the program has to write another sample. With this approach, it is easy to quickly collect a large set of data for learning the model.document.onmousemove
. Our function, in addition, normalizes the coordinates so that they fit into the range [-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>
and save it as a tensor, TensorFlow.js offers an auxiliary function tf.fromPixels()
. Use it to save and then normalize the image from the <canvas>
, which displays a rectangle containing the user's eyes: 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()
function is used to restore order after the completion of work. 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; }); }
key: $('body').keyup(function(event) { // if (event.keyCode == 32) { captureExample(); event.preventDefault(); return false; } });
pressed, the image of the eyes and the coordinates of the mouse pointer are added to one of the data sets.conv2d
layer, a maxPooling2d
layer, and, finally, a dense
layer with two output values (they represent screen coordinates). Along the way, I added to the network, as a regularizer, a dropout
layer, and a flatten
layer in order to convert two-dimensional data into one-dimensional. Network training is performed using the Adam optimizer. 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], }); }
<button id="train">Train!</button> <style> #train { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24pt; } </style>
$('#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);
Source: https://habr.com/ru/post/426055/
All Articles