python获取连通区域的点坐标PIL python计算连通区域_c++


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矩阵,该怎么办?


python获取连通区域的点坐标PIL python计算连通区域_二维_02


我仔细研究了一下,发现可以通过ctypes传指针,而且numpy矩阵的指针也可以得到,惊喜。


python获取连通区域的点坐标PIL python计算连通区域_python_03


利用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*类型的,故,此思路行不通。


python获取连通区域的点坐标PIL python计算连通区域_numpy怎么求回归线_04


转念又一想,既然它是int*类型,那我就p[x]这样来取值,就把它当成一个一维数组,于是成功。


python获取连通区域的点坐标PIL python计算连通区域_numpy怎么求回归线_05


那么现在问题来了,本来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


可见速度差别之大。


python获取连通区域的点坐标PIL python计算连通区域_python获取连通区域的点坐标PIL_06


内存连续问题

可能有小伙伴已经想到了,该方法可行的另一个重要前提是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++。