Оптимизация программ на основе OpenCV для встроенной ОС Linux

Оптимизация программ на основе OpenCV для встроенной ОС Linux

Я создаю собственную встроенную ОС Linux для Raspberry PI3 с помощью Buildroot. Эта ОС будет использоваться для обработки нескольких приложений, одно из которых выполняет обнаружение объектов на основе OpenCV (v3.3.0).

Я начал с Raspbian Jessy + Python, но оказалось, что выполнение простого примера занимает много времени. Поэтому я решил разработать собственную ОСРВ с оптимизированными функциями + разработку на C++ вместо Python.

Я думал, что с этими оптимизациями 4 ядра RPI + 1 ГБ ОЗУ справятся с такими приложениями. Проблема в том, что даже с этими вещами самые простые программы Computer Vision занимают много времени.

Сравнение ПК и Raspberry PI3

Это простая программа, которую я написал, чтобы иметь представление о порядке величины времени выполнения каждой части программы.

#include <stdio.h>
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"

#include <time.h>       /* clock_t, clock, CLOCKS_PER_SEC */

using namespace cv;
using namespace std;

int main()
{
    setUseOptimized(true);
    clock_t t_access, t_proc, t_save, t_total;

    // Access time.
    t_access = clock();
    Mat img0 = imread("img0.jpg", IMREAD_COLOR);// takes ~90ms
    t_access = clock() - t_access;

    // Processing time
    t_proc = clock();
    cvtColor(img0, img0, CV_BGR2GRAY); 
    blur(img0, img0, Size(9,9));// takes ~18ms
    t_proc = clock() - t_proc;

    // Saving time
    t_save = clock();
    imwrite("img1.jpg", img0);
    t_save = clock() - t_save;

    t_total = t_access + t_proc + t_save;

    //printf("CLOCKS_PER_SEC = %d\n\n", CLOCKS_PER_SEC);

    printf("(TEST 0) Total execution time\t %d cycles \t= %f ms!\n", t_total,((float)t_total)*1000./CLOCKS_PER_SEC);
    printf("---->> Accessing  in\t %d cycles \t= %f ms.\n", t_access,((float)t_access)*1000./CLOCKS_PER_SEC);
    printf("---->> Processing in\t %d cycles \t= %f ms.\n", t_proc,((float)t_proc)*1000./CLOCKS_PER_SEC);
    printf("---->> Saving     in\t %d cycles \t= %f ms.\n", t_save,((float)t_save)*1000./CLOCKS_PER_SEC);

    return 0;
}

Результаты выполнения на ПК i7 введите описание изображения здесь

Результаты выполнения на Raspberry PI (ОС, сгенерированная из Buildroot) введите описание изображения здесь

Как вы видите, разница огромная. Мне нужно оптимизировать каждую деталь, чтобы этот примеробработкашаг происходит в «почти» реальном времени вза максимальное время 15 мс.

Мой вопрос касается:

  • Как оптимизировать ОС, чтобы она могла обрабатывать ресурсоемкие вычислительные приложения, и как контролировать приоритеты каждой части?
  • Как можно в полной мере использовать 4 ядра RPI3 для выполнения требований?
  • Есть ли другие возможности вместо OpenCV?
  • Стоит ли использовать C вместо C++?
  • Какие улучшения оборудования вы рекомендуете?

решение1

Чтобы:

Как оптимизировать ОС, чтобы она могла обрабатывать ресурсоемкие вычислительные приложения, и как контролировать приоритеты каждой части?

Для общей оптимизации вы не можете сделать многого на стороне ОС, кроме обычных вещей, таких как обеспечение того, чтобы в фоновом режиме работало только то, что вам действительно нужно. На оригинальном Pi вы могли ускорить memmove()и подобные функции, используя LD_PRELOADбиблиотеку под названием 'cofi', которая предоставляла оптимизированные для сборки версии этих функций, но я не уверен, поможет ли это на Pi 3.

Что касается приоритизации, то это действительно то, о чем стоит почитать в man-страницах, но, как правило, вы не сможете сделать это, если не распараллелите процессы (в вашем случае, похоже, очевидным решением будет запускать каждый шаг по мере его завершения и использовать IPC (вероятно, общую память из соображений производительности) для перемещения данных между ними).

Что касается результатов, которые вы привели из вашей тестовой программы, обратите внимание, в частности, что этапы обработки и сохранения примерно в 10 раз медленнее на Pi, в то время как этап доступа всего в 5 раз медленнее, и эти цифры совпадают с грубой оценкой того, что я ожидал бы, сравнивая Pi 3 с обычным ПК, которому меньше года. Процессор в Pi почти наверняка значительно медленнее того, на котором вы запускали тест ПК (а если вы вообще не распараллеливали все, то разрыв увеличивается еще больше, поскольку большинство современных процессоров x86 могут запускать одно ядро ​​при полной нагрузке гораздо быстрее, чем все свои ядра при полной нагрузке), и это будет иметь влияние. ARM ISA также существенно отличается от x86 ISA (ARM, как правило, выполняет меньше операций за такт по сравнению с x86, но обычно не нуждается в столь частом доступе к оперативной памяти и обычно не делает промахи предсказания ветвлений такими дорогими, как x86), поэтому любой код, оптимизированный для того, как GCC организует вещи на ПК, не будет столь же оптимальным на Pi.

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

Как можно в полной мере использовать 4 ядра RPI3 для выполнения требований?

Распараллеливание в вашем собственном коде. Вам просто нужно убедиться, что SMP включен в вашем ядре (и если вы используете официальную конфигурацию от RPi Foundation, так и должно быть), а затем попытаться запустить все параллельно. Я не уверен, насколько OpenCV сам по себе делает для распараллеливания, но вы можете также взглянуть на OpenMP (он предоставляет достаточно простой способ распараллеливания итераций в циклах, которые не являются взаимозависимыми).

Есть ли другие возможности вместо OpenCV?

Возможно, так и есть, но все стандартизировали OpenCV, поэтому я бы рекомендовал использовать его (вам будет проще получить техническую помощь по внедрению чего-либо, поскольку все его используют).

Стоит ли использовать C вместо C++?

Это зависит от того, как вы используете вещи. Хотя писать медленный код на C++ намного проще, чем на C, писать быстрый код на любом из этих языков не сложнее. Многие методы оптимизации довольно похожи в обоих языках (например, предварительное выделение всего при запуске, чтобы вы не вызывали malloc()критические секции, или избегание вызова stat()). В случае C++, однако, избегайте std::stringкак чумы, он вызывает malloc()везде и в результате безумно медленный (я видел преобразования, которые переключаются со std::stringстрок в стиле C, в некоторых случаях повышая производительность более чем на 40%).

Какие улучшения оборудования вы рекомендуете?

Если предположить, что вы пытаетесь снизить стоимость оборудования и ограничены пространством (отсюда и выбор Raspberry Pi), то на самом деле я не могу ничего придумать. Pi (во всех его итерациях) использует SoC, который довольно уникально подходит для работы с компьютерным зрением в этом ценовом диапазоне. Если вы готовы пойти на что-то немного больше и немного дороже, я мог бы порекомендовать плату NVIDIA Jetson (они используют Tegra SoC, в которой есть эквивалентный Quadro графический процессор со 192 ядрами CUDA, так что он, вероятно, может обрабатывать вашу рабочую нагрузку по обработке данных намного быстрее), но заставить Buildroot работать на ней значительно сложнее, чем на Pi.

Изменения в ответ на комментарии:

Распараллеливание на уровне процесса — это не то же самое, что многопоточность, это кардинально разные вещи (самое большое различие в том, как распределяются ресурсы: по умолчанию потоки делят все, а процессы — ничего). В общем, когда задействовано много обработки, вам (обычно) лучше использовать распараллеливание на основе процессов, так как проще писать эффективный код, не беспокоясь о безопасности потоков.

Что касается опций, то две из упомянутых вами могут оказать большое влияние на производительность системы, но обе они в конечном итоге являются компромиссами между пропускной способностью и задержкой. Модель вытеснения управляет тем, как можно перепланировать то, что работает в режиме ядра (например, системные вызовы). Три опции здесь:

  1. No Preemption: Это в значительной степени означает, что все, что работает в режиме ядра, не может быть прервано. Это соответствует поведению SVR4 и 4.4BSD, а также тому, как работают большинство других старых систем UNIX. Это очень хорошо для пропускной способности, но очень плохо для задержки, поэтому обычно используется только на больших серверах с большим количеством ЦП (чем больше ЦП, тем выше вероятность, что на одном из них будет запущено что-то, что может быть вытеснено).
  2. Добровольное прерывание: позволяет каждой функции в ядре определять места, в которых она может быть перепланирована. Это настройка, которую используют большинство дистрибутивов Linux, ориентированных на настольные ПК, поскольку она обеспечивает хороший баланс между пропускной способностью и задержкой.
  3. Полное вытеснение: Это означает, что (почти) любой код в ядре может быть прерван в (почти) любое время. Это полезно для систем, которым требуется очень низкая задержка относительно ввода и внешних событий, например, систем, используемых для работы с мультимедиа в реальном времени. Это абсолютно ужасно для пропускной способности, но вы не можете победить задержку.

Частоту таймера, напротив, объяснить гораздо проще. Она контролирует самый длительный период времени, в течение которого что-то может работать без прерываний, если есть что-то еще, ожидающее запуска. Более высокие значения приводят к более короткому периоду времени (более низкая задержка и более низкая пропускная способность), более низкие значения — к более длительному периоду (более высокая задержка и более высокая пропускная способность). Для общего начала я бы предложил установить модель упреждения на добровольное, а частоту таймера на 300 Гц, а затем начать экспериментировать с изменением частоты таймера в первую очередь (так как это обычно имеет более заметный эффект).

Что касается Movidius NCS, то стоит ли он того или нет, зависит от того, с каким объемом данных вам нужно работать, поскольку пропускная способность будет ограничена USB-подключением (Pi имеет только один контроллер USB 2.0, поэтому вы не только ограничены менее чем десятой частью пропускной способности, на которую рассчитан Movidius, но и должны делить шину как минимум с адаптером Ethernet, что скажется как на вашей задержке, так и на вашей пропускной способности). Если вы делаете только отдельные кадры 1920x1080 с 32-битным цветом на низкой частоте, то он может быть жизнеспособным, но если вам нужно выполнять потоковую обработку того же видео на полной частоте кадров, то вы, вероятно, столкнетесь с проблемами задержки. Если вы все же решите использовать его, убедитесь, что у вас есть активный концентратор для него (в противном случае у вас могут возникнуть проблемы с его попыткой потреблять больше энергии, чем может обеспечить Pi).

Связанный контент