用Arduino Nano和OpenCV做个五子棋机器人:从机械臂运动学到视觉识别的完整避坑指南
用Arduino Nano和OpenCV打造五子棋机器人:从零开始的实战避坑指南
五子棋机器人听起来像是实验室里的高端设备,但实际上只需要一块Arduino Nano开发板、一个三自由度机械臂和普通的USB摄像头就能实现。这个项目完美融合了机械控制、计算机视觉和算法设计,是入门嵌入式开发和机器视觉的绝佳实践。我在大学宿舍里花了四个月时间从零开始搭建这个系统,期间踩过的坑比下过的五子棋还多——从机械臂突然抽风到摄像头识别错乱,各种奇葩问题层出不穷。本文将带你一步步复现整个项目,重点分享那些教程里不会告诉你的实战经验。
1. 硬件选型与机械结构搭建
1.1 核心组件选型要点
选择硬件时最容易犯的错误就是盲目追求高性能。实际上对于五子棋机器人来说,稳定可靠比性能参数更重要:
- 控制主板 :Arduino Nano是性价比之选,但要注意买CH340芯片版本(价格约35元),避免驱动兼容问题。我最初用的Nano克隆板就因USB转串口不稳定导致机械臂失控。
- 机械臂 :三自由度(3DOF)足够用,舵机建议选择MG996R(扭矩11kg/cm),比SG90更可靠。实测下棋过程中舵机发热是正常现象,只要不超过60℃就无需担心。
- 摄像头 :普通720P USB摄像头即可,但必须带手动对焦功能。自动对焦会在机械臂移动时不断重新对焦,导致识别失败。推荐罗技C270(二手约80元),它的固定焦距模式表现稳定。
1.2 机械结构组装技巧
组装机械臂时最关键的参数是 工作空间 ——要确保机械臂能覆盖整个棋盘(通常20×20cm)。这是我的结构参数:
| 部件 | 长度 | 角度范围 | 安装要点 |
|---|---|---|---|
| 底座舵机 | - | 0-180° | 需用螺丝固定在5mm亚克力板上 |
| 大臂 | 15cm | 30-150° | 与底座垂直安装 |
| 小臂 | 12cm | 45-135° | 末端加装电磁铁吸盘 |
提示:先用3D打印或纸板制作1:1模型测试运动范围,避免反复拆装损坏舵机。
组装完成后需要校准机械臂的零位。简单的方法是让所有舵机转到90°位置,此时机械臂应呈"L"形。用以下Arduino代码测试每个舵机的运动范围:
#include <Servo.h>
Servo servo1;
void setup() {
servo1.attach(9); // 连接D9引脚
}
void loop() {
servo1.write(0); // 转到0°位置
delay(2000);
servo1.write(180); // 转到180°位置
delay(2000);
}
2. 开发环境配置与基础通信
2.1 软件环境避坑指南
OpenCV版本选择是个大坑。最新版OpenCV 4.x对旧代码兼容性差,而3.x的某些功能在2.x中又不支持。经过多次测试,我推荐以下组合:
- OpenCV 3.4.9 :最后一个支持SIFT/SURF等专利算法的版本
- Visual Studio 2019 :社区版完全免费,配置时注意勾选"使用C++的桌面开发"
- Arduino IDE 1.8.19 :新版2.0的串口监视器有bug
安装OpenCV时最容易出错的是环境变量配置。正确步骤是:
- 下载OpenCV 3.4.9的Windows版exe并运行
-
解压到
C:\opencv(路径不要有中文或空格) -
添加系统变量:
OPENCV_DIR = C:\opencv\build\x64\vc15 - 在VS2019项目属性中添加包含目录和库目录
2.2 串口通信稳定性优化
机械臂控制最大的痛点就是串口通信不稳定。经过两周的调试,我总结出以下稳定通信方案:
-
硬件层面 :
- 使用带磁环的USB线减少干扰
- 在Arduino的RX引脚加装1kΩ上拉电阻
- 电源单独供电(不要用电脑USB供电)
-
软件层面 :
- 设置115200波特率(实测比9600更稳定)
- 添加校验和与超时重发机制
- 使用以下改进的通信协议:
# Python端发送指令示例
import serial
import time
ser = serial.Serial('COM3', 115200, timeout=1)
def send_command(angle1, angle2, angle3):
cmd = f"#{angle1:03d}{angle2:03d}{angle3:03d}"
checksum = sum(ord(c) for c in cmd) % 256
full_cmd = f"{cmd}{checksum:03d}\n"
ser.write(full_cmd.encode())
while True:
ack = ser.readline().decode().strip()
if ack == 'OK':
break
time.sleep(0.1)
3. 视觉识别系统实现
3.1 棋盘检测与校正
棋盘识别最棘手的两个问题是透视变形和光照变化。我的解决方案分三步:
- 边缘检测 :先用高斯模糊去噪,再用自适应阈值处理解决反光问题
Mat gray, blur, thresh;
cvtColor(frame, gray, COLOR_BGR2GRAY);
GaussianBlur(gray, blur, Size(5,5), 0);
adaptiveThreshold(blur, thresh, 255,
ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 11, 2);
- 透视变换 :找到棋盘轮廓后,用霍夫线检测校正倾斜
vector<Vec2f> lines;
HoughLines(edges, lines, 1, CV_PI/180, 150);
// 计算平均角度
float avg_angle = 0;
for(auto line : lines) {
avg_angle += line[1];
}
avg_angle /= lines.size();
// 旋转校正
Mat rotation = getRotationMatrix2D(center, avg_angle*180/CV_PI, 1.0);
warpAffine(frame, corrected, rotation, frame.size());
- 网格定位 :用投影法确定每个交叉点坐标
def find_grid_lines(image):
# 水平投影
h_proj = np.sum(image, axis=1)
h_peaks = np.where(h_proj > np.max(h_proj)*0.8)[0]
# 垂直投影
v_proj = np.sum(image, axis=0)
v_peaks = np.where(v_proj > np.max(v_proj)*0.8)[0]
return h_peaks, v_peaks
3.2 棋子状态识别技巧
识别棋子状态时,传统方法是基于颜色阈值,但在不同光照下效果很差。我改用以下方案:
- 背景差分法 :记录空棋盘作为背景,实时图像与背景做差
- 圆形检测 :用HoughCircles检测棋子位置
- 亮度补偿 :根据棋盘格灰度自动调整识别阈值
// 动态阈值计算示例
Scalar mean, stddev;
meanStdDev(roi, mean, stddev);
double threshold = mean[0] + stddev[0]*1.5;
4. 运动控制与决策算法
4.1 机械臂逆运动学实现
三自由度机械臂的运动学计算需要解一组非线性方程。我的简化解法是:
- 将三维问题分解为二维平面问题
- 使用几何法求解关节角度
- 添加平滑轨迹规划
def inverse_kinematics(x, y, z):
# 底座旋转角度
theta1 = np.arctan2(y, x)
# 平面投影距离
r = np.sqrt(x**2 + y**2) - base_radius
d = np.sqrt(r**2 + z**2)
# 大臂和小臂角度
cos_theta3 = (d**2 - L1**2 - L2**2) / (2*L1*L2)
theta3 = np.arccos(np.clip(cos_theta3, -1, 1))
alpha = np.arctan2(z, r)
beta = np.arctan2(L2*np.sin(theta3), L1 + L2*np.cos(theta3))
theta2 = alpha + beta
return np.degrees([theta1, theta2, theta3])
4.2 五子棋AI简易实现
完整的五子棋AI比较复杂,这里分享一个基于规则的高效简化版:
-
优先级规则 :
- 第一优先级:自己有四连时直接获胜
- 第二优先级:阻止对手形成四连
- 第三优先级:在自己活三位置下子
- 第四优先级:阻止对手活三
-
评分函数 :
def evaluate(board):
score = 0
patterns = {
'11111': 100000, # 五连
'011110': 10000, # 活四
'011112': 500, # 冲四
'01110': 1000, # 活三
'001112': 300, # 眠三
'0110': 500, # 活二
}
# 检查所有行、列、对角线
...
return score
5. 系统集成与调试技巧
联调阶段最常见的问题是视觉系统与机械臂的坐标不一致。我的解决方案是:
-
建立统一坐标系 :
- 在棋盘四个角放置标记物
- 机械臂依次触碰这些点记录实际坐标
- 计算视觉坐标到机械坐标的变换矩阵
-
校准流程 :
def calibrate():
points_camera = [] # 视觉检测到的坐标
points_arm = [] # 机械臂实际到达的坐标
for i in range(4):
x, y = detect_marker(i)
points_camera.append([x, y])
arm_move_to(x, y)
input("按下回车记录当前位置")
points_arm.append(get_arm_position())
# 计算单应性矩阵
H, _ = cv2.findHomography(np.array(points_camera),
np.array(points_arm))
return H
- 运动平滑处理 : 添加中间过渡点避免机械臂急停:
void smooth_move(float target[3]) {
float step = 0.05;
float current[3];
get_current_angles(current);
while(distance(current, target) > 1.0) {
for(int i=0; i<3; i++) {
current[i] += (target[i]-current[i])*step;
}
set_angles(current);
delay(20);
}
}
电磁铁控制也有讲究——吸棋时间太短会掉落,太长影响速度。经过测试,50ms是最佳值:
void pick_piece(bool pick) {
digitalWrite(ELECTRO_PIN, pick ? HIGH : LOW);
delay(pick ? 50 : 20); // 吸棋50ms,放棋20ms
}
这个项目最让我意外的是机械臂的重复定位精度——经过精心校准后,实际测试能达到±0.3mm,完全满足五子棋对局需求。记得在正式运行前做至少100次取放测试,观察是否有异常情况。当看到机械臂准确识别并落子的那一刻,之前所有的调试痛苦都值了。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)