Computer vision, Java, Нейронные сети

Распознавание рисунков на JavaScript

Время прочтения: 10 мин.

Что делает программа: Рисуешь смайлик радостный или грустный, нейронной сети нужно распознать эмоцию по картинке. При этом каждый раз можно рисовать смайлики ± по-разному.

 Создаем пустой файл html, все будет хранится в одном файле для простоты реализации, открываем файл под редактирование, рядом папка brain.js (это библиотека скачена и

распакована с сайта github.com. Затем подключаем библиотеку и объявляем холст, прописываем базовые стили и объявляем тег скрипт в самом конце, где мы будем писать весь код.

Получаем

Можно настроить ширину и высоту через функцию function Dcanvas(el) она будет принимать только один параметр el. В ней объявляем несколько констант и переменных. Константы ctx стандартный и pixel в значении (20), потому что изображение на холсте, которое мы нарисуем будет пиксилизироваться. Ширина и высота 500 на 500 пикселей. Переменная is_mouse_down нужна для того, чтобы создать возможность рисовать на холсте.  Два метода: this.drawLine и для того, чтобы не повторять одни и те же действия требующие для того чтобы рисовать линию и рисовать ячейки в сетке – this.drawCell: метод для отрисовки ячеек сетки.

Добавим метод this.clear, который будет при вызове отчищать холст (при помощи метода clearRect).

Если мы запустим браузер, то получим пустой холст, который не инициализирован. После объявления функции произведем инициализацию, чтобы сразу проверять работает ли все.

Реализуем функцию для отрисовки сетки. Т.к. пиксель 20, а ширина холста 500, получаем 500/20=25 ячеек по ширине и 25 ячеек по высоте.

После того, как мы вызовем метод на объект d.drawGrid() должна появиться сетка:

То, что мы нарисуем на этом холсте, нейросеть переведет в 0 и 1 (нарисованное мной изображение переведется в ячейки-пиксели, 0-это незаполненная ячейка, а 1-заполненная). Разработаем функции для отрисовки:

Теперь на холсте можно рисовать.

Создадим функцию, которая будет переводить наши рисунки в сетку, т. е. если картинка задевает две ячейки, то они заполняются, а соседние пустые — там будет 0. А там, где есть изображение — будет 1. Метод calculate с параметром draw с набором переменных и констант.

Это минимальный набор переменных и констант который нам понадобится.

Первые пять констант нужны для определения шага по двум осям, т. е. по горизонтали и вертикали. Массив vector – здесь хранится результат подсчёта (хранятся все 0 и 1 при переводе изображения). Массив __draw нужен для того, чтобы рисовать все это после подсчета, потому что до подсчета нельзя изменять холст. Просчитываем все по двум осям — пробегаемся сначала по горизонтальной оси и в ней внутри подсчитываем вертикальные оси.

Мы получаем информацию о кубике, который сейчас просматриваем, т. е. сначала это самые левые верхние 20 пикселей по ширине и по высоте при помощи метода getImageData (возвращает информацию о пикселях в конкретные переданные области), благодаря этому будем понимать пустой ли этот участок изображения или нет. Если ничего не нарисовано передадим 1, если что-то нарисовано, то 0.

В массив __draw выкидываем текущую позицию ячейки, которую нужно будет после отрисовать, т. е. Х, Y и шаги.

Наполняем вектор: если больше 1 не пустых пикселей, то тогда там что-то есть; иначе, ничего нет — 0. Здесь и определяется 0 или 1. После того, как мы просчитали и наполнили вектор можно все отрисовать. В параметре draw можно отчистить холст и здесь в массиве __draw отрисовать. Можно воспользоваться методом drawCell и в него передать все четыре аргумента которые у нас хранятся. В конце возвращаем вектор.

Чтобы проверить правильность выполненной работы, нужно разработать систему управления холстом. Т.е. вывести кнопки, либо привязать к клавиатуре. Привяжем к клавиатурным кнопкам, например, к кнопке «С» будет отвечать за отчистку холста, кнопка «V» будет отвечать за просчет вектора (передаем true, чтобы он адресовывался) и за то, чтобы научить нейронную сеть. К кнопке «B»- распознавание того, что мы нарисовали.

Теперь я рискую смайлик, жму «V» и он переводится в такой вид, где все синие ячейки принимают единицу, а пустые белые-нули.

Т.к. у нас подключена библиотека brain.js,все будем делать через нее. Нужно объявить три переменные: vector, net, train_data- это те данные, где мы будем хранить и накапливать тренировочные данные.

Для начала пусть она будет отличать только два объекта, поэтому добавим confirm- пусть отличает галочку от знака отрицания (креста)- будем спрашивать «positive» или «negative».

Если positive, то наполняем массив train_data следующим видом данных input в котором будет вектор, который нам вернулся после подсчета перевода изображения в сетку. И должен быть также output. Здесь positive пусть будет 1(если нарисовать позитивную картинку (галку)). Если же нет, то в train_data выкидываем уже другого вида данные, т. е. тоже самое, только уже «negative» будет 1. На основе этих данных мы сможем отличать две разные картинки друг от друга.

Теперь нужно добавить еще одну кнопку «В», которая позволит нам рисовать и распознавать что мы нарисовали.

Создаем саму нейронную сеть brain.NeuralNetwork(). После этого ее нужно научить. Net вызываем метод train — первым аргументом передаем те данные, которые мы насобирали (train_data), вторым аргументам указать log:true здесь можно ничего не указывать, но пусть будет логирование включено.

Создаем переменную result и вызываем метод brain.likely —  он позволит определить, какой объект мы нарисовали. Первым аргументом передается текущий вектор, который нужно проверить, и вторым аргументом сама нейронная сеть (переменная net). Теперь выведем результат alert(result)

Рисуем галочку. Нажимаем клавишу «V». У нас спрашивает позитив это или нет. Нажимаем да. В массиве train_data должны появится соответствующие данные.

Затем отчищаем экран, рисуем снова галочку. Указываем что это позитив. Нужно рисовать разные галочки для того, чтобы нейронная сеть понимала, как она выглядит. Чем больше мы нарисуем галочек (забросим данных в бд нейронной сети), тем она будет точнее.

Теперь научим ее другому знаку — отрицания (х). Рисуем крестик, жмем «V», отмена. Она понимает, что это не позитив, и запоминает его. Нарисуем еще несколько крестов (х).

Нарисуем еще 4-5 крестов — маленьких, больших, кривых.

Теперь нарисуем галочку и нажмем клавишу «B». Эта картинка переведется в вектор, он будет сравниваться с теми данными, которые мы раннее занесли (нарисовали).

Рисуем крестик (х) и жмем «B»:

Нейронная сеть работает. Она может различать любые два объекта, переводя их в сетку. Например, рисуем улыбающийся смайлик, указываем, что это позитив.

Рисуем грустный смайлик, и нажимаем отмена — негативный.

После того как мы снова рисуем улыбающийся смайлик, нажимаем клавишу «В», получаем результат позитив.

Рисуем грустный смайлик, нажимаем «В», получаем результат негатив. Каждый раз я рисую смайлики по-разному, т.к. мышкой одно и тоже я нарисовать не смогу, хотя научили мы его одним примером радостного смайлика, и одним примером грустного смайлика.

Код программы:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>НС</title> <script src="brain.js/browser.min.js"></script>
 <style> body { background-color: #333; }
 #canv { position: absolute; top: 0;right: 0;bottom: 0;left: 0; margin: auto; background-color: white; }
 </style> </head> <body>
 <canvas id="canv" style="display: block;">Ваш браузер устарел, обновитесь.</canvas>
 <script> function DCanvas(el) {
 const ctx = el.getContext('2d'); const pixel = 20; let is_mouse_down = false; canv.width = 500; canv.height = 500; this.drawLine = function(x1, y1, x2, y2, color = 'gray') { ctx.beginPath(); ctx.strokeStyle = color; ctx.lineJoin = 'miter'; ctx.lineWidth = 1; ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); }
 this.drawCell = function(x, y, w, h) { ctx.fillStyle = 'blue'; ctx.strokeStyle = 'blue'; ctx.lineJoin = 'miter'; ctx.lineWidth = 1; ctx.rect(x, y, w, h); ctx.fill(); }
 this.clear = function() { ctx.clearRect(0, 0, canv.width, canv.height); }
 this.drawGrid = function() { const w = canv.width; const h = canv.height; const p = w / pixel; const xStep = w / p; const yStep = h / p; for( let x = 0; x < w; x += xStep ) {
 this.drawLine(x, 0, x, h); }
 for( let y = 0; y < h; y += yStep ) {
 this.drawLine(0, y, w, y); }
 }
 this.calculate = function(draw = false) { const w = canv.width; const h = canv.height; const p = w / pixel; const xStep = w / p; const yStep = h / p; const vector = []; let __draw = []; for( let x = 0; x < w; x += xStep ) {
 for( let y = 0; y < h; y += yStep ) {
 const data = ctx.getImageData(x, y, xStep, yStep); let nonEmptyPixelsCount = 0; for( i = 0; i < data.data.length; i += 10 ) {
 const isEmpty = data.data[i] === 0; if( !isEmpty ) {
 nonEmptyPixelsCount += 1; }
 }
 if( nonEmptyPixelsCount > 1 && draw ) {
 __draw.push([x, y, xStep, yStep]); }
 vector.push(nonEmptyPixelsCount > 1 ? 1 : 0); }
 }
 if( draw ) {
 this.clear(); this.drawGrid(); for( _d in __draw ) {
 this.drawCell( __draw[_d][0], __draw[_d][1], __draw[_d][2], __draw[_d][3] ); }
 }
 return vector; }
 el.addEventListener('mousedown', function(e) { is_mouse_down = true; ctx.beginPath(); })
 el.addEventListener('mouseup', function(e) { is_mouse_down = false; })
 el.addEventListener('mousemove', function(e) { if( is_mouse_down ) {
 ctx.fillStyle = 'red'; ctx.strokeStyle = 'red'; ctx.lineWidth = pixel; ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); ctx.beginPath(); ctx.arc(e.offsetX, e.offsetY, pixel / 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.moveTo(e.offsetX, e.offsetY); }
 })
 }
 let vector = []; let net = null; let train_data = []; const d = new DCanvas(document.getElementById('canv')); document.addEventListener('keypress', function(e) { if( e.key.toLowerCase() == 'c' ) {
 d.clear(); }
 if( e.key.toLowerCase() == 'v' ) {
 vector = d.calculate(true); //train if( confirm('Positive?') ) {
 train_data.push({ input: vector, output: {positive: 1} });
 } else {
 train_data.push({ input: vector, output: {negative: 1} });
 }
 }
 if( e.key.toLowerCase() == 'b' ) {
 net = new brain.NeuralNetwork(); net.train(train_data, {log: true}); const result = brain.likely(d.calculate(), net); alert(result); }
 });
 </script> </body> </html>
Советуем почитать