记录一下参与的第一个开源项目暨PikaCV开发不完全指北

一、前言

其实挺早就听说了PikaScript,那会还不叫这个名字,虽然我没做过MCU的相关开发工作,但是之前在做大创的时候也学习过一些基本知识,之前看队友都是用CubeMX和keil开发的,但是我们的项目并不需要特别复杂的单片机程序,只是做了些串口通信和GPIO控制电机,这一套下来还是略显复杂。而PikaScript只需要几行Python指令居然就可以完成这些需求,但是还是比较好奇和吃惊的,所以就抱着参观的心态加入了交流群,也没怎么在群里说过话。今年六月忙完了手头的一部分事情之后看到Lyon大佬开始施工PikaCV,在他搭建完基础框架后我也就试着写了些算法。之前只是粗浅的学习过计算机视觉的一些算法,调调库实现了需求就完事了,这次参与PikaCV的开发,用C实现了一些算法和编写了文档还是很有成就感和参与感的。以上是我对PikaScript的个人见解可能有失偏颇,想了解更多关于PikaScript的内容请移步Github。

本文写作于PikaScript v1.9.0,PikaCV 0.0.1,如果想参考本文进行开发请对齐版本,本文中存在部分伪代码,可运行源码请参考Github中package/PikaCV文件夹。

若仅想了解关于开发的知识请直接跳至第三节开始阅读,第二节是写给和我一样的第一次参与开源项目的小白(以下对PikaScipt简称Pika)。

二、环境配置与Git命令

之前用torch、cuda的时候深受环境折磨,为了统一版本与防止环境冲突,Pika采用了Docker环境,这样可以保证每个开发者的环境都是一模一样的。关于Docker的安装和使用可参考Pika文档从 Docker 开发环境开始,作者写的很详细了,按照教程一步一步来就可以。

Git是参与每个开源项目必不可少的工具,除了git add,git commit,git push这三个平时用git作自己个人仓库会用到的命令外,还需要知道git fetch,git pull(等价于git fetch + git merge)这两个命令的用法。如果代码出现了冲突,一般情况是自己和别人共同修改了同一段代码,可以使用vscode中的git插件很方便的解决。我一般的习惯是每次进行开发之前将远端仓库与主仓库同步,然后再pull到本地,以保证与最新代码同步。

开源项目的每次提交都应该有清晰明确的commit message,这样后面的开发者就可以很清楚的明白本次提交解决了什么问题,如果之前随便写commit message习惯了在参与开源项目之前还是需要注意这个问题。

更多关于Docker与Git的用法请参考官方文档:

三、PikaCV文件架构

日常开发需要用到的几个文件分别是PikaCV.pyi,pikascript-lib/xxx.c,test/PikaCV-test.cpp,下面分别展开说说具体作用。

1. PikaCV.pyi

PikaCV.pyi是对外的python接口文件,声明了用到的类与函数,在设计好api之后在这里写好函数名,参数,返回值,然后运行init.sh,会进行预编译在对应的c头文件中生成函数定义。例如Image类下的add方法,定义如下:

2. pikascript-lib/PikaCV_xxx.c

这里的xxx代表的pyi中定义的类名,例如:Image类对应的实现的c文件就是PikaCV_Image.c。将预编译生成的头文件(位于pikascript-api)中的函数定义复制进c文件进行算法的实现。

3.test/PikaCV-test.cpp

这个cpp文件是基于GoogleTest的测试,每次实现了新的算法都应该加入相关的测例,及时发现问题,避免最后多个问题重叠在一起很难定位。

四、PikaCV数据结构

了解了PikaCV的文件架构之后,我们再来看看PikaCV使用的数据结构。

由于PikaCV的应用场景是算力极其有限的mcu上,所以目前只使用了uint8作为存储图像的格式,没有opencv这些运算在pc上的库中的float,double等格式。

PikaCV使用tjpgd库读取jpeg格式的图像文件,并将图片以uint8数组的形式存储在PikaObj中,供后续算法使用。

通过PikaObj获取uint8数组的方法如下:

PikaCV_Image* src = obj_getStruct(self, "image");
uint8_t* src_data = _image_getData(self);

其中image是一个PikaObj对象,通过obj_getStruct()函数得到了一个PikaCV_Image结构体,PikaCV_Image中包含了图片的长、宽、格式信息。之后在使用_image_getData()函数得到uint8数组。PikaCV_Image结构体的定义如下:

typedef struct PikaCV_Image {
    PikaCV_ImageFormat_Type format;
    int width;
    int height;
    int size;
} PikaCV_Image;

拿到uint8数组之后,我们就可以根据需求对其进行各种运算操作。最后,使用obj_setBytes()函数将得到的结果写进PikaObj中就结束了。

obj_setBytes(self, "_data", src_data, src->size);

更多关于PikaCV的函数请参考官方文档:

五、使用PikaCV实现图像相加

在具体实现算法前,我们还需要知道图片在uint8数组中是如何存储的。单通道的灰度图像是先从左到右然后从上到下顺序存储的,而三通道的图像(RGB888)在此基础上,按R-G-B的顺序存储。Talk is cheap,show you the code.

for (int i = 0; i < (src->size) / 3; i++) {
    src_data[i * 3]; //Channel-R
    src_data[i * 3 + 1]; //Channel-G
    src_data[i * 3 + 2]; //Channel-B
}

掌握了如何取出各通道的数据之后,就可以着手实现两个图片的相加操作了。

既然要在函数内实现两个图像的相加,除了调用此函数的自身外,还需要将另一幅图像以参数的形式传入。

void PikaCV_Image_add(PikaObj* self, PikaObj* image); //函数定义

PikaCV_Image* src = obj_getStruct(self, "image");
PikaCV_Image* img = obj_getStruct(image, "image");

接着要满足相加的条件,两幅图像的格式和大小都应该是相同的,对这几个条件分别进行检查,如果不满足则报错退出。

    if (NULL == src || NULL == img) {
        pika_assert(0);
        return;
    }
    if (img->format != src->format) {
        obj_setErrorCode(self, PIKA_RES_ERR_OPERATION_FAILED);
        __platform_printf("unsupported image format\n");
        return;
    }
    if (img->size != src->size || img->width != src->width || img->height != src->height) {
        obj_setErrorCode(self, PIKA_RES_ERR_OPERATION_FAILED);
        __platform_printf("illegal image size\n");
        return;
    }

满足条件后就可以直接进行加法运算了,实际上就是两个矩阵的加法,不过需要注意的是,图像的取值范围为0-255,而两个uint8_t变量相加很明显会出现越界的现象,因此需要加入额外的判断,如果发现了越界的现象,那么就直接赋值为255,同理,在相减时如果小于0就赋值为0,所以在PikaCV中两个图片的加减法是不可逆的。

    for (i = 0; i < (src->size) / 3; i++) {
        result = src_data[i * 3] + img_data[i * 3];
        src_data[i * 3] =
            ((result < MAX(src_data[i * 3], img_data[i * 3])) ? 255 : result);

        result = src_data[i * 3 + 1] + img_data[i * 3 + 1];
        src_data[i * 3 + 1] =
            ((result < MAX(src_data[i * 3 + 1], img_data[i * 3 + 1])) ? 255
                                                                      : result);

        result = src_data[i * 3 + 2] + img_data[i * 3 + 2];
        src_data[i * 3 + 2] =
            ((result < MAX(src_data[i * 3 + 2], img_data[i * 3 + 2])) ? 255
                                                                      : result);
    }

最后,将运算的结果赋值给self,就完成了图片相加的全部操作。

obj_setBytes(self, "_data", src_data, src->size);

很重要的一点是:如果在函数中手动分配了空间,一定要在函数结束时释放。

六、总结

本文以简单的图片相加为例,讲解了如何在PikaCV中实现一个算法,我也只是起到一个抛砖引玉的作用,希望各位感兴趣的大佬一起参与开发。开发群里有很多大佬,大家也都很友善,回答了很多我很蠢的问题。希望可以一起将Pika打造成一个可以支持目标识别的框架。

编辑于 2022-07-23 17:54