写这篇文章:记录一下项目中用到的AprilTag码部分内容。
本来以为是简单的应用一下,结果发现环境四边形多时,检测时间真的太久了,根本满足不了要求,只能硬着头皮看源码。这篇文章就是看源码后的一些总结。
在测试中发现基本上大部分时间都花在了算法的quad检测部分,所以我主要是学习了前半部分代码(其实是因为后面的操作不感兴趣🙂)
开始代码部分(看我这部分你需要打开Apirltag代码一起跟着走)
首先介绍一下AprilTag中我认为比较重要的结构体变量。
这个是AprilTag最基本的单元 类似于 vector<类型> var; var.reserve(alloc);
typedef struct zarray zarray_t;
struct zarray
{
size_t el_sz; // 记录每一个元素的大小
int size; // 当前有多少的元素 可以用来计算剩余的内存空间 具体 alloc - size * int(el_sz)
int alloc; // 我们分配了多少的内存大小
char *data; // 以字节为单位进行数据存储 (说白了就是容器, 没有啥具体含义) 1Byte 1Byte ...
};
zarray_t的一些重要方法
static inline zarray_t *zarray_create(size_t el_sz) // size_t el_sz 表示即将存储的元素大小
// 配合sizeof(变量)使用
{
assert(el_sz > 0); // 这个不用看
zarray_t *za = (zarray_t*) calloc(1, sizeof(zarray_t)); // 创建zarray_t对象
za->el_sz = el_sz; // 指定zarray中存储的元素大小
return za; // 返回对象
}
zarray中的扩容规则,可以进行修改,减少扩容次数,加速算法
static inline void zarray_ensure_capacity(zarray_t *za, int capacity)
{
assert(za != NULL); // 判断 不用看
if (capacity <= za->alloc) // 判断 不用看
return;
while (za->alloc < capacity) {
za->alloc *= 2; // 每次x2 进行扩容 与 C++ Java 类似
if (za->alloc < 8)
za->alloc = 8;
}
za->data = (char*) realloc(za->data, za->alloc * za->el_sz); // 重新分配
}
元素添加操作
static inline void zarray_add(zarray_t *za, const void *p) // p待添加的元素
{
assert(za != NULL); // 不用看
assert(p != NULL); // 不用看
zarray_ensure_capacity(za, za->size + 1); // 确保够大
memcpy(&za->data[za->size*za->el_sz], p, za->el_sz); // 将p指向的内存放入zarray后面 是一个copy操作
za->size++; // 元素数加1
}
元素获取
static inline void zarray_get(const zarray_t *za, int idx, void *p) // idx获取第几个元素,p存放获取的元素
{
assert(za != NULL); // 不用看
assert(p != NULL); // 不用看
assert(idx >= 0); // 不用看
assert(idx < za->size); // 不用看
memcpy(p, &za->data[idx*za->el_sz], za->el_sz); // 拷贝到p中
}
图像结构体
typedef struct image_u8 image_u8_t; // 图像结构体
struct image_u8 // uint8_t 的图像 uchar Mat
{
const int32_t width; // 图像的宽度
const int32_t height; // 图像的高度
const int32_t stride; // 图像的遍历方式 类似于Mat中的step
// 举个例子 1280x720 的图 width = 1280
// height = 720
// stride = 1280
// 使用时 image_u8_t* image;
// 访问元素 image.buf[rowId*stride + colId]; // 访问(rowId, colId)元素
uint8_t *buf; // 存储像素的空间
};
核心检测代码部分
最为核心的函数 **zarray_t *apriltag_detector_detect(apriltag_detector_t td, image_u8_t im_orig)
// apriltag_detector_t *td 这个是检测器的一些基本参数配置(你可以设置的参数)
// image_u8_t *im_orig 原始图像
zarray_t *apriltag_detector_detect(apriltag_detector_t *td, image_u8_t *im_orig)
{
if (zarray_size(td->tag_families) == 0) {
// 一个设置参数问题,如果没有对应的tag码就会报错
zarray_t *s = zarray_create(sizeof(apriltag_detection_t*));
debug_print("No tag families enabled\n");
return s;
} // 基本上没用
// 这个也是一个线程操作 根据用户设置的参数决定
if (td->wp == NULL || td->nthreads != workerpool_get_nthreads(td->wp)) {
workerpool_destroy(td->wp);
td->wp = workerpool_create(td->nthreads);
if (td->wp == NULL) {
// creating workerpool failed - return empty zarray
return zarray_create(sizeof(apriltag_detection_t*));
}
}
///
// Step 1. 图像预处理环节
image_u8_t *quad_im = im_orig;
if (td->quad_decimate > 1) {
// 如果使用者设置了这个参数,就会执行图像下采样(可以加速算法)
quad_im = image_u8_decimate(im_orig, td->quad_decimate); // 主要由这个函数实现,这个没什么困难的,可以自己看一下
// 作者在这里进行了一个改动,对下采样为1.5的情况进行了加权采样
}
// 如果设置了这个参数,就会进行高斯滤波操作,具体可以看图像处理的文章,这里不是关键
if (td->quad_sigma != 0) {
// 高斯滤波
...
}
if (td->debug) // 如果设置了debug就可以看处理后的结果图 这里需要转换 网上有pnm转化的工具,也可以自己写一下
image_u8_write_pnm(quad_im, "debug_preprocess.pnm");
// 最最重要的部分来了 (单独解释一下这个函数)
zarray_t *quads = apriltag_quad_thresh(td, quad_im);
... 后面就是解码部分,没怎么细看,感觉应该是将图像点通过Hinv转到原始三维平面点进行的快速解码操作
}
重点方法分析
*zarray_t quads = apriltag_quad_thresh(td, quad_im);
// apriltag_detector_t *td 检测器设置变量
// image_u8_t *im // 下采样/高斯后的图像
zarray_t *apriltag_quad_thresh(apriltag_detector_t *td, image_u8_t *im)
{
// step 1. 图像"二值化"操作,其实按照作者的操作,一共保留了三个状态 0 255 127
// 0: 黑
// 255:白
// 127:默认不进行操作的值,这个值是由我们设定的黑白像素差来决定的,小于这个差就认为边界差距不大,不做处理(后面会讲)
int w = im->width, h = im->height; // 这个就是保存图像的宽和高
image_u8_t *threshim = threshold(td, im); // 这个就是黑白阈值操作(单独讲,见下面)
int ts = threshim->stride;
if (td->debug)
image_u8_write_pnm(threshim, "debug_threshold.pnm");
// step 2. 寻找连通域 学习一下connected_components 见下面
unionfind_t* uf = connected_components(td, threshim, w, h, ts);
// make segmentation image.
if (td->debug) {
// 我删除了debug部分的代码,这是一个画图展示的部分,太占篇幅
}
zarray_t* clusters = gradient_clusters(td, threshim, w, h, ts, uf);
if (td->debug) {
// 老规矩我删除了画图
}
image_u8_destroy(threshim);
// step 3. 处理四边形部分我没怎么看,感觉优化不了啥 sorry
zarray_t* quads = fit_quads(td, w, h, clusters, im);
unionfind_destroy(uf);
for (int i = 0; i < zarray_size(clusters); i++) {
zarray_t *cluster;
zarray_get(clusters, i, &cluster);
zarray_destroy(cluster);
}
zarray_destroy(clusters);
return quads;
}
threshold 黑白阈值操作
代码中部分assert操作我删除了,不然太长了。我基本保留了所有的代码。
// apriltag_detector_t *td 检测器配置变量
// image_u8_t *im 下采样/高斯滤波处理后的图像
image_u8_t *threshold(apriltag_detector_t *td, image_u8_t *im)
{
int w = im->width, h = im->height, s = im->stride; // 取出图像的基本信息
image_u8_t *threshim = image_u8_create_alignment(w, h, s); // 一个对齐操作,threshim 这个是最终的返回图
const int tilesz = 4; // 这个是论文中所提到的4x4小块 也可以进行调参
int tw = w / tilesz; // 计算宽上有多少个块
int th = h / tilesz; // 计算高上有多少个块 总的块数为 tw * th,因为这里是进行int操作,可能会存在余数的情况,后面作者进行了操作 (请注意这些块不存在重叠)
uint8_t *im_max = calloc(tw*th, sizeof(uint8_t)); // 申明总块数的操作 记录每一个块中的最大值
uint8_t *im_min = calloc(tw*th, sizeof(uint8_t)); // 申明总块数的操作 记录每一个块中的最小值
struct minmax_task *minmax_tasks = malloc(sizeof(struct minmax_task)*th); // 这是线程操作 不用太在意, 主要意思就是 申明了th行的任务数,进行操作
// 开始操作,按照行进行块的最大最小值搜索
for (