"); //-->
4. C++ And CUDA Extensions
For Python/ PyTorch
C++ 与 Python 或 PyTorch 的交互,业界主流做法是采用 pybind11,关于Pybind11 的更多详细说明可以参看文献 [15],其核心原理如下图所示:
pybind11 pipeline
由于 PyTorch 的 C++ 拓展与纯 Python 有一些区别,因为 PyTorch 的基础数据类型是 torch.Tensor,该数据类型可以认为是 Pytorch 库对 np.array 进行了更高一层的封装。所以,在写拓展程序时,其接口函数所需要的数据类型以及调用的库会有些区别,下面会详细解释。
4.1. C++ Extensions For Python
首先我们看 Python 代码,如下所示(scripts/test_warpaffine_opencv.py):
import cv2import torch # 不能删掉, 因为需要动态加载torch的一些动态库,后面会详细说明.import numpy as npfrom orbbec.warpaffine import affine_opencv # C++ interface
data_path = "./demo.png"img = cv2.imread(data_path, cv2.IMREAD_GRAYSCALE)
# python中的numpy.array()与 pybind中的py::array_t一一对应.src_point = np.array([[262.0, 324.0], [325.0, 323.0], [295.0, 349.0]], dtype=np.float32)dst_point = np.array([[38.29, 51.69], [73.53, 51.69], [56.02, 71.73]], dtype=np.float32)# python interface mat_trans = cv2.getAffineTransform(src_point, dst_point)res = cv2.warpAffine(img, mat_trans, (600,800))cv2.imwrite("py_img.png", res)
# C++ interfacewarpffine_img = affine_opencv(img, src_point, dst_point)cv2.imwrite("cpp_img.png", warpffine_img)
从上述代码可以看到,Python 文件中调用了 affine_opencv 函数,而 affine_opencv 的 C++ 实现在 orbbec/warpaffine/src/cpu/warpaffine_opencv.cpp 中,如下所示:
#include<vector>#include<iostream>#include<pybind11/pybind11.h>#include<pybind11/numpy.h>#include<pybind11/stl.h>#include<opencv2/opencv.hpp>
namespace py = pybind11;
/* Python->C++ Mat */cv::Mat numpy_uint8_1c_to_cv_mat(py::array_t<unsigned char>& input){ ...}
cv::Mat numpy_uint8_3c_to_cv_mat(py::array_t<unsigned char>& input){ ...}
/* C++ Mat ->numpy */py::array_t<unsigned char> cv_mat_uint8_1c_to_numpy(cv::Mat& input){ ...}
py::array_t<unsigned char> cv_mat_uint8_3c_to_numpy(cv::Mat& input){ ...}
py::array_t<unsigned char> affine_opencv(py::array_t<unsigned char>& input, py::array_t<float>& from_point, py::array_t<float>& to_point){ ...}
由于本工程同时兼容了 PyTorch 的 C++/CUDA 拓展,为了更加规范,这里在拓展接口程序(orbbec/warpaffine/src/warpaffine_ext.cpp)中通过 PYBIND11_MODULE 定义好接口,如下所示:
#include <torch/extension.h>#include<pybind11/numpy.h>
// python的C++拓展函数申明py::array_t<unsigned char> affine_opencv(py::array_t<unsigned char>& input, py::array_t<float>& from_point, py::array_t<float>& to_point);
// Pytorch的C++拓展函数申明(CPU)at::Tensor affine_cpu(const at::Tensor& input, /*[B, C, H, W]*/ const at::Tensor& affine_matrix, /*[B, 2, 3]*/ const int out_h, const int out_w);
// Pytorch的CUDA拓展函数申明(GPU)#ifdef WITH_CUDAat::Tensor affine_gpu(const at::Tensor& input, /*[B, C, H, W]*/ const at::Tensor& affine_matrix, /*[B, 2, 3]*/ const int out_h, const int out_w);#endif
// 通过WITH_CUDA宏进一步封装Pytorch的拓展接口at::Tensor affine_torch(const at::Tensor& input, /*[B, C, H, W]*/ const at::Tensor& affine_matrix, /*[B, 2, 3]*/ const int out_h, const int out_w){ if (input.device().is_cuda()) {#ifdef WITH_CUDA return affine_gpu(input, affine_matrix, out_h, out_w);#else AT_ERROR("affine is not compiled with GPU support");#endif } return affine_cpu(input, affine_matrix, out_h, out_w);}
// 使用pybind11模块定义python/pytorch接口PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { m.def("affine_opencv", &affine_opencv, "affine with c++ opencv"); m.def("affine_torch", &affine_torch, "affine with c++ libtorch");}
从上面代码可以看出,Python 中的 np.array 数组与 pybind11 的 py::array_t 相互对应,也即 Python 接口函数中,传入的 np.array 数组,在 C++ 对应的函数中用 py::array_t 接收,操作 Numpy 数组,需要引入头文件。
数组本质上在底层是一块一维的连续内存区,通过 pybind11 中的 request() 函数可以把数组解析成 py::buffer_info 结构体,buffer_info 类型可以公开一个缓冲区视图,它提供对内部数据的快速直接访问,如下代码所示:
struct buffer_info { void *ptr; // 指向数组(缓冲区)数据的指针 py::ssize_t itemsize; // 数组元素总数 std::string format; // 数组元素格式(python表示的类型) py::ssize_t ndim; // 数组维度信息 std::vector<py::ssize_t> shape; // 数组形状 std::vector<py::ssize_t> strides; // 每个维度相邻元素的间隔(字节数表示)};
在写好 C++ 源码以后,在 setup.py 中将相关 C++ 源文件,以及依赖的第三方库:opencv、pybind11 的路径写入对应位置(本工程已经写好,请具体看 setup.py 文件),然后进行编译和安装:
# 切换工作路径step 1: cd F:/code/python_cpp_extension# 编译step 2: python setup.py develop# 安装, 如果没有指定--prefix, 则最终编译成功的安装包(.egg)文件会安装到对应的python环境下的site-packages下.step 3: python setup.py install
【注】:关于工程文件中的 setup.py 相关知识可以参考文献 [7]、[12]、[13],该三篇文献对此有详细的解释。
执行 step 2 和 step3 之后,如下图所示,最终源码文件会编译成 .pyd 二进制文件(Linux 系统下编译成 .so 文件),且会生成一个 Python 包文件:orbbec-0.0.1-py36-win-amd64.egg,包名取决于 setup.py 中规定的 name 和 version 信息,该安装包会被安装在当前 Python环境的 site-packages 文件夹下。
同时,在终端执行命令:pip list,会发现安装包以及对应的版本信息。安装成功后,也就意味着,在该 Python环境(本工程的 Python环境是 cpp_extension)下,可以在任何一个 Python 文件中,导入 orbbec 安装包中的接口函数,比如上述 scripts/test_warpaffine_opencv.py 文件中的语句:from orbbec.warpaffine import affine_opencv。
编译和安装成功
pip list 显示相关安装包信息
编译完成后,可以运行 tools/collect_env.py,查看当前一些必要工具的版本等一系列信息,输出如下:
sys.platform : win32Python : 3.6.13 |Anaconda, Inc.| (default, Mar 16 2021, 11:37:27) [MSC v.1916 64 bit (AMD64)]CUDA available : TrueCUDA_HOME : C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.1NVCC : Not AvailableGPU 0 : NVIDIA GeForce GTX 1650OpenCV : 3.4.0PyTorch : 1.5.0PyTorch compiling details : PyTorch built with: - C++ Version: 199711 - MSVC 191627039 - Intel(R) Math Kernel Library Version 2020.0.0 Product Build 20191125 for Intel(R) 64 architecture applications - Intel(R) MKL-DNN v0.21.1 (Git Hash 7d2fd500bc78936d1d648ca713b901012f470dbc) - OpenMP 200203 - CPU capability usage: AVX2 - CUDA Runtime 10.1 - NVCC architecture flags: -gencode;arch=compute_37,code=sm_37;-gencode;arch=compute_50,code=sm_50;-gencode;arch=compute_60,code=sm_60;-gencode;arch=compute_61,code=sm_61;-gencode;arch=compute_70,code=sm_70;-gencode;arch=compute_75,code=sm_75;-gencode;arch=compute_37,code=compute_37 - CuDNN 7.6.4 - Magma 2.5.2 - Build settings: BLAS=MKL, BUILD_TYPE=Release, CXX_FLAGS=/DWIN32 /D_WINDOWS /GR /w /EHa /bigobj -openmp -DNDEBUG -DUSE_FBGEMM, PERF_WITH_AVX=1, PERF_WITH_AVX2=1, PERF_WITH_AVX512=1, USE_CUDA=ON, USE_EXCEPTION_PTR=1, USE_GFLAGS=OFF, USE_GLOG=OFF, USE_MKL=ON, USE_MKLDNN=ON, USE_MPI=OFF, USE_NCCL=OFF, USE_NNPACK=OFF, USE_OPENMP=ON, USE_STATIC_DISPATCH=OFF,
TorchVision : 0.6.0C/C++ Compiler : MSVC 191627045CUDA Compiler : 10.1
在运行 scripts/test_warpaffine_opencv.py 文件之前,由于 warpaffine_opencv.cpp 源码用到相关 opencv 库,因此,还需要配置动态库路径,Windows 系统配置如下:
Windows 相关环境配置(opencv 第三方库)
Linux 系统同样也需要配置进行配置,命令如下:
root@aistation:/xxx/code/python_cpp_extension# export LD_LIBRARY_PATH=/xxx/code/python_cpp_extension/3rdparty/opencv/linux/libroot@aistation:/xxx/code/python_cpp_extension# ldconfig
也可以通过修改 ~/.bashrc 文件,加入上述 export LD_LIBRARY_PATH=/...,然后命令:source ~/.bashrc。也可以直接修改配置文件 /etc/profile,与修改 .bashrc 文件 一样,对所有用户有效。
可以通过 tools 下的 Dependencies_x64_Release 工具(运行:DependenciesGui.exe),查看编译好的文件(.pyd)依赖的动态库是否都配置完好,如下图所示:
检查编译好的动态库依赖的动态库路径
可以发现,该工具没有找到 python36.dll、c10.dll、torch_cpu.dll、torch_python.dll 和 c10_cuda.dll 的路径。
这里说明一下,Python 相关的 dll 库以及 torch 相关的动态库是动态加载的,也就是说,如果你在 Python 代码中写一句:import torch,只有在程序运行时才会动态加载 torch 相关库。
所以,Dependencies_x64_Release工具检查不到编译好的 warpaffine_ext.cp36-win_amd64.pyd 文件依赖完好性。
这里还需要说明一下为什么 warpaffine_ext.cp36-win_amd64.pyd 需要依赖 torch 相关库,这是因为源文件 orbbec/warpaffine/src/warpaffine_ext.cpp 兼容了 PyTorch 的 C++ 拓展,所以依赖 torch 和 cuda 相关动态库文件,如果你单纯只在 orbbec/warpaffine/src/warpaffine_ext.cpp 实现纯粹 Python 的 C++拓展,则是不需要依赖 torch 和 cuda 相关动态库。
配置好之后,还需要将 warpaffine_ext.cp36-win_amd64.pyd 无法动态加载的动态库文件(opencv_world453.dll)放到 scripts/test_warpaffine_opencv.py 同路径之下(Linux 系统也一样),如下图所示:
拷贝动态库与测试脚本同一目录
需要注意一个问题,有时候,如果在 docker 中进行编译和安装,其最终生成的 Python 安装包(.egg)文件并不会安装到当前 Python 环境下的 site-packages 中。
也就意味着,在 Python 文件中执行:from orbbec.warpaffine import affine_opencv 会失败。
原因是 orbbec.warpaffine 并不在其 Python 的搜索路径中,这个时候有两种解决办法:一种是在执行:python setup.py install 时,加上 --prefix='install path',但是经过本人验证,有时候不可行,另外一种办法是在 Python 文件中,将 orbbec 文件夹路径添加到 Python 的搜索路径中,如下所示:
import cv2import torch # 不能删掉, 因为需要动态加载torch的一些动态库.import numpy as np
# 添加下述两行代码,这里默认此python脚本所在目录的上一层目录路径包含orbbec文件夹._FILE_PATH = os.path.dirname(os.path.abspath(__file__))sys.path.insert(0, os.path.join(_FILE_PATH, "../"))
from orbbec.warpaffine import affine_opencv # C++ interface
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。