English

Распознание QR кодов на потоковом видео — Этап 2

Заказчик

Группа компаний «ТЕХНОНИКОЛЬ»

Корпорация Технониколь – ведущий международный производитель надежных и эффективных строительных материалов и систем.

В прошлый раз мы писали про распознавание QR в потоковом видео. C того момента мы сделали ещё несколько доработок. В данной статье мы немного приоткроем завесу тайн над данным проектом и расскажем вам что изменилось с прошлого раза.

Обновление логики работы

Если начинать рассказывать, то стоит сразу писать о самом главном – об обновлённом функционале приложения.

Пойдём по порядку и упомянем что все важные параметры были вынесены в файл конфигурации – как новые, так и старые.

Среди важных нововведений – была добавлена обрезка кадра. Теперь обработка производиться не всего кадра, а только заданной его части. Границы можно настроить через файл конфигурации, так как этот параметр сильно зависит от положения установленной камеры.

Ранее, мы, наверное, не упоминали, но для каждого кадра было необходимо производить операцию выравнивания гистограммы, но из данной реализации мы её убрали. В основном это сделано из-за того, что после обрезки кадра QR попадает в хорошо освещённую область и в выравнивании гистограммы просто нет смысла, да и к тому же она только ухудшает точность распознавания.

Но это мелкие изменения, а самое основное – реализация цепочки алгоритмов распознавания. Алгоритмы в цепочке выстроены от самого быстрого к более медленным и представлены на рисунке.

Давайте пойдём по порядку и рассмотрим все этапы обработки кадра. С самого начала мы передаём кадр библиотеке Quirc, которая работает очень быстро и имеет довольно высокую точность распознавания. Если же она не справляется с распознаванием QR кода, то от неё мы получаем координаты местоположения QR кода и переходим к этапу обработки малого кадра.

Этап обработки малого кадра заключается в вырезании QR кода из кадра и применение операции масштабирования для небольшого уменьшения размера изображения. Практика показала, что QR код хуже распознаётся на кадре в разрешении FHD, чем на кадре, который меньше в два раза. Так мы получаем прирост количества распознанных QR кодов на пару процентов. Конечно, операция масштабирования является довольно специфичной и не везде срабатывает достаточно хорошо, и поэтому её настройку также вынесли в файл конфигурации.

Далее к кадру применяются аффинные преобразования, и он прогоняется через алгоритмы из блока малого кадра: Quirc, zbar, WeChat QR.

Обычно на данном этапе QR код успешно распознаётся одним из алгоритмов, и работа по распознаванию заканчивается. Так как по статистике около 95% кадров легко распознаются на первых двух алгоритмах, но, если и тут не успех до в бой идёт тяжёлая артиллерия – блок полный кадр.

Этап полного кадра состоит в основном из библиотеки WeChat QR и двух алгоритмов из OpenCV. Вообще WeChat QR тоже располагается в библиотеке OpenCV, но она основана на нейросетевых методах и поэтому её стоит выделить отдельно. На данном этапе кадр редко доходит до последнего алгоритма (OpenCV decodeCurved), а если и доходит, то QR скорее всего очень сильно искажён, и его обработка последним методом займёт довольно долго ~250 мс.

Кроме этих алгоритмов заказчик ещё захотел специфичную работу с очередью кадров. Одна из главных настроек – троттлинг очереди. Если в очереди скопилось больше, чем 50 кадров, то начинаем помещать в очередь каждый третий кадр до того момента, пока очередь не разберётся обработчиками. В случае если очередь вырастает больше некоторого порогового значения (также настраивается из файла конфигурации), то мы перестаём вообще добавлять новые кадры.

Реализовав всё это мы значительно повысили точность распознавания QR кодов в потоковом видео, но вообще не всё было так радужно и далее вы узнаете почему.

OpenCV всему голова

Думаю, при разработке довольно сложного приложения на C++, а в особенности работающего в многопоточном режиме, невозможно уследить за всем. И тут у нас не обошлось без segfault’ов. Но самое странное что они были не на стороне нашего приложения, а в библиотеке OpenCV.

И тут начинается наша детективная история по поиску багов в OpenCV.

Все началось с анализа аварийного дампа, который вывел нас на функцию divideIntoEvenSegments, а точнее на вот этот блок кода:

float temp_dist = distancePointToLine(*it, segment_start, segment_end);
if (temp_dist > max_dist_to_line)
{
max_dist_to_line = temp_dist;
}

Первая проблема – функция distancePointToLine возвращает NaN. Зайдя в данную функцию мы поняли что происходит деление на ноль, так как там реализовано некорректное сравнение float с нулём.

float QRDecode::distancePointToLine(Point2f a, Point2f b , Point2f c)
{
    float A, B, C, result;
    A = c.y - b.y;
    B = c.x - b.x;
    C = c.x * b.y - b.x * c.y;
    float dist = sqrt(A*A + B*B);
    if (dist == 0) return 0;
    result = abs((A * a.x - B * a.y + C)) / dist;
    return result;
}

Исправив данный момент, мы не исправили главную проблему – приложение всё ещё падало. Продолжили дальше искать проблему и натолкнулись на то, что функция createSpline возвращает вот такие линии

[-nan, 177], [-nan, 178], [-nan, 179], [-nan, 180], [-nan, 181], [-nan, 182], [-nan, 183], [-nan, 184], [-nan, 185], [-nan, 186], [-nan, 187], [-nan, 188], [-nan, 189], [-nan, 190], [-nan, 191], [-nan, 192], [-nan, 193], [-nan, 194], [165, 177],
...

Погрузившись глубже в код мы определили, как нам кажется, проблемное место. В случае когда y_arr[i + 1] и y_arr[i] очень близки (или равны), то мы получаем NaN из-за последующего деления на h[i].

for (int i = 0; i < n - 1; i++)
{
    h[i] = static_cast(y_arr[i + 1] - y_arr[i]);
}
for (int i = 1; i < n - 1; i++)
{
    alpha[i] = 3 / h[i] * (a[i + 1] - a[i]) - 3 / (h[i - 1]) * (a[i] - a[i - 1]);
}

На данный момент мы уже обрадовались, так как приложение больше не падало… так мы думали… какими же мы были наивными.

В процессе подготовки контейнера с приложением решили, на всякий случай, проверить работу приложения в контейнере и не прогадали. Тут нас любезно встретил наш друг segfault. Самое интересное что вне контейнера приложение работает без проблем, а в нём падает.

Пришлось уже изворачиваться по полной и снимать аварийный дамп внутри контейнера. После просмотра дампа определили что проблема в функции straightenQRCodeInParts.

#0  0x00007f8cbc799f45 in cv::QRDecode::straightenQRCodeInParts() () from /lib/x86_64-linux-gnu/libopencv_objdetect.so.406

После долго ковыряния в коде (читай как скрупулёзный анализ) дошли вот до такого кода:

for (int i = 0; i < NUM_SIDES; i++)
{
    Point2f temp_point_start = segments_points[i].front();
    Point2f temp_point_end   = segments_points[i].back();

Вроде код безобидный – просто получения первого и последнего элемента, но самое интересное происходит, когда segments_points или segments_points[i] не содержат элементов.

Добавив проверку, мы наконец победили злосчастный segfault!

Если вам интересно на какой версии OpenCV всё это было проделано, то это 4.6.0. Для любознательных – вот вам хэш коммита b0dc474160e389b9c9045da5db49d03ae17c6a6b.

Контейнеризация

Так как мы теперь используем пропатченную версию OpenCV, то встал вопрос о том, как предоставить приложение заказчику. Недолго думаю мы сошлись на использовании docker.

Данный этап был намного проще, чем прошлые два. Даже описывать почти нечего, но всё равно нужно сказать пару слов.

Взяли за основу контейнер ubuntu:latest, добавили пропатченные библиотеки OpenCV и через apt-get поставили все остальные зависимости, и конечно положили в контейнер само приложение с прописанным entrypoint.

Осталось только написать файл сервиса для systemd, чтобы было удобно запускать контейнер с нужными флагами (проброс файла конфигурации в контейнер, каталог с логами, доступ к сети), и можно полноценно использовать приложение.

Отзыв клиента

В июле 2019 года компания ООО «Аэро-Трейд» искала подрядчика для выполнения работ по внедрению системы безналичной оплаты товаров самолетах авиакомпании «AZUR air». После тщательного изучения рынка, мы решили обратиться для реализации данного проекта в компанию ООО «МСТ Компани». Основной задачей было обеспечить каждый борт авиакомпании «AZUR air» терминалом, который сможет принимать платежи не только на земле, но и во время полёта.

В течении всего времени нашего сотрудничества, специалисты ООО «МСТ Компани» продемонстрировали отличные профессиональные навыки при подготовке проекта, и разработке документации. В результате мы получили гибкое и надёжное решение, которое удовлетворяет нашим требованиям.

По итогам работы с компанией ООО «МСТ Компани» хочется отметить соблюдение принципов делового партнерства, а также четкое соблюдение сроков работ и выполнение взятых на себя обязательств. ООО «Аэро-Трейд» выражает благодарность специалистам компании за проделанную работу в рамках внедрения системы безналичной оплаты на самолетах авиакомпании «AZUR air». И рекомендует компанию ООО «МСТ Компани» как надёжного партнёра в области платёжных решений.