用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时最容易出错的是环境变量配置。正确步骤是:

  1. 下载OpenCV 3.4.9的Windows版exe并运行
  2. 解压到 C:\opencv (路径不要有中文或空格)
  3. 添加系统变量: OPENCV_DIR = C:\opencv\build\x64\vc15
  4. 在VS2019项目属性中添加包含目录和库目录

2.2 串口通信稳定性优化

机械臂控制最大的痛点就是串口通信不稳定。经过两周的调试,我总结出以下稳定通信方案:

  1. 硬件层面 :

    • 使用带磁环的USB线减少干扰
    • 在Arduino的RX引脚加装1kΩ上拉电阻
    • 电源单独供电(不要用电脑USB供电)
  2. 软件层面 :

    • 设置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 棋盘检测与校正

棋盘识别最棘手的两个问题是透视变形和光照变化。我的解决方案分三步:

  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);
  1. 透视变换 :找到棋盘轮廓后,用霍夫线检测校正倾斜
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());
  1. 网格定位 :用投影法确定每个交叉点坐标
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 棋子状态识别技巧

识别棋子状态时,传统方法是基于颜色阈值,但在不同光照下效果很差。我改用以下方案:

  1. 背景差分法 :记录空棋盘作为背景,实时图像与背景做差
  2. 圆形检测 :用HoughCircles检测棋子位置
  3. 亮度补偿 :根据棋盘格灰度自动调整识别阈值
// 动态阈值计算示例
Scalar mean, stddev;
meanStdDev(roi, mean, stddev);
double threshold = mean[0] + stddev[0]*1.5;

4. 运动控制与决策算法

4.1 机械臂逆运动学实现

三自由度机械臂的运动学计算需要解一组非线性方程。我的简化解法是:

  1. 将三维问题分解为二维平面问题
  2. 使用几何法求解关节角度
  3. 添加平滑轨迹规划
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比较复杂,这里分享一个基于规则的高效简化版:

  1. 优先级规则 :

    • 第一优先级:自己有四连时直接获胜
    • 第二优先级:阻止对手形成四连
    • 第三优先级:在自己活三位置下子
    • 第四优先级:阻止对手活三
  2. 评分函数 :

def evaluate(board):
    score = 0
    patterns = {
        '11111': 100000,  # 五连
        '011110': 10000,   # 活四
        '011112': 500,     # 冲四
        '01110': 1000,     # 活三
        '001112': 300,     # 眠三
        '0110': 500,       # 活二
    }
    # 检查所有行、列、对角线
    ...
    return score

5. 系统集成与调试技巧

联调阶段最常见的问题是视觉系统与机械臂的坐标不一致。我的解决方案是:

  1. 建立统一坐标系 :

    • 在棋盘四个角放置标记物
    • 机械臂依次触碰这些点记录实际坐标
    • 计算视觉坐标到机械坐标的变换矩阵
  2. 校准流程 :

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
  1. 运动平滑处理 : 添加中间过渡点避免机械臂急停:
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次取放测试,观察是否有异常情况。当看到机械臂准确识别并落子的那一刻,之前所有的调试痛苦都值了。

Logo

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

更多推荐