OpenCV 4.11.0
开源计算机视觉库
|
下一个教程: ArUco棋盘检测
原作者 | Sergio Garrido,Alexander Panov |
兼容性 | OpenCV >= 4.7.0 |
姿态估计在许多计算机视觉应用中至关重要:机器人导航、增强现实等等。此过程基于查找真实环境中的点及其二维图像投影之间的对应关系。这通常是一个困难的步骤,因此通常使用合成或基准标记来简化它。
最流行的方法之一是使用二元方块基准标记。这些标记的主要优点是单个标记提供了足够的对应关系(其四个角)来获得相机姿态。此外,内部二元编码使它们特别稳健,允许应用错误检测和纠正技术。
aruco模块基于ArUco库,这是一个流行的用于检测Rafael Muñoz和Sergio Garrido开发的方形基准标记的库 [98]。
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()
函数返回的检测到的标记结构。请注意,此函数仅用于可视化,其使用可以省略。
使用这两个函数,我们可以创建一个基本的标记检测循环,以从我们的相机中检测标记
请注意,一些可选参数已被省略,例如检测参数对象和被拒绝候选对象的输出向量。
完整的可运行示例包含在samples/cpp/tutorial_code/objectDetection/
目录内的detect_markers.cpp
文件中。
示例现在使用cv::CommandLineParser从命令行获取输入。对于此文件,示例参数将如下所示:
detect_markers.cpp
的参数
检测到标记后,您可能接下来想要做的事情就是使用它们来获取相机姿态。
要执行相机姿态估计,您需要知道相机的标定参数。这些参数是相机矩阵和畸变系数。如果您不知道如何标定您的相机,您可以查看calibrateCamera()
函数和OpenCV的标定教程。您也可以使用aruco模块标定您的相机,如**使用ArUco和ChArUco进行标定**教程中所述。请注意,除非修改相机光学器件(例如更改其焦点),否则只需要执行一次此操作。
标定结果会生成一个相机矩阵:一个 3x3 元素的矩阵,包含焦距和相机中心坐标(也称为内参),以及畸变系数:一个包含 5 个或更多元素的向量,用于模拟相机产生的畸变。
使用 ArUco 标记估计姿态时,可以单独估计每个标记的姿态。如果要根据一组标记估计一个姿态,请使用 ArUco 棋盘格(参见**ArUco 棋盘格检测**教程)。使用 ArUco 棋盘格而不是单个标记可以容忍部分标记被遮挡。
相对于标记的相机姿态是标记坐标系到相机坐标系的 3D 变换。它由旋转向量和平移向量指定。OpenCV 提供了 `cv::solvePnP()` 函数来实现这一点。
此函数假设的标记坐标系位于标记的中心(默认)或左上角,Z 轴指向外,如下图所示。轴线颜色对应关系为:X:红色,Y:绿色,Z:蓝色。请注意图像中旋转标记的轴向方向。
OpenCV 提供了一个函数来绘制如上图所示的轴线,以便检查姿态估计结果。
示例视频
完整的可运行示例包含在samples/cpp/tutorial_code/objectDetection/
目录内的detect_markers.cpp
文件中。
示例现在使用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` 对象的参数。必须注意的是,除非你有特殊原因需要手动执行此操作,否则最好使用前面提到的替代方法之一。
`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 的最小相对标记周长,由于 640 是图像的最大尺寸,则最小标记周长将为 640x0.05 = 32 像素。maxMarkerPerimeterRate
参数也适用同样的计算方法。
如果minMarkerPerimeterRate
太低,则检测性能会显著下降,因为在后续阶段将会有更多轮廓被考虑。对于maxMarkerPerimeterRate
参数,这种惩罚并不明显,因为通常小轮廓比大轮廓多得多。minMarkerPerimeterRate
值为 0 且maxMarkerPerimeterRate
值为 4(或更大)等同于考虑图像中的所有轮廓,但是出于性能原因,这并不推荐。
对每个候选对象应用多边形逼近,只有那些逼近正方形形状的对象才会被接受。此值决定多边形逼近可以产生的最大误差(有关更多信息,请参阅approxPolyDP()
函数)。
此参数相对于候选对象的长度(以像素为单位)。因此,如果候选对象的周长为 100 像素,并且polygonalApproxAccuracyRate
的值为 0.04,则最大误差将为 100x0.04=5.4 像素。
在大多数情况下,默认值都可以正常工作,但是对于高度扭曲的图像,可能需要更高的误差值。
同一标记中任意一对角点之间的最小距离。它相对于标记周长表示。以像素为单位的最小距离为 周长 * minCornerDistanceRate。
两个不同标记中任意一对角点之间的最小距离。它相对于两个标记的最小标记周长表示。如果两个候选对象过于接近,则较小的候选对象将被忽略。
标记的任何角点到图像边框的最小距离(以像素为单位)。如果遮挡较小,则可以正确检测部分被图像边框遮挡的标记。但是,如果其中一个角点被遮挡,则返回的角点通常位于图像边框附近的错误位置。
如果标记角点的位置很重要,例如,如果您想进行姿态估计,最好丢弃角点太靠近图像边框的任何标记。否则,则无需这样做。
候选对象检测后,将分析每个候选对象的位,以确定它们是否是标记。
在分析二进制代码本身之前,需要提取位。为此,需要校正透视畸变,并使用 Otsu 阈值对生成的图像进行阈值处理,以分离黑色和白色像素。
这是去除标记透视畸变后获得的图像示例
然后,将图像划分为与标记中位数相同的单元格数的网格。在每个单元格中,计算黑色和白色像素的数量,以确定分配给单元格的位值(来自多数值)。
有一些参数可以自定义此过程
此参数指示标记边框的宽度。它相对于每个位的尺寸。因此,值为 2 表示边框的宽度为两个内部位。
此参数需要与您正在使用的标记的边框尺寸一致。边框尺寸可以在标记绘图函数(例如generateImageMarker()
)中配置。
此值确定执行 Otsu 阈值处理的像素值的最小标准差。如果偏差较低,则可能意味着整个正方形为黑色(或白色),并且应用 Otsu 没有意义。如果是这种情况,则所有位将设置为 0(或 1),具体取决于平均值是否高于或低于 128。
此参数确定校正透视畸变后获得的图像中每个单元格的像素数(包括边框)。这是上图中红色正方形的大小。
例如,假设我们正在处理 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
表示停止过程之前的最小误差值。
如果迭代次数过高,可能会影响性能。另一方面,如果它太低,则可能会导致亚像素细化效果不佳。