O autor do artigo, cuja tradução estamos publicando, se oferece para falar sobre a solução de problemas no campo da visão computacional, usando exclusivamente um navegador da web. Resolver esse problema não é tão difícil graças à biblioteca JavaScript do
TensorFlow . Em vez de treinar nosso próprio modelo e oferecê-lo aos usuários como parte do produto final, daremos a eles a oportunidade de coletar dados de forma independente e treinar o modelo diretamente em um navegador, em nosso próprio computador. Com essa abordagem, o processamento de dados no servidor é completamente desnecessário.
Você pode experimentar o que este material é dedicado a criar
aqui . Você precisará de um navegador moderno, webcam e mouse para isso.
Aqui está o código fonte do projeto. Ele não foi projetado para funcionar em dispositivos móveis, o autor do material diz que não teve tempo para melhorias apropriadas. Além disso, ele observa que a tarefa considerada aqui se tornará mais complicada se você precisar processar o fluxo de vídeo de uma câmera em movimento.
Idéia
Vamos usar a tecnologia de aprendizado de máquina para descobrir exatamente para onde o usuário está olhando quando ele olha para uma página da web. Fazemos isso observando os olhos dele usando uma webcam.
É muito fácil acessar a webcam no navegador. Se assumirmos que toda a imagem da câmera será usada como entrada para a rede neural, podemos dizer que ela é muito grande para esses fins. O sistema terá que fazer muito trabalho apenas para determinar o local da imagem onde estão os olhos. Essa abordagem pode se mostrar bem se estivermos falando de um modelo que o desenvolvedor treina por si próprio e implanta no servidor, mas se estamos falando de treinamento e uso do modelo em um navegador, isso é demais.
Para facilitar a tarefa da rede, podemos fornecer apenas uma parte da imagem - a que contém os olhos do usuário e uma pequena área ao seu redor. Essa área, que é um retângulo ao redor dos olhos, pode ser identificada usando uma biblioteca de terceiros. Portanto, a primeira parte do nosso trabalho é assim:
Entrada da webcam, reconhecimento de rosto, detecção de olhos, imagem cortadaPara detectar o rosto na imagem, usei uma biblioteca chamada
clmtrackr . Não é perfeito, mas difere em tamanho pequeno, bom desempenho e, em geral, lida com sua tarefa com dignidade.
Se uma imagem pequena, mas inteligentemente selecionada, for usada como entrada para uma rede neural convolucional simples, a rede poderá aprender sem problemas. Aqui está a aparência desse processo:
A imagem de entrada, o modelo, é uma rede neural convolucional, coordenadas, o local previsto pela rede na página em que o usuário está olhando.Uma implementação mínima totalmente funcional das idéias discutidas nesta seção será descrita aqui. O projeto, cujo código está
neste repositório, possui muitos recursos adicionais.
Preparação
Para começar, faça o
clmtrackr.js
no
repositório apropriado. Iniciaremos o trabalho com um arquivo HTML vazio, que importa jQuery, TensorFlow.js, clmtrackr.js e o arquivo
main.js
com nosso código, no qual trabalharemos um pouco mais tarde:
<!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>
Receba um fluxo de vídeo de uma webcam
Para ativar a webcam e exibir o fluxo de vídeo na página, precisamos obter permissão do usuário. Aqui não forneço código que resolva os problemas de compatibilidade do projeto com vários navegadores. Vamos partir da suposição de que nossos usuários trabalham na Internet usando a versão mais recente do Google Chrome.
Adicione o seguinte código ao arquivo HTML. Ele deve estar localizado na
<body>
, mas acima das tags
<script>
:
<video id="webcam" width="400" height="300" autoplay></video>
Agora vamos trabalhar com o arquivo
main.js
$(document).ready(function() { const video = $('#webcam')[0]; function onStreaming(stream) { video.srcObject = stream; } navigator.mediaDevices.getUserMedia({ video: true }).then(onStreaming); });
Experimente esse código por conta própria. Quando você abre a página, o navegador deve pedir permissão e, em seguida, uma imagem da webcam aparece na tela.
Mais tarde, estenderemos o código da função
onStreaming()
.
Pesquisa de rosto
Agora vamos usar a biblioteca clmtrackr.js para procurar rostos no vídeo. Primeiro, inicialize o sistema de rastreamento de faces adicionando o seguinte código após
const video = ...
:
const ctrack = new clm.tracker(); ctrack.init();
Agora, na função
onStreaming()
, conectamos o sistema de pesquisa de faces adicionando o seguinte comando:
ctrack.start(video);
É tudo o que precisamos. Agora o sistema poderá reconhecer o rosto no fluxo de vídeo.
Não acredita? Vamos desenhar uma "máscara" em volta do seu rosto para garantir que isso seja verdade.
Para fazer isso, precisamos exibir a imagem em cima do elemento responsável por exibir o vídeo. Você pode desenhar algo nas páginas HTML usando a
<canvas>
. Portanto, criaremos esse elemento sobrepondo-o ao elemento que exibe o vídeo. O código a seguir nos ajudará com isso, que deve ser adicionado ao arquivo HTML no elemento
<video>
que já está lá:
<canvas id="overlay" width="400" height="300"></canvas> <style> #webcam, #overlay { position: absolute; top: 0; left: 0; } </style>
Se desejar, você pode mover o estilo embutido para um arquivo CSS separado.
Aqui, adicionamos um
<canvas>
mesmo tamanho à página que o elemento
<video>
. O fato de os elementos estarem localizados na mesma posição é garantido pelos estilos usados aqui.
Agora, toda vez que o navegador exibir o próximo quadro do vídeo, desenharemos algo no
<canvas>
. A execução de qualquer código durante a saída de cada quadro é realizada usando o mecanismo
requestAnimationLoop()
. Antes de enviar algo para o
<canvas>
, precisamos remover dele o que estava nele antes, limpando-o. Podemos sugerir ao
clmtrackr
a saída do gráfico diretamente para o
<canvas>
.
Aqui está o código que implementa o que acabamos de falar. Adicione-o abaixo do comando
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); } }
Agora chame a função
trackingLoop()
na função
onStreaming()
imediatamente após
ctrack.start()
. Essa função planejará sua própria reinicialização em cada quadro.
Atualize a página e observe a webcam. Você deve ver uma "máscara" verde ao redor do seu rosto na janela de vídeo. Às vezes, para que o sistema reconheça corretamente o rosto, você precisa mover ligeiramente a cabeça no quadro.
Resultados de reconhecimento de rostoIdentificação da área da imagem que contém os olhos
Agora precisamos encontrar a área retangular da imagem na qual os olhos estão localizados e colocá-la em um
<canvas>
separado.
Felizmente, o cmltracker fornece não apenas informações sobre a localização do rosto, mas também 70 pontos de controle. Se você consultar a
documentação do cmltracker, poderá selecionar exatamente os pontos de controle necessários.
Pontos de controleDecidimos que os olhos são a parte retangular da imagem, cujas bordas tocam os pontos 23, 28, 24 e 26, expandidas em 5 pixels em cada direção. Esse retângulo deve incluir tudo o que é importante para nós, a menos que o usuário incline demais a cabeça.
Agora, antes de podermos usar esse fragmento da imagem, precisamos de mais um
<canvas>
para sua saída. Suas dimensões serão 50x25 pixels. Um retângulo com olhos se encaixará nesse elemento. Uma leve deformação da imagem não é um problema.
Adicione este código ao arquivo HTML que descreve o
<canvas>
, que incluirá a parte da imagem que possui olhos:
<canvas id="eyes" width="50" height="25"></canvas> <style> #eyes { position: absolute; top: 0; right: 0; } </style>
A função a seguir retornará as coordenadas
y
, assim como a largura e a altura do retângulo ao redor dos olhos. Como entrada, assume uma matriz de
positions
recebidas do clmtrackr. Observe que cada coordenada recebida do clmtrackr possui componentes
x
e
y
. Esta função deve ser adicionada ao
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]; }
Agora, em cada quadro, vamos extrair um retângulo com os olhos do fluxo de vídeo, circulá-lo com uma linha vermelha no
<canvas>
, sobreposta ao elemento
<video>
, e depois copiá-lo para o novo
<canvas>
. Observe que, para identificar corretamente a área de que precisamos, calcularemos os indicadores
resizeFactorX
e
resizeFactorY
.
Substitua o bloco
if
na função
trackingLoop()
pelo seguinte código:
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 ); }
Depois de recarregar a página agora, você verá um retângulo vermelho ao redor dos olhos, e o que esse retângulo contém está no
<canvas>
correspondente. Se seus olhos são maiores que os meus, experimente a função
getEyeRectangle
.
elemento <canvas> que desenha um retângulo contendo a imagem dos olhos do usuárioColeta de dados
Existem muitas maneiras de coletar dados. Decidi usar as informações que podem ser obtidas com o mouse e o teclado. Em nosso projeto, a coleta de dados se parece com isso.
O usuário move o cursor pela página e o observa com os olhos, pressionando a
no teclado toda vez que o programa precisa gravar outra amostra. Com essa abordagem, é fácil coletar rapidamente um grande conjunto de dados para o treinamento do modelo.
OuseMouse Tracking
Para descobrir exatamente onde o ponteiro do mouse está localizado na página da web, precisamos de um manipulador de eventos
document.onmousemove
. Nossa função, além disso, normaliza as coordenadas para que elas se ajustem ao intervalo [-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;
▍ Captura de imagem
Para capturar a imagem exibida pelo
<canvas>
e salvá-la como um tensor, o TensorFlow.js oferece a função auxiliar
tf.fromPixels()
. Nós o usamos para salvar e normalizar a imagem do
<canvas>
que exibe um retângulo contendo os olhos do usuário:
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)); }); }
Observe que a função
tf.tidy()
é usada para limpar após a conclusão.
Poderíamos salvar todas as amostras em um grande conjunto de treinamento, no entanto, no aprendizado de máquina, é importante verificar a qualidade do treinamento do modelo. É por isso que precisamos salvar algumas amostras em uma amostra de controle separada. Depois disso, podemos verificar o comportamento do modelo em novos dados e descobrir se o modelo foi treinado em excesso. Para esse fim, 20% do número total de amostras são incluídas na amostra de controle.
Aqui está o código usado para coletar dados e amostra:
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; }); }
E, finalmente, precisamos vincular essa função à
:
$('body').keyup(function(event) { // if (event.keyCode == 32) { captureExample(); event.preventDefault(); return false; } });
Agora, toda vez que você pressiona a
, a imagem do olho e as coordenadas do ponteiro do mouse são adicionadas a um dos conjuntos de dados.
Modelo de treinamento
Crie uma rede neural convolucional simples. O TensorFlow.js fornece uma API remanescente do Keras para essa finalidade. A rede deve ter uma camada
conv2d
, uma camada
maxPooling2d
e, finalmente, uma camada
dense
com dois valores de saída (eles representam as coordenadas da tela). Ao longo do caminho, adicionei uma camada
dropout
e uma
flatten
layer à rede, como regularizador, para converter dados bidimensionais em unidimensionais. O treinamento em rede é feito usando o otimizador Adam.
Observe que estabeleci as configurações de rede usadas aqui depois de experimentar o meu MacBook Air. Você pode escolher sua própria configuração do modelo.
Aqui está o código do modelo:
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; }
Antes de começar a treinar a rede, definimos um número fixo de eras e um tamanho de pacote variável (já que provavelmente trabalharemos com conjuntos de dados muito pequenos).
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], }); }
Agora adicione um botão à página para começar a aprender. Este código vai para o arquivo HTML:
<button id="train">Train!</button> <style> #train { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24pt; } </style>
Este código deve ser adicionado ao arquivo JS:
$('#train').click(function() { fitModel(); });
Para onde o usuário está olhando?
Agora que podemos coletar os dados e preparar o modelo, podemos começar a prever o local na página em que o usuário está procurando. Apontamos para este local com a ajuda de um círculo verde, que se move pela tela.
Primeiro, adicione um círculo à página:
<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>
Para movê-lo pela página, transmitimos periodicamente a imagem atual dos olhos da rede neural e fazemos uma pergunta sobre onde o usuário está olhando. O modelo em resposta produz duas coordenadas ao longo das quais o círculo deve ser movido:
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);
Defino o intervalo para 100 milissegundos. Se o seu computador não for tão poderoso quanto o meu, você poderá ampliá-lo.
Sumário
Agora, temos tudo o que precisamos para implementar a ideia apresentada no início deste material. Experimente o que fizemos. Mova o cursor do mouse, seguindo os olhos dele, e pressione a barra de espaço. Em seguida, clique no botão Iniciar treinamento.
Colete mais dados, clique no botão novamente. Depois de um tempo, o círculo verde começará a se mover pela tela após o seu olhar. No início, não será particularmente bom chegar ao local em que você está procurando, mas, começando com cerca de 50 amostras coletadas, após várias etapas do treinamento e, se tiver sorte, ele será movido com bastante precisão para o ponto da página em que você está visualizando. . O código completo do exemplo analisado neste material pode ser encontrado
aqui .
Embora o que já fizemos pareça bastante interessante, ainda há muitas melhorias que podem ser feitas. E se o usuário mover a cabeça ou mudar de posição na frente da câmera? Nosso projeto não prejudicaria as possibilidades de seleção do tamanho, posição e ângulo do retângulo que limita a área da imagem em que os olhos estão. De fato, alguns recursos adicionais são implementados na
versão completa do exemplo discutido aqui. Aqui estão alguns deles:
- As opções para personalizar o retângulo delineador descrito acima.
- Converta uma imagem em escala de cinza.
- Usando CoordConv .
- Um mapa de calor para verificar onde o modelo teve bom desempenho e onde não teve.
- Capacidade de salvar e carregar conjuntos de dados.
- Capacidade de salvar e carregar modelos.
- Preservação de pesos que mostraram perda mínima de treinamento após o treinamento.
- Interface de usuário aprimorada com breves instruções para trabalhar com o sistema.
Caros leitores! Você usa o TensorFlow?
