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