200字
个人项目:基于OpenCV的视觉抓取机械臂
2026-02-16
2026-02-16

个人项目:基于OpenCV的视觉抓取机械臂

arm.jpg

基本信息

  • 时间:2024.12 – 2025.01
  • 关键词:Python / OpenCV / C++ / Arduino / Inverse Kinematics / Serial JSON
  • 演示视频(B站):BV1uHrmYbEYr
  • 代码仓库:https://github.com/ruali-dev/vision-arm-grasp

演示


项目简介

基于视觉引导的机械臂抓取系统,使用 OpenCV 进行目标检测,通过串口通信控制 Arduino 驱动的 6 轴机械臂。(实际只用了5轴)

  • 视觉定位:OpenCV 基于颜色阈值(HSV)+ 连通域,得到目标中心点。
  • 坐标映射:把像素坐标映射到机械臂工作平面坐标,用于定位目标。
  • 运动学与控制链路:逆运动学用 C++ 写成 .so 动态库,Python 调用;再通过 JSON 串口协议把关节角下发给 Arduino 执行(抓取→投放→复位)。

项目亮点

  • 3D 打印机械臂 + USB 摄像头,成本低,适合入门
  • 上位机用 N100 工控机跑 Ubuntu 22.04,Arduino UNO 控制六个舵机
  • 逆运动学核心算法用 C++ 实现为动态库,Python 调用,兼顾开发效率和运行速度
  • 串口 JSON 协议通信,一条指令同时控制六个舵机角度

系统架构

层级 硬件
上位机 N100 工控机(Ubuntu 22.04)
控制器 Arduino UNO
执行器 MG996 × 3 + MG90S × 3(共 6 个舵机)
感知 USB 摄像头
机械结构 3D 打印件组装

流程:摄像头采集 → 图像处理得到目标中心 → 坐标映射 → 逆运动学解算 → 串口下发指令 → 舵机执行抓取投放。

关键技术实现

1. 目标检测

用 OpenCV 做颜色识别。HSV 空间过滤出目标颜色,然后通过连通域分析找到目标中心点。简单够用。

2. 坐标转换

像素坐标到机械臂基坐标的转换,用了一个线性变换:

P = T + R × (P_pixel - image_center) × scale_factor

手工标定,试了几次就差不多能用了。

3. 逆运动学

参考了 B 站其他大佬的做法,用三角函数求解四个关节的角度。写成了 C++ 动态库让 Python 调用,比纯 Python 解算快一些。

4. 通信协议

Python 通过串口发 JSON 给 Arduino,格式大概这样:

{"servo1":90,"servo2":90,"servo3":90,"servo4":145,"servo5":90,"servo6":180}

Arduino 解析 JSON 后直接控制对应舵机。


踩过的坑

  • MG996 舵机有一个磨损了,抓取力度不够,视频里只演示抓一个
  • Linux 串口设备名需要处理,可能和 brltty 冲突
  • 坐标映射精度一般,手工标定的上限就在那

硬件清单

配件 数量
MG90S 舵机 3
MG996 舵机(180°) 3
Arduino UNO 1
5V 3A 电源适配器 1
M3 螺丝螺母若干 -

整体成本大概两三百块(不算N100)。

代码展示(节选)

gitHub.webp

详细代码见Github仓库:ruali-dev/vision-arm-grasp: Vision-guided robotic arm grasping with OpenCV, C++ inverse kinematics and Arduino control.

下面列出部分代码:

上位机流程

# ====== 上位机(Python):坐标 -> IK(ctypes) -> JSON -> 串口 ======
import os, json, time, ctypes
import numpy as np
import serial
import cv2

# 1) ctypes:加载运动学 .so/.dll + 返回结构体
if os.name == "nt":
    lib = ctypes.CDLL("./calculate_servo_angles.dll")
else:
    lib = ctypes.CDLL("./calculate_servo_angles.so")

class ServoAngles(ctypes.Structure):
    _fields_ = [
        ("servo1", ctypes.c_float),
        ("servo2", ctypes.c_float),
        ("servo3", ctypes.c_float),
        ("servo4", ctypes.c_float),
    ]

lib.calculate_servo_angles.argtypes = [ctypes.c_float, ctypes.c_float, ctypes.c_float]
lib.calculate_servo_angles.restype = ServoAngles


# 2) 串口协议:JSON 下发
class RobotArm:
    def __init__(self, port="/dev/robotarm", baudrate=115200):
        self.ser = serial.Serial(port, baudrate, timeout=5, bytesize=8, parity='N', stopbits=1)
        time.sleep(3)

    def send_command(self, angles: ServoAngles, servo6_value: int):
        command = {
            "servo1": round(angles.servo1, 2),
            "servo2": round(angles.servo2, 2),
            "servo3": round(angles.servo3, 2),
            "servo4": 145,
            "servo5": round(angles.servo4, 2),
            "servo6": servo6_value,
        }
        json_data = json.dumps(command, separators=(",", ":")) + "\n"
        self.ser.reset_input_buffer()
        self.ser.reset_output_buffer()
        self.ser.write(json_data.encode("utf-8"))
        time.sleep(3)
        return True

    def catch(self, x, y):
        angles = lib.calculate_servo_angles(x, y, 5)
        self.send_command(angles, 180)  # 张开
        time.sleep(2)
        self.send_command(angles, 47)   # 闭合
        time.sleep(2)
        return True


# 3) 像素坐标 -> 机械臂平面坐标(你的标定/变换参数)
def calculate_coordinates(image_x, image_y):
    scale_factor = 0.0460
    image_center = np.array([320, 240])
    theta = np.radians(-90)
    T = np.array([-0.5, 30.1])

    P_pixel = np.array([image_x, image_y])
    P_prime = (P_pixel - image_center) * scale_factor
    R = np.array([[np.cos(theta), -np.sin(theta)],
                  [np.sin(theta),  np.cos(theta)]])
    P = T + R @ P_prime
    return float(P[0]), float(P[1] - 15)


# 4) 视觉检测(HSV + 连通域中心点)
def detect_white_center(frame_bgr):
    hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV)

    lower = np.array([109, 0, 223])
    upper = np.array([180, 255, 255])

    mask = cv2.inRange(hsv, lower, upper)
    num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8)

    for i in range(1, num_labels):
        x, y, w, h, area = stats[i]
        if area < 150:
            continue
        cx, cy = centroids[i]
        return int(cx), int(cy)  # 只取一个目标(演示)
    return None


def main():
    arm = RobotArm("/dev/robotarm", 115200)
    cap = cv2.VideoCapture(0)

    while True:
        ok, frame = cap.read()
        if not ok:
            continue

        frame = cv2.flip(frame, 0)
        center = detect_white_center(frame)
        cv2.imshow("camera", frame)

        if center is not None:
            u, v = center
            x, y = calculate_coordinates(u, v)
            arm.catch(x, y)
            break

        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

Arduino

// ====== 下位机(Arduino):串口收 JSON -> 写 6 路舵机 ======
#include <Arduino.h>
#include <Servo.h>
#include <ArduinoJson.h>

Servo servo1, servo2, servo3, servo4, servo5, servo6;

int s1Angle = 90, s2Angle = 90, s3Angle = 90, s4Angle = 145, s5Angle = 90, s6Angle = 90;

void setup() {
  Serial.begin(115200);

  servo1.attach(7, 500, 2500);
  servo2.attach(6, 500, 2500);
  servo3.attach(5, 500, 2500);
  servo4.attach(4, 500, 2500);
  servo5.attach(2, 500, 2500);
  servo6.attach(3, 500, 2500);

  servo1.write(s1Angle);
  servo2.write(s2Angle);
  servo3.write(s3Angle);
  servo4.write(s4Angle);
  servo5.write(s5Angle);
  servo6.write(s6Angle);
  delay(50);
}

void loop() {
  if (Serial.available() > 0) {
    String jsonData = Serial.readStringUntil('\n');
    Serial.println("Received: " + jsonData);

    JsonDocument doc;
    DeserializationError error = deserializeJson(doc, jsonData);
    if (error) {
      Serial.print("JSON 解析失败: ");
      Serial.println(error.c_str());
      return;
    }

    s1Angle = doc["servo1"];
    s2Angle = doc["servo2"];
    s3Angle = doc["servo3"];
    s4Angle = doc["servo4"];
    s5Angle = doc["servo5"];
    s6Angle = doc["servo6"];
  }

  servo1.write(s1Angle);
  servo2.write(s2Angle);
  servo3.write(s3Angle);
  servo4.write(s4Angle);
  servo5.write(s5Angle);
  servo6.write(s6Angle);
}

运动学

// ====== 运动学(C++):基于遍历的 IK,输出 4 个舵机角(摘自你现有 kinematics.cpp 的核心逻辑) ======
#include <math.h>

struct ServoAngles {
  float servo1, servo2, servo3, servo4;
};

// X,Y,Z:目标点(cm)
// 说明:这里保留了你原来的“遍历 j_all -> 过滤关节约束 -> 取中位解”的做法
ServoAngles calculate_servo_angles(float X, float Y, float Z) {
  float a1 = 12.5f, a2 = 12.f, a3 = 12.f, a4 = 12.f;   // 机构参数
  float P  = 15.f;                                     // 底座半径

  float j1;
  if (X == 0) j1 = 90;
  else        j1 = atan((Y + P) / X) * 57.3f;

  float len = sqrt((Y + P) * (Y + P) + X * X);
  float high = Z;

  float n = 0;
  float j2 = 0, j3 = 0, j4 = 0;

  // 1) 统计可行解数量
  for (int i = 0; i <= 180; i++) {
    float j_all = 3.1415927f * i / 180.f;

    float L = len  - a4 * sin(j_all);
    float H = high - a4 * cos(j_all) - a1;

    float Cosj3 = (L * L + H * H - a2 * a2 - a3 * a3) / (2.f * a2 * a3);
    float Sinj3 = sqrt(1.f - Cosj3 * Cosj3);

    float tmp_j3 = atan(Sinj3 / Cosj3) * 57.3f;

    float K2 = a3 * sin(tmp_j3 / 57.3f);
    float K1 = a2 + a3 * cos(tmp_j3 / 57.3f);

    float Cosj2 = (K2 * L + K1 * H) / (K1 * K1 + K2 * K2);
    float Sinj2 = sqrt(1.f - Cosj2 * Cosj2);

    float tmp_j2 = atan(Sinj2 / Cosj2) * 57.3f;
    float tmp_j4 = j_all * 57.3f - tmp_j2 - tmp_j3;

    if (tmp_j2 >= 0 && tmp_j3 >= 0 && tmp_j4 >= 0 && tmp_j2 <= 90 && tmp_j3 <= 90 && tmp_j4 <= 90) {
      n += 1.f;
    }
  }

  // 2) 再遍历一次,取中位解
  float m = 0;
  for (int i = 0; i <= 180; i++) {
    float j_all = 3.1415927f * i / 180.f;

    float L = len  - a4 * sin(j_all);
    float H = high - a4 * cos(j_all) - a1;

    float Cosj3 = (L * L + H * H - a2 * a2 - a3 * a3) / (2.f * a2 * a3);
    float Sinj3 = sqrt(1.f - Cosj3 * Cosj3);

    j3 = atan(Sinj3 / Cosj3) * 57.3f;

    float K2 = a3 * sin(j3 / 57.3f);
    float K1 = a2 + a3 * cos(j3 / 57.3f);

    float Cosj2 = (K2 * L + K1 * H) / (K1 * K1 + K2 * K2);
    float Sinj2 = sqrt(1.f - Cosj2 * Cosj2);

    j2 = atan(Sinj2 / Cosj2) * 57.3f;
    j4 = j_all * 57.3f - j2 - j3;

    if (j2 >= 0 && j3 >= 0 && j4 >= 0 && j2 <= 90 && j3 <= 90 && j4 <= 90) {
      m += 1.f;
      if (m == n / 2.f || m == (n + 1.f) / 2.f) break;
    }
  }

  // 3) 关节角 -> 舵机角(按你原来的映射)
  float s1 = 90;
  if (j1 > 0 && j1 < 90) s1 = 90 - j1;
  else if (j1 < 0)       s1 = 90 + fabs(j1);

  float s2 = (j2 > 0) ? (j2 + 90) : (j2 < 0 ? (90 - fabs(j2)) : 90);
  float s3 = (j3 > 0) ? (90 - j3) : (j3 < 0 ? (90 + fabs(j3)) : 90);
  float s4 = (j4 > 0) ? (j4 + 90) : (j4 < 0 ? (90 - fabs(j4)) : 90);

  return ServoAngles{ s1, s2, s3, s4 };
}

个人项目:基于OpenCV的视觉抓取机械臂
作者
若离
发表于
2026-02-16
License
CC BY-NC-SA 4.0

评论