В одной (и даже не в одной, а в нескольких) из предыдущих статей мы разбирали совместную работу камеры High Quality Camera с Raspberry Pi посредством стека libcamera. Так вот сегодня в некотором роде продолжим эту тему и рассмотрим, как собрать приложения libcamera из исходников, и, что не менее важно, разберем пример, чтобы понять, зачем это вообще может быть нужно.
Собирать будем именно libcamera-apps, так как это более востребовано на практике, к этому и переходим. Зафиксируем исходные данные – у меня плата Raspberry Pi 4 Model B 8 Gb и операционная система на данный момент следующая:
Raspberry Pi OS Release date: September 22nd 2022 System: 32-bit Kernel version: 5.15 Debian version: 11 (bullseye)
Это не критично, но чуть другой набор может потребовать незначительных изменений в осуществляемых процессах. А может и не потребовать. Если что, пишите в комментариях – с любыми нюансами разберемся, это не проблема. Все, приступаем.
Сборка libcamera-apps.
Начало традиционное – устанавливаем необходимые пакеты:
sudo apt install -y libcamera-dev libepoxy-dev libjpeg-dev libtiff5-dev sudo apt install -y qtbase5-dev libqt5core5a libqt5gui5 libqt5widgets5 sudo apt install -y cmake libboost-program-options-dev libdrm-dev libexif-dev
Update
В определенный момент в libcamera-apps обновилась система сборки (где-то в районе мая 2023), поэтому добавляю обновленные же команды/инструкции. Соответственно, если используем обновленную версию, дальше по тексту нас интересуют секции с обновленными командами 👍 Итак необходимые пакеты:
sudo apt install -y cmake libboost-program-options-dev libdrm-dev libexif-dev sudo pip3 install ninja meson
Далее клонируем исходники:
mkdir my-libcamera-apps cd my-libcamera-apps git clone https://github.com/raspberrypi/libcamera-apps.git cd libcamera-apps
В результате получили каталог:
Создаем отдельный каталог для сборки, в который и переходим:
mkdir my-build cd my-build
В общем, стандартные, не обременительные шаги 👍 Далее нужно запустить cmake
, скормив ему необходимые флаги. Допустимые опции:
-DENABLE_COMPILE_FLAGS_FOR_TARGET=armv8-neon | Пост-обработка может происходить быстрее на 32-битной ОС (для Raspberry Pi 3 и Raspberry Pi 4) |
-DENABLE_DRM=1 / -DENABLE_DRM=0 | DRM/KMS рендеринг превью |
-DENABLE_X11=1 / -DENABLE_X11=0 | Превью на базе X Windows |
-DENABLE_QT=1 / -DENABLE_QT=0 | Поддержка превью-окна на базе Qt. Обычно не рекомендуется включать |
-DENABLE_OPENCV=1 / -DENABLE_OPENCV=0 | Пост-обработка на базе OpenCV |
-DENABLE_TFLITE=1 / -DENABLE_TFLITE=0 | Включение / отключение TensorFlow Lite пост-обработки |
Собственно, короткий путь – официальная документация предлагает два конкретных варианта, для Raspberry Pi OS:
cmake .. -DENABLE_DRM=1 -DENABLE_X11=1 -DENABLE_QT=1 -DENABLE_OPENCV=0 -DENABLE_TFLITE=0
Для Raspberry Pi OS Lite:
cmake .. -DENABLE_DRM=1 -DENABLE_X11=0 -DENABLE_QT=0 -DENABLE_OPENCV=0 -DENABLE_TFLITE=0
Update
И обновленные варианты, для Raspberry Pi OS:
meson setup build -Denable_libav=true -Denable_drm=true -Denable_egl=true -Denable_qt=true -Denable_opencv=false -Denable_tflite=false
Для Raspberry Pi OS Lite:
meson setup build -Denable_libav=false -Denable_drm=true -Denable_egl=false -Denable_qt=false -Denable_opencv=false -Denable_tflite=false
Первый случай мне отлично подходит, нет поводов им не воспользоваться, само собой, все что указано в таблице выше по-прежнему в силе. Я от случая к случаю использую разные ОС, так что, по итогу, выбираем опции в соответствии с таблицей и своим конкретным случаем, и все будет в порядке.
Приготовления завершены, непосредственно сборка:
make -j4
Update
meson compile -C build
Для предыдущих модификаций Raspberry Pi, среди которых в данном контексте Raspberry Pi 3 и более ранние, используем:
make -j1
Update
meson compile -j1 -C build
Здесь мы не собираем libcamera вручную, нам нужны только libcamera-apps, поэтому существует вероятность несовместимости текущей версии пакета libcamera-dev и самой актуальной версии libcamera-apps, которую мы взяли с github'а. В данном случае самым правильным будет собрать libcamera также руками. Если будет необходимость, добавлю аналогичные инструкции для сборки отдельным постом, пишите в комментарии. В случае ошибок также пишите туда же )
Вновь собранные приложения получаем в libcamera-apps/my-build, как и ожидалось:
Далее официальный манускрипт рекомендует выполнить установку:
sudo make install sudo ldconfig # this is only necessary on the first build
Update
sudo meson install -C build sudo ldconfig # this is only necessary on the first build
Но это такое себе дело, особенно в современных дистрибутивах, в общем, данная операция требует как минимум вдумчивости и осознанности, так что смотрите сами, надо вам это или нет. Моя рекомендация – с этим не спешить, так что этот этап пропускаем.
И на данный момент мы имеем базовую версию libcamera-vid
, которая чаще всего по умолчанию уже установлена в ОС и нашу кастомную версию, собранную руками. Внесем простейшие тестовые изменения в:
my-libcamera-apps/libcamera-apps/apps/libcamera_vid.cpp
Кинем в лог:
LOG(1, "This is my custom libcamera-vid version");
try { LOG(1, "This is my custom libcamera-vid version"); LibcameraEncoder app; VideoOptions *options = app.GetOptions(); if (options->Parse(argc, argv)) { if (options->verbose >= 2) options->Print(); event_loop(app); } } catch (std::exception const &e) { LOG_ERROR("ERROR: *** " << e.what() << " ***"); return -1; }
Пересобираем:
make -j4
Update
meson compile -C build
Переходим в my-build и запускаем кастомную версию:
./libcamera-vid -t 2000
В итоге в консоли видим вывод:
Отлично, проверим работоспособность базовой версии, сохранившей свою первозданность:
libcamera-vid -t 2000
В данном случае вывод в консоль будет иметь первоначальный вид. Все, работоспособность не нарушена, процессы осуществлены успешно. Переходим к парочке конкретных случаев-примеров, как и зачем это может использоваться.
Практический пример использования.
Собственно, даже нет особой необходимости заниматься придумыванием искусственных задач – реальный пример из недавних. Мне нужно разветвить поток с камеры и вывести в два отдельных окна. При всем при этом в одном окне должно быть live-preview, то есть изображение с камеры в реальном времени. Во втором же необходимо выводить то же самое, но с задержкой.
Непосредственно для формирования задержки один из вариантов – использовать команду delay
. Представляет она из себя следующее:
delay introduces a constant delay between its standard input and its standard output. The data from its stdin will be stored until it has been written to stdout.
Лучше и не скажешь, устанавливаем:
sudo apt -y install delay
Ах да, я забыл упомянуть, что параллельно необходимо записывать видео в файл. Ключевой нюанс 🙂 Если бы не это, то можно было бы просто перенаправить поток в stdout
и воспроизвести хоть тем же mpv:
./libcamera-vid -t 0 -o /dev/stdout | delay 2s | mpv /dev/stdin
Аналогичный эффект дает упрощенно-сокращенная запись:
./libcamera-vid -t 0 -o - | delay 2s | mpv -
Величина на практике будет иной, то есть ровно 2 секунды мы таким образом не получим, здесь в случае необходимости нужно провести дополнительную настройку/калибровку. Суть в том, что работает все корректно:
Но(!) Как мы выяснили, параллельно нужно скинуть видео в файл, то есть будем использовать:
./libcamera-vid -t 0 -o test_video.h264
А для решения полной задачи отредактируем libcamera-vid
так, чтобы поток выводился в stdout
всегда, вне зависимости от аргументов. Досконально описывать не буду, в случае чего, пишите в комментарии или на форуме, по мере возможности я всегда стараюсь помочь. В область нашего интереса, в первую очередь, попадает класс FileOutput
:
- my-libcamera-apps\libcamera-apps\output\file_output.cpp
- my-libcamera-apps\libcamera-apps\output\file_output.hpp
По умолчанию поток отправляется в файл:
FILE *fp_;
Добавляем свой:
FILE *fp_stdout;
В конструкторе:
fp_stdout = stdout;
Все максимально просто и логично 👍 Основную работу произведем в функции:
void FileOutput::outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags)
Она вызывается после каждого фрейма – то что надо:
void FileOutput::outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags) { if (fp_stdout && size) { if (fwrite(mem, size, 1, fp_stdout) != 1) throw std::runtime_error("failed to write output bytes to stdout"); if (options_->flush) fflush(fp_stdout); } // We need to open a new file if we're in "segment" mode and our segment is full // (though we have to wait for the next I frame), or if we're in "split" mode // and recording is being restarted (this is necessarily an I-frame already). if (fp_ == nullptr || (options_->segment && (flags & FLAG_KEYFRAME) && timestamp_us / 1000 - file_start_time_ms_ > options_->segment) || (options_->split && (flags & FLAG_RESTART))) { closeFile(); openFile(timestamp_us); } LOG(2, "FileOutput: output buffer " << mem << " size " << size); if (fp_ && size) { if (fwrite(mem, size, 1, fp_) != 1) throw std::runtime_error("failed to write output bytes"); if (options_->flush) fflush(fp_); } }
Ничего лишнего, только необходимое. Пробуем собрать и запустить, сборка по-прежнему командой:
make -j4
И запуск:
./libcamera-vid -t 0 -o test_video.h264 | delay 2s | mpv -
Результат:
Иначе как успехом не назовешь, все в соответствии с ТЗ:
- Превью в реальном времени
- Отложенное видео в отдельном окне
- Параллельно все пишется в файл
Таким образом, внеся незначительные изменения и проведя модификации, получили решение конкретной имеющейся задачи. Еще один пример, пожалуй, приведу в отдельной статье, чтобы не перегружать объемом эту, так что оставайтесь на связи!
/* SPDX-License-Identifier: BSD-2-Clause */ /* * Copyright (C) 2020, Raspberry Pi (Trading) Ltd. * * file_output.cpp - Write output to file. */ #include "file_output.hpp" FileOutput::FileOutput(VideoOptions const *options) : Output(options), fp_(nullptr), count_(0), file_start_time_ms_(0) { fp_stdout = stdout; } FileOutput::~FileOutput() { closeFile(); } void FileOutput::outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags) { if (fp_stdout && size) { if (fwrite(mem, size, 1, fp_stdout) != 1) throw std::runtime_error("failed to write output bytes to stdout"); if (options_->flush) fflush(fp_stdout); } // We need to open a new file if we're in "segment" mode and our segment is full // (though we have to wait for the next I frame), or if we're in "split" mode // and recording is being restarted (this is necessarily an I-frame already). if (fp_ == nullptr || (options_->segment && (flags & FLAG_KEYFRAME) && timestamp_us / 1000 - file_start_time_ms_ > options_->segment) || (options_->split && (flags & FLAG_RESTART))) { closeFile(); openFile(timestamp_us); } LOG(2, "FileOutput: output buffer " << mem << " size " << size); if (fp_ && size) { if (fwrite(mem, size, 1, fp_) != 1) throw std::runtime_error("failed to write output bytes"); if (options_->flush) fflush(fp_); } } void FileOutput::openFile(int64_t timestamp_us) { if (options_->output == "-") fp_ = stdout; else if (!options_->output.empty()) { // Generate the next output file name. char filename[256]; int n = snprintf(filename, sizeof(filename), options_->output.c_str(), count_); count_++; if (options_->wrap) count_ = count_ % options_->wrap; if (n < 0) throw std::runtime_error("failed to generate filename"); fp_ = fopen(filename, "w"); if (!fp_) throw std::runtime_error("failed to open output file " + std::string(filename)); LOG(2, "FileOutput: opened output file " << filename); file_start_time_ms_ = timestamp_us / 1000; } } void FileOutput::closeFile() { if (fp_ && fp_ != stdout) fclose(fp_); fp_ = nullptr; }
/* SPDX-License-Identifier: BSD-2-Clause */ /* * Copyright (C) 2020, Raspberry Pi (Trading) Ltd. * * file_output.hpp - Write output to file. */ #pragma once #include "output.hpp" class FileOutput : public Output { public: FileOutput(VideoOptions const *options); ~FileOutput(); protected: void outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags) override; private: void openFile(int64_t timestamp_us); void closeFile(); FILE *fp_; FILE *fp_stdout; unsigned int count_; int64_t file_start_time_ms_; };
Добрый день! В статье видимо пропущены некоторые шаги. При попытке сделать "cmake .. ". Пишет что нет CMakeList.txt и папка сборки действительна пуста. И даже если make делать в папке ~/my-libcamerf-apps в которой файлы есть сборка не начинается.
Подскажите что сделать?
Приветствую! Пробежался по тексту - файлы должны быть в libcamera-apps, в ней подпапка build, из которой делаем cmake .. args, попадая таким образом на уровень выше.
Очередной день Сурка... Ничего не изменилось, ошибка та же. Под папки build нет совсем, есть my-build, она пуста. Есть папка apps, там какие то файлы. Но сборка по прежнему не происходит
Ну да, my-build, ее сами создаем, она пустая перед сборкой. На уровень выше должны быть файлы с репозитория (в папке libcamera-apps).
Вот что там в наличии
Понятно, там систему сборки изменили в репозитории, пробуй так:
Не сильно помогло, сборка началась, но посыпалось множество ошибок и сборка завершилась крахом.
Какие именно ошибки?
И так далее ещё на страницу
Изменится что-то?
Нет не изменило. Пакеты эти я раньше ставил, согласно данной статье (и другим урокам). Было установлено ноль пакетов и ноль обновлено.
Больше всего похоже на то, что apt-install ставит не самую актуальную по коду версию libcamera, которая не соответствует версии libcamera-apps, потому что, например вот:
Проще и правильнее всего будет libcamera тоже руками собрать из исходников.
Спасибо за помощь. Победил! Просто снёс 64 битную версию, поставил на 32 бита - и сразу из коробки камера заработала! Просто в raspbery-config включил. И дал команду покажи картинку! 🙂
Стоп ) Так из коробки так и так должно все работать, без сборки руками, можно просто пакеты поставить сразу ) Собрать-то удалось в итоге или получается и не требовалось?
Да, пакеты поставил и да всё работает, осталось только на Qt прикрутить свой интерфейс, это уже проще 🙂
👍🙂