深度解決添加復雜數(shù)據(jù)增強導致訓練模型耗時長的痛點(2)
4. C++ And CUDA Extensions
For Python/ PyTorch
C++ 與 Python 或 PyTorch 的交互,業(yè)界主流做法是采用 pybind11,關于Pybind11 的更多詳細說明可以參看文獻 [15],其核心原理如下圖所示:
pybind11 pipeline
由于 PyTorch 的 C++ 拓展與純 Python 有一些區(qū)別,因為 PyTorch 的基礎數(shù)據(jù)類型是 torch.Tensor,該數(shù)據(jù)類型可以認為是 Pytorch 庫對 np.array 進行了更高一層的封裝。所以,在寫拓展程序時,其接口函數(shù)所需要的數(shù)據(jù)類型以及調(diào)用的庫會有些區(qū)別,下面會詳細解釋。
4.1. C++ Extensions For Python
首先我們看 Python 代碼,如下所示(scripts/test_warpaffine_opencv.py):
import cv2import torch # 不能刪掉, 因為需要動態(tài)加載torch的一些動態(tài)庫,后面會詳細說明.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 文件中調(diào)用了 affine_opencv 函數(shù),而 affine_opencv 的 C++ 實現(xiàn)在 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 拓展,為了更加規(guī)范,這里在拓展接口程序(orbbec/warpaffine/src/warpaffine_ext.cpp)中通過 PYBIND11_MODULE 定義好接口,如下所示:
#include <torch/extension.h>#include<pybind11/numpy.h>
// python的C++拓展函數(shù)申明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++拓展函數(shù)申明(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拓展函數(shù)申明(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 數(shù)組與 pybind11 的 py::array_t 相互對應,也即 Python 接口函數(shù)中,傳入的 np.array 數(shù)組,在 C++ 對應的函數(shù)中用 py::array_t 接收,操作 Numpy 數(shù)組,需要引入頭文件。
數(shù)組本質上在底層是一塊一維的連續(xù)內(nèi)存區(qū),通過 pybind11 中的 request() 函數(shù)可以把數(shù)組解析成 py::buffer_info 結構體,buffer_info 類型可以公開一個緩沖區(qū)視圖,它提供對內(nèi)部數(shù)據(jù)的快速直接訪問,如下代碼所示:
struct buffer_info { void *ptr; // 指向數(shù)組(緩沖區(qū))數(shù)據(jù)的指針 py::ssize_t itemsize; // 數(shù)組元素總數(shù) std::string format; // 數(shù)組元素格式(python表示的類型) py::ssize_t ndim; // 數(shù)組維度信息 std::vector<py::ssize_t> shape; // 數(shù)組形狀 std::vector<py::ssize_t> strides; // 每個維度相鄰元素的間隔(字節(jié)數(shù)表示)};
在寫好 C++ 源碼以后,在 setup.py 中將相關 C++ 源文件,以及依賴的第三方庫:opencv、pybind11 的路徑寫入對應位置(本工程已經(jīng)寫好,請具體看 setup.py 文件),然后進行編譯和安裝:
# 切換工作路徑step 1: cd F:/code/python_cpp_extension# 編譯step 2: python setup.py develop# 安裝, 如果沒有指定--prefix, 則最終編譯成功的安裝包(.egg)文件會安裝到對應的python環(huán)境下的site-packages下.step 3: python setup.py install
【注】:關于工程文件中的 setup.py 相關知識可以參考文獻 [7]、[12]、[13],該三篇文獻對此有詳細的解釋。
執(zhí)行 step 2 和 step3 之后,如下圖所示,最終源碼文件會編譯成 .pyd 二進制文件(Linux 系統(tǒng)下編譯成 .so 文件),且會生成一個 Python 包文件:orbbec-0.0.1-py36-win-amd64.egg,包名取決于 setup.py 中規(guī)定的 name 和 version 信息,該安裝包會被安裝在當前 Python環(huán)境的 site-packages 文件夾下。
同時,在終端執(zhí)行命令:pip list,會發(fā)現(xiàn)安裝包以及對應的版本信息。安裝成功后,也就意味著,在該 Python環(huán)境(本工程的 Python環(huán)境是 cpp_extension)下,可以在任何一個 Python 文件中,導入 orbbec 安裝包中的接口函數(shù),比如上述 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 庫,因此,還需要配置動態(tài)庫路徑,Windows 系統(tǒng)配置如下:
Windows 相關環(huán)境配置(opencv 第三方庫)
Linux 系統(tǒng)同樣也需要配置進行配置,命令如下:
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)依賴的動態(tài)庫是否都配置完好,如下圖所示:
檢查編譯好的動態(tài)庫依賴的動態(tài)庫路徑
可以發(fā)現(xiàn),該工具沒有找到 python36.dll、c10.dll、torch_cpu.dll、torch_python.dll 和 c10_cuda.dll 的路徑。
這里說明一下,Python 相關的 dll 庫以及 torch 相關的動態(tài)庫是動態(tài)加載的,也就是說,如果你在 Python 代碼中寫一句:import torch,只有在程序運行時才會動態(tài)加載 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 相關動態(tài)庫文件,如果你單純只在 orbbec/warpaffine/src/warpaffine_ext.cpp 實現(xiàn)純粹 Python 的 C++拓展,則是不需要依賴 torch 和 cuda 相關動態(tài)庫。
配置好之后,還需要將 warpaffine_ext.cp36-win_amd64.pyd 無法動態(tài)加載的動態(tài)庫文件(opencv_world453.dll)放到 scripts/test_warpaffine_opencv.py 同路徑之下(Linux 系統(tǒng)也一樣),如下圖所示:
拷貝動態(tài)庫與測試腳本同一目錄
需要注意一個問題,有時候,如果在 docker 中進行編譯和安裝,其最終生成的 Python 安裝包(.egg)文件并不會安裝到當前 Python 環(huán)境下的 site-packages 中。
也就意味著,在 Python 文件中執(zhí)行:from orbbec.warpaffine import affine_opencv 會失敗。
原因是 orbbec.warpaffine 并不在其 Python 的搜索路徑中,這個時候有兩種解決辦法:一種是在執(zhí)行:python setup.py install 時,加上 --prefix='install path',但是經(jīng)過本人驗證,有時候不可行,另外一種辦法是在 Python 文件中,將 orbbec 文件夾路徑添加到 Python 的搜索路徑中,如下所示:
import cv2import torch # 不能刪掉, 因為需要動態(tài)加載torch的一些動態(tài)庫.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
*博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權請聯(lián)系工作人員刪除。