- PVSM.RU - https://www.pvsm.ru -
OpenCV — библиотека с историей непрерывной разработки в 20 лет. Возраст, когда начинаешь копаться в себе, искать предназначение. Есть ли проекты на ее основе, которые сделали чью-то жизнь лучше, кого-то счастливее? А можешь ли ты сделать это сам? В поисках ответов и желании открыть для себя ранее неизвестные модули OpenCV, хочу собрать приложения, которые "делают красиво" — так, чтобы сначала было "вау" и только потом ты скажешь "о да, это компьютерное зрение".
Право первой статьи получил эксперимент с переносом стилей мировых художников на фотографии. Из статьи вы узнаете, что является сердцем процедуры и об относительно новом OpenCV.js — JavaScript версии библиотеки OpenCV.
Да простят меня противники машинного обучения, но главной компонентой в сегодняшней статье будет глубокая сверточная сеть. Потому что работает. В OpenCV нет возможности тренировать нейронные сети, но можно запускать уже существующие модели. Мы будем использовать предобученную сеть CycleGAN [1]. Авторы, за что им большая благодарность, предлагают совершенно свободно скачать сети, которые конвертируют изображения яблок в апельсины, лошадей в зебр, снимков со спутника в карты, фотографий зимы в фотографии лета и много чего ещё. Более того, процедура обучения сети позволяет иметь сразу две модели генератора, работающих в обе стороны. То есть, обучая преобразование зимы в лето вы получите и модель для рисования зимних пейзажей на летних фотографиях. Уникальное предложение, от которого невозможно отказаться.
В нашем примере мы возьмём модели, которые превращают фотографии в картины художников. А именно, Винсента Ван Гога, Клода Моне, Поля Сезанна или в целый жанр японских гравюр Ukiyo-e. То есть в нашем распоряжении будет четыре отдельные сети. Стоит заметить, что для обучения каждой использовалась не одна картина художника, а целое множество, тем самым авторы пытались обучить нейронную сеть не перекладывать стиль одного произведения, а, как бы, перенять стиль письма.
OpenCV — библиотека, разрабатываемая на языке C++, при этом для большей части ее функционала существует возможность создания автоматических оберток, которые вызывают нативные методы. Официально, поддерживаются обертки для языков Python и Java. Кроме того, существуют пользовательские решения для Go [2], PHP [3]. Если у вас есть опыт использования в других языках — было бы здорово узнать, в каких, и благодаря чьим стараниям.
OpenCV.js — это проект, который получил право на жизнь благодаря программе Google Summer of Code в 2017 году. К слову, когда-то и сам deep learning модуль OpenCV был создан и значительно улучшался в его рамках. В отличие от других языков, OpenCV.js на данный момент — это не обертка нативных методов в JavaScript, а полноценная компиляция с помощью Emscripten, использующего LLVM и Clang. Он позволяет сделать из вашего C и C++ приложения или библиотеки .js
файл, который можно запускать, скажем, в браузере.
Для примера,
#include <iostream>
int main(int argc, char** argv) {
std::cout << "Hello, world!" << std::endl;
return 0;
}
Компилируем в asm.js
emcc main.cpp -s WASM=0 -o main.js
И подгружаем:
<!DOCTYPE html>
<html>
<head>
<script src="main.js" type="text/javascript"></script>
</head>
</html>
Подключить OpenCV.js к своему проекту можно следующим образом (ночная сборка):
<script src="https://docs.opencv.org/master/opencv.js" type="text/javascript"></script>
Полезным может также оказаться дополнительная библиотека для чтения изображений, работы с камерой и прочего, которая написана вручную на JavaScript:
<script src="https://docs.opencv.org/master/utils.js" type="text/javascript"></script>
Изображения в OpenCV.js могут быть прочитаны с элементов типа canvas
или img
. Это значит, что загрузка непосредственно файлов картинок на них остается задачей пользователя. Для удобства, вспомогательная функция addFileInputHandler
, автоматически загрузит изображение в нужный элемент canvas
при выборе картинки с диска по нажатию кнопки.
var utils = new Utils('');
utils.addFileInputHandler('fileInput', 'canvasInput');
var img = cv.imread('canvasInput');
где
<input type="file" id="fileInput" name="file" accept="image/*" />
<canvas id="canvasInput" ></canvas>
Важным моментом является то, что img
будет 4-х канальным RGBA изображением, что отличается от привычного поведения cv::imread
, который создает BGR картинку. Это нужно учитывать, например, при портировании алгоритмов с других языков.
С отрисовкой всё просто — достаточно одного вызова imshow
с указанием id
нужного canvas
(ожидает RGB или RGBA).
cv.imshow("canvasOutput", img);
Весь алгоритм обработки изображения — это запуск нейронной сети. Пусть то, что происходит внутри — останется магией, нам нужно будет только подготовить правильный вход и правильно интерпретировать предсказание (выход сети).
Сеть, рассматриваемая в этом примере, принимает на вход четырехмерный тензор со значениями типа float
в интервале [-1, 1]
. Каждая из размерностей, в порядке скорости изменения — это индекс картинки, каналы, высота и ширина. Такую укладку принято называть NCHW, а сам тензор — блобом (blob, binary large object). Задача предобработки заключается в том, чтобы преобразовать изображение OpenCV, значения интенсивностей которого лежат вперемешку (interleaved), имеют интервал значений [0, 255]
типа unsigned char
в NCHW блоб с диапазоном значений [-1, 1]
.
кусочек нижегородского кремля (как видит человек)
interleaved представление (как хранит OpenCV)
planar представление (то, что нужно сети)
В качестве постобработки необходимо будет произвести обратные преобразования: сеть возвращает NCHW блоб со значениями в интервале [-1, 1]
, который нужно перепаковать в картинку, нормировать в [0, 255]
и перевести в unsigned char
.
Таким образом, с учётом всех особенностей чтения и записи картинок OpenCV.js, у нас вырисовываются следующие шаги:
imread -> RGBA -> BGR [0, 255] -> NCHW [-1, 1] -> [сеть]
[сеть] -> NCHW [-1, 1] -> RGB [0, 255] -> imshow
Глядя на полученный конвейер, возникают вопросы, почему сеть не может работать сразу на interleaved RGBA и возвращать interleaved RGB? Почему нужны лишние преобразования по перестановке пикселей и нормировке? Ответ в том, что нейронная сеть — это математический объект, который выполняет преобразования над входными данными определенного распределения. В нашем случае её обучили принимать данные именно в таком виде, поэтому для получения желаемых результатов, придется воспроизвести предобработку, которую использовали авторы при обучении.
Нейронная сеть, которую мы будем запускать, хранится в виде бинарного файла, который нужно предварительно подгрузить в локальную файловую систему.
var net;
var url = 'style_vangogh.t7';
utils.createFileFromUrl('style_vangogh.t7', url, () => {
net = cv.readNet('style_vangogh.t7');
});
Кстати, url
— это полноценная ссылка на файл. В данном случае мы просто подгружаем файл, лежащий рядом с текущей HTML страницей, но вы можете заменить её на оригинальный источник [4] (в таком случае время скачивания может быть больше).
Чтение изображения с canvas
и конвертация из RGBA в BGR:
var imgRGBA = cv.imread('canvasInput');
var imgBGR = new cv.Mat(imgRGBA.rows, imgRGBA.cols, cv.CV_8UC3);
cv.cvtColor(imgRGBA, imgBGR, cv.COLOR_RGBA2BGR);
Создание 4D блоба, где функция blobFromImage
выполняет конвертацию в тип данных float
, применяя нормировочные константы. Затем — запуск сети.
var blob = cv.blobFromImage(imgBGR, 1.0 / 127.5, // множитель
{width: imgBGR.cols, height: imgBGR.rows}, // размеры
[127.5, 127.5, 127.5, 0]); // вычитание среднего
net.setInput(blob);
var out = net.forward();
Полученный результат преобразуется обратно в картинку нужного типа и интервалом значений [0, 255]
// Нормировка значений из интервала [-1, 1] в [0, 255]
var outNorm = new cv.Mat();
out.convertTo(outNorm, cv.CV_8U, 127.5, 127.5);
// Создание interleaved изображения из planar блоба
var outHeight = out.matSize[2];
var outWidth = out.matSize[3];
var planeSize = outHeight * outWidth;
var data = outNorm.data;
var b = cv.matFromArray(outHeight, outWidth, cv.CV_8UC1, data.slice(0, planeSize));
var g = cv.matFromArray(outHeight, outWidth, cv.CV_8UC1, data.slice(planeSize, 2 * planeSize));
var r = cv.matFromArray(outHeight, outWidth, cv.CV_8UC1, data.slice(2 * planeSize, 3 * planeSize));
var vec = new cv.MatVector();
vec.push_back(r);
vec.push_back(g);
vec.push_back(b);
var rgb = new cv.Mat();
cv.merge(vec, rgb);
// Отрисовка результата
cv.imshow("canvasOutput", rgb);
На данный момент, OpenCV.js собирается в полуавтоматическом режиме. В том смысле, что не все модули и методы из них получают соответствующие сигнатуры в JavaScript. Например, для dnn модуля список допустимых функций определяется так:
dnn = {'dnn_Net': ['setInput', 'forward'],
'': ['readNetFromCaffe', 'readNetFromTensorflow',
'readNetFromTorch', 'readNetFromDarknet',
'readNetFromONNX', 'readNet', 'blobFromImage']}
Последнее преобразование, разделяющее блоб на три канала и затем перемешивающее их в картинку, на самом деле, можно выполнить одним методом imagesFromBlob
, которое просто ещё не добавили в список выше. Возможно, это будет твоим первым вкладом в развитие OpenCV? ;)
В качестве демонстрации, подготовил страничку на GitHub, где вы можете протестировать результирующий код: https://dkurtaev.github.io/opencv4arts [5] (Осторожно! Скачивание сети около 22MB, берегите свой трафик. Также рекомендуется перезагружать страницу для каждого нового изображения, иначе качество последующих обработок как-то сильно искажается). Будьте готовы к долгому процессу обработки или попробуйте поменять размеры картинки, которая будет в результате, слайдером.
Работая над статьей и выбирая то самое изображение, которое станет ее лицом, случайно нашел фотографию своего знакомого, на которой изображен кремль нашего города и тут все сошлось — придумал название статьи и только тогда почувствовал, что именно такой она должна быть. Предлагаю вам испытать приложение на фотографии своего любимого места и, возможно, рассказать о нём что-нибудь интересное в комментариях или личным письмом.
От меня — забавный факт. Большинство жителей Нижнего Новгорода и Нижегородской области употребляют слово “убраться” в смысле слова “поместиться” (найти себе свободное место). Например, вопрос “Мы уберемся в вашей машине?” означает “Хватит ли нам места в вашей машине?”, а не “Можно ли нам навести порядок в вашей машине?”. Когда к нам на летние стажировки приезжают студенты из других областей, любим рассказывать этот факт — многие искренне удивляются.
Автор: Дмитрий Куртаев
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/306651
Ссылки в тексте:
[1] CycleGAN: https://github.com/junyanz/CycleGAN
[2] Go: https://github.com/hybridgroup/gocv
[3] PHP: https://github.com/php-opencv
[4] оригинальный источник: https://people.eecs.berkeley.edu/~taesung_park/CycleGAN/models/style_vangogh.t7
[5] https://dkurtaev.github.io/opencv4arts: https://dkurtaev.github.io/opencv4arts
[6] Документация по OpenCV.js: https://docs.opencv.org/master/d5/d10/tutorial_js_root.html
[7] Модели CycleGAN: https://people.eecs.berkeley.edu/~taesung_park/CycleGAN/models/
[8] Другие style transfer модели: https://github.com/jcjohnson/fast-neural-style
[9] Источник: https://habr.com/ru/post/437600/?utm_source=habrahabr&utm_medium=rss&utm_campaign=437600
Нажмите здесь для печати.