Python有很多种调c++的方法,有的复杂有的简单,有时使用的时候反而不知道到底该用哪一种比较好,其实没有最好的方法,只有适合不适合自己。本文从我所遇到的问题说起,然后讲述另一种比较简单的python调c++并且传参numpy矩阵的方法。该方法调用的是python自带的ctypes库,所以使用该方法不用安装或配置任何地第三方库。
背景
之前项目遇到一个问题,求二值图像连通区域,对于一般的图像其实很简单,opencv有函数可以直接调。但是我要处理的是一个3D图像,opencv没有求3D图像连通区域的函数,所以只能自己写算法实现。该算法的核心思想是使用深度遍历来判断有多少像素点是连通的,所以,看到遍历两个字那python就肯定得排除了,所以就选择了c++。
从工程上来看,我的问题可以简单描述为:python环境下有一个numpy矩阵,以及若干参数,现需要传递给c++程序计算,然后返回一个新的numpy矩阵或者直接修改原始numpy矩阵。
然后,我开始思考我到底该使用哪种方式调c++,我也调研了一些方法,在此不一一列举,总的来说,它们的缺点是:额外学习成本相对较高、得另外安装软件或库支持(例如boost)、调试起来各种奇怪的错误等等。再加上我使用的Linux服务器我并没有sudo权限,故更加限制了这些方法的使用。这些方法或许很强大,但是我的要求很简单,只要能实现我上述的要求即可,于是我开始把目光转向ctypes。
解决方案
Python使用ctypes调c++很简单,网上有一堆相关教程,但这些教程中传递的参数基本都是int、float等等一些最基本的类型,而我现在要传的是一个很大的numpy矩阵,该怎么办?
我仔细研究了一下,发现可以通过ctypes传指针,而且numpy矩阵的指针也可以得到,惊喜。
利用ctypes,numpy矩阵的指针可以这样得到:
p = src.ctypes.data_as(ctypes.POINTER(ctypes.c_int32))
于是我想,把numpy指针和numpy的shape信息传递过去不就相当于把整个numpy传递过去了吗?比如src是一个10*10的矩阵,指针为p,那么在c++中是不是就可以使用p[x][y]来定位具体的元素了?
想法是美好的,c++中可以使用p[x][y]的前提是p一个指针的指针,具体来说应该是int**类型的,而事实是p仅仅只是一个指针,是int*类型的,故,此思路行不通。
转念又一想,既然它是int*类型,那我就p[x]这样来取值,就把它当成一个一维数组,于是成功。
那么现在问题来了,本来numpy矩阵是多维的,怎样跟一维数组对应呢?对,很简单,就是把多维的矩阵拉成一维的即可,相当于numpy的一个函数reshape,想想reshape成一维的之后元素是怎样跟之前对应的,就是这个对应关系。
比如一个二维矩阵shape为(10,20),那么其中的(i,j)点对应到一维数组里面的坐标就是i*20+j,三维矩阵道理相同。
至此,关键问题解决了。
后续思考
速度问题
现有这样一个任务:三维矩阵所有元素值加一,用遍历方法实现。我们分别使用两种方法来实现并且对比速度,1,纯python实现;2,c++实现,使用刚刚的方法调c++。
c++代码如下(demo.cpp):
#include <stdio.h>
#include <stdlib.h>
void fn(int* p, int h_, int w_, int c_)
{
int h, w, c;
for (h = 0; h < h_; h++)
for (w = 0; w < w_; w++)
for (c = 0; c < c_; c++)
{
int ind = h*(w_*c_) + w*c_ + c;
p[ind] = p[ind] + 1;
}
}
python代码如下(main.py):
import numpy as np
import ctypes as ct
def test_demo():
def add1(x, h_, w_, c_):
for h in range(h_):
for w in range(w_):
for c in range(c_):
x[h, w, c] += 1
m = np.ones([10, 512, 512], np.int32)
# m = np.ones([1024, 1024, 1024], np.int32)
print(m.shape)
h, w, c = m.shape[:3]
so = ct.cdll.LoadLibrary("demo.so")
p = m.ctypes.data_as(ct.POINTER(ct.c_int32))
t1 = time.time()
so.fn(p, h, w, c)
print('c++ : ', time.time() - t1)
t1 = time.time()
add1(m, h, w, c)
print('python: ', time.time() - t1)
if __name__ == '__main__':
test_demo()
然后使用下面命令编译成动态链接库:
g++ -o demo.so -std=c++11 -shared -fPIC demo.c
最后运行main.py,结果如下:
(10, 512, 512)
c++ : 0.009840011596679688
python: 7.284840822219849
可见速度差别之大。
内存连续问题
可能有小伙伴已经想到了,该方法可行的另一个重要前提是numpy矩阵所有元素在内存中必须是连续的,若不连续肯定会出错。但是有时候我们传递的numpy矩阵可能就是不连续的,那该怎么办?很简单,仅仅只需改变一下传递姿势。
还拿刚才的例子举例,还是元素自加一,但是为了方便展示,我们把numpy矩阵改为二维的。首先创建一个10*10的值为1的二维矩阵m0,然后只取其左上角5*5的一部分m,可以看出m肯定是不连续的,现把m传参测试,python代码如下:
def test_demo():
m0 = np.ones([10, 10], np.int32)
m = m0[:5, :5]
print(m.shape)
h, w = m.shape[:2]
so = ct.cdll.LoadLibrary("demo.so")
p = m.ctypes.data_as(ct.POINTER(ct.c_int32))
so.add1(p, h, w)
print(m0)
print(m)
结果:
(5, 5)
[[2 2 2 2 2 2 2 2 2 2]
[2 2 2 2 2 2 2 2 2 2]
[2 2 2 2 2 1 1 1 1 1]
[1 1 1 1 1 1 1 1 1 1]
[1 1 1 1 1 1 1 1 1 1]
[1 1 1 1 1 1 1 1 1 1]
[1 1 1 1 1 1 1 1 1 1]
[1 1 1 1 1 1 1 1 1 1]
[1 1 1 1 1 1 1 1 1 1]
[1 1 1 1 1 1 1 1 1 1]]
[[2 2 2 2 2]
[2 2 2 2 2]
[2 2 2 2 2]
[1 1 1 1 1]
[1 1 1 1 1]]
可以看出,m0和m的值都被改变了,m的值并没有全部改变,只改变了一部分,而且m0刚好前5*5=25个值改变了,这说明了m并不是深拷贝,它仍然和m0共用一块内存空间,c++处理的时候只会连续着处理,所以只改变了连续空间的这一段长度为5*5的值。
当然,解决方法很简单,只需不让它们共用一份内存重新拷贝一份即可。上述第三行修改为:
m = copy.deepcopy(m0[:5, :5])
或:
m = m0[:5, :5].copy()
即可。
写在后面
前面已经说过,没有最好的方法,只有最适合自己的。我认为这种方法足够简单高效,前提是它能满足你的需求,比如类似传numpy矩阵这种。
对了,还有一点需要注意的地方,就是一定要保证numpy元素的数据类型跟c++中的一致,比如一个numpy的dtype为np.uint8,在c++中却使用int*,那肯定会出错,可以先使用astype函数把数据类型转成一致,然后再调c++。