paint-brush
开发您自己的 Apple Metal 插件并将其集成到 DaVinci Resolve 中的简单方法经过@denissvinarchuk
3,614 讀數
3,614 讀數

开发您自己的 Apple Metal 插件并将其集成到 DaVinci Resolve 中的简单方法

经过 Denis Svinarchuk13m2024/03/13
Read on Terminal Reader

太長; 讀書

OFX,又名 OFX 图像处理 API,是创建 2D 视觉效果和视频合成的开放标准。它以类似插件的应用程序开发模型运行。本质上,它既充当主机(提供一组方法的应用程序),又充当插件(实现这组方法的应用程序或模块)。 这种配置提供了无限扩展主机应用程序功能的潜力。
featured image - 开发您自己的 Apple Metal 插件并将其集成到 DaVinci Resolve 中的简单方法
Denis Svinarchuk HackerNoon profile picture

OFX,又名OFX 图像处理 API ,是用于创建 2D 视觉效果和视频合成的开放标准。它以类似插件的应用程序开发模型运行。本质上,它既充当主机(提供一组方法的应用程序),又充当插件(实现这组方法的应用程序或模块)。


这种配置提供了无限扩展主机应用程序功能的潜力。

达芬奇决心和金属

Final Cut X 和 DaVinci Resolve Studio 等应用程序从版本 16 开始,完全支持 Apple Metal 管道。与 OpenCL 和 Cuda 类似,对于 OFX,您可以获得特定于平台的命令队列的描述符或处理程序。主机系统还负责分配此类队列池并平衡它们的计算。


此外,它将源和目标图像剪辑数据放置在 GPU 内存中,显着简化了可扩展功能的开发。

Resolve 中的 OFX 版本支持

有了Resolve,事情就稍微复杂了一些。达芬奇宣布支持 OFX v1.4,尽管有一些限制。具体来说,一些使用接口函数的方法不可用。为了确定可用的方法,OFX 允许您通过键/值查询来检查支持的套件。


插件代码中的发布方法基于C 调用。但我们将使用适用于 C++17 的 OpenFXS C++ shell。为了方便起见,我将所有内容都编译到一个存储库中: dehancer-external取自开源Dehancer 项目

OFXS概念

在这个项目中,我将使用 OpenFXS,它是 OpenFX 的 C++ 扩展,最初由Bruno Nicoletti编写,随着时间的推移,它在商业和开源视频处理项目中变得流行。


原始的OpenFXS不适合现代 C++ 方言,因此我对其进行了更新以使其与C++17兼容。


OFX 以及 OFXS 是一个由主机程序动态加载的独立软件模块。本质上,它是一个在主应用程序启动时加载的动态库。 OpenFXS 与 OFX 一样,必须发布方法签名。因此,我们在代码中使用一种 C 方法。


要开始在 OpenFXS 中进行开发,您需要同意一些常用的类集,这些类用于在应用程序中创建新功能。通常,在新项目中,您需要继承这些类并实现或重写一些虚拟方法。


要在主机系统上创建您自己的插件,让我们首先熟悉以下公共类和相同的方法:


  • OFX::PluginFactoryHelper是用于创建插件的数据结构套件和控制面板的基本模板(尽管可以留空)。继承的类创建一个单例对象,该对象在主机系统中注册一组参数和预设,开发人员可以使用该对象注册他的模块;


  • OFX::ParamSetDescriptor - 用于创建和存储结构属性的基容器类;


  • OFX::ImageEffectDescriptor - 调用数据处理过程时操作图形数据时使用的属性容器。由主机应用程序用于在内部数据库中保存处理参数的上下文,并使用为其每个实例定义的插件属性;


  • OFX::ParamSet - 一组设置,允许您操作已注册的数据结构;


  • OFX::ImageEffect - 一组图形数据效果的设置,继承自 OFX::ParamSet;


  • OFX::MultiThread::Processor - 在子类中,需要实现数据流处理:图像或视频;


  • OFX::Plugin::getPluginIDs - 在主机应用程序中注册插件(工厂)的方法;

假色

视频拍摄过程与简单地捕捉照片中的图像的区别的一个特征是场景以及整个场景和图像中区域的照明的动态变化。这决定了拍摄过程中控制曝光的方式。


在数字视频中,有一种供操作员使用的控制监视器模式,其中区域的曝光级别被映射到一组有限的区域,每个区域都有自己的颜色。


此模式有时称为“捕食者”或假色模式。这些量表通常参考 IRE 量表。


这样的监视器可以让您在设置相机拍摄参数时看到曝光区域并避免重大错误。在摄影中曝光时,会使用类似含义的东西 - 例如,根据亚当斯的分区。


您可以使用曝光计测量特定目标并查看它位于哪个区域,我们可以实时看到这些区域,并整齐地着色以便于感知。


区域的数量由控制监视器的目标和能力决定。例如,与Arri Alexa摄像机一起使用的监视器最多可以包含 6 个区域。


软件“掠食者”版本,有 16 个区域


添加扩展

在继续该示例之前,我们需要添加一些简单的代理类来实现 OpenFXS 作为处理源数据(例如金属纹理)的平台。这些课程包括:


  • imetaling::Image :OFX 剪辑数据的代理类。


  • imetaling::Image2Texture :用于将数据从剪辑缓冲区传输到 Metal 纹理的函子。从达芬奇中,您可以提取任何结构的缓冲区并将图像通道值打包到插件中,并且它应该以类似的形式返回。


    为了更轻松地在 OFX 中使用流格式,您可以请求主机提前准备特定类型的数据。我将使用 RGBA 格式的浮点数 - 红/绿/蓝/alpha。


  • imetaling::ImageFromTexture :用于将流转换为主机系统缓冲区的反向函子。正如您所看到的,如果您教导 Metal 计算核心不使用纹理,而是直接使用缓冲区,则有可能显着优化计算。


我们继承 OFXS 基类并编写我们的功能,而无需深入了解 Metal 核心如何工作的细节:


  • imetaling::falsecolor::Processor :在这里,我们实现流转换并启动处理。


  • imetaling::falsecolor::Factory :这将是插件套件描述的特定部分。我们需要实现几个与设置结构相关的强制调用,并创建具有特定功能的 OFX::ImageEffect 类的实例,我们在实现中将其分为两个子类:Interaction 和 Plugin。


  • imetaling::falsecolor::Interaction :实现效果处理的交互部分。本质上,这只是 OFX::ImageEffect 中与处理插件参数更改相关的虚拟方法的实现。


  • imetaling::falsecolor::Plugin :线程渲染的实现,即启动imetaling::Processor。


此外,我们需要在 Metal 之上构建几个实用程序类,以在逻辑上分离 MSL 上的主机代码和内核代码。这些包括:


  • imetaling::Function :一个基类,用于隐藏 Metal 命令队列的工作。主要参数是MSL代码中内核的名称,以及内核调用的执行者。


  • imetaling:Kernel :用于将源纹理转换为目标纹理的通用类,扩展 Function 以简单设置调用 MSL 内核的参数。


  • imetaling::PassKernel :绕过内核。


  • imetaling::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并重载五个方法:


  • load() :首次加载插件时调用此方法来全局配置实例。重载此方法是可选的。


  • unload() :当卸载实例时调用此方法,例如清除内存。重载此方法也是可选的。


  • describe(ImageEffectDescriptor&) :这是加载插件时 OFX 主机调用的第二个方法。它是虚拟的,必须在我们的类中定义。在此方法中,我们需要设置插件的所有属性,无论其上下文类型如何。有关属性的更多详细信息,请参阅ImageEffectDescriptor代码。


  • describeInContext(ImageEffectDescriptor&,ContextEnum) :与describe方法类似,该方法也会在插件加载时调用,并且必须在我们的类中定义。它应该定义与当前上下文关联的属性。


    上下文决定了应用程序使用的操作类型,例如剪辑中的滤镜、绘画、过渡效果或帧重定时器。


  • createInstance(OfxImageEffectHandle, ContextEnum) :这是我们重载的最关键的方法。我们返回一个指向ImageEffect类型的对象的指针。换句话说,我们的imetalling::falsecolor::Plugin中定义了所有功能,包括主机程序中的用户事件以及将源帧渲染(转换)为目标帧:
 OFX::ImageEffect *Factory::createInstance(OfxImageEffectHandle handle,OFX::ContextEnum) {     return new Plugin(handle);   }


处理事件

在此阶段,如果您使用 OFX 模块编译捆绑包,则该插件将已在主机应用程序中可用,并且在 DaVinci 中,它可以加载到校正节点上。


但是,要充分使用插件实例,您至少需要定义交互部分和与处理传入视频流相关的部分。


为此,我们继承OFX::ImageEffect类并重载虚拟方法:


  • changeParam(const OFX::InstanceChangedArgs&, const std::string&) - 此方法允许我们定义处理事件的逻辑。事件类型由 OFX::InstanceChangedArgs::reason 的值确定,可以是: eChangeUserEdit、eChangePluginEdit、eChangeTime - 由于用户编辑属性、在插件或主机应用程序中更改属性而发生事件,或者由于时间线的变化。


    第二个参数指定我们在插件初始化阶段定义的字符串名称,在我们的例子中,它是一个参数: false_color_enabled_check_box


  • isIdentity(...) - 此方法允许我们定义对事件做出反应的逻辑,并返回一个状态来确定某些内容是否已更改以及渲染是否有意义。该方法必须返回 false 或 true。这是一种优化和减少不必要计算数量的方法。


您可以在Interaction.cpp代码中阅读与OFX交互交互的实现。正如您所看到的,我们收到指向剪辑的指针:源剪辑和我们将在其中放置目标转换的内存区域。

渲染启动的实现

我们将添加另一个逻辑层,在该层上定义启动转换的所有逻辑。在我们的例子中,这是迄今为止唯一的覆盖方法:


  • render(const OFX::RenderArguments& args) - 在这里,您可以找到剪辑的属性并决定如何渲染它们。此外,在这个阶段,Metal 命令队列和一些与当前时间线属性相关的有用属性可供我们使用。

加工

在启动阶段,我们可以使用一个具有有用属性的对象:我们至少有一个指向视频流的指针(更准确地说,是一个包含帧图像数据的内存区域),最重要的是,还有一个 Metal 命令队列。


现在,我们可以构造一个通用类,这将使我们更接近重用内核代码的简单形式。 OpenFXS扩展已经有这样一个类:OFX::ImageProcessor;我们只需要超载它。


在构造函数中,它具有 OFX::ImageEffect 参数,即在其中,我们不仅会收到插件参数的当前状态,还会收到使用 GPU 所需的所有信息。


在这个阶段,我们只需要重载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,您必须首先创建一个构建目录。


创建build目录后,可以执行以下命令:


 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面板中找到并选择它,并将其添加为节点。



工作假色



外部链接

  1. 伪色 OFX 插件代码
  2. 开放效果协会
  3. 下载DaVinci Resolve - OFX头文件版本和Resolve下的OFXS库代码+示例