【Python】OpenCV ArUco 使用指南

1. ArUco 介绍

ArUco 是一种方形人工视觉标记(fiducial marker)
它不是用于存放网址或长文本的通用二维码,而是为了让计算机稳定地获得以下信息:

  • 这个标记是谁;
  • 它在图像中的什么位置:四个有序角点和中心点;
  • 它朝向哪里:编码可以消除 0°、90°、180°、270°之间的旋转歧义;
  • 它相对相机的三维位置和姿态:在已知标记尺寸及相机内参时,通过 PnP 求解。

ArUco 是机器人视觉中常用的技术栈,其原始项目是一个基于 OpenCV 的独立 C++ 库,后来其核心能力进入 OpenCV 的 aruco 模块。

典型处理链如下:

相机图像
   ↓
检测候选方形轮廓
   ↓
透视展开并读取内部黑白编码
   ↓
在指定字典中识别 ID 和旋转方向
   ↓
获得 ID、四个有序角点、中心点
   ↓(可选:已知相机内参和标记尺寸)
solvePnP
   ↓
获得标记相对相机的三维旋转和平移

2. 一个 ArUco 码长什么样

一个标准 ArUco 标记包含:

  • 正方形外形;
  • 连续的黑色边框,用于快速寻找候选轮廓;
  • 内部 N × N 黑白编码单元;
  • 标记外部的白色留白,用于与背景分离。

DICT_4X4_50 为例,4X4 表示内部编码矩阵为 4×4,而 50 表示该字典中共有 50 个合法 ID,即 0~49。黑色外框不计入 4×4 的编码矩阵。

需要特别注意:字典名是编码协议的一部分DICT_4X4_50 的 ID 0 和其他字典的 ID 0 并不是同一个图案。生成端与检测端必须选择完全相同的字典。

3. ArUco 是如何工作的

OpenCV 的经典检测过程可以简化为两个阶段。

3.1 定位

检测器先对图像进行自适应阈值处理,再寻找轮廓,并排除不满足要求的候选,例如:

  • 不是凸四边形;
  • 面积过小或过大;
  • 四个角距离不合理;
  • 距离图像边缘太近;
  • 与其他候选过于接近。

这一步只说明“它看起来像一个方形标记”,还不能确定它是合法 ArUco。

3.2 解码

对每个候选四边形,检测器会:

  1. 通过透视变换把候选区域展开成正方形;
  2. 对展开图进行二值化;
  3. 按编码网格读取每个黑白单元;
  4. 检查黑色边框;
  5. 将读取结果的四种旋转与字典中的合法编码比较;
  6. 根据汉明距离完成识别和有限的错误校正。

由于会比较四种旋转,检测器不仅能得到 ID,还能恢复标记的标准方向。OpenCV 随后按标记自身的标准方向返回四个角:

0:标准左上角
1:标准右上角
2:标准右下角
3:标准左下角

4. 携带信息

ArUco 内部编码的主要输出是字典 ID。业务层一般建立一张映射表:

MARKER_TYPES = {
    0: "机器人基座",
    1: "抓取工位",
    2: "放置工位",
    3: "工件A",
    4: "工件B",
}

这种做法的优势是编码紧凑、解码稳健,适合数量有限且含义预先约定的对象。其限制也很明确:

  • 可以表达有限的 ID 或类型;
  • 不能像 QR Code 一样直接保存网址、JSON 或长序列号;
  • 业务系统必须知道“字典 + ID”与业务含义的映射;
  • 如果映射会变更,应在配置或数据库中维护,而不要散落在算法代码中。

如果现场需要标记直接携带较长信息,可以让二维码负责业务数据,另加 ArUco/AprilTag 负责高效定位;也可以只在标签上打印短 ID,再由系统查询数据库。

OpenCV 提供了多组预定义字典,常见的有:

字典内部网格ID 数量适合场景
DICT_4X4_504×450类型不多、标记较小、入门验证
DICT_4X4_1004×4100需要 50~100 个 ID
DICT_5X5_1005×5100希望比 4×4 有更大的编码区分空间
DICT_6X6_2506×6250ID 较多、图像分辨率和标记尺寸充足
DICT_7X7_10007×71000大规模编号,要求每个单元仍有足够像素
DICT_ARUCO_ORIGINAL原始 ArUco1024兼容历史项目
DICT_APRILTAG_36h11AprilTag 族587用 OpenCV API 检测 AprilTag 编码

选择原则不是“位数越高越好”,而是:

  1. 先确定业务所需的 ID 数量;
  2. 选择能容纳这些 ID 的尽量小的字典;
  3. 再根据打印尺寸、最远距离和相机分辨率决定网格大小;
  4. 用真实相机和真实光照做检出率、误识别率及角点抖动测试。

同样的物理尺寸下,7×7 的单元比 4×4 更小。远距离成像时,每个编码单元可能只剩很少像素,网格更多反而可能更难识别。OpenCV 官方也建议选择能够满足应用规模的较小字典,因为较小字典通常可以保留更好的码间距离。

5. 开发测试

5.1 运行环境

先检查当前 OpenCV 是否包含 aruco

import cv2

print(cv2.__version__)
print(hasattr(cv2, "aruco"))
print(hasattr(cv2.aruco, "ArucoDetector"))

本文使用环境的输出为:

4.13.0
True
True

一些较早的 Python 安装只包含基础 OpenCV,可能没有 cv2.aruco。项目已有固定环境时,不应为解决这一问题直接升级或混装依赖,应先核对现有依赖约束和部署环境。

5.2 生成码

下面生成 DICT_4X4_50 字典中的 ID 0:

from pathlib import Path

import cv2


output_path = Path("aruco_dict4x4_50_id0.png")

dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
marker = dictionary.generateImageMarker(0, 800)

# 白边不属于 ArUco 编码,但有利于黑色外框与环境分离。
printable = cv2.copyMakeBorder(marker, 120, 120, 120, 120, cv2.BORDER_CONSTANT, value=255)

if not cv2.imwrite(str(output_path), printable):
    raise RuntimeError(f"保存标记失败:{output_path}")

参数含义:

  • DICT_4X4_50:使用的字典;
  • 0:字典中的 ID;
  • 800:生成图像中黑色标记主体的边长,单位是像素;
  • 120:额外添加的白色留边,打印尺寸应另行控制。

不要从网页截图后打印,也不要用会产生模糊插值的方式缩放。用于测量时,优先输出足够分辨率的图像或矢量标定板,并关闭打印软件的“适应页面”“缩放填充”等选项。

5.3 检测示例

import cv2


image = cv2.imread("camera_image.png")
if image is None:
    raise FileNotFoundError("camera_image.png")

dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
parameters = cv2.aruco.DetectorParameters()
detector = cv2.aruco.ArucoDetector(dictionary, parameters)

corners, ids, rejected = detector.detectMarkers(image)

if ids is None:
    print("没有检测到 ArUco 标记")
else:
    for marker_id, marker_corners in zip(ids.flatten(), corners):
        points = marker_corners.reshape(4, 2)
        print(f"ID={int(marker_id)}")
        print(points)

    cv2.aruco.drawDetectedMarkers(image, corners, ids)
    cv2.imwrite("detection_result.png", image)

三个输出分别是:

  • corners:每个已识别标记的四个有序角点;
  • ids:与 corners 一一对应的 ID;
  • rejected:外观像方形标记、但未通过字典解码的候选区域,主要用于调试。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐