OFX, он же OFX Image Processing API , представляет собой открытый стандарт для создания 2D-визуальных эффектов и видеокомпозиции. Он работает по модели разработки приложений, подобной плагинам. По сути, он выполняет функции как Хоста — приложения, предоставляющего набор методов, так и Плагина — приложения или модуля, реализующего этот набор.
Эта конфигурация предлагает возможность неограниченного расширения функциональности хост-приложения.
Такие приложения, как Final Cut X и DaVinci Resolve Studio, начиная с версии 16, полностью поддерживают конвейеры Apple Metal. Подобно OpenCL и Cuda, в случае OFX вы можете получить дескриптор или обработчик очереди команд для конкретной платформы. Хост-система также берет на себя ответственность за выделение пула таких очередей и балансировку вычислений по ним.
Более того, он помещает исходные и целевые данные клипов изображений в память графического процессора, что значительно упрощает разработку расширяемых функций.
С Resolve дела обстоят немного сложнее. DaVinci объявляет о поддержке OFX v1.4, хотя и с некоторыми ограничениями. В частности, некоторые методы работы с функциями интерфейса недоступны для использования. Чтобы определить, какой метод доступен, OFX позволяет вам проверить поддерживаемый пакет с помощью запросов «ключ-значение».
Методы публикации в коде плагина основаны на вызовах C. Но мы будем использовать оболочку OpenFXS C++, адаптированную для C++17. Для удобства я собрал всё в один репозиторий: dehancer-external, взятый из открытого проекта Dehancer .
В этом проекте я буду использовать OpenFXS, расширение C++ для OpenFX, которое изначально было написано Бруно Николетти и со временем стало популярным в коммерческих проектах и проектах обработки видео с открытым исходным кодом.
Исходный OpenFXS не был адаптирован к современным диалектам C++, поэтому я обновил его, чтобы сделать совместимым с C++17 .
OFX и, следовательно, OFXS — это автономный программный модуль, динамически загружаемый хост-программой. По сути, это динамическая библиотека, которая загружается при запуске основного приложения. OpenFXS, как и OFX, должен публиковать сигнатуры методов. Следовательно, мы используем один метод C из кода.
Чтобы начать разработку в OpenFXS, вам необходимо согласиться с несколькими общими наборами классов, которые используются для создания новых функций в вашем приложении. Обычно в новом проекте вам необходимо наследовать эти классы и реализовать или переопределить некоторые виртуальные методы.
Чтобы создать собственный плагин в хост-системе, давайте начнем с ознакомления со следующими общедоступными классами и тем же методом:
Одной особенностью, отличающей процесс видеосъемки от простого захвата изображения на фото, является динамическая смена сцен и освещение как сцен в целом, так и участков на изображении. Это определяет способ управления экспозицией в процессе съемки.
В цифровом видео для операторов существует режим контрольного монитора, в котором уровень экспозиции зон отображается в ограниченном наборе зон, каждая из которых окрашена в свой цвет.
Этот режим иногда называют «хищником» или режимом ложного цвета. Шкалы обычно относятся к шкале IRE.
Такой монитор позволяет видеть зоны экспозиции и избежать существенных ошибок при настройке параметров съемки камеры. Нечто похожее по смыслу используется при экспонировании в фотографии – зонирование по Адамсу, например.
Можно измерить экспонометром конкретную цель и посмотреть, в какой зоне она находится, причем в реальном времени мы видим зоны, аккуратно тонированные для удобства восприятия.
Количество зон определяется задачами и возможностями монитора управления. Например, монитор, используемый с камерами Arri Alexa , может включать до 6 зон.
Прежде чем приступить к примеру, нам нужно добавить несколько простых прокси-классов для реализации OpenFXS в качестве платформы для обработки исходных данных, таких как текстуры металла. Эти классы включают в себя:
Metalling::Image2Texture : Функтор для передачи данных из буфера клипа в текстуру металла. Из DaVinci можно извлечь в плагин буфер любой структуры и упаковки значений каналов изображения, и он должен быть возвращен в аналогичном виде.
Чтобы упростить работу с форматом потока в OFX, вы можете попросить хост заранее подготовить данные определенного типа. Я буду использовать поплавки, упакованные в RGBA — красный/зеленый/синий/альфа.
Мы наследуем базовые классы OFXS и пишем свою функциональность, не вдаваясь в подробности работы ядра Metal:
Кроме того, нам понадобится несколько служебных классов, созданных поверх Metal, чтобы логически разделить код хоста и код ядра на MSL. К ним относятся:
Metalling::FalseColorKernel : Наш основной функциональный класс, эмулятор «хищника», который постеризует (понижает разрешение) до заданного количества цветов.
Код ядра для режима «хищник» может выглядеть так:
static constant float3 kIMP_Y_YUV_factor = {0.2125, 0.7154, 0.0721}; constexpr sampler baseSampler(address::clamp_to_edge, filter::linear, coord::normalized); inline float when_eq(float x, float y) { return 1.0 - abs(sign(x - y)); } static inline float4 sampledColor( texture2d<float, access::sample> inTexture, texture2d<float, access::write> outTexture, uint2 gid ){ float w = outTexture.get_width(); return mix(inTexture.sample(baseSampler, float2(gid) * float2(1.0/(w-1.0), 1.0/float(outTexture.get_height()-1))), inTexture.read(gid), when_eq(inTexture.get_width(), w) // whe equal read exact texture color ); } kernel void kernel_falseColor( texture2d<float, access::sample> inTexture [[texture(0)]], texture2d<float, access::write> outTexture [[texture(1)]], device float3* color_map [[ buffer(0) ]], constant uint& level [[ buffer(1) ]], uint2 gid [[thread_position_in_grid]]) { float4 inColor = sampledColor(inTexture,outTexture,gid); float luminance = dot(inColor.rgb, kIMP_Y_YUV_factor); uint index = clamp(uint(luminance*(level-1)),uint(0),uint(level-1)); float4 color = float4(1); if (index<level) color.rgb = color_map[index]; outTexture.write(color,gid); }
Инициализация плагина OFX
Начнем с определения класса imetalling::falsecolor::Factory.
В этом классе мы зададим единственный параметр — состояние монитора (включен или выключен). Это необходимо для нашего примера.
Мы унаследуем от OFX::PluginFactoryHelper
и перегрузим пять методов:
ImageEffectDescriptor
.
описатьInContext(ImageEffectDescriptor&,ContextEnum) : Подобно методу describe
, этот метод также вызывается при загрузке плагина и должен быть определен в нашем классе. Он должен определять свойства, связанные с текущим контекстом.
Контекст определяет тип операций, с которыми работает приложение, например фильтр, рисование, эффект перехода или таймер повтора кадра в клипе.
ImageEffect
. Другими словами, наш imetalling::falsecolor::Plugin
, в котором мы определили все функциональные возможности, как в отношении пользовательских событий в хост-программе, так и в отношении рендеринга (преобразования) исходного кадра в целевой: OFX::ImageEffect *Factory::createInstance(OfxImageEffectHandle handle,OFX::ContextEnum) { return new Plugin(handle); }
На этом этапе, если вы скомпилируете связку с модулем OFX, плагин уже будет доступен в хост-приложении, а в DaVinci его можно будет загрузить на узел коррекции.
Однако для полноценной работы с экземпляром плагина необходимо определить как минимум интерактивную часть и часть, связанную с обработкой входящего видеопотока.
Для этого мы наследуем класс OFX::ImageEffect и перегружаем виртуальные методы:
измененоParam(const OFX::InstanceChangedArgs&, const std::string&) — этот метод позволяет нам определить логику обработки события. Тип события определяется значением OFX::InstanceChangedArgs::reason и может быть: eChangeUserEdit, eChangePluginEdit, eChangeTime — событие произошло в результате редактирования свойства пользователем, изменения в плагине или хост-приложении или в результате изменения временной шкалы.
Второй параметр указывает имя строки, которое мы определили на этапе инициализации плагина, в нашем случае это один параметр: false_color_enabled_check_box .
Реализацию интерактивного взаимодействия с OFX вы можете прочитать в коде Interaction.cpp . Как видите, мы получаем указатели на клипы: исходный и область памяти, в которую мы поместим целевое преобразование.
Добавим еще один логический слой, на котором определим всю логику запуска трансформации. В нашем случае это пока единственный метод переопределения:
На этапе запуска нам стал доступен объект с полезными свойствами: у нас есть как минимум указатель на видеопоток (точнее, область памяти с данными изображения кадра), и, самое главное, очередь команд Metal.
Теперь мы можем создать универсальный класс, который приблизит нас к простой форме повторного использования кода ядра. В расширении OpenFXS уже есть такой класс: OFX::ImageProcessor; нам просто нужно его перегрузить.
В конструкторе у него есть параметр OFX::ImageEffect, т.е. в нем мы получим не только текущее состояние параметров плагина, но и все необходимое для работы с графическим процессором.
На этом этапе нам просто нужно перегрузить методprocessImagesMetal() и инициировать обработку уже реализованных на Metal ядер.
Processor::Processor( OFX::ImageEffect *instance, OFX::Clip *source, OFX::Clip *destination, const OFX::RenderArguments &args, bool enabled ) : OFX::ImageProcessor(*instance), enabled_(enabled), interaction_(instance), wait_command_queue_(false), /// grab the current frame of a clip from OFX host memory source_(source->fetchImage(args.time)), /// create a target frame of a clip with the memory area already specified in OFX destination_(destination->fetchImage(args.time)), source_container_(nullptr), destination_container_(nullptr) { /// Set OFX rendering arguments to GPU setGPURenderArgs(args); /// Set render window setRenderWindow(args.renderWindow); /// Place source frame data in Metal texture source_container_ = std::make_unique<imetalling::Image2Texture>(_pMetalCmdQ, source_); /// Create empty target frame texture in Metal destination_container_ = std::make_unique<imetalling::Image2Texture>(_pMetalCmdQ, destination_); /// Get parameters for packing data in the memory area of the target frame OFX::BitDepthEnum dstBitDepth = destination->getPixelDepth(); OFX::PixelComponentEnum dstComponents = destination->getPixelComponents(); /// and original OFX::BitDepthEnum srcBitDepth = source->getPixelDepth(); OFX::PixelComponentEnum srcComponents = source->getPixelComponents(); /// show a message to the host system that something went wrong /// and cancel rendering of the current frame if ((srcBitDepth != dstBitDepth) || (srcComponents != dstComponents)) { OFX::throwSuiteStatusException(kOfxStatErrValue); } /// set in the current processor context a pointer to the memory area of the target frame setDstImg(destination_.get_ofx_image()); } void Processor::processImagesMetal() { try { if (enabled_) FalseColorKernel(_pMetalCmdQ, source_container_->get_texture(), destination_container_->get_texture()).process(); else PassKernel(_pMetalCmdQ, source_container_->get_texture(), destination_container_->get_texture()).process(); ImageFromTexture(_pMetalCmdQ, destination_, destination_container_->get_texture(), wait_command_queue_); } catch (std::exception &e) { interaction_->sendMessage(OFX::Message::eMessageError, "#message0", e.what()); } }
Для сборки проекта вам понадобится CMake версии не ниже 3.15. Дополнительно вам потребуется Qt5.13, который поможет легко и удобно собрать комплект с установщиком плагина в системном каталоге. Чтобы запустить cmake, вы должны сначала создать каталог сборки.
После создания каталога сборки вы можете выполнить следующую команду:
cmake -DPRINT_DEBUG=ON -DQT_INSTALLER_PREFIX=/Users/<user>/Develop/QtInstaller -DCMAKE_PREFIX_PATH=/Users/<user>/Develop/Qt/5.13.0/clang_64/lib/cmake -DPLUGIN_INSTALLER_DIR=/Users/<user>/Desktop -DCMAKE_INSTALL_PREFIX=/Library/OFX/Plugins .. && make install
После этого установщик с именем IMFalseColorOfxInstaller.app появится в каталоге, указанном вами в параметре PLUGIN_INSTALLER_DIR . Давайте продолжим и запустим его! После успешной установки вы можете запустить DaVinci Resolve и начать использовать наш новый плагин.
Вы можете найти и выбрать его на панели OpenFX на странице цветокоррекции и добавить в качестве узла.
Внешние ссылки