基於pybind11實現Python調用c++編寫的CV算法--下 (Linux+Cmake)


C++ 是一種編譯型(compiled)語言,設計重點是性能、效率和使用靈活性,偏向於系統編程、嵌入式、資源受限的軟件和系統。

Python是一種解釋型(interpreted)語言,同樣也支持不同的編程范式。Python 內置了常用數據結構(str, tuple, list, dict),簡潔的語法、豐富的內置庫(os,sys,urllib,...)和三方庫(numpy, tf, torch ...),功能強大。最為重要的是和能夠和多種服務(flask…)和tensorflow、pytorch等無縫聯合,從而方便將你的算法開放出去。

一方面,我們需要編譯型語言(C++)性能;一方面,也需要解釋型語言(Python)的靈活。這時,pybind11 可以用作 C++ 和 Python 之間溝通的橋梁。

Pybind11 是一個輕量級只包含頭文件的庫,用於 Python 和 C++ 之間接口轉換,可以為現有的 C++ 代碼創建 Python 接口綁定。Pybind11 通過 C++ 編譯時的自省來推斷類型信息,來最大程度地減少傳統拓展 Python 模塊時繁雜的樣板代碼, 已經實現了 STL 數據結構、智能指針、類、函數重載、實例方法等到Python的轉換,其中函數可以接收和返回自定義數據類型的值、指針或引用。

由於在Windows上和在Linux上使用會有較大不同,所以我這里將分為兩個部分來說明問題,本文為下篇,具體說明Linux+Cmake實現。

我認為在Linux上使用python調用c++函數更有現實價值,畢竟許多新的服務、深度運算等都是運行在linux上的。具體步驟可以參考如下。

1、Linux下python調用c++的 安裝配置

下載pybind11
git clone https://github.com/pybind/pybind11.git

安裝pytest
pip install pytest

編譯安裝。這個地方我建議你首先將下載下來的pybind11備份一份

cd pybind11
mkdir build
cd build
cmake ..
cmake  -- build .  -- config Release  -- target check
 
這個編譯的過程非常專業。

2、編譯最簡單的代碼
在Linux上編譯,我們一般選擇gcc的方式。

$ c ++  - O3  - Wall  - shared  - std = c ++ 11  - fPIC  ` python3  - m pybind11  -- includes `  example.cpp  - o example ` python3 - config  -- extension - suffix `
成功調用。但是目前直接是使用gcc進行編譯的,實際情況是可能需要調用其它的庫,比如OpenCV,這樣就需要進一步研究。

3、使用 Cmake進行編譯
使用 cmake 創建工程,編譯為動態庫,然后使用 python 測試。 
寫一個CMakeLists.txt,注意要理解它的意思
cmake_minimum_required(VERSION  2. 8. 12)
project(example)   

add_subdirectory(pybind11)
pybind11_add_module(example example.cpp)
這里要求example.cpp放在和pybind11同一級的目錄下,因為我們在CMakeLists.txt中調用了同目錄pybind11和同目錄的example.cpp文件。在當前目錄下執行。這里需要注意,正確的文件方法:
就是CMakeList.txt和example.cpp和pybind11(最高層)放在一個目錄下面。
cmake .
make
會生成example.cpython-36m-x86_64-linux-gnu.so文件。
這個文件就是python可以調用的文件。還是在相同目錄下運行python,進入python命令行
import example
example.add( 34)
[out] :  7
非常好的效果,對於解決系列問題來說都是有幫助的。

4、如何和OpenCV相結合
這里困難的一點就是使用CMake編譯OpenCV程序,在之前,我都是借助QT或者VS來完成這個工作,而這里只能給使用CMake來完成這個工作,這個是首先要解決的問題,然后才是PYD(so)的問題。經過尋覓,終於找到(參考1)。這個部分一定要注意,可以說是本篇博客最有價值的地方,也是我花費時間最長的地方。
project(example) 
cmake_minimum_required(VERSION 2.8.12)
find_package(OpenCV REQUIRED) 
include_directories(${OpenCV_INCLUDE_DIRS})
add_subdirectory(pybind11)

pybind11_add_module(example example.cpp)
target_link_libraries(example PRIVATE ${OpenCV_LIBS})
project(example)
cmake_minimum_required(VERSION  2. 8. 12)
 
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
add_subdirectory(pybind11)
 
SET(SOURCES
  ${CMAKE_CURRENT_SOURCE_DIR} /example.cpp
)
 
pybind11_add_module(example ${SOURCES})
target_link_libraries(example  PRIVATE  ${OpenCV_LIBS})
簡單分析一下這段Cmake,除了必須的項目名稱等以外,就是簡單地去尋找OpenCV等的地址,而后將lib輸入進去。 pybind11_add_module相當於建立項目,使用Set方法方便批量處理。
其中注意兩點:
1、target_link_libraries(example PRIVATE ${OpenCV_LIBS}) 放最后
2、xample PRIVATE 不可缺少,否則報這個錯
成功調用結果, 注意絕對地址。

5、Mat輸入,Vector輸出
這里繼續實際問題的研究,這里仍然會有一些新的Cmake問題。
project(example) 
cmake_minimum_required(VERSION  2. 8. 12)

find_package(OpenCV REQUIRED) 
include_directories(${OpenCV_INCLUDE_DIRS})
add_subdirectory(pybind11)

SET(SOURCES
  ${CMAKE_CURRENT_SOURCE_DIR} /example.cpp
  ${CMAKE_CURRENT_SOURCE_DIR} /mat_warper.h
  ${CMAKE_CURRENT_SOURCE_DIR} /mat_warper.cpp
)
 
pybind11_add_module(example ${SOURCES})
target_link_libraries(example PRIVATE ${OpenCV_LIBS})
 
應該是一次性通過的。這些沒有太大問題。比較關鍵的問題就是部署,然后就是總結備份了。
可以直接將現有算法以“三明治”的方式添加上去。其中需要注意GOCVHelper改動較多。

6、移植和封裝
我希望FindPip能夠成為一個比較標准的庫,也就是在Python中能夠以標准的方法調用:輸入圖片,輸出圓心組;並且在新的系統中能夠直接通過Cmake+make進行部署。這樣的結果才方便別人使用。現在的話,應該可以將.so文件和它的支持庫文件,一起拷貝吧。
現在的話,只要他們兩個在一起,main.py可以直接調用GOPyWarper***.so。這個結果是能夠被接受的。
我需要在另一台Ubuntu上進行實驗,如果可行就可以發布了。但是需要注意pybind11可能產生級聯問題。【還需要跟多異構實驗】
具體方法:
1、下載解壓;
 tar  -xvf GOPyWarper0429.tar 

2、編譯(后附完整編譯)
mkdir build
cd build
cmake .. 
make
cp GOPyWarper.cpython - 36m -x86_64 -linux -gnu.so ../demo
cd ../demo
python3 main.py

3、 demo.py解讀

import cv2
import GOPyWarper
import numpy as np

#獲取圖片,彩色3通道。
#中文和空格不支持
src  = cv2.imread( 'pip.jpg', 1

#GO_FindPips
#輸入mat,輸出為list(point1,point2,……),其中point代表一個找到的圓心。.
varCircles  = GOPyWarper.GO_FindPips(src)
#print(varCircles)

#GO_Resize
#輸入mat,輸出為規則化后文件大小
varResize  = GOPyWarper.GO_Resize(src)

#繪圖
dst =cv2.resize(src,(( int)(varResize[ 0]),( int)(varResize[ 1])),interpolation =cv2.INTER_CUBIC)
for i  in varCircles[ :] :
    cv2.circle(dst,(i[ 0],i[ 1]), 5,( 0, 255, 0), - 1)
    
cv2.imshow( "dst",dst)
cv2.waitKey( 0)

var1為圓心數組。這里生成的結果,只有這個*.so文件是需要保留的,可以拷貝出來,其他文件可以刪除。
結果截圖:
全部命令:
helu@helu -virtual -machine : ~ /sandbox$ tar  -cvf GOPyWarper0430.tar GOPyWarper0430
......
helu@helu -virtual -machine : ~ /sandbox$ cd GOPyWarper0430
helu@helu -virtual -machine : ~ /sandbox /GOPyWarper0430$ mkdir build
helu@helu -virtual -machine : ~ /sandbox /GOPyWarper0430$ cd build
helu@helu -virtual -machine : ~ /sandbox /GOPyWarper0430 /build$ cmake ..
......
helu@helu -virtual -machine : ~ /sandbox /GOPyWarper0430 /build$ make 
Scanning dependencies of target GOPyWarper
20 %] Building CXX object CMakeFiles /GOPyWarper.dir /src /GOPyWarper.cpp.o
40 %] Building CXX object CMakeFiles /GOPyWarper.dir /src /mat_warper.cpp.o
60 %] Building CXX object CMakeFiles /GOPyWarper.dir /src /GOCVHelper_2019_11_29.cpp.o
80 %] Building CXX object CMakeFiles /GOPyWarper.dir /src /GOFindPips.cpp.o
[ 100 %] Linking CXX shared module GOPyWarper.cpython - 36m -x86_64 -linux -gnu.so
[ 100 %] Built target GOPyWarper
helu@helu -virtual -machine : ~ /sandbox /GOPyWarper0430 /build$  cp GOPyWarper.cpython - 36m -x86_64 -linux -gnu.so .. /demo /
helu@helu -virtual -machine : ~ /sandbox /GOPyWarper0430 /build$ cd .. /demo /
helu@helu -virtual -machine : ~ /sandbox /GOPyWarper0430 /demo$ python3 main.py 

需要注意的一點是, 實際部署的時候,發現在沒有安裝OpenCV的機器上,報這個錯誤

那么也就是說opencv_python那種命令行安裝的方式是不行的,必須采用cmake完整安裝。我在一個全新的ubuntu上安裝最新版OpenCV后獲得如下回顯:
helu@helu -virtual -machine : ~ /workstation /GOPyWarper0430$ cd build /
helu@helu -virtual -machine : ~ /workstation /GOPyWarper0430 /build$ cmake ..
-- Found OpenCV :  /usr /local (found version  "4.3.0"
-- Found PythonInterp :  /usr /bin /python3. 8 (found version  "3.8.2"
-- Found PythonLibs :  /usr /lib /x86_64 -linux -gnu /libpython3. 8.so
-- pybind11 v2. 5.dev1
-- Performing Test HAS_FLTO
-- Performing Test HAS_FLTO  - Success
-- LTO enabled
-- Configuring  done
-- Generating  done
-- Build files have been written to :  /home /helu /workstation /GOPyWarper0430 /build
helu@helu -virtual -machine : ~ /workstation /GOPyWarper0430 /build$ make
Scanning dependencies of target GOPyWarper
20 %] Building CXX object CMakeFiles /GOPyWarper.dir /src /GOPyWarper.cpp.o
40 %] Building CXX object CMakeFiles /GOPyWarper.dir /src /mat_warper.cpp.o
60 %] Building CXX object CMakeFiles /GOPyWarper.dir /src /GOCVHelper_2019_11_29.cpp.o
80 %] Building CXX object CMakeFiles /GOPyWarper.dir /src /GOFindPips.cpp.o
[ 100 %] Linking CXX shared module GOPyWarper.cpython - 38 -x86_64 -linux -gnu.so
[ 100 %] Built target GOPyWarper
helu@helu -virtual -machine : ~ /workstation /GOPyWarper0430 /build$ 

7、性能比較

從原理上來說,基於python調用C++函數,其性能應該是依次劣於c++原生代碼和 opencv_python 的。為了驗證這個結論是否正確,我選擇對lena.jpg做經典的GaussBlur操作,並且分別統計在c++原生、opencv_python和pbind11調用情況下的速度。全部以ms計數。我選擇了一個比較大的核,這樣才能夠將時間差異拉出來。

GaussBlur windows實體機 c++原生 ubuntu虛擬機 opencv_python ubuntu虛擬機 pbind11
1次 32 34 40
重復100次 2819 3740 3891

參考代碼
原生c++
int main() {
    string path  =  "e:/template/lena.jpg";
    cv : :Mat src  = cv : :imread(path);
    Mat     dst;
 
     //開始計時
     double dstart  = ( double)cv : :getTickCount();
     for ( int i = 0;i < = 100;i ++)
    {
        cv : :GaussianBlur(src, dst, cv : :Size( 101101),  1. 01. 0);
        printf( "%d times %f ms\n", i, 1000  * (getTickCount()  - dstart)  / getTickFrequency());
    
    }
    cv : :waitKey( 0);
     return  0;
}
原生python
import cv2
import GOPyWarper
import numpy as np


src  = cv2.imread( '/home/helu/images/lena.jpg', 1
dstart  = cv2.getTickCount()

for i  in  range( 100) :
    blur  = cv2.GaussianBlur(src,( 101, 101), 1. 0, None, 1. 0,borderType = 4)
     print( 1000  * ( cv2.getTickCount()  - dstart)  /cv2.getTickFrequency())

pybind11調用
import cv2
import GOPyWarper
import numpy as np

#獲取圖片,彩色3通道。
#中文和空格不支持
src  = cv2.imread( '/home/helu/images/lena.jpg', 1
dstart  = cv2.getTickCount()

for i  in  range( 100) :
    blur  = GOPyWarper.test_gaussblur(src)
     print( 1000  * ( cv2.getTickCount()  - dstart)  /cv2.getTickFrequency())

這里基本能夠認識到一些問題,但是也必須認識到,一方面開發效率也是效率,對於現有代碼的整合使用,pybind11是不可替代的;此外,對於集成的函數,可能相關結果不一定如這里的單個函數這樣明確。如果現在沒有非常明確的需求要使用python編寫,那么c++&pybind11的方法是首選。

8、遺留問題
命名問題,也就是目前在c++函數 、project名稱、add_module三者都必須是統一的。這里不一定必須是這樣,但是目前是有效的。

9、小結:主要是在CMake上花費了不少時間,但是只要方向正確,有用的資源一定會源源不斷。積累相關經驗,繼續前進。



========================參考====== ====== ====== ====== ======








免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM