Aruco靶标是无人机导航常用的一种靶标,其可以携带编码信息,用于多台设备,现实增强,相机标定等等。
下面我会对齐进行细致的算法分析,各位按照这个流程阅读OpenCV源码会非常清晰,我主要是对关键函数进行分析,分析其算法流程。
文章目录
0 参数设置
算法一共20个参数,非常庞大了,建议不要上来看这节参数含义,很容易看不懂,建议结合流程来看,这样很清晰了。
参数定义 | 参数含义 | 参数值 |
---|---|---|
adaptiveThreshWinSizeMin | 自适应二值化最小窗口大小 | 3 |
adaptiveThreshWinSizeMax | 自适应二值化最大窗口大小 | 23 |
adaptiveThreshWinSizeStep | 二值化窗口大小步长 | 10 |
adaptiveThreshConstant | 自适应二值化函数所需要的一个常数阈值 | 7 |
minMarkerPerimeterRate | 轮廓最小周长比率 | 0.03 |
maxMarkerPerimeterRate | 轮廓最大周长比率 | 4 |
polygonalApproxAccuracyRate | 多边形逼近精度控制率 | 0.03 |
minCornerDistanceRate | 4边形4个角点之间的最小距离率值 | 0.05 |
minDistanceToBorder | 角点到边界的最小距离 | 3 |
minMarkerDistanceRate | 角点最小距离率值 | 0.05 |
cornerRefinementMethod | 角点细化方法 | CORNER_REFINE_NONE |
cornerRefinementWinSize | 细化窗口大小(仅用于CORNER_REFINE_SUBPIX) | 5 |
cornerRefinementMaxIterations | 细化最大迭代次数(仅用于CORNER_REFINE_SUBPIX) | 30 |
cornerRefinementMinAccuracy | 细化最小精度(仅用于CORNER_REFINE_SUBPIX) | 0.1 |
markerBorderBits | 编码区域的边框位数 | 1 |
perspectiveRemovePixelPerCell | 透视变换后编码每个比特位的尺寸 | 4 |
perspectiveRemoveIgnoredMarginPerCell | 编码每个比特块忽略的宽度 | 0.13 |
maxErroneousBitsInBorderRate | 边框误差比率 | 0.35 |
minOtsuStdDev | 判断区域是否为全黑或全白的方差 | 5 |
errorCorrectionRate | 解码误差率 | 0.6 |
1 检测候选框
Aruco靶标外部由正方形框构成,因此,首先肯定要检测出候选框,因为图像受到透视变换的影响,导致正方形在图像上的投影可以为任意4边形,唯一可以知道的性质就是,这个4变形一定是具有凸性的,代码也是利用这个性质进行筛选的。如下图所示,该阶段检测绿色所示的4边形。
OpenCV中检测目标候选框的代码是_detectCandidates(grey, candidates, contours, _params);
,其中,grey为输入的灰度图,candidates为候选框的4个点,是一个vector变量,每个vector元素就存了4个点。contours为候选框对应的轮廓像素集合,也是个vector变量,每个元素存储的是对应矩形框的像素集。_params为算法的对应参数集合了。contours和candidates个数一致,个数表示检测出的候选四边形个数。
候选4边形检测算法分为三个阶段:
- 候选检测
- 角点排序
- 去除相似框
下面对每个阶段进行流程说明。
1.1 候选检测
候选检测阶段仅利用目标的凸性检测出4边形集合,对应函数为_detectInitialCandidates(grey, candidates, contours, _params);
因为靶标的颜色仅有黑白,且与周围有明显的区分。因此,直接使用自适应二值化方法adaptiveThreshold提取候选目标,这个函数时OpenCV自带的函数,需要制定二值化窗口,和一个常数。
在这个阶段,算法使用了多种不同的窗口大小来检测4边形,这个时候adaptiveThreshWinSizeMin控制的是最小窗口,adaptiveThreshWinSizeMax控制的是最大窗口,adaptiveThreshWinSizeStep控制窗口步长,adaptiveThreshConstant是这个函数需要的一个常数。举例来说,算法设置的窗口范围为[3,23],步长为10,那么最后选择的窗口大小就为,3,13,23,记住,窗口最好设置为奇数。
在得到二值化窗口之后呢,利用findContours(contoursImg, contours, RETR_LIST, CHAIN_APPROX_NONE);
查找这个二值化图的所有轮廓,每个轮廓存在contours里面。下面对每一个轮廓进行如下的一个过程
- 特别大特别小的轮廓一般都不会是目标,最小周长阈值为
minPerimeterPixels = minPerimeterRate *max(rows, cols)
,最大周长阈值为maxPerimeterPixels = maxPerimeterRate *max(rows, cols)
- 对这个轮廓进行多边形逼近
approxPolyDP(contours[i], approxCurve, double(contours[i].size()) * polygonalApproxAccuracyRate, true);
。approxCurve是这个轮廓的逼近点,double(contours[i].size()) * polygonalApproxAccuracyRate是对应的逼近精度,简单来说,越大的轮廓具有更大的逼近阈值,主要还是防止算法收到噪声影响。 - 轮廓逼近点必须为4个点(4边形嘛),且这个4边形一定是凸的,否则这个轮廓就一定不是目标轮廓。
- 4边形的4个角点之间的最小距离
minDistSq
必须大于轮廓周长*minCornerDistanceRate,否则就不是候选。 - 判断4边形是否存在一个角点离图像的边界非常近,打个比方,角点的坐标为(1,1),与图像边界非常近,如果有那么这个就扔掉,边界距离阈值为minDistanceToBorder。
- 到这,就说明找到一个候选,存储对应的4个角点和对应的轮廓像素集。
1.2 角点排序
角点排序使用函数_reorderCandidatesCorners(candidates);
,保证角点的顺序是顺时针方向,仅此而已。
1.3 去除相似4边形
距离特别近的4边形应该被扔掉,在_filterTooCloseCandidates(candidates, candidatesOut, contours, contoursOut, _params->minMarkerDistanceRate);
实现这个功能。
对于两个4边形,其4个角点之间的最短平均距离如果小于两个矩形的最小周长*minMarkerDistanceRate,那么就认为这两个矩形是相似的。矩形的周长就是轮廓像素个数,两个相似4边形,周长小的会被扔掉。
到这,将会得到一组候选的4边形集合,然后下一步对其进行精准识别。
2 四边形识别
下图是一个Aruco码标,一个黑色方块或白色方块就叫做一个比特,码标周围用了一圈黑色框围着,框的宽度为markerBorderBits,这里值为1。下图是一个6*6的码标套上一个宽度为1的框,所以实际上比特区域是8*8.
对每个检测出的4边形,算法利用多线程对其进行解码,解码成功这说明这个是个靶标,解码流程如下所示啦。
- 对每个四边形利用
getPerspectiveTransform
和warpPerspective
进行透视变换,透视变换的目标为方形,其边长为perspectiveRemovePixelPerCell*码标的比特边长 - 判断四边形内部是否为全黑或全白。先移除1/2边框,然后利用
meanStdDev
计算区域的均值和方差,如果方差小于minOtsuStdDev,说明是全黑或全白,如果均值大于127,则认为是全白,否则全黑。 - 利用大津阈值对透视变换后的图像做个二值化
threshold(resultImg, resultImg, 125, 255, THRESH_BINARY | THRESH_OTSU);
- 因为已经知道码标的边长的比特数,那么就直接对二值化图像提取出对应的图像块。图像块的四周靠近边界部分肯定有噪声,所以四周边界部分扔掉,那么这个边界宽度为cellMarginPixels = perspectiveRemoveIgnoredMarginPerCell * perspectiveRemovePixelPerCell
- 利用
countNonZero
统计比特块的非0个数,如果非0个数超过图像一般,则认为这个块是白色,否则是黑色,最后将这个码标用一个8*8的bits矩阵存储。 - 码标边界都是黑色,因此利用
int _getBorderErrors(const Mat &bits, int markerSize, int borderSize)
统计边界黑色的个数,如果错误块个数大于编码块总数*maxErroneousBitsInBorderRate,则认为边界错误,导致编码错误。 - 剔除边界信息,提取码标比特信息存进onlyBits输入
dictionary->identify(onlyBits, idx, rotation, params->errorCorrectionRate)
进行解码,解码失败就说明这个不是目标,解码成功,并返回一个值,来确定哪个角点是左上第一个角点。
到这码标识别结束,返回码标信息和对应的轮廓及角点。
3 对检测出的靶标进行过滤
检测结果中可能会出现两个码标ID一样,且其中一个码标在另一个码标里面,属于包含关系(OpenCV源码说双边框情况会出现这个问题,算是算法的一个Bug)。
那么针对这个情况就要先找出具有包含关系的矩形,判断ID是否一样,一样就删掉。函数声明如下所示。
void _filterDetectedMarkers(vector< vector< Point2f > >& _corners, vector< int >& _ids, vector< vector< Point> >& _contours)
4 角点修正
前面步骤检测出的角点都是像素级的,用于后续位姿估计时候可能会有误差,因此检测出角点之后,需要对其进行细化,得到亚像素角点,细化方法有两种,分别为:角点细化(CORNER_REFINE_SUBPIX)和拟合直线细化(CORNER_REFINE_CONTOUR)。细化方法的选择,指定参数cornerRefinementMethod即可,算法默认是不细化的。
4.1 角点细化方法
利用opencv自带函数cv::cornerSubPix()
可将像素级角点细化为亚像素,其中涉及到三个参数,cornerRefinementWinSize细化窗口大小,cornerRefinementMaxIterations细化最大迭代数,cornerRefinementMinAccuracy细化误差。
4.2 拟合直线细化方法
该细化方法是对4边形的每个边进行拟合,然后利用拟合直线的交点作为最终细化的角点。
- 如果相机有畸变,利用
undistortPoints
对四边形像素点进行校正。 - 提取两个角点之间的像素集,也就是4边形的每个边
- 对每个边进行直线拟合
- 计算交点
最终的交点就作为亚像素角点。
5 总结
Aruco是14年提出的一个算法,使用次数很多,非常经典,同样地,其参数过多,对环境有需求,也导致了其无法用于自动化系统(误差干扰很大),从流程也是可以发现的。
但这也很正常,刚产生的算法不可能通用,这就需要我们针对其约束的部分进行放松,用一个更好的办法消除误差,可以用在更广的方向上。
PS:目前为止我都没找到相关的数据集,数据集的采集也是很关键的。