为 C++ 运算符编写 Python 绑定
为了在保持高性能的同时提供便利,用 C++ 编写的运算符 可以封装在 Python 中。 通用方法是使用 Pybind11 来简洁地创建绑定,从而为应用程序作者提供熟悉的 Pythonic 体验。
虽然我们提供了一些实用程序来简化部分流程,但本节是为高级开发人员设计的,因为使用 pybind11 封装 C++ 类主要是手动操作,并且在每个运算符之间可能会有所不同。
现有的 Pybind11 文档很好,建议至少阅读关于封装 函数 和 类 的基础知识。 以下材料将假定您对 Pybind11 有一定的基本了解,涵盖创建 C++ Operator
绑定的详细信息。 作为一个具体的例子,我们将介绍从 ToolTrackingPostprocessorOp 到 Holohub 的绑定的创建,作为一个简单的案例,然后重点介绍可能遇到的其他场景。
在 Holohub 的 operators 文件夹 中有几个绑定的示例。 在 C++ 实现之上提供 Python 封装的运算符子集将在公共文件夹中包含任何 C++ 头文件和源文件,而任何相应的 Python 绑定都将在 “python” 子文件夹中(例如,请参阅 tool_tracking_postprocessor 文件夹布局)。
还有几个 绑定示例 用于 SDK 的内置运算符。 与 Holohub 不同,对于 SDK,运算符的相应 C++ 头文件 和 源文件 存储在单独的目录树下。
建议将 C++ 运算符的 initialize()
和/或 start()
方法中分配的任何资源的清理操作放入运算符的 stop()
方法中,而不要放入其析构函数中。 这是必要的,因为目前存在一个问题,即不能保证析构函数总是在 Python 应用程序终止之前被调用。 stop()
方法将始终被显式调用,因此我们可以确保任何清理操作都按预期发生。
创建 PyToolTrackingPostprocessorOp trampoline 类
在 C++ 文件中(在本例中为 tool_tracking_postprocessor.cpp),创建 C++ Operator 类的子类进行封装。 采用的通用方法是创建一个特定于 Python 的类,该类提供一个构造函数,该构造函数接受 Fragment*
、运算符参数的显式列表(对于任何可选参数都带有默认值)以及运算符名称。 此构造函数需要按照 Fragment::make_operator
中的方式设置运算符,以便它已准备好由 GXF 执行器进行初始化。 我们使用在 C++ 类名称前加上 “Py” 的约定来表示这一点(因此,在本例中为 PyToolTrackingPostprocessorOp
)。
列表 22 tool_tracking_post_processor/python/tool_tracking_post_processor.cpp
class PyToolTrackingPostprocessorOp : public ToolTrackingPostprocessorOp {
public:
/* Inherit the constructors */
using ToolTrackingPostprocessorOp::ToolTrackingPostprocessorOp;
// Define a constructor that fully initializes the object.
PyToolTrackingPostprocessorOp(
Fragment* fragment, const py::args& args, std::shared_ptr<Allocator> device_allocator,
std::shared_ptr<Allocator> host_allocator, float min_prob = 0.5f,
std::vector<std::vector<float>> overlay_img_colors = VIZ_TOOL_DEFAULT_COLORS,
std::shared_ptr<holoscan::CudaStreamPool> cuda_stream_pool = nullptr,
const std::string& name = "tool_tracking_postprocessor")
: ToolTrackingPostprocessorOp(ArgList{Arg{"device_allocator", device_allocator},
Arg{"host_allocator", host_allocator},
Arg{"min_prob", min_prob},
Arg{"overlay_img_colors", overlay_img_colors},
}) {
if (cuda_stream_pool) { this->add_arg(Arg{"cuda_stream_pool", cuda_stream_pool}); }
add_positional_condition_and_resource_args(this, args);
name_ = name;
fragment_ = fragment;
spec_ = std::make_shared<OperatorSpec>(fragment);
setup(*spec_.get());
}
};
此构造函数将允许为创建运算符提供 Pythonic 体验。 具体来说,用户可以为任何参数传递 Python 对象,而无需显式创建任何 holoscan::Arg
对象(通过 holoscan.core.Arg
)。 例如,可以将标准 Python 浮点数传递给 min_prob
,并且可以将 Python list[list[float]]
传递给 overlay_img_colors
(Pybind11 处理 C++ 和 Python 类型之间的转换)。 Pybind11 还将负责将 Python 分配器类(如 holoscan.resources.UnboundedAllocator
或 holoscan.resources.BlockMemoryPool
)转换为底层的 C++ std::shared_ptr<holoscan::Allocator>
类型。 参数 device_allocator
和 host_allocator
对应于 C++ 类的必需参数,可以从 Python 以位置方式或通过关键字提供,而参数 min_prob
和 overlay_img_colors
将是可选的关键字参数。 cuda_stream_pool
也是可选的,但仅当它不是 nullptr
时,才作为参数有条件地传递给底层的 ToolTrackingPostprocessorOp
构造函数。
对于所有运算符,第一个参数应为
Fragment* fragment
,并且是运算符将分配到的片段。 在单片段应用程序(即非分布式应用程序)的情况下,片段只是应用程序本身。应提供一个(可选的)
const std::string& name
参数,以使应用程序作者能够设置运算符的名称。const py::args& args
参数对应于 Python 中的*args
表示法。 它是一组 0 个或多个位置参数。 函数签名中不需要提供此参数,但建议提供,以便能够将其他条件(如CountCondition
或PeriodicCondtion
)作为位置参数传递给运算符。 下面对add_positional_condition_and_resource_args(this, args);
的调用使用了 operator_util.hpp 中定义的辅助函数,以添加在位置参数列表中找到的任何
Condition
或Resource
参数。其他所有参数都对应于为 C++
ToolTrackingPostProcessorOp
类定义的各种参数(holoscan::Parameter
)。除
cuda_stream_pool
以外的所有其他参数都直接在参数列表中传递给父ToolTrackingPostProcessorOp
类。 C++ 运算符上存在的参数可以在其头文件 此处 中看到,默认值取自源文件 此处 的setup
方法。 请注意,CudaStreamHandler
是一个实用程序,它将添加Parameter<std::shared_ptr<CudaStreamPool>>
类型的参数。仅当
cuda_stream_pool
参数不是nullptr
(Python 的None
) 时,才有条件地添加它。 这是通过if (cuda_stream_pool) { this->add_arg(Arg{"cuda_stream_pool", cuda_stream_pool}); }
完成的,而不是将其作为提供给
ToolTrackingPostprocessorOp
构造函数调用的holoscan::ArgList
的一部分传递。
构造函数的其余行
name_ = name;
fragment_ = fragment;
spec_ = std::make_shared<OperatorSpec>(fragment);
setup(*spec_.get());
是正确初始化它所必需的,并且在所有运算符中都应相同。 这些 对应于 Fragment::make_operator 方法中的等效代码。
定义 Python 模块
对于此运算符,除了运算符本身之外,没有其他自定义类,因此我们使用 PYBIND11_MODULE
定义一个模块,如下所示,只有一个类定义。 这在定义 PyToolTrackingPostprocessorOp
trampoline 类的同一 tool_tracking_postprocessor.cpp 文件中完成。
始终需要以下头文件。
#include <pybind11/pybind11.h>
namespace py = pybind11;
using pybind11::literals::operator""_a;
在这里,我们通常还会将 py
命名空间定义为 pybind11
的简写,并指示我们将使用 _a
字面量(当 定义关键字参数 时,它提供了一种简写符号)。
如果运算符的任何参数涉及 C++ 标准库容器(如 std::vector
或 std::unordered_map
),则通常需要包含以下头文件。
#include <pybind11/stl.h>
这允许 pybind11 在 C++ 容器类型和相应的 Python 类型之间进行转换(例如,Python dict
/ C++ std::unordered_map
)。
列表 23 tool_tracking_post_processor/python/tool_tracking_post_processor.cpp
PYBIND11_MODULE(_tool_tracking_postprocessor, m) {
py::class_<ToolTrackingPostprocessorOp,
PyToolTrackingPostprocessorOp,
Operator,
std::shared_ptr<ToolTrackingPostprocessorOp>>(
m,
"ToolTrackingPostprocessorOp",
doc::ToolTrackingPostprocessorOp::doc_ToolTrackingPostprocessorOp_python)
.def(py::init<Fragment*,
const py::args& args,
std::shared_ptr<Allocator>,
std::shared_ptr<Allocator>,
float,
std::vector<std::vector<float>>,
std::shared_ptr<holoscan::CudaStreamPool>,
const std::string&>(),
"fragment"_a,
"device_allocator"_a,
"host_allocator"_a,
"min_prob"_a = 0.5f,
"overlay_img_colors"_a = VIZ_TOOL_DEFAULT_COLORS,
"cuda_stream_pool"_a = py::none(),
"name"_a = "tool_tracking_postprocessor"s,
doc::ToolTrackingPostprocessorOp::doc_ToolTrackingPostprocessorOp_python);
} // PYBIND11_MODULE NOLINT
如果您正在 Holohub 中实现 python 封装,则传递给
PYBIND_11_MODULE
的<module_name>
必须与_<CPP_CMAKE_TARGET>
匹配,如 下文所述)。如果您正在独立的 CMake 项目中实现 python 封装,则传递给
PYBIND_11_MODULE
的<module_name>
必须与传递给 pybind11-add-module CMake 函数的模块名称匹配。
在 PYBIND_11_MODULE
中使用不匹配的名称将导致无法从 Python 导入模块。
在 py::class_<>
模板调用中指定类的顺序很重要,应遵循此处所示的约定。 列表中的第一个是 C++ 类名称 (ToolTrackingPostprocessorOp
),第二个是我们上面定义的 PyToolTrackingPostprocessorOp
类,带有额外的显式构造函数。 我们还需要列出父 Operator
类,以便父类已封装的所有方法(如 start
、stop
、compute
、add_arg
等)不需要在此处重新定义。
单个 .def(py::init<...
调用封装了我们上面编写的 PyToolTrackingPostprocessorOp
构造函数。 因此,提供给 py::init<>
的参数类型必须与该构造函数的函数签名中的参数顺序和类型完全匹配。 def
的后续参数是命名参数的名称和默认值(如果有),其顺序与函数签名相同。 请注意,const py::args& args
(Python *args
) 参数未列出,因为这些是位置参数,没有对应的名称。 对 cuda_stream_pool
默认值使用 py::none()
(Python 的 None
) 对应于 C++ 函数签名中的 nullptr
。 定义中使用的 “_a” 字面量由文件中较早位置的以下声明启用。
.def
的最后一个参数是一个文档字符串,它将用作该函数的 Python 文档字符串。 它是可选的,我们在此处选择在单独的头文件中定义它,如下一节所述。
文档字符串
为您python 类及其参数准备文档字符串 (const char*
)。
下面我们使用 SDK 中定义的 PYDOC
宏,该宏在 HoloHub 中也可用作删除前导空格的实用程序。 在本例中,文档代码位于头文件 tool_tracking_post_processor_pydoc.hpp 中,位于自定义 holoscan::doc::ToolTrackingPostprocessorOp
命名空间下。 这些都不是必需的,您只需要使任何文档字符串可用作 py::class_
构造函数或方法定义调用的参数即可。
列表 24 tool_tracking_post_processor/python/tool_tracking_post_processor_pydoc.hpp
#include "../macros.hpp"
namespace holoscan::doc {
namespace ToolTrackingPostprocessorOp {
// PyToolTrackingPostprocessorOp Constructor
PYDOC(ToolTrackingPostprocessorOp_python, R"doc(
Operator performing post-processing for the endoscopy tool tracking demo.
**==Named Inputs==**
in : nvidia::gxf::Entity containing multiple nvidia::gxf::Tensor
Must contain input tensors named "probs", "scaled_coords" and "binary_masks" that
correspond to the output of the LSTMTensorRTInfereceOp as used in the endoscopy
tool tracking example applications.
**==Named Outputs==**
out_coords : nvidia::gxf::Tensor
Coordinates tensor, stored on the host (CPU).
out_mask : nvidia::gxf::Tensor
Binary mask tensor, stored on device (GPU).
Parameters
----------
fragment : Fragment
The fragment that the operator belongs to.
device_allocator : ``holoscan.resources.Allocator``
Output allocator used on the device side.
host_allocator : ``holoscan.resources.Allocator``
Output allocator used on the host side.
min_prob : float, optional
Minimum probability (in range [0, 1]). Default value is 0.5.
overlay_img_colors : sequence of sequence of float, optional
Color of the image overlays, a list of RGB values with components between 0 and 1.
The default value is a qualitative colormap with a sequence of 12 colors.
cuda_stream_pool : ``holoscan.resources.CudaStreamPool``, optional
`holoscan.resources.CudaStreamPool` instance to allocate CUDA streams.
Default value is ``None``.
name : str, optional
The name of the operator.
)doc")
} // namespace ToolTrackingPostprocessorOp
} // namespace holoscan::doc
我们倾向于对参数使用 NumPy 风格的文档字符串,但也鼓励在顶部添加一个自定义部分,描述输入和输出端口以及预期的数据类型。 这可以使开发人员更容易使用运算符,而无需检查源代码来确定此信息。
使用 CMake 进行配置
我们使用 CMake 来配置 pybind11 并为您希望封装的 C++ 运算符构建绑定。 下面详细介绍了两种方法,一种用于 HoloHub(推荐),另一种用于独立的 CMake 项目。
要构建您的绑定,请确保以下 CMake 代码作为 CMake 项目的一部分执行,该项目已将 C++ 运算符定义为 CMake 目标,或者在您的项目中构建(使用 add_library
),或者导入(使用 find_package
或 find_library
)。
我们在 HoloHub 中提供了一个名为 pybind11_add_holohub_module 的 CMake 实用程序函数,以方便配置和构建您的 python 绑定。
在我们下面的骨架代码中,已经为 C++ 运算符定义了 tool_tracking_postprocessor
目标的顶层 CMakeLists.txt 需要执行 add_subdirectory(tool_tracking_postprocessor)
以包含以下 CMakeLists.txt。 pybind11_add_holohub_module
列出了该 C++ 运算符目标、要封装的 C++ 类以及我们上面实现的 C++ 绑定源代码的路径。 请注意,作为 PYPBIND11_MODULE 的第一个参数提供的模块名称需要与 _<CPP_CMAKE_TARGET>
(_tool_tracking_postprocessor_op
在本例中) 匹配。
列表 25 tool_tracking_postprocessor/python/CMakeLists.txt
include(pybind11_add_holohub_module)
pybind11_add_holohub_module(
CPP_CMAKE_TARGET tool_tracking_postprocessor
CLASS_NAME "ToolTrackingPostprocessorOp"
SOURCES tool_tracking_postprocessor.cpp
)
此处的关键细节是 CLASS_NAME
应与正在封装的 C++ 类的名称匹配,并且也是将用于 Python 的类的名称。 SOURCES
应指向定义正在封装的 C++ 运算符的文件。 CPP_CMAKE_TARGET
名称将是包含运算符的 holohub 包子模块的名称。
请注意,由于 上一级文件夹中的 CMakeLists.txt 中的 add_subdirectory(python)
,此 CMakeLists.txt 所在的 python 子目录是可访问的,但这只是一个任意的、带有主观意见的位置,而不是必需的目录结构。
按照 pybind11 文档 配置您的 CMake 项目以使用 pybind11。 然后,使用带有包含上述代码的 cpp 文件的 pybind11_add_module 函数,并链接到 holoscan::core
和将您的 C++ 运算符公开以进行封装的库。
列表 26 my_op_python/CMakeLists.txt
pybind11_add_module(my_python_module my_op_pybind.cpp)
target_link_libraries(my_python_module
PRIVATE holoscan::core
PUBLIC my_op
)
示例:在 SDK 中,这是在 此处 完成的。
为 CPP_CMAKE_TARGET
选择的名称必须也用作(以及前导下划线)作为传递给 bindings 中的 PYBIND11_MODULE 宏 的第一个参数的模块名称。
请注意,名称前面有一个初始下划线。 这是用于共享库和相应的 __init__.py
文件的命名约定,该文件将由上面的 pybind11_add_holohub_module
辅助函数生成。
如果名称指定不正确,构建仍将完成,但在应用程序运行时,将发生 ImportError
,例如以下错误
[command] python3 /workspace/holohub/applications/endoscopy_tool_tracking/python/endoscopy_tool_tracking.py --data /workspace/holohub/data/endoscopy
Traceback (most recent call last):
File "/workspace/holohub/applications/endoscopy_tool_tracking/python/endoscopy_tool_tracking.py", line 38, in <module>
from holohub.tool_tracking_postprocessor import ToolTrackingPostprocessorOp
File "/workspace/holohub/build/python/lib/holohub/tool_tracking_postprocessor/__init__.py", line 19, in <module>
from ._tool_tracking_postprocessor import ToolTrackingPostprocessorOp
ImportError: dynamic module does not define module export function (PyInit__tool_tracking_postprocessor)
在 Python 中导入类
在构建项目时,将在 <build_or_install_dir>/python/lib/holohub/<CPP_CMAKE_TARGET>
(例如 build/python/lib/holohub/tool_tracking_postprocessor/
) 内生成两个文件
绑定的共享库 (
_tool_tracking_postprocessor_op.cpython-<pyversion>-<arch>-linux-gnu.so
)一个
__init__.py
文件,该文件进行必要的导入以在 python 中公开此文件
假设您有 export PYTHONPATH=<build_or_install_dir>/python/lib/
,那么您应该能够在 Holohub 中创建一个应用程序,该应用程序通过以下方式导入您的类
from holohub.tool_tracking_postprocessor_op import ToolTrackingPostProcessorOp
示例:ToolTrackingPostProcessorOp
在 HoloHub 上的内窥镜工具跟踪应用程序中导入,此处。
在构建项目时,将在 <build_or_install_dir>/my_op_python
(可通过 CMake 中的 OUTPUT_NAME
和 LIBRARY_OUTPUT_DIRECTORY
分别配置) 内生成一个名为 my_python_module.cpython-<pyversion>-<arch>-linux-gnu.so
的共享库文件,其中包含 python 绑定。
从那里,您可以通过以下方式在 python 中导入它
import holoscan.core
import holoscan.gxf # if your c++ operator uses gxf extensions
from <build_or_install_dir>.my_op_python import MyOp
要模仿 HoloHub 的行为,您还可以将该文件与 .so 文件放在一起,将其命名为 __init__.py
,并将 <build_or_install_dir>.
替换为 .
。 然后可以将其作为 python 模块导入,假设 <build_or_install_dir>
是 PYTHONPATH
环境变量下的模块。
在本节中,我们将介绍在为运算符编写 Python 绑定时可能偶尔遇到的其他情况。
可选参数
也可以使用 std::optional
来处理可选参数。 例如,上面的 ToolTrackingProcessorOp
示例在规范中为 min_prob
定义了一个默认参数。
constexpr float DEFAULT_MIN_PROB = 0.5f;
// ...
spec.param(
min_prob_, "min_prob", "Minimum probability", "Minimum probability.", DEFAULT_MIN_PROB);
在上面的 ToolTrackingProcessorOp
教程中,我们在 PyToolTrackingProcessorOp
构造函数签名以及为其定义的 Python 绑定中都重现了 0.5 的默认值。 这带来了风险,即 C++ 运算符级别的默认值可能会更改,而 Python 的相应更改未进行。
定义构造函数的另一种方法是使用 std::optional
,如下所示
// Define a constructor that fully initializes the object.
PyToolTrackingPostprocessorOp(
Fragment* fragment, const py::args& args, std::shared_ptr<Allocator> device_allocator,
std::shared_ptr<Allocator> host_allocator, std::optional<float> min_prob = 0.5f,
std::optional<std::vector<std::vector<float>>> overlay_img_colors = VIZ_TOOL_DEFAULT_COLORS,
std::shared_ptr<holoscan::CudaStreamPool> cuda_stream_pool = nullptr,
const std::string& name = "tool_tracking_postprocessor")
: ToolTrackingPostprocessorOp(ArgList{Arg{"device_allocator", device_allocator},
Arg{"host_allocator", host_allocator},
}) {
if (cuda_stream_pool) { this->add_arg(Arg{"cuda_stream_pool", cuda_stream_pool}); }
if (min_prob.has_value()) { this->add_arg(Arg{"min_prob", min_prob.value() }); }
if (overlay_img_colors.has_value()) {
this->add_arg(Arg{"overlay_img_colors", overlay_img_colors.value() });
}
add_positional_condition_and_resource_args(this, args);
name_ = name;
fragment_ = fragment;
spec_ = std::make_shared<OperatorSpec>(fragment);
setup(*spec_.get());
}
现在 min_prob
和 overlay_img_colors
是可选的,只有当它们有值时,才会有条件地将其作为参数添加到 ToolTrackingPostprocessorOp。 如果使用此方法,则应更新构造函数的 Python 绑定,以使用 py::none()
作为默认值,如下所示
.def(py::init<Fragment*,
const py::args& args,
std::shared_ptr<Allocator>,
std::shared_ptr<Allocator>,
float,
std::vector<std::vector<float>>,
std::shared_ptr<holoscan::CudaStreamPool>,
const std::string&>(),
"fragment"_a,
"device_allocator"_a,
"host_allocator"_a,
"min_prob"_a = py::none(),
"overlay_img_colors"_a = py::none(),
"cuda_stream_pool"_a = py::none(),
"name"_a = "tool_tracking_postprocessor"s,
doc::ToolTrackingPostprocessorOp::doc_ToolTrackingPostprocessorOp_python);
C++ 枚举参数作为参数
有时,运算符可能使用带有枚举类型的参数。 需要封装 C++ 枚举,以便在为运算符提供参数时能够将其用作 Python 类型。
内置的 holoscan::ops::AJASourceOp
是一个 C++ 运算符的示例,它接受 枚举参数(NTV2Channel
枚举)。
可以通过 py::enum_
轻松封装枚举以供 Python 使用,如 此处 所示。 在这种情况下,建议遵循 Python 的约定,在枚举中使用大写名称。
(高级)自定义 C++ 类作为参数
有时,需要在运算符的构造函数中接受自定义 C++ 类类型作为参数。 在这种情况下,可能需要额外的接口代码和绑定来支持该类型。
一个相对简单的示例是 DataVecMap
类型,InferenceProcessorOp
使用了它。 在这种情况下,该类型是一个结构,其中包含一个内部 std::map<std:string, std::vector<std::string>>
。 编写绑定以接受 Python 字典 (py::dict
),并在构造函数中使用 辅助函数 将该字典转换为相应的 C++ DataVecMap
。
一个更复杂的情况是在 HolovizOp
绑定中使用 InputSpec
类型。 这种情况涉及为类 InputSpec
和 View
以及几个枚举类型创建 Python 绑定。 为了避免用户必须直接构建 list[holoscan.operators.HolovizOp.InputSpec]
以作为 tensors
参数传递,在 __init__.py
中定义了一个 额外的 Python 封装类,以允许为 tensors
参数传递简单的 Python 字典,并且在调用底层的 Python 绑定类之前,在其构造函数中自动创建任何相应的 InputSpec 类。
自定义 Python 运算符可以发出或接收的 C++ 类型
在某些情况下,用户可能希望能够让 Python 运算符接收和/或发出自定义 C++ 类型。 作为第一个示例,假设我们正在封装一个发出自定义 C++ 类型的运算符。 我们需要任何下游的本机 Python 运算符都能够接收该类型。 默认情况下,SDK 能够处理内置运算符所需的 C++ 类型,例如 std::vector<holoscan::ops::HolovizOp::InputSpec>
。 SDK 提供了一个 EmitterReceiverRegistry
类,第三方项目可以使用该类为任何需要处理的自定义 C++ 类型注册 receiver
和 emitter
方法。 要处理新类型,用户应为所需的类型实现 emitter_receiver<T>
结构,如下例所示。 我们将首先介绍注册此类类型所需的常规步骤,然后介绍在某些简单情况下可以省略哪些步骤。
步骤 1:定义 emitter_receiver::emit 和 emitter_receiver::receive 方法
以下是内置 std::vector<holoscan::ops::HolovizOp::InputSpec>
的示例,HolovizOp
使用它来定义其接收张量的输入规范。
#include <holoscan/python/core/emitter_receiver_registry.hpp>
namespace py = pybind11;
namespace holoscan {
/* Implements emit and receive capability for the HolovizOp::InputSpec type.
*/
template <>
struct emitter_receiver<std::vector<holoscan::ops::HolovizOp::InputSpec>> {
static void emit(py::object& data, const std::string& name, PyOutputContext& op_output,
const int64_t acq_timestamp = -1) {
auto input_spec = data.cast<std::vector<holoscan::ops::HolovizOp::InputSpec>>();
py::gil_scoped_release release;
op_output.emit<std::vector<holoscan::ops::HolovizOp::InputSpec>>(input_spec, name.c_str(), acq_timestamp);
return;
}
static py::object receive(std::any result, const std::string& name, PyInputContext& op_input) {
HOLOSCAN_LOG_DEBUG("py_receive: std::vector<HolovizOp::InputSpec> case");
// can directly return vector<InputSpec>
auto specs = std::any_cast<std::vector<holoscan::ops::HolovizOp::InputSpec>>(result);
py::object py_specs = py::cast(specs);
return py_specs;
}
};
}
此 emitter_receiver
类定义了一个 receive
方法,该方法接受 std::any
消息并将其强制转换为相应的 Python list[HolovizOp.InputSpect]
对象。 此处的 pybind11::cast
调用有效,因为我们已在此处 封装了 HolovizOp::InputSpec
类。
类似地,emit
方法接受一个 pybind11::object
(类型为 list[HolovizOp.InputSpect]
),并将其强制转换为相应的 C++ 类型 std::vector<holoscan::ops::HolovizOp::InputSpec>
。 std::vector
和 Python 列表之间的转换是 Pbind11 的内置转换之一(只要包含 “pybind11/stl.h” 即可使用)。
emit
和 receive
方法的签名必须与此处显示的示例完全匹配。
步骤 2:创建一个 register_types 方法,用于将自定义类型添加到 EmitterReceiverRegistry。
此运算符模块中的绑定应定义一个名为 register_types
的方法,该方法接受对 EmitterReceiverRegistry
的引用作为其唯一参数。 在此函数中,对于此运算符希望注册的每种类型,都应调用 EmitterReceiverRegistry::add_emitter_receiver
。 HolovizOp 使用 lambda 函数定义此方法
// Import the emitter/receiver registry from holoscan.core and pass it to this function to
// register this new C++ type with the SDK.
m.def("register_types", [](EmitterReceiverRegistry& registry) {
registry.add_emitter_receiver<std::vector<holoscan::ops::HolovizOp::InputSpec>>(
"std::vector<HolovizOp::InputSpec>"s);
// array camera pose object
registry.add_emitter_receiver<std::shared_ptr<std::array<float, 16>>>(
"std::shared_ptr<std::array<float, 16>>"s);
// Pose3D camera pose object
registry.add_emitter_receiver<std::shared_ptr<nvidia::gxf::Pose3D>>(
"std::shared_ptr<nvidia::gxf::Pose3D>"s);
// camera_eye_input, camera_look_at_input, camera_up_input
registry.add_emitter_receiver<std::array<float, 3>>("std::array<float, 3>"s);
});
在此,以下行注册了我们为上面编写 emitter_receiver
的 std::vector<holoscan::ops::HolovizOp::InputSpec>
类型。
registry.add_emitter_receiver<std::vector<holoscan::ops::HolovizOp::InputSpec>>(
"std::vector<HolovizOp::InputSpec>"s);
在内部,注册表存储模板参数中指定的类型的 C++ std::type_index
与为该类型定义的 emitter_receiver
之间的映射。第二个参数是用户可以选择的字符串,它是类型的标签。正如我们稍后将看到的,此标签可以从 Python 中使用,以指示我们想要使用为特定标签注册的 emitter_receiver::emit
方法进行发射。
步骤 3:在定义运算符的 Python 模块的 init.py 文件中调用 register_types
要向核心 SDK 注册类型,我们需要从 holoscan.core
导入 io_type_registry
类(类型为 EmitterReceiverRegistry
)。然后,我们将该类作为输入传递给步骤 2 中定义的 register_types
方法,以向核心 SDK 注册第三方类型。
from holoscan.core import io_type_registry
from ._holoviz import register_types as _register_types
# register methods for receiving or emitting list[HolovizOp.InputSpec] and camera pose types
_register_types(io_type_registry)
我们在其中选择导入带有初始下划线的 register_types
,这是 Python 的常见约定,表示它旨在“私有”于此模块。
在某些情况下,如上所示的步骤 1 和 3 不是必需的。
当为 Holohub 上的运算符创建 Python 绑定时,上面提到的 pybind11_add_holohub_module.cmake 实用程序将负责自动生成步骤 3 中所示的 __init__.py
,因此在这种情况下无需手动创建它。
对于 Pybind11 的 C++ 和 Python 之间的默认转换已足够使用的类型,无需显式定义如步骤 1 中所示的 emitter_receiver
类。这是因为对于 emitter_receiver<T>
和 emitter_receiver<std::shared_ptr<T>>
有几个 默认实现 已经涵盖了常见情况。默认的 emitter_receiver 适用于上面显示的 std::vector<HolovizOp::InputSpec>
类型,这就是为什么那里用于说明的代码 在运算符的绑定中找不到 的原因。在这种情况下,可以直接从步骤 2 实现 register_types
,而无需显式创建 emitter_receiver
类。
默认 emitter_receiver
不起作用的一个示例是 SDK 为 pybind11::dict
定义的自定义示例。在这种情况下,为了方便通过将 dict[holoscan::Tensor]
传递给 op_output.emit
来方便地发射多个张量,我们对 Python 字典进行了特殊处理。将检查字典,如果所有键都是字符串且所有值都是类似张量的对象,则会发射一个包含所有张量作为 nvidia::gxf::Tensor
的单个 C++ nvidia::gxf::Entity
。如果字典不是张量映射,则它仅作为指向 Python 字典对象的共享指针发射。emitter_receiver
用于核心 SDK 的实现在 emitter_receivers.hpp 中定义。这些可以作为为其他类型创建新类型时的参考。
发射和接收的运行时行为
注册新类型后,将自动处理任何输入端口上该类型的接收。这是因为由于 C++ 的强类型,运算符的 compute
方法中的任何 op_input.receive
调用都可以找到与类型 std::type_index
匹配的已注册 receive
方法,并使用该方法转换为相应的 Python 对象。
由于 Python 不是强类型语言,因此在 emit
上,默认行为仍然是发射指向 Python 对象本身的共享指针。如果我们想 emit
C++ 类型,我们可以将第 3 个参数传递给 op_output.emit
,以指定我们在上面通过 add_emitter_receiver
调用注册类型时使用的名称。
发射 C++ 类型的示例
作为一个具体的示例,SDK 已经默认注册了 std::string
。例如,如果我们想将 Python 字符串作为 C++ std::string
发射,以供下游运算符使用,该运算符包装了一个期望字符串输入的 C++ 运算符,我们将在 op_output.emit
调用中添加第 3 个参数,如下所示
# emit a Python filename string on port "filename_out" using registered type "std::string"
my_string = "filename.raw"
op_output.emit(my_string, "filename_out", "std::string")
这指定应使用转换为 C++ std::string
的 emit
方法,而不是发射 Python 字符串的默认行为。
另一个示例是将 Python List[float]
作为 std::array<float, 3>
参数发射,作为 HolovizOp
的 camera_eye
、camera_look_at
或 camera_up
输入端口的输入。
op_output.emit([0.0, 1.0, 0.0], "camera_eye_out", "std::array<float, 3>")
只有在 SDK 中注册的类型才能在此 emit
的第三个参数中按名称指定。
核心 SDK 注册的类型表
下表给出了在 SDK 的 EmitterReceiverRegistry
中注册的类型列表。
C++ 类型 | EmitterReceiverRegistry 中的名称 |
---|---|
holoscan::Tensor | “holoscan::Tensor” |
std::shared_ptr |
“PyObject” |
std::string | “std::string” |
pybind11::dict | “pybind11::dict” |
holoscan::gxf::Entity | “holoscan::gxf::Entity” |
holoscan::PyEntity | “holoscan::PyEntity” |
nullptr_t | “nullptr_t” |
CloudPickleSerializedObject | “CloudPickleSerializedObject” |
std::array |
“std::array |
std::shared_ptr |
“std::shared_ptr |
std::shared_ptr |
“std::shared_ptr |
std::vector |
“std::vector |
注册的名称没有要求必须符合任何特定约定。我们通常使用 C++ 类型作为名称以避免歧义,但这不是必需的。
以上各节介绍了如何在绑定中添加 register_types
函数来扩展此列表。还可以获取当前注册的所有类型的列表,包括任何其他导入的第三方模块注册的类型。这可以通过以下方式完成
from holoscan.core import io_type_registry
print(io_type_registry.registered_types())