OpenCV — библиотека с историей непрерывной разработки в 20 лет. Возраст, когда начинаешь копаться в себе, искать предназначение. Есть ли проекты на ее основе, которые сделали чью-то жизнь лучше, кого-то счастливее? А можешь ли ты сделать это сам? В поисках ответов и желании открыть для себя ранее неизвестные модули OpenCV, хочу собрать приложения, которые "делают красиво" — так, чтобы сначала было "вау" и только потом ты скажешь "о да, это компьютерное зрение".
Право первой статьи получил эксперимент с переносом стилей мировых художников на фотографии. Из статьи вы узнаете, что является сердцем процедуры и об относительно новом OpenCV.js — JavaScript версии библиотеки OpenCV.

Style transfer
Да простят меня противники машинного обучения, но главной компонентой в сегодняшней статье будет глубокая сверточная сеть. Потому что работает. В OpenCV нет возможности тренировать нейронные сети, но м��жно запускать уже существующие модели. Мы будем использовать предобученную сеть CycleGAN. Авторы, за что им большая благодарность, предлагают совершенно свободно скачать сети, которые конвертируют изображения яблок в апельсины, лошадей в зебр, снимков со спутника в карты, фотографий зимы в фотографии лета и много чего ещё. Более того, процедура обучения сети позволяет иметь сразу две модели генератора, работающих в обе стороны. То есть, обучая преобразование зимы в лето вы получите и модель для рисования зимних пейзажей на летних фотографиях. Уникальное предложение, от которого невозможно отказаться.
В нашем примере мы возьмём модели, которые превращают фотографии в картины художников. А именно, Винсента Ван Гога, Клода Моне, Поля Сезанна или в целый жанр японских гравюр Ukiyo-e. То есть в нашем распоряжении будет четыре отдельные сети. Стоит заметить, что для обучения каждой использовалась не одна картина художника, а целое множество, тем самым авторы пытались обучить нейронную сеть не перекладывать стиль одного произведения, а, как бы, перенять стиль письма.
OpenCV.js
OpenCV — библиотека, разрабатываемая на языке C++, при этом для большей части ее функционала существует возможность создания автоматических оберток, которые вызывают нативные методы. Официально, поддерживаются обертки для языков Python и Java. Кроме того, существуют пользовательские решения для Go, PHP. Если у вас есть опыт использования в других языках — было бы здорово узнать, в каких, и благодаря ч��им стараниям.
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 страницей, но вы можете заменить её на оригинальный источник (в таком случае время скачивания может быть больше).
Чтение изображения с 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 (Осторожно! Скачивание сети около 22MB, берегите свой трафик. Также рекомендуется перезагружать страницу для каждого но��ого изображения, иначе качество последующих обработок как-то сильно искажается). Будьте готовы к долгому процессу обработки или попробуйте поменять размеры картинки, которая будет в результате, слайдером.
Работая над статьей и выбирая то самое изображение, которое станет ее лицом, случайно нашел фотографию своего знакомого, на которой изображен кремль нашего города и тут все сошлось — придумал название статьи и только тогда почувствовал, что именно такой она должна быть. Предлагаю вам испытать приложение на фотографии своего любимого места и, возможно, рассказать о нём что-нибудь интересное в комментариях или личным письмом.
От меня — забавный факт. Большинство жителей Нижнего Новгорода и Нижегородской области употребляют слово “убраться” в смысле слова “поместиться” (найти себе свободное место). Например, вопрос “Мы уберемся в вашей машине?” означает “Хватит ли нам места в вашей машине?”, а не “Можно ли нам навести порядок в вашей машине?”. Когда к нам на летние стажировки приезжают студенты из других областей, любим рассказывать этот факт — многие искренне удивляются.
Полезные ссылки
- Документация по OpenCV.js
- Модели CycleGAN
- Другие style transfer модели (отличаются нормировкой)

