再Notes: 对编译文件setup.py
进行了修改。修改后内容见https://github.com/232525/ViBe.Cython
我发现ViBe官方开源的C代码里面提供的Makefile文件对代码进行编译时,是添加了优化参数-O3
的,于是我把它使用了的编译参数直接添加到了setup.py
里面,然后重新编译,再经由python调用的速度确实就和直接运行C源码差不多了。
改动部分:
assert os.system("gcc -shared -fPIC -c vibe-background-sequential.c -o vibe-background-sequential.o") == 0
改为
assert os.system("gcc -std=c99 -O3 -Wall -Werror -pedantic -Wno-unused-function -Wno-unused-parameter -Wno-deprecated -Wno-deprecated-declarations -Wno-sign-compare -Wno-unused-but-set-parameter -shared -fPIC -c vibe-background-sequential.c -o vibe-background-sequential.o") == 0
2020/07/23添加
------------------分割线--------------------
Notes: 对.pyx
文件进行了一点修改。修改后内容见https://github.com/232525/ViBe.Cython
主要是之前写的用Cython可视化之后发现和python交互的部分有点多,就改了一下,虽然交互变少了,但是速度没有啥明显变化,我单独测试了Cython中调用C函数的语句,和Python调用Cython的包装函数相比,时间差不多,所以说时间瓶颈应该就来自于C函数的调用?但是为什么通过Cython调用的C函数(Ubuntu 16.04环境下)相比我直接运行C代码(Windows 10)速度慢了不少呢?因为平台编译的差异?
2020/07/22 添加
------------------分割线--------------------
0 引言
直接用python复现的ViBe速度太慢,所以想用Cython直接复用ViBe开源的C源码,昨天(2020/07/20)找了一天的资料,翻遍整个百度,都是些不痛不痒的教程,官方的API又太长了,没耐心看。反正后面莫名其妙就打包复用成功了,对Cython还是一知半解(啥也不懂)。
所有代码:https://github.com/232525/ViBe.Cython
1 主要参考
- Cython中文文档:很短,主要参考了第三章(英文),整个复用的逻辑框架,基础的基础,实际上也就是Cython官方文档中的这一小节内容;
- Cython官方文档1:解决了如何将numpy数组(一帧图像)转换为C源码可用的指针;
- Cython官方文档2:Cython中C代码结构体的声明
- Cython源码提供的Demo:解决了setup.py的基本编写问题,如何编译出python可用的动态链接库文件;
- StackOverflow的一个问题【Cython: “fatal error: numpy/arrayobject.h: No such file or directory”】:解决了将numpy数组转为C指针之后,编译报错的问题(也就是需要需要改setup.py)。
2 复用逻辑
首先,手头上有的工具:
- ViBe的C源码,包括
vibe-background-sequential.h
和vibe-background-sequential.c
,分别提供了ViBe算法相关函数的声明和实现 - Cython,用来打包封装C源码,提供Python可调用的动态库文件
- Python,调用Cython提供的动态库文件,间接调用了ViBe的C源码
2.1 Cython封装ViBe的C源码
参考Cython中文文档,就像C语言有着头文件和C文件一样,Cython有.pxd
和.pyx
文件,其中.pxd
包含我们需要从C源码中复用的函数声明,而.pyx
文件需要编写复用这些函数的python接口。
2.1.1.pxd
文件
对于.pxd
文件,我们需要复用如下数据结构及核心函数(输入均为灰度图):
- 结构体
vibeModel_Sequential_t
,定义了ViBe算法需要使用的相关参数,核心之核心 - 函数
libvibeModel_Sequential_New
,用于创建一个结构体对象 - 函数
libvibeModel_Sequential_Free
,用于释放资源 - 函数
libvibeModel_Sequential_AllocInit_8u_C1R
,结构体参数初始化操作,ViBe算法核心操作之一,于视频第一帧时调用 - 函数
libvibeModel_Sequential_Segmentation_8u_C1R
,获取视频帧前景背景识别mask,ViBe算法核心操作之二(算法的意义所在),于视频每一帧调用 - 函数
libvibeModel_Sequential_Update_8u_C1R
,结构体参数更新操作,ViBe算法核心操作之三,于视频每一帧调用
.pxd
文件内函数的声明完全复制自C源码的头文件,结构体的声明参考了Cython中文文档和Cython官方文档2,数据类型的定义参考了这篇博客,也就是C99中实际定义的数据类型别名吧?
# -*- coding: utf-8 -*-
"""
Created on 2020/7/20
@author: curya
"""
# filename: c_lib.pxd
cdef extern from "vibe-background-sequential.h":
ctypedef unsigned char uint8_t
ctypedef unsigned short int uint16_t
ctypedef unsigned int uint32_t
ctypedef int int32_t
ctypedef struct vibeModel_Sequential_t:
pass
vibeModel_Sequential_t *libvibeModel_Sequential_New()
int32_t libvibeModel_Sequential_Free(vibeModel_Sequential_t *model)
int32_t libvibeModel_Sequential_AllocInit_8u_C1R(vibeModel_Sequential_t *model, const uint8_t *image_data,
const uint32_t width, const uint32_t height)
int32_t libvibeModel_Sequential_Segmentation_8u_C1R(vibeModel_Sequential_t *model, const uint8_t *image_data, uint8_t *segmentation_map)
int32_t libvibeModel_Sequential_Update_8u_C1R(vibeModel_Sequential_t *model, const uint8_t *image_data, uint8_t *updating_mask)
2.1.2.pyx
文件
现在我们有了Cython的头文件.pxd
,需要编写.pyx
文件进行实际的封装。同样,参考Cython中文文档,我们对.pxd
文件中声明的结构体编写一个包装类(将其定义为ViBe
),编译之后,可以在python中导入这个包装类ViBe
,通过它的内部函数来间接调用C代码(它的内部函数直接调用C代码,需要我们编写内部函数)。
我们需要将之前的.pxd
文件导入进来cimport c_vibe
,然后对于里面声明了的C函数(即我们需要复用的C函数),可以直接通过c_vibe.function_name()
进行调用;除此之外还有import numpy as np
,数据类型的定义和.pxd
文件一致。
我们的包装类ViBe
只有一个变量,即结构体c_vibe.vibeModel_Sequential_t
定义的指针model
,对于前面我们声明了的五个函数,分别包装在类的不同函数中,当调用类ViBe
的函数时,即相应的调用了其包含的C函数:
c_vibe.libvibeModel_Sequential_New
,在包装类的函数__cinit__()
中调用,在创建ViBe
对象时自动调用,即相当于通过__cinit__
调用的C函数创建了一个C结构体对象;c_vibe.libvibeModel_Sequential_Free
,在包装类的函数__dealloc__()
中调用c_vibe.libvibeModel_Sequential_AllocInit_8u_C1R
,函数AllocInit()
c_vibe.libvibeModel_Sequential_Segmentation_8u_C1R
,函数Segmentation()
c_vibe.libvibeModel_Sequential_Update_8u_C1R
,函数Update()
问题: 函数c_vibe.libvibeModel_Sequential_AllocInit_8u_C1R
、c_vibe.libvibeModel_Sequential_Segmentation_8u_C1R
和c_vibe.libvibeModel_Sequential_Update_8u_C1R
的输入参数包含视频图像帧的指针,而我们用python-opencv读取视频帧的话,数据类型为np.array,因此需要进行转换,参考Cython官方文档1。
if not image.flags['C_CONTIGUOUS']:
image = np.ascontiguousarray(image) # 保证内存连续
cdef uint8_t[:, :] image_data = image # 类型转换,此处image为灰度图,即大小为H x W,不含通道channel维度
# Pointer: &image_data[0, 0] # 取地址
# -*- coding: utf-8 -*-
"""
Created on 2020/7/20
@author: curya
"""
# filename: py_vibe.pyx
# refer to:
# https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html#pass-data-from-a-c-function-via-pointer
# https://moonlet.gitbooks.io/cython-document-zh_cn/content/ch3-using_c_libraries.html
# https://github.com/cython/cython/blob/master/Demos/libraries/setup.py
# https://stackoverflow.com/questions/14657375/cython-fatal-error-numpy-arrayobject-h-no-such-file-or-directory
import numpy as np
# cimport numpy as np
cimport c_vibe
ctypedef unsigned char uint8_t
ctypedef unsigned short int uint16_t
ctypedef unsigned int uint32_t
ctypedef int int32_t
cdef class ViBe:
cdef c_vibe.vibeModel_Sequential_t* model
def __cinit__(self):
self.model = c_vibe.libvibeModel_Sequential_New()
if self.model is NULL:
raise MemoryError()
def __dealloc__(self):
if self.model is not NULL:
c_vibe.libvibeModel_Sequential_Free(self.model)
def AllocInit(self, image):
cdef uint32_t width = <uint32_t>image.shape[1]
cdef uint32_t height = <uint32_t>image.shape[0]
if not image.flags['C_CONTIGUOUS']:
image = np.ascontiguousarray(image)
cdef uint8_t[:, :] image_data = image
# &image_data[0, 0] --> the pointer of image
# call C function
c_vibe.libvibeModel_Sequential_AllocInit_8u_C1R(self.model, &image_data[0, 0], width, height)
def Segmentation(self, image):
if not image.flags['C_CONTIGUOUS']:
image = np.ascontiguousarray(image)
cdef uint8_t[:, :] image_data = image
cdef uint8_t[:, :] segmentation_map = np.zeros_like(image)
# call C function
c_vibe.libvibeModel_Sequential_Segmentation_8u_C1R(self.model, &image_data[0, 0], &segmentation_map[0, 0])
# Error: need convert segmentation_map to numpy.array
# convert on python main function
return segmentation_map
def Update(self, image, segmentation):
if not image.flags['C_CONTIGUOUS']:
image = np.ascontiguousarray(image)
if not segmentation.flags['C_CONTIGUOUS']:
segmentation = np.ascontiguousarray(image)
cdef uint8_t[:, :] image_data = image
cdef uint8_t[:, :] updating_mask = segmentation
# call C function
c_vibe.libvibeModel_Sequential_Update_8u_C1R(self.model, &image_data[0, 0], &updating_mask[0, 0])
2.1.3 setup.py
文件
编写了.pxd
和.pyx
文件之后,我们需要编写setup.py
文件将我们写的Cython代码编译为Python可调用的动态库文件。(也可以直接通过gcc进行命令行编译)
python setup.py build_ext --inplace
一个大致的逻辑就是:
- 将C源码编译为动态库文件
.so
或者静态库文件.a
- 将Cython代码转换为C代码
- 将Cython转换过来的C代码,联合第一步编译的库文件,编译为Python可调用的动态库文件
# -*- coding: utf-8 -*-
"""
Created on 2020/7/20
@author: curya
"""
from __future__ import absolute_import, print_function
import os
import sys
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import numpy as np
# For demo purposes, we build our own tiny library.
try:
print("building libmymath.a")
# 将C代码编译为.o目标文件
assert os.system("gcc -shared -fPIC -c vibe-background-sequential.c -o vibe-background-sequential.o") == 0
# 将.o目标文件编译为.a静态库文件
assert os.system("ar rcs libvibe-background-sequential.a vibe-background-sequential.o") == 0
except:
if not os.path.exists("libvibe-background-sequential.a"):
print("Error building external library, please create libmymath.a manually.")
sys.exit(1)
# Here is how to use the library built above.
ext_modules = cythonize([
Extension("py_vibe",
sources=["py_vibe.pyx"],
include_dirs=[os.getcwd(), np.get_include()], # path to .h file(s), np.get_include(): avoid fatal error: numpy/arrayobject.h: No such file or directory”
library_dirs=[os.getcwd()], # path to .a or .so file(s)
libraries=['vibe-background-sequential']) # 静态库文件libvibe-background-sequential.a的名称
])
# , annotate=True
setup(
name='Demos',
ext_modules=ext_modules,
)
2.2 Python调用Cython封装的动态库
测试代码参考上一篇博客,只需要将导入部分from lib_vibe import vibe_gray
替换为from lib_vibe.py_vibe import ViBe
,以及vibe = vibe_gray()
替换为vibe = ViBe()
即可。
测试结果,相比python肯定是快了不止一丁点,对比纯C,效率有一点折扣。