- PVSM.RU - https://www.pvsm.ru -

opencv4arts: Нарисуй мой город, Винсент

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

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

opencv4arts: Нарисуй мой город, Винсент - 1

Style transfer

Да простят меня противники машинного обучения, но главной компонентой в сегодняшней статье будет глубокая сверточная сеть. Потому что работает. В OpenCV нет возможности тренировать нейронные сети, но можно запускать уже существующие модели. Мы будем использовать предобученную сеть CycleGAN [1]. Авторы, за что им большая благодарность, предлагают совершенно свободно скачать сети, которые конвертируют изображения яблок в апельсины, лошадей в зебр, снимков со спутника в карты, фотографий зимы в фотографии лета и много чего ещё. Более того, процедура обучения сети позволяет иметь сразу две модели генератора, работающих в обе стороны. То есть, обучая преобразование зимы в лето вы получите и модель для рисования зимних пейзажей на летних фотографиях. Уникальное предложение, от которого невозможно отказаться.

В нашем примере мы возьмём модели, которые превращают фотографии в картины художников. А именно, Винсента Ван Гога, Клода Моне, Поля Сезанна или в целый жанр японских гравюр Ukiyo-e. То есть в нашем распоряжении будет четыре отдельные сети. Стоит заметить, что для обучения каждой использовалась не одна картина художника, а целое множество, тем самым авторы пытались обучить нейронную сеть не перекладывать стиль одного произведения, а, как бы, перенять стиль письма.

OpenCV.js

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>

opencv4arts: Нарисуй мой город, Винсент - 2

Подключить 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].

opencv4arts: Нарисуй мой город, Винсент - 3
кусочек нижегородского кремля (как видит человек)

opencv4arts: Нарисуй мой город, Винсент - 4
interleaved представление (как хранит OpenCV)

opencv4arts: Нарисуй мой город, Винсент - 5
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