OpenCV 4.10.0
开放源代码计算机视觉
|
下一篇教程: ArUco 棋盘的检测
作者 | Sergio Garrido、Alexander Panov |
兼容性 | OpenCV >= 4.7.0 |
位姿估计在许多计算机视觉应用中至关重要:机器人导航、增强现实等等。此过程基于环境中点的对应点与其 2D 图像投影之间的对应关系。这通常是一个困难的步骤,因此通常使用合成或标记物标记以便于进行此步骤。
最流行的方法之一是使用二进制正方形标记物标记。这些标记物的主要好处是,一个标记物就提供足够的对应关系(其四个角)以获得相机位姿。此外,内部二进制编码使它们特别稳健,从而可以使用错误检测和更正技术。
aruco 模块基于 ArUco 库,这是一个由 Rafael Muñoz 和 Sergio Garrido 开发的用于检测正方形标记物标记的流行库 [99]。
aruco 功能包括在
ArUco 标记物是一个合成正方形标记物,由宽黑色边框和一个内部二进制矩阵组成,该矩阵确定其标识符 (id)。黑色边框便于在其图像中快速检测,二进制编码允许将其识别并使用错误检测和更正技术。标记物大小决定内部矩阵的大小。例如,4x4 的标记物大小由 16 位组成。
一些 ArUco 标记物示例
必须注意,可以在环境中找到旋转的标记物,但是检测过程需要能够确定其原始旋转,以便明确地识别每个角。这也是基于二进制编码来完成的。
一个标记物字典是一组在一个特定应用中要考虑的标记物。它只是每个标记物的二进制编码的列表。
字典的主要属性是字典大小和标记物大小。
aruco 模块包括一些预定义的字典,涵盖各种不同的字典大小和标记物大小。
人们可能会认为标记 id 是将二进制编码转换为十进制的数字。然而,这并不可能,因为对于高标记大小,比特数太高,管理如此大的数字不切实际。取而代之的是,标记 id 只是它所属字典中标记的索引。例如,字典中的前 5 个标记具有 id:0、1、2、3 和 4。
有关字典的详细信息,请参见“字典选取”部分。
在检测到标记之前,需要打印标记以便将其放置在环境中。可以使用 generateImageMarker()
函数生成标记图像。
例如,分析以下调用
首先,通过选择 aruco 模块中预定义的字典之一来创建 cv::aruco::Dictionary
对象。具体来说,该字典由 250 个标记和 6x6 比特标记大小组成(cv::aruco::DICT_6X6_250
)。
cv::aruco::generateImageMarker()
的参数为
cv::aruco::Dictionary
对象。cv::aruco::DICT_6X6_250
中的标记 23。请注意,每个词典都由不同数量的标记组成。对于此例,有效的 ID 范围为 0 至 249。任何超出有效范围的 ID 都会生成异常。生成的图像为
完整的工作示例包含在 samples/cpp/tutorial_code/objectDetection/
中的 create_marker.cpp
内。
现在,示例通过使用 cv::CommandLineParser 从命令行获取输入。对于此文件,示例参数将类似于
create_marker.cpp
参数
给定包含 ArUco 标记的图像,检测过程必须返回列表 [已检测标记]。每个检测标记包括
标记检测过程包含两个主要步骤
考虑以下图像
图像打印件照片中的标记
以下检测出标记(以绿色显示)。请注意,一些标记已旋转。小红方框表示标记的左上角
以下识别步骤中已拒绝的标记候选者(以粉色显示)
在 aruco 模块中,会在 cv::aruco::ArucoDetector::detectMarkers()
函数中执行检测。此函数是该模块中最重要的函数,因为其他所有功能都基于由 cv::aruco::ArucoDetector::detectMarkers()
返回的检测标记。
标记检测示例
当创建cv::aruco::ArucoDetector
对象时,需要向构造函数传递以下参数
cv::aruco::DICT_6X6_250
)。cv::aruco::DetectorParameters
类型的对象。此对象包含在检测过程中可以定制的所有参数。这些参数将在下一部分中说明。cv::aruco::ArucoDetector::detectMarkers()
的参数为
markerCorners
和 markerIds
结构中markerCorners
是检测到的标记的角点列表。对于每个标记,其四个角点按其原始顺序返回(从左上角开始顺时针)。因此,第一个角点是左上角,然后是右上角、右下角和左下角。markerIds
是 markerCorners
中每个检测到的标记的 ID 列表。请注意,返回的 markerCorners
和 markerIds
向量具有相同的大小。rejectedCandidates
是返回的标记候选列表,即已找到并考虑但未包含有效标记的形状。每个候选也由其四个角点定义,其格式与 markerCorners
参数相同。可以省略此参数,它仅在出于调试目的和用于“优化”策略(参见 cv::aruco::ArucoDetector::refineDetectedMarkers()
)时才有用。执行 cv::aruco::ArucoDetector::detectMarkers()
之后,您可能想要做的下一件事是检查标记是否已正确检测。幸运的是,aruco 模块提供了一个在输入图像中绘制检测到的标记的函数,此函数为 drawDetectedMarkers()
。例如
outputImage
是输入/输出图像,其中绘制标记(通常会与检测标记的图像相同)。markerCorners
和 markerIds
是 cv::aruco::ArucoDetector::detectMarkers()
函数返回的已检测到标记的结构。注意这个函数只提供用于可视化,可以省略其使用。
使用这两个函数,我们可以创建一个基本的标记检测循环,用来从相机检测标记
注意,已省略某些可选参数,如检测参数对象和拒绝候选的输出向量。
detect_markers.cpp 中包含完整的操作示例,位于 samples/cpp/tutorial_code/objectDetection/ 中。
现在,示例通过使用 cv::CommandLineParser 从命令行获取输入。对于此文件,示例参数将类似于
detect_markers.cpp 的参数
在检测到标记后,你可能会想做的下一步是使用它们来获取摄像机位姿。
若要执行摄像机位姿估计,你需要知道摄像机的校准参数。这些是相机矩阵和畸变系数。如果你不知道如何校准你的相机,你可以查看 OpenCV 的 calibrateCamera()
函数和校准教程。你还可以按照通过 ArUco 和 ChArUco 校准教程中的说明,使用 aruco 模块来校准你的相机。请注意,除非相机光学元件发生更改(例如改变其焦点),否则只需执行此操作一次。
经过校准后,会得到一个相机矩阵:一个包含焦距和相机中心坐标(又称内参)的 3x3 元素矩阵,以及畸变系数:一个由 5 个或更多元素组成的向量,用于对你的相机产生的畸变进行建模。
当使用 ArUco 标记估算位姿时,你可以估算每个标记的位姿。如果想要从一组标记中估算一个位姿,请使用 ArUco 板(参见检测 ArUco 板教程)。使用 ArUco 板而不是单个标记允许遮挡某些标记。
相对于标记的摄像机位姿是从标记坐标系到摄像机坐标系的一个 3D 变换。它由旋转向量和平移向量指定。OpenCV 提供cv::solvePnP()
函数来执行此操作。
corners
参数是由 cv::aruco::ArucoDetector::detectMarkers()
函数返回的角标记向量。camMatrix
和 distCoeffs
是在相机校准过程中创建的相机校准参数。rvecs
和 tvecs
分别是检测到的 corners
中每个标记的旋转和平移向量。此函数假定的标记坐标系位于标记的中心(默认情况下)或左上角,Z 轴指向外部,如下面的图像所示。轴颜色对应关系:X:红色,Y:绿色,Z:蓝色。请注意此图像中旋转标记的轴方向。
OpenCV 提供了一个函数可以绘制如上图所示的轴,以便检查位姿估计
imageCopy
是将展示检测到的标记的输入/输出图像。camMatrix
和 distCoeffs
是相机校准参数。rvecs[i]
和 tvecs[i]
分别是检测到的每个标记的旋转和平移向量。示例视频
detect_markers.cpp 中包含完整的操作示例,位于 samples/cpp/tutorial_code/objectDetection/ 中。
现在,示例通过使用 cv::CommandLineParser 从命令行获取输入。对于此文件,示例参数将类似于
detect_markers.cpp 的参数
aruco 模块提供 Dictionary
类来表示标记词典。
除了词典中的标记大小和标记数量之外,还有词典的另一个重要参数 - 标记间距离。标记间距离是词典标记之间最小的汉明距离,它决定了词典检测和纠正错误的能力。
一般来说,较小的词典大小和较大的标记尺寸会增加标记间距离,反之亦然。但是,由于需要从图像中提取的位数更多,检测尺寸较大的标记会更困难。
例如,如果您只需在应用程序中使用 10 个标记,最好使用仅由这 10 个标记组成的词典,而不是使用由 1000 个标记组成的词典。原因是仅由 10 个标记组成的词典会有较高的标记间距离,因此它对错误更鲁棒。
因此,aruco 模块包含多种选择标记词典的方式,以便提高系统的可靠性
这是选择词典的最简单方式。aruco 模块包含一组预定义词典,其中包含各种标记大小和标记数量。例如
cv::aruco::DICT_6X6_250
是预定义标记词典的一个示例,其中包含 6x6 位,共有 250 个标记。
在所有提供的词典中,建议选择最适合您应用程序的词典。例如,如果您需要 200 个 6x6 位标记,最好使用 cv::aruco::DICT_6X6_250
而不是 cv::aruco::DICT_6X6_1000
。词典越小,标记间距越大。
可以在 PredefinedDictionaryType
枚举的文档中找到可用的预定义词典的列表。
可以自动生成一个词典来调整所需的标记和位数,以便优化标记间距
这将生成一个定制词典,其中包含 36 个 5x5 位标记。根据参数的不同,这个过程可能需要几秒钟(对于较大的词典和较高的位数,速度较慢)。
您还可以在 opencv/samples/cpp
中使用 aruco_dict_utils.cpp
样本。此样本计算生成词典的最小汉明距离,还允许您创建可抵御反射的标记。
最后,可以手动配置词典,以便可以使用任何编码。为此,需要手动分配 cv::aruco::Dictionary
对象参数。必须注意,除非您有特殊原因需要手动执行此操作,否则最好使用前面的某个替代方案。
bytesList
是包含标记代码的所有信息的数组。markerSize
是每个标记维度的尺寸(例如,对于 5x5 位的标记,尺寸为 5)。最后,maxCorrectionBits
是在标记检测期间可纠正的错误位数的最大值。如果此值过大,则会导致大量误报。
bytesList
中的每一行都表示一个字典标记。但是,标记不是以二进制形式存储的,而是以特殊格式存储来简化检测过程。
幸运的是,可以使用静态方法 Dictionary::getByteListFromBits()
轻松地将标记转换为此格式。
例如
cv::aruco::ArucoDetector
参数之一是 cv::aruco::DetectorParameters
对象。此对象包含标记检测过程中可自定义的所有选项。
此部分介绍每个检测器参数。根据涉及的参数所属的流程,对其进行分类。
标记检测过程中的第一步是对输入图像进行自适应阈值处理。
例如,上面使用的样本图像的阈值图像为
可以使用以下参数自定义此阈值处理
adaptiveThreshWinSizeMin
和 adaptiveThreshWinSizeMax
参数表示在其中为自适应阈值处理选择阈值窗口大小(以像素为单位)的间隔(有关更多详细信息,请参阅 OpenCV 的 threshold()
和 adaptiveThreshold()
函数)。
参数 adaptiveThreshWinSizeStep
表示从 adaptiveThreshWinSizeMin
到 adaptiveThreshWinSizeMax
的窗口大小增量。
例如,对于值 adaptiveThreshWinSizeMin
= 5 且 adaptiveThreshWinSizeMax
= 21 且 adaptiveThreshWinSizeStep
= 4,将有 5 个阈值处理步骤,窗口大小分别为 5、9、13、17 和 21。在每个阈值图像上,将提取标记候选。
如果标记大小过大,窗口大小过小可能会“破坏”标记边框,导致无法检测到该标记,如下面的图像所示
另一方面,如果标记过小,过大的值也会产生相同的效果,而且还会降低性能。此外,此过程将趋向于全局阈值处理,从而导致自适应优势丢失。
最简单的情况是对 adaptiveThreshWinSizeMin
和 adaptiveThreshWinSizeMax
使用相同的值,这会生成一个阈值处理步骤。但是,通常最好对窗口大小使用一个值范围,尽管许多阈值处理步骤也会显著降低性能。
adaptiveThreshConstant
参数表示在阈值处理操作中添加的常数值(有关更多详细信息,请参见 OpenCV threshold()
和 adaptiveThreshold()
函数)。大多数情况下,其默认值是一个不错的选择。
在进行阈值处理后,将检测轮廓。但是,并非所有轮廓都被视为标记候选。它们在不同的步骤中被滤出,以便丢弃不太可能是标记的轮廓。此部分中的参数自定义此滤波过程。
必须注意,在大多数情况下,这是检测能力和性能之间平衡的问题。所有考虑的轮廓都将在以下阶段进行处理,这些阶段通常具有更高的计算成本。因此,最好在此阶段而非在后面的阶段丢弃无效候选。
另一方面,如果滤波条件过于严格,实际标记轮廓可能会被丢弃,从而无法检测到。
这些参数确定标记的最小尺寸和最大尺寸,具体来说是标记的最小周长和最大周长。它们不是用绝对像素值指定的,而是相对于输入图像的最大维度指定的。
例如,大小为 640x480 的图像和最小相对标记周长为 0.05 的图像将导致最小标记周长为 640x0.05 = 32 个像素,因为 640 是图像的最大维度。maxMarkerPerimeterRate
参数也是如此。
如果 minMarkerPerimeterRate
太低,检测性能可能会显着下降,因为未来阶段将考虑更多轮廓。maxMarkerPerimeterRate
参数没有这么明显的惩罚,因为通常大轮廓远多于小轮廓。minMarkerPerimeterRate
值为 0,maxMarkerPerimeterRate
值为 4(或更大)将等同于考虑图像中的所有轮廓,但出于性能原因不建议这样做。
对每个候选应用多边形近似,仅接受近似为方形的候选。此值确定多边形近似可能产生的最大误差(有关详细信息,请参见 approxPolyDP()
函数)。
此参数与候选长度相关(以像素为单位)。因此,如果候选者的周长为 100 个像素,而 polygonalApproxAccuracyRate
的值为 0.04,则最大误差为 100x0.04=5.4 个像素。
在大多数情况下,默认值有效,但对于高度失真的图像,可能需要更高的误差值。
同个标记中任意两个角之间的最小距离。它相对于标记周长表示。以像素为单位的最小距离为周长 * minCornerDistanceRate。
来自两个不同标记的任意两个角之间的最小距离。它相对于两个标记的最小标记周长表示。如果两个候选值太接近,较小的候选项将被忽略。
标记角到图像边界的最小距离(以像素为单位)。如果遮挡较小,则图像边界部分遮挡的标记可以被正确检测。但是,如果其中一个角被遮挡,则返回的角通常会错误地放置在图像边界附近的错误位置。
如果标记角的位置很重要,例如你想进行位姿估计,则最好丢弃其角距离图像边界很近的所有标记。在其他情况下,就没必要。
在候选者检测之后,会对每个候选者的位进行分析,以确定它们是不是标记。
在分析二进制代码本身之前,需要先摘取这些位。为此,纠正了透视畸变,并使用大津阈值分割所得图像,以区分黑色和白色像素。
以下是在去除标记透视畸变后获得的图像示例
然后,将图像分成与标记中位数相同的方块数的网格。在每个方块中,统计黑色和白色像素的数量,以确定分配给方块的位值(取自大多数值)
有几个参数可以自定义此流程
此参数指示标记边框的宽度。它相对于每个位的尺寸。因此,值 2 指示边框的宽度为两个内部位的宽度。
该参数需要与你使用的标记的边框大小一致。边框大小可以在标记绘制函数(例如 generateImageMarker()
)中进行配置。
该值确定执行 Otsu 阈值处理的像素值的最小标准差。如果偏差低,可能意味着所有正方形都是黑色(或白色),并且应用 Otsu 没有意义。如果是这种情况,则根据平均值高于或低于 128,所有位都将设置为 0(或 1)。
此参数用于确定纠正透视失真(包括边框)后所获得图像中每个格子(像素)的数量。这是上图中红色方块的大小。
例如,假设我们处理的是 5x5 位且边框大小为 1 位(参见 markerBorderBits
)的标记。然后,每个维度的总格子/位数为 5 + 2*1 = 7(边框要计算两次)。总格子数为 7x7。
如果 perspectiveRemovePixelPerCell
的值为 10,则所获得图像的大小将为 10*7 = 70 -> 70x70 像素。
此参数值越高,可以提高一定程度的位提取过程,但会降低性能。
在提取每个格子的位时,会对黑色和白色像素的数量进行计数。通常,不建议考虑所有格子像素。而最好忽略掉格子边距中的一些像素。
原因在于,在消除透视失真后,格子的颜色通常不会完全分离,并且白色格子可能会侵入黑色格子的某些像素(反之亦然)。因此,最好忽略一些像素,以避免统计错误的像素。
例如,在以下图像中
只考虑绿色方框内的像素。在右侧的图像中可以看到,结果像素包含较少来自相邻格子的噪声。perspectiveRemoveIgnoredMarginPerCell
参数表示红色方块和绿色方块之间的差异。
此参数相对于格子的总大小。例如,如果格子大小为 40 个像素,并且此参数的值为 0.1,则在格子中会忽略 40*0.1=4 个像素的边距。这意味着每个格子中实际分析的像素总数将为 32x32,而不是 40x40。
提取位后,下一步是检查提取的代码是否属于标记词典,并且在有需要时可以执行错误校正。
标记边框的位应为黑色。此参数指定边框中允许的白位最大数,例如边框中允许的白位最大数。它是相对于标记中位总数来表示的。
每个标记字典都具有理论上可校正位数的上限(Dictionary.maxCorrectionBits
)。不过,此值可以通过 errorCorrectionRate
参数进行修改。
例如,如果可校正的位数(用于字典的位数)允许值为 6,并且 errorCorrectionRate
的值为 0.5,则可校正位的实际最大数为 6*0.5=3 位。
此值有助于降低错误校正能力,以免出现误报。
在检测并识别出标记后,最后一步执行的是对角点位置进行亚像素级精化(请参见 OpenCV cornerSubPix()
和 cv::aruco::CornerRefineMethod
)。
请注意,此步骤是可选的,它仅在需要角点位置精确时才有意义,例如姿势估计。它通常是一个耗时的步骤,因此默认情况下会禁用它。
此参数决定是否执行角点亚像素处理,以及在执行角点亚像素处理时使用哪种方法。如果不需要精确的角点,则可以禁用它。可能的值有 CORNER_REFINE_NONE
、CORNER_REFINE_SUBPIX
、CORNER_REFINE_CONTOUR
和 CORNER_REFINE_APRILTAG
。
此参数决定角点精化处理的最大窗口大小。
高值可能导致图像中的靠近角点被包含在窗口区域中,这样在处理过程中标记的角点会移动到不同的、不正确的位置。它还可能影响性能。如果 ArUco 标记过小,窗口大小可能会减小,请查看 cv::aruco::DetectorParameters::relativeCornerRefinmentWinSize。最终窗口大小计算为:min(cornerRefinementWinSize, averageArucoModuleSize*relativeCornerRefinmentWinSize),其中 averageArucoModuleSize 是 ArUco 标记在像素中的平均模块大小。
角点精化相对于 ArUco 模块大小的动态窗口大小(默认值 0.3)。
最终窗口大小计算为:min(cornerRefinementWinSize, averageArucoModuleSize*relativeCornerRefinmentWinSize), 其中 averageArucoModuleSize 是以像素为单位的 ArUco 标记的平均模块大小。如果标记彼此距离较大,建议将该参数的值上调至 0.4-0.5。如果标记彼此距离较近,则建议将参数值下调至 0.1-0.2。
这两个参数决定了次像素级细化过程的停止准则。cornerRefinementMaxIterations
表示最大迭代次数,cornerRefinementMinAccuracy
表示停止此过程之前的最小误差值。
如果迭代次数太大,可能会影响性能。另一方面,如果次数太少,可能会导致次像素级细化效果不佳。