Компьютерное зрение и машинное обучение в PHP используя библиотеку opencv

Всем привет. Это моя юбилейная статья на хабре. За почти 7 лет я написал 10 статей (включая эту), 8 из них — технические. Общее количество просмотров всех статей — около полумиллиона.
Основной вклад я внёс в два хаба: PHP и Серверное администрирование. Мне нравится работать на стыке этих двух областей, но сфера моих интересов гораздо шире.
Как и многие разработчики я часто пользуюсь результатами чужого труда (статьи на хабре, код на гитхабе, …), поэтому я всегда рад делиться с сообществом своими результатами в ответ. Написание статей — это не только возврат долга сообществу, но так же позваляет найти единомышленников, получить комментарии от профессионалов в узкой сфере и ещё больше углубить свои знания в исследуемой области.

Собственно эта статья об одном из таких моментов. В ней я опишу чем занимался почти всё своё свободное время за последние полгода. Кроме тех моментов, когда я ходил купаться в море через дорогу, смотрел сериалы или игрался в игры.

e26ow_xce0cixkxg3u2tjy74bhq.jpeg

Сейчас очень сильно развивается «Машинное обучение», по нему написано уже очень много статей, в том числе на хабре и практически каждый разработчик хотел бы взять и начать его использовать в своих рабочих задачах и домашних проектах, но с чего начать и к чему применять не всегда понятно. Большинство статей для начинающих предлагают кучу литературы, на прочтение которой не хватит и жизни, англоязычные курсы (а не все из нас могут усваивать материал на английском так же эффективно как и на русском), «недорогие» русскоязычные курсы и т.д.
Регулярно выходят новые статьи, в которых описаны новые подходы к решению той или иной задачи. На гитхабе можно найти реализацию описанного в статьях подхода. В качестве языков программирования чаще используются: c / c++, python 2/3, lua и matlab, а в качестве фремворков: caffe, tensorflow, torch. Каждый пишет — кто на чём горазд. Большая сегментация по языкам программирования и фреймворкам сильно усложняет процедуру поиска того, что тебе нужно и интеграцию этого в проект. К тому же в последнее время очень много исходного кода с комментариями на китайском языке.

Чтобы как-то уменьшить весь этот хаос в opencv добавили модуль dnn, который позволяет использовать модели, натренированные в основных фреймворках. Я со своей стороны покажу как этот модуль можно использовать из php.

Как_множатся_стандарты.jpg
Наверно, внимательный читатель сразу подумал об этой картинке и он частично будет прав.

ffac727fd7f9547d36657745fe7ea3a1.png


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

xeb1-3zehldav7rbvwl0whogb80.png

Howard говорит, что для начала изучения машинного обучения достаточно одного года опыта программирования. Я с ним полностью согласен и надеюсь, что моя статья поможет снизить порог вхождения в opencv для php-разработчиков, которые мало знакомы с машинным обучением и ещё не уверены хотят ли они вообще этим заниматься или нет, а также постараюсь описать все моменты, на которые я тратил часы и дни, чтобы у вас на это уходило не больше минуты.

Итак, что же я сделал кроме логотипа?
p-8t4atpbokicdui2rpotkrvixw.png
(надеюсь, что opencv не засудит меня за плагиат)

Я рассматривал возможность написать модуль php-opencv самостоятельно с помощью SWIG и потратил на это кучу времени, но так ничего и не добился. Всё осложнялось тем, что я не знал с/с++ и не писал расширений под php 7. К сожалению большинство материалов в интернете по php-расширениям было написано для php 5, поэтому приходилось собирать информацию по крупицам, а возникающие проблемы решать самостоятельно.

Потом я нашёл на просторах гитхаба библиотеку php-opencv, она представляет из себя модуль для php7, который делает вызовы методов opencv. Чтобы скомпилировать, установить и запустить примеры у меня ушло несколько вечеров. Я начал пробовать различные возможности этого модуля, но мне не хватало некоторых методов, я их добавил самостоятельно, создал пулреквест, а автор библиотеки принял. Позже я добавил ещё больше функций.

Возможно читатель на этом моменте задаст себе вопрос: зачем вообще автору нужны были такие проблемы, почему было просто не начать использовать python и tensorflow?

Ответ. Осторожно, занудство и отмазки!
Дело в том, что я не профессиональный специалист по машинному обучению, я не могу на данном этапе разработать свой собственный подход к решению той или иной узкой задачи, в которой я достигну результатов на пару процентов лучше, чем другие исследователи, а потом ещё получить на это дело патент. Например, так сделали пять китайских парней с научными степенями, которые разработали mtcnn и написали реализацию на matlab и caffe. Потом другие три китайских парня перенесли этот код на C++ & caffe, Python & mxnet, Python & caffe. Как вы наверное уже догадались, на знании только python и tensorflow далеко не уедешь. Придётся постоянно сталкиваться с кодом на разных языках с использованием разных фреймворков и комментариями на китайском.
Другой пример, я хотел использовать facemark из opencv, но к сожалению авторы не добавили поддержку этого модуля при работе из python. При этом, чтобы добавить биндинги facemark в php у меня ушёл один вечер.
Я так же пытался скомпилировать opencv для работы с nodejs, согласно нескольким инструкциям, но у меня выдавались различные ошибки и не получилось достигнуть результата.
По большей части мне было интересно этим заниматься не смотря на все трудности.


Вообщем, пока меня устраивает работать с opencv на php.

Вот так выглядит загрузка изображения:

$image = cv\imread("images/faces.jpg");


Для сравнения, на питоне это выглядит так:

image = cv2.imread("images/faces.jpg")

При чтении изображения в php (также как и в с++) информация сохраняется в объект Mat (матрица). В php её аналогом является многомерный массив, но в отличие от многомерного массива этот объект позволяет различные быстрые манипуляции, например, деление всех элементов на число. В питоне при загрузке изображения возвращается объект numpy.

Осторожно, легаси! Так уж вышло, что imread (в php, c++ и pyton) загружает изображение не в формате RGB, а в BGR. Поэтому в примерах с opencv можно часто увидеть процедуру конвертации BGR→RGB и обратно.

Поиск лиц на фото


Первым делом я попробовал эту функцию. Для неё в opencv есть класс CascadeClassifier, который может использовать предобученную модель в формате xml. Перед нахождением лица рекомендуется переводить изображение в чёрно-белый формат.

$src = imread("images/faces.jpg");
$gray = cvtColor($src, COLOR_BGR2GRAY);

$faceClassifier = new CascadeClassifier();
$faceClassifier->load('models/lbpcascades/lbpcascade_frontalface.xml');

$faceClassifier->detectMultiScale($gray, $faces);


полный код примера
Результат:
txjejjddsgkbbkwn7672sb54pui.jpeg
Как видно из примера, не составляет проблем найти лицо даже на фото в гриме зомби. Очки также не мешают нахождению лица.

Распознавание (узнавание) лиц на фото


Для этого в opencv есть класс LBPHFaceRecognizer и методы train/predict.
Если мы хотим узнать кто присутствует на фотографии, то сначала нужно натренировать модель с помощью метода train, он принимает два параметра: массив изображений лиц и массив числовых меток для этих изображений. После можно вызвать метод predict на тестовом изображении (лице) и получить числовую метку, которой оно соответствует.

$faceRecognizer = LBPHFaceRecognizer::create();
$faceRecognizer->train($myFaces, $myLabels = [1,1,1,1]); // 4 мои лица
$faceRecognizer->update($angelinaFaces, $angelinaLabels = [2,2,2,2]); // 4 лица Анжелины
$label = $faceRecognizer->predict($faceImage, $confidence);
// получаем label (1 или 2) и $confidence (уверенность)


полный код примера
Наборы лиц:
4b6cljug2tt5nfgfuqnagwl_e3e.jpeg

xu2cqnxotgg-crf8tzzicv3ghq4.png

Результат:
cqeqiqxqyi6itocp-rzp-9v2lqa.jpeg
Когда я начинал работать с LBPHFaceRecognizer, у него не было возможности сохранения/загрузки/дообучения готовой модели. Собственно первый мой пулреквест добавил эти методы: write/read/update.

Нахождение меток на лицах


Когда я начинал знакомиться с opencv, то часто натыкался на фотографии лиц, на которых точками отмечены глаза, нос, губы и т.д. Мне хотелось повторить этот эксперимент самостоятельно, но в версии opencv для питона этого не реализовали. У меня ушёл вечер, чтобы добавить поддержку FacemarkLBF на php и отправить второй пулреквест. Всё работает просто, загружаем предобученную модель, подаём на вход массив лиц, получаем массив точек для каждого лица.

$facemark = FacemarkLBF::create();
$facemark->loadModel('models/opencv-facemark-lbf/lbfmodel.yaml');
$facemark->fit($src, $faces, $landmarks);


полный код примера
Результат:
e26ow_xce0cixkxg3u2tjy74bhq.jpeg
Как видно из примера, грим зомби может ухудшить нахождение опорных точек на лице. Очки также могут помешать нахождению лица. Засветка тоже влияет. При этом посторонние предметы во рту (клубника, сигарета и т.д.) могут и не мешать.

После моего первого пулреквеста я вдохновился и стал смотреть, что можно сделать ещё с помощью opencv и наткнулся на статью Deep Learning, теперь и в OpenCV. Не долго думая, я решил добавить в php-opencv возможность использования предобученных моделей, которых полно на просторах интернета. Это оказалось не сильно сложно для загрузки caffe-моделей, правда позже у меня ушло куча времени чтобы получить научиться работать с многомерными матрицами, половина из которого ушла на разбирательство с c++ и изучение внутренностей opencv, а вторая на python и работу с моделями caffe/torch/tensorflow без использования opencv.

Поиск лиц на фото с помощью модуля dnn


Итак, opencv позволяет загружать предобученные модели в Caffe с помощью функции readNetFromCaffe. Она принимает два параметра — пути до файлов .prototxt и .caffemodel. В prototxt-файле лежит описание модели, а в caffemodel — веса, вычисленные во время тренировки модели.
Вот пример начала prototxt-файла:

input: "data"
input_shape {
  dim: 1
  dim: 3
  dim: 300
  dim: 300
}


Этот кусок файла описывает, что на вход ожидается 4-х мерная матрица 1×3x300×300. В описании моделей обычно пишут, что ожидается в таком формате, но чаще всего этого означает, что на вход ожидается изображение RGB (3 канала) размером 300×300.
Загружая RGB-изображение размером 300×300 c помощью функции imread мы получаем матрицу 300×300x3.
Для приведения матрицы 300×300x3 к виду 1×3x300×300 в opencv есть функция blobFromImage.
После этого нам остаётся только подать blob на вход сети с помощью метода setInput и вызвать метод forward, который вернёт нам готовый результат.

$src = imread("images/faces.jpg");

$net = \CV\DNN\readNetFromCaffe('models/ssd/res10_300x300_ssd_deploy.prototxt', 'models/ssd/res10_300x300_ssd_iter_140000.caffemodel');

$blob = \CV\DNN\blobFromImage($src, $scalefactor = 1.0, $size = new Size(300, 300), $mean = new Scalar(104, 177, 123), $swapRB = true, $crop = false);

$net->setInput($blob, "");

$result = $net->forward();


В данном случае результат — это матрица 1×1x200×7, т.е. 200 массивов по 7 элементов каждый. На фото с четырьмя лицами сеть нашла нам 200 кандидатов. Каждый из которых выглядит так [,, $confidence, $startX, $startY, $endX, $endY]. Элемент $confidence отвечает за «уверенность», т.е. то что вероятность предсказания удачна, например 0.75. Следующие элементы отвечают за координаты прямоугольника с лицом. В данном примере было найдено только 3 лица с уверенностью больше 50%, а оставшиеся 197 кандидатов лиц имеют уверенность менее 15%.

Размер модели 10 МБ, полный код примера.
Результат:
5a3emxkpv9qyjtkexib2poqhpx8.jpeg
Как видно из примера, нейронная сеть не всегда выдаёт хорошие результаты при использовании её «в лоб». Не было найдено четвёртое лицо, при этом если четвёртое фото вырезать и отправить в сеть отдельно, то лицо будет найдено.

Улучшение качества изображения с помощью нейронной сети


Я уже давно слышал про библиотеку waifu2x, которая позволяет устранять шум и увеличивать размеры иконок/фото. Сама библиотека написана на lua, а под капотом использует несколько моделей (для увеличения иконок, устранения шума фото и т.д.) натренированных в torch. Автор библиотеки экспортировал эти модели в caffe и помог мне использовать их из opencv. В результате чего был написан пример на php для увеличения разрешения иконок.
Размер модели 2 МБ, полный код примера.
Оригинал:
7iv1qtn-tffutokvnmsl4eewhc8.png
Результат:
cpexcd6x03yusszmkde-icleem0.png
Увеличение картинки без использования нейронной сети:
kci-ohgo2cvw9nacaxlj4taasok.png

Классификация изображений


Нейронная сеть MobileNet, обученная на наборе данных ImageNet позволяет классифицировать изображение. Всего она может определять 1000 классов, что по-моему достаточно не мало.
Размер модели 16 МБ, полный код примера.
Оригинал:
oiihukgiiqcxtxy2jzzsx0pvd2q.jpeg
Результат:
87% — Egyptian cat, 4% — tabby, tabby cat, 2% — tiger cat

Tensorflow Object Detection API


Нейронная сеть MobileNet SSD (Single Shot MultiBox Detector), натренированная в Tensorflow на датасете COCO может не только классифицировать изображение, но и возвращать регионы, правда всего определять она может только 182 класса.
Размер модели 19 МБ, полный код примера.
Оригинал:
f2i64u-oqc9rijpx72cmjnoqkg8.jpeg
Результат:
ka80mhdcd7ewmjn90byg_ppl6ji.png

Подсветка синтаксиса и автодополнение кода


В репозиторий с примерами я также добавил файл phpdoc.php. Благодаря ему Pphstorm подсвечивает синтакис функций, классов и их методов, а также работает автодополнение кода. Этот файл не нужно подключать в свой код (иначе будет ошибка), его достаточно положить в свой проект. Лично мне это упрощает жизнь. В этом файле описано большинство функций opencv, но не все, так что пулреквесты приветствуются.

Установка


Модуль dnn появился в opencv только в версии 3.4 (до этого он был в opencv-contrib).
В ubuntu 18.04 самая последняя версия opencv — 3.2. Сборка opencv из исходников занимает где-то полчаса, поэтому я собрал пакет под ubuntu 18.04 (работает и для 17.10, размер 25МБ), а также собрал пакеты php-opencv для php 7.2 (ubuntu 18.04) и php 7.1 (ubuntu 17.10) (размер 100КБ). Зарегистрировал ppa: php-opencv, но пока не осилил туда заливку и не нашёл ничего лучше, чем просто залить пакеты на гитхаб. Также я создал заявку на создание аккаунта в pecl, но спустя несколько месяцев так и не получил ответа.
Таким образом сейчас установка под ubuntu 18.04 выглядит так:

apt update && apt install -y wget && \
wget https://raw.githubusercontent.com/php-opencv/php-opencv-packages/master/opencv_3.4_amd64.deb && dpkg -i opencv_3.4_amd64.deb && rm opencv_3.4_amd64.deb && \
wget https://raw.githubusercontent.com/php-opencv/php-opencv-packages/master/php-opencv_7.2-3.4_amd64.deb && dpkg -i php-opencv_7.2-3.4_amd64.deb && rm php-opencv_7.2-3.4_amd64.deb && \
echo "extension=opencv.so" > /etc/php/7.2/cli/conf.d/opencv.ini


Установка таким вариантом занимает около 1 минуты. Все варианты установки на ubuntu.
Также я собрал docker-образ размером 168 МБ.

Использование примеров


Скачивание:

git clone github.com/php-opencv/php-opencv-examples.git && cd php-opencv-examples


Запуск:

php detect_face_by_dnn_ssd.php


PS


Прошу всех заинтересованных лиц ответить на опросы после статьи, ну и подписывайтесь, чтобы не пропустить мои следующие статьи, ставьте лайки, чтобы мотивировать меня на их написание и пишите в комментария вопросы, предлагайте варианты для новых экспериментов/статей.
Традиционно предупреждаю, что я не консультирую и не помогаю через личные сообщения хабра и соцсети.
Вы всегда можете задать вопросы, создав Issue на гитхабе (можно на русском).

Ссылки:
php-opencv-examples — все примеры из статьи
php-opencv/php-opencv — мой форк с поддержкой модуля dnn
hihozhou/php-opencv — оригинальный репозиторий, без поддержки модуля dnn (я создал пулреквест, но он пока ещё не был принят).
Перевод статьи на английский язык — я слышал, что англичане и американцы очень терпеливы к тем кто делает ошибки на английском, но мне кажется, что всему есть передел и я пересёк эту черту :) вообщем лайкните, кому не жалко. Тоже самое на реддите.

© Habrahabr.ru