深度学习框架接口深入优化(一)
背景
继上次实现一个基础的优化版本完成后,我们提到了进一步优化的思路,而那之后又有了一些新的想法,在这里重新整理一下
- 上一个次更新进度中的内容本身是框架性质的东西,还不能用到项目中,需要对之前提到的draw_detection函数和test_detector函数进行修改
- 上一次实现的并行并不充分,计算量更大的部分——letterbox_image放置没管,依旧是串行的实现
这一次我们的工作就是将整个系统连成可用的框架,然后进行更深层次的并行优化
项目整理
上一次更新之后我们又尝试了很多工作,产生了不少代码,所以对这个模块的代码进行了整理,内容如下
-
pydarknet.c
调用darknet的python拓展,就是我们上一次实现的接口,在那之后我有对接口进行了一些修缮,如下:
/******************************************************* * * 这个项目是讲darknet detector封装成Python Extention * 的工作,为了提高资源利用率,讲opencv的imagePython对象 * 有效转化成darknet可直接利用的对象,避免反复磁盘访问, * 具体方法支持: * * PyObject -(pyopencv_to)-> Mat -(IplImage(Mat*))-> * IplImage -(ipl_to_image)->Image(darknet原生结构) * 其中ipl_to_image有优化空间 * * * update: * PyObject的类型是numpy的ndarray,而且numpy又有c++接口 * 那么我们只要开发出一套C-numpy -> image的转换就可以了 * 技术测试:tech_test.cpp * *******************************************************/ #include "pydarknet.h" /** * @brief build_pyarr * 单独写出来,反正就是为了把int类型转换成一个可以返回的python对象 * @param coordinates 表示边界的四维数组 * @return 创建好的可以直接返回的Python对象 */ inline static PyObject* build_pyarr(int** coordinates) { //int* i_data = calloc(batch_size*4, sizeof(int)); //printf("access : %d \n",coordinates[0][1]); //memcpy(i_data, coordinates[0], sizeof(int)*4*batch_size); npy_intp* dims = calloc(2,sizeof(npy_intp)); dims[0] = batch_size; dims[1] = 4; PyObject* ret = PyArray_SimpleNewFromData(2, dims, NPY_INT, coordinates[0]); Py_INCREF(ret); return ret; } #include <Python.h> /** * @brief pydarknet_detect * python 直接调用的函数,现在做一个过渡,真正的实现不要和Python接口这种东西混在一起 * @param self * @param args * @return */ static PyObject* pydarknet_detect(PyObject* self, PyObject* args) { float thresh; PyObject* parr; PyArg_ParseTuple(args, "fO",&thresh, &parr); //---------------------------------- //printf("Your thresh: %f\n",thresh); //---------------------------------- PyArrayObject *ndarr = (PyArrayObject* ) parr; //获得对象指针 /* if(ndarr->nd != 4 || ndarr->dimensions[1] != IN_H || ndarr->dimensions[2] != IN_W || ndarr->dimensions[3] != 3 || ndarr->data == NULL) { printf("fatal error: I don't know what happened to input data!\n"); printf("Your Pic:\n\tsize: %d * %d\n",ndarr->dimensions[1],ndarr->dimensions[2]); exit(-1); //意思就是说这个numpy的nd数组肯定是4维的,提交前注释掉此分支 } */ batch_size = ndarr->dimensions[0]; if(!batch_size) Py_RETURN_NONE; //---------------------------------- //printf("Batch with %d pics\n",batch_size); //---------------------------------- //把核心逻辑放到别的文件里吧 int** coord = do_detection(ndarr->data, thresh); /* 注意:这个coord千万别free。 这是用来创建Python对象的,free了就什么都没有了 */ //cord进行一些处理,生成可以用来返回的对象 PyObject* pyArray = build_pyarr(coord); batch_size = 0; //最后记得归0 return pyArray; } /** * @brief pydarknet_init_detector * 完成头文件中关键组件的初始化,同时调用不同实现的初始化函数 * datacfg data文件 * cfgfile cfg文件 * weightfile 权值文件 */ static PyObject* pydarknet_init_detector(PyObject* self, PyObject* args) { char *datacfg, *cfgfile, *weightfile; PyArg_ParseTuple(args, "sss", &datacfg, &cfgfile, &weightfile); //--------------------------------------------------------------------- printf("data: %s\ncfg: %s\nweights: %s\n",datacfg, cfgfile, weightfile); //--------------------------------------------------------------------- srand(2222222); __init(); //根据功能不同调用不同的 list *options = read_data_cfg(datacfg); char *name_list = option_find_str(options, "names", "data/names.list"); names = get_labels(name_list); net = load_network(cfgfile, weightfile, 0); set_batch_network(net, 1); Py_RETURN_NONE;//用这个维持Python解释器环境的正常 } /** * @brief pydarknet_finalize_detector * 当工作结束后回收资源用,并不是所有实现都用的着 * @param self * @param args * @return */ static PyObject* pydarknet_finalize_detector( PyObject* self, PyObject* args ) { __finalize(); Py_RETURN_NONE; } static PyMethodDef meth_list[] = { { "detect", pydarknet_detect, METH_VARARGS}, { "init_detector", pydarknet_init_detector, METH_VARARGS}, { "finalize_detector", pydarknet_finalize_detector, METH_NOARGS}, { NULL, NULL, 0, NULL} }; void initpydarknet() { Py_InitModule("pydarknet", meth_list); _import_array(); }
其中
__init
和__finalize
是不同实现(并行、串行)所需的特定初始化流程,都有分别定义。同理,
do_detection
函数现在是识别的真正过程,返回识别结果 -
det.c
串行实现的版本,完成的是功能优化的正确性测试
-
det_para.c
并行优化版本,包含的是我们最终要使用的内容
letterbox_image函数的深入剖析
在我们之前分析的时候我们仅仅把这个函数视为图像处理的一个简单步骤,直到最后总结工作的时候才发现这个部分的计算量比我们rbgbr(指图像存储方式转换)的计算量更大,我们首先来看一下这个函数
image letterbox_image(image im, int w, int h)
{
int new_w = im.w;
int new_h = im.h;
//保证图像宽高比不变,计算放缩后的宽高
if (((float)w/im.w) < ((float)h/im.h)) {
new_w = w;
new_h = (im.h * w)/im.w;
} else {
new_h = h;
new_w = (im.w * h)/im.h;
}
image resized = resize_image(im, new_w, new_h);//进行放缩
image boxed = make_image(w, h, im.c);//创建空白图片
fill_image(boxed, .5);
//int i;
//for(i = 0; i < boxed.w*boxed.h*boxed.c; ++i) boxed.data[i] = 0;
embed_image(resized, boxed, (w-new_w)/2, (h-new_h)/2);//将放缩后的图片复制入空白图片正中央
free_image(resized);//释放放缩后的图片
return boxed;
}
这里还涉及到两个函数,我们依次分析
image resize_image(image im, int w, int h)
{
image resized = make_image(w, h, im.c);
image part = make_image(w, im.h, im.c);
int r, c, k;
float w_scale = (float)(im.w - 1) / (w - 1);
float h_scale = (float)(im.h - 1) / (h - 1);
//为了保证访存效率,都是按照行遍历
//重置宽度
for(k = 0; k < im.c; ++k){
for(r = 0; r < im.h; ++r){
for(c = 0; c < w; ++c){
float val = 0;
if(c == w-1 || im.w == 1){
val = get_pixel(im, im.w-1, r, k);
} else {
//重置方式
float sx = c*w_scale; //利用新图片中的坐标找到原图中坐标的位置(一般不是整数)
int ix = (int) sx; //将分数坐标分成整数和小数部分
float dx = sx - ix;
val = (1 - dx) * get_pixel(im, ix, r, k) + dx * get_pixel(im, ix+1, r, k); //整数部分作为真正坐标,小数部分作为其和相邻像素的混合系数
}
set_pixel(part, c, r, k, val);
}
}
}
//重置高度
for(k = 0; k < im.c; ++k){
for(r = 0; r < h; ++r){
float sy = r*h_scale;
int iy = (int) sy;
float dy = sy