Компьютерное зрение и машинное обучение в PHP используя библиотеку opencv
Всем привет. Это моя юбилейная статья на хабре. За почти 7 лет я написал 10 статей (включая эту), 8 из них — технические. Общее количество просмотров всех статей — около полумиллиона.
Основной вклад я внёс в два хаба: PHP и Серверное администрирование. Мне нравится работать на стыке этих двух областей, но сфера моих интересов гораздо шире.
Как и многие разработчики я часто пользуюсь результатами чужого труда (статьи на хабре, код на гитхабе, …), поэтому я всегда рад делиться с сообществом своими результатами в ответ. Написание статей — это не только возврат долга сообществу, но так же позваляет найти единомышленников, получить комментарии от профессионалов в узкой сфере и ещё больше углубить свои знания в исследуемой области.
Собственно эта статья об одном из таких моментов. В ней я опишу чем занимался почти всё своё свободное время за последние полгода. Кроме тех моментов, когда я ходил купаться в море через дорогу, смотрел сериалы или игрался в игры.
Сейчас очень сильно развивается «Машинное обучение», по нему написано уже очень много статей, в том числе на хабре и практически каждый разработчик хотел бы взять и начать его использовать в своих рабочих задачах и домашних проектах, но с чего начать и к чему применять не всегда понятно. Большинство статей для начинающих предлагают кучу литературы, на прочтение которой не хватит и жизни, англоязычные курсы (а не все из нас могут усваивать материал на английском так же эффективно как и на русском), «недорогие» русскоязычные курсы и т.д.
Регулярно выходят новые статьи, в которых описаны новые подходы к решению той или иной задачи. На гитхабе можно найти реализацию описанного в статьях подхода. В качестве языков программирования чаще используются: c / c++, python 2/3, lua и matlab, а в качестве фремворков: caffe, tensorflow, torch. Каждый пишет — кто на чём горазд. Большая сегментация по языкам программирования и фреймворкам сильно усложняет процедуру поиска того, что тебе нужно и интеграцию этого в проект. К тому же в последнее время очень много исходного кода с комментариями на китайском языке.
Чтобы как-то уменьшить весь этот хаос в opencv добавили модуль dnn, который позволяет использовать модели, натренированные в основных фреймворках. Я со своей стороны покажу как этот модуль можно использовать из php.
Jeremy Howard (создатель бесплатного практического курса «машинное обучение для кодеров») считает, что сейчас есть большой порог между изучением машинного обучения и применении его на практике.
Howard говорит, что для начала изучения машинного обучения достаточно одного года опыта программирования. Я с ним полностью согласен и надеюсь, что моя статья поможет снизить порог вхождения в opencv для php-разработчиков, которые мало знакомы с машинным обучением и ещё не уверены хотят ли они вообще этим заниматься или нет, а также постараюсь описать все моменты, на которые я тратил часы и дни, чтобы у вас на это уходило не больше минуты.
Итак, что же я сделал кроме логотипа?
(надеюсь, что opencv не засудит меня за плагиат)
Я рассматривал возможность написать модуль php-opencv самостоятельно с помощью SWIG и потратил на это кучу времени, но так ничего и не добился. Всё осложнялось тем, что я не знал с/с++ и не писал расширений под php 7. К сожалению большинство материалов в интернете по php-расширениям было написано для php 5, поэтому приходилось собирать информацию по крупицам, а возникающие проблемы решать самостоятельно.
Потом я нашёл на просторах гитхаба библиотеку php-opencv, она представляет из себя модуль для php7, который делает вызовы методов opencv. Чтобы скомпилировать, установить и запустить примеры у меня ушло несколько вечеров. Я начал пробовать различные возможности этого модуля, но мне не хватало некоторых методов, я их добавил самостоятельно, создал пулреквест, а автор библиотеки принял. Позже я добавил ещё больше функций.
Возможно читатель на этом моменте задаст себе вопрос: зачем вообще автору нужны были такие проблемы, почему было просто не начать использовать 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);
полный код примера
Результат:
Как видно из примера, не составляет проблем найти лицо даже на фото в гриме зомби. Очки также не мешают нахождению лица.
Распознавание (узнавание) лиц на фото
Для этого в 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 (уверенность)
полный код примера
Наборы лиц:
Результат:
Когда я начинал работать с LBPHFaceRecognizer, у него не было возможности сохранения/загрузки/дообучения готовой модели. Собственно первый мой пулреквест добавил эти методы: write/read/update.
Нахождение меток на лицах
Когда я начинал знакомиться с opencv, то часто натыкался на фотографии лиц, на которых точками отмечены глаза, нос, губы и т.д. Мне хотелось повторить этот эксперимент самостоятельно, но в версии opencv для питона этого не реализовали. У меня ушёл вечер, чтобы добавить поддержку FacemarkLBF на php и отправить второй пулреквест. Всё работает просто, загружаем предобученную модель, подаём на вход массив лиц, получаем массив точек для каждого лица.
$facemark = FacemarkLBF::create();
$facemark->loadModel('models/opencv-facemark-lbf/lbfmodel.yaml');
$facemark->fit($src, $faces, $landmarks);
полный код примера
Результат:
Как видно из примера, грим зомби может ухудшить нахождение опорных точек на лице. Очки также могут помешать нахождению лица. Засветка тоже влияет. При этом посторонние предметы во рту (клубника, сигарета и т.д.) могут и не мешать.
После моего первого пулреквеста я вдохновился и стал смотреть, что можно сделать ещё с помощью 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 МБ, полный код примера.
Результат:
Как видно из примера, нейронная сеть не всегда выдаёт хорошие результаты при использовании её «в лоб». Не было найдено четвёртое лицо, при этом если четвёртое фото вырезать и отправить в сеть отдельно, то лицо будет найдено.
Улучшение качества изображения с помощью нейронной сети
Я уже давно слышал про библиотеку waifu2x, которая позволяет устранять шум и увеличивать размеры иконок/фото. Сама библиотека написана на lua, а под капотом использует несколько моделей (для увеличения иконок, устранения шума фото и т.д.) натренированных в torch. Автор библиотеки экспортировал эти модели в caffe и помог мне использовать их из opencv. В результате чего был написан пример на php для увеличения разрешения иконок.
Размер модели 2 МБ, полный код примера.
Оригинал:
Результат:
Увеличение картинки без использования нейронной сети:
Классификация изображений
Нейронная сеть MobileNet, обученная на наборе данных ImageNet позволяет классифицировать изображение. Всего она может определять 1000 классов, что по-моему достаточно не мало.
Размер модели 16 МБ, полный код примера.
Оригинал:
Результат:
87% — Egyptian cat, 4% — tabby, tabby cat, 2% — tiger cat
Tensorflow Object Detection API
Нейронная сеть MobileNet SSD (Single Shot MultiBox Detector), натренированная в Tensorflow на датасете COCO может не только классифицировать изображение, но и возвращать регионы, правда всего определять она может только 182 класса.
Размер модели 19 МБ, полный код примера.
Оригинал:
Результат:
Подсветка синтаксиса и автодополнение кода
В репозиторий с примерами я также добавил файл 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 (я создал пулреквест, но он пока ещё не был принят).
Перевод статьи на английский язык — я слышал, что англичане и американцы очень терпеливы к тем кто делает ошибки на английском, но мне кажется, что всему есть передел и я пересёк эту черту :) вообщем лайкните, кому не жалко. Тоже самое на реддите.