1. 项目概述:先搞清楚这台“捡来”的机械臂到底能干啥
先说结论:这个项目本质上是给一台没有视觉能力的机械臂“装上一双眼睛”,让它能通过摄像头识别物体的颜色,算出目标在空间里的位置,再驱动机械臂完成抓取、分拣之类的动作。标题里“路上捡到”当然是玩笑话,实际就是我从工位角落里翻出来一台吃灰多年的桌面级四轴机械臂,配套的夹爪还能动,但没有任何视觉模块,属于纯粹的“聋哑设备”。颜色识别这个需求,正好是这类设备最容易上手的视觉改造方向。
为什么选颜色识别而不是更复杂的方案?因为颜色识别在工业现场和实验室里都足够典型:目标物体颜色差异明显、环境光相对可控、抓取平面固定。它不需要训练模型,不需要昂贵的计算平台,一台普通笔记本加一个USB摄像头就能跑起来。如果是刚接触机器视觉和机械臂控制的同学,这个项目可以帮你把“图像处理”“坐标系变换”“运动控制”这三块知识完整串起来,成就感来得很快。
整个项目拆开看,可以分成四个子任务:图像采集与颜色识别、像素坐标到机械臂坐标的换算、机械臂运动规划与夹爪控制、整体联调与稳定性优化。每一块都有坑,但也都对应着明确的技术点。接下来我会按照自己实际调通的顺序,把方案选型、核心原理、实操步骤和踩坑记录完整写出来。
2. 核心方案与工具选型:为什么没用深度学习也能玩得转
2.1 方案对比:传统视觉 vs 深度学习
做目标识别,现在大家第一反应可能是深度学习,YOLO、实例分割这些名词张口就来。但放在“捡来的机械臂”这个项目上,深度学习不一定是最优解。我对比过三种方案,直接说结论。
| 方案 | 计算开销 | 部署复杂度 | 识别稳定性 | 适用场景 |
|---|---|---|---|---|
| OpenCV + HSV颜色阈值 | 极低 | 低,几十行代码搞定 | 依赖光照,颜色差异大时很稳 | 固定环境下的颜色分拣 |
| 颜色 + 形状/轮廓筛选 | 低 | 中,需要调轮廓参数 | 抗干扰更好 | 目标物颜色相近但有固定形状 |
| 深度学习目标检测 | 高,需要GPU或NPU | 高,需要标注训练 | 环境适应性强 | 复杂背景、多目标、遮挡严重 |
最终选了纯HSV颜色阈值方案。原因很实在:项目目标物体是红、绿、蓝三种纯色方块,颜色差异肉眼可见,环境光基本可控,摄像头固定在一个位置不动。传统视觉方案每秒能跑几十帧,响应延迟低到几乎感知不到,而深度学习方案哪怕用轻量模型,在普通笔记本上也要几十毫秒到上百毫秒的推理时间,对实时抓取来说并不友好。更关键的是,HSV阈值方案的每一个判断逻辑都是可解释的,颜色识别出错了,拧几个参数就能看效果,不用重新训练。
2.2 机械臂本体的设备底细
我手上这台四轴机械臂是很多年前入门的桌面级产品,控制方式很简单,串口发指令,支持基本的点到点运动、直线运动和夹爪开合。它的控制精度号称1mm左右,实际空载运行时重复定位精度还可以,但带负载后末端会有轻微下垂。这种设备做颜色识别抓取正好,因为精度要求不算极端,但又能把视觉闭环的完整流程跑通。
有一点需要明确:摄像头放哪里,决定了坐标换算的复杂度。通常两种方案,一种是“眼在手上”,摄像头装在机械臂末端,跟着机械臂动,好处是观察角度灵活,坏处是标定复杂;另一种是“眼在手外”,摄像头固定在机械臂旁边的高处,俯视工作台面。这个项目我选了眼在手外,理由很简单:抓取平面是固定的桌面,俯视安装让目标的像素坐标与桌面平面坐标之间近似一个平面映射关系,数学上只要一个单应性矩阵就能搞定,不需要完整的手眼标定。
2.3 整体流程串起来
整个系统的工作顺序是这样的:
- 摄像头实时采集画面,传给上位机。
- 上位机对每一帧做颜色识别,找到目标物体的轮廓中心点,得到像素坐标(u, v)。
- 通过预先标定好的单应性矩阵,把像素坐标换算成机械臂基座坐标(X, Y)。
- 根据目标物体的高度,补偿夹爪下降的Z轴值。
- 上位机向机械臂发送运动指令,先移动到目标上方安全高度,再下爪抓取,抬升,放到指定位置。
- 循环执行。
这里面最核心的“为什么”有两个:为什么像素坐标不能直接发给机械臂?为什么标定之后还要做Z轴补偿?第一个问题是因为摄像头看到的是一张平面图像,图像上的一个点只对应一条射线,不能唯一确定空间位置,必须结合摄像头安装高度和桌面的几何关系才能得到真实坐标。第二个问题是因为物体本身有一定高度,如果忽略高度直接用桌面坐标,夹爪会撞到物体顶部或者抓空。这两个问题会在下一节重点展开。
3. 核心细节拆解:HSV颜色空间与坐标变换到底是怎么回事
3.1 颜色识别为什么必须用HSV而不是RGB
很多新手上来就写RGB判断,比如“红色就是R大于200且G小于50”,结果一换光线就全线崩溃。原因在于RGB三个通道和人感知颜色的方式不对齐,它把颜色的亮度信息和色度信息混在一起。比如说,同一个红色方块,在明亮环境下RGB可能是(200, 30, 30),在阴影里可能变成(120, 20, 20),R值的波动非常大,固定阈值根本罩不住。
HSV色彩空间解决了这个问题。它把颜色拆成三个独立维度:
- H(Hue,色调):表示颜色种类,用角度衡量,范围0到180度(OpenCV里减半)。
- S(Saturation,饱和度):表示颜色的鲜艳程度,0是灰色,255是纯色。
- V(Value,明度):表示亮度,0是黑色,255是最亮。
用生活类比:RGB像告诉你“这杯颜料里红、绿、蓝各放了多少毫升”,HSV则像说“这是偏橙色的红,很浓,而且比较亮”。颜色识别的关键就是锁定H的范围,S和V只用来排除太暗、太灰的区域。举个例子,我识别红色时用到的阈值是这样一组范围:
| 颜色 | H范围 | S范围 | V范围 |
|---|---|---|---|
| 红色(两段) | 0~10 和 170~180 | 80~255 | 60~255 |
| 绿色 | 35~85 | 80~255 | 60~255 |
| 蓝色 | 100~130 | 80~255 | 60~255 |
注意红色的H范围要分两段,因为红色在色相环上正好跨过0度边界。这是新手最容易漏掉的地方,只写一个0到10的范围,结果偏紫红的目标就识别不到了。
3.2 从像素到机械臂坐标:单应性矩阵的实战理解
假设摄像头固定在高处俯视桌面,桌面是个平面,图像平面是个平面,两个平面之间就存在一个射影变换关系。数学上,这个关系叫做单应性变换,用3×3的矩阵H来表示。给定图像坐标(u, v),就能算出桌面上的物理坐标(x, y),公式是:
[x'] [h11 h12 h13] [u] [y'] = [h21 h22 h23] * [v] [w ] [h31 h32 h33] [1] x = x' / w y = y' / w注意这里要做一步除法,因为齐次坐标转换后w不一定等于1。实际代码里可以直接用OpenCV的cv2.findHomography求H矩阵,然后调用cv2.perspectiveTransform做转换。求H矩阵需要至少四组对应的点,我采用的是四点标定法。
3.3 手把手理解四点标定怎么做
标定的逻辑是这样的:在机械臂的工作桌面上放一张标定板,或者直接在桌面上画四个已知坐标的标记点。机械臂有自己的基座坐标系,你可以通过示教功能把末端移动到某一个标记点上,读出机械臂当前坐标(X, Y)。然后把这个标记点在摄像头画面里的像素坐标(u, v)记录下来。这样我们就得到了第一组对应点:像素坐标对应物理坐标。
重复操作四个不共线的标记点后,把这四组对应点传给findHomography,就能得到完整的单应性矩阵。实际操作中,我的桌面角落贴了一张自制的标定纸,四个黑色十字标记点的物理坐标是按照机械臂基座原点量出来的,分别是:
- 点A:(100, 100),像素约(220, 320)
- 点B:(300, 100),像素约(460, 320)
- 点C:(100, 300),像素约(220, 560)
- 点D:(300, 300),像素约(460, 560)
这里有个经验:四个点尽量分布在工作空间四周,不要挤在一个角落,否则矩阵在边缘区域的外推误差会很大。标定完成后,我还特意验证过几个中间点,像素坐标转出来的物理坐标和实际用尺子量的偏差控制在5mm以内。这个精度对夹爪抓取3cm以上的方块来说完全够用。
3.4 Z轴高度补偿为什么不能省
单应性矩阵只能解决X和Y,解决不了Z。因为平面映射的前提是目标都在同一个平面上,但物体本身有厚度。假设红色方块高3cm,摄像头看到的是方块顶面的中心,换算出来的坐标是顶面中心在桌面上的投影位置,这个位置其实和底面中心的位置在水平方向是重合的(只要摄像头光轴不严重倾斜),但夹爪下降的高度如果按桌面高度来算,就会直接撞在方块顶部。
所以我在代码里维护了一个物体高度表,颜色识别出物体后,根据它的颜色查表得到高度,夹爪到达目标位置上方后,下降距离是"物体高度+夹爪安全间隙",而不是固定值。这样换成不同厚度的物体也能适应。
4. 实操环节:从环境搭建到第一次成功抓取
4.1 硬件环境和连接方式
硬件清单非常简单:
- 桌面四轴机械臂一台(串口控制,支持PTP直线运动,带平行夹爪)。
- USB摄像头一个,我用的是1080p免驱摄像头,实际运行分辨率调到640×480,帧率30fps。
- 上位机一台,装Python和OpenCV。
- 一个稳定的5V/2A电源给机械臂供电。
摄像头用支架固定在桌面上方约50cm处,俯视角度尽量垂直,倾斜太大会导致物体高度对像素坐标的影响变大。机械臂基座放在摄像头视野正前方,确保机械臂的工作区域完全落在画面里。
连接上有一个教训:机械臂的电源一定要单独供电,不要和上位机共用USB口。我刚开始图省事,把机械臂控制板直接插在笔记本USB口上,结果夹爪一动作,电压跌落,机械臂直接原地抽搐。后来换独立电源就再没出现过。
4.2 颜色识别核心代码:OpenCV实战写法
我把颜色识别模块写成独立函数,输入一帧图像,输出目标颜色对应的中心点像素坐标。核心逻辑如下:
import cv2 import numpy as np color_ranges = { "red": [(0, 80, 60), (10, 255, 255), (170, 80, 60), (180, 255, 255)], "green": [(35, 80, 60), (85, 255, 255)], "blue": [(100, 80, 60), (130, 255, 255)], } def detect_color_center(frame, color_name): hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = np.zeros(frame.shape[:2], dtype=np.uint8) ranges = color_ranges[color_name] # 红色分两段区间,分别生成掩码后合并 for i in range(0, len(ranges), 2): lower = np.array(ranges[i], dtype=np.uint8) upper = np.array(ranges[i+1], dtype=np.uint8) mask = cv2.bitwise_or(mask, cv2.inRange(hsv, lower, upper)) # 开运算去掉噪点,闭运算填补内部空洞 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None # 取面积最大的轮廓,过滤过小的噪点 largest = max(contours, key=cv2.contourArea) if cv2.contourArea(largest) < 500: return None M = cv2.moments(largest) if M["m00"] == 0: return None cx = int(M["m10"] / M["m00"]) cy = int(M["m01"] / M["m00"]) return (cx, cy)这段代码里有两个细节值得说:一个是形态学操作。原图直接二值化后,往往有很多孤立噪点,开运算(先腐蚀后膨胀)能去掉这些离散点,闭运算(先膨胀后腐蚀)能把物体内部因反光产生的黑色空洞补上。第二个是轮廓筛选,只取面积最大的轮廓,防止桌面其他杂色物体干扰。实际跑下来,只要光照稳定,这一套的识别率非常高,连续几百帧都不会丢目标。
4.3 坐标系标定脚本:四点映射的完整实现
标定环节我单独写了一个脚本,步骤是:先在画面里点击四个标记点的像素坐标,同时手动在机械臂示教界面读出对应的物理坐标,然后把数据填入脚本,得到H矩阵。
# 标定点对:像素坐标(u,v) -> 机械臂坐标(x,y) src_pts = np.array([ [220, 320], # 对应物理坐标(100, 100) [460, 320], # 对应物理坐标(300, 100) [220, 560], # 对应物理坐标(100, 300) [460, 560], # 对应物理坐标(300, 300) ], dtype=np.float32) dst_pts = np.array([ [100, 100], [300, 100], [100, 300], [300, 300], ], dtype=np.float32) H, _ = cv2.findHomography(src_pts, dst_pts) # 保存矩阵,后续抓取时直接加载 np.save("homography.npy", H) # 使用:像素坐标转机械臂坐标 def pixel_to_robot(px, py): pts = np.array([[[px, py]]], dtype=np.float32) result = cv2.perspectiveTransform(pts, H) return result[0][0] # (x, y)标定做完一定要验证。我当时在桌面上随机放了十个不同位置的小物体,逐个用程序算出坐标,再用机械臂示教模式移动到这些坐标,观察夹爪中心是否对准目标。这一步能同时排查两个问题:一个是单应性矩阵本身准不准,另一个是夹爪中心与末端坐标是否一致。
4.4 机械臂控制流程:先安全后效率
机械臂控制代码的逻辑不复杂,但顺序很关键。我的控制序列是:
- 机械臂先回到固定的Home点(安全高度,Z轴抬到最高)。
- 上位机收到目标像素坐标,转换成物理坐标。
- 规划一个中转点:目标正上方5cm处。
- 控制机械臂先移动到中转点,再直线下降到抓取高度。
- 夹爪闭合,抬升回到Home点。
- 移动到放置区,张开夹爪。
这个过程里,“先到上方,再垂直下降”是避免碰撞的关键。如果直接从Home点斜线飞向目标,夹爪可能在半路撞到工作区域的其他物体。而且对于四轴机械臂来说,从上往下接近目标也是逆解最稳定的路径,不容易出现关节奇异。我用串口发送指令,机械臂底层自己完成逆解和插补,上位机只需要发目标坐标和速度参数。
def pick_object(robot, x, y, z_height, color_height): # 1. 回到安全点 robot.move_home() # 2. 移动到目标上方安全高度 robot.move_ptp(x, y, 50) # 相对桌面50mm高度 # 3. 直线下降到物体顶面 robot.move_line(x, y, z_height + color_height + 10) # 4. 闭合夹爪 robot.gripper_close() # 5. 抬升 robot.move_line(x, y, 50) # 6. 回Home再放料 robot.move_home() robot.gripper_open()速度参数我调得比较保守,默认设为最大速度的30%。机械臂高速运动时,末端会因为惯性产生过冲,哪怕只差几毫米也会导致抓空。低速运行虽然慢一点,但稳定性和重复性都大幅提升。等整个流程跑顺了再逐步提速也不迟。
4.5 一次完整的运行演示
最终联调时,我在桌面上随意摆了红、绿、蓝三个方块,程序启动后摄像头逐帧扫描,一旦检测到目标颜色,立即输出坐标并把柱状图打印在终端,随后机械臂自动完成抓取。第一次成功抓起的瞬间,其实挺有成就感的:摄像头识别到红色方块,坐标算出来是(196.7, 248.3),机械臂滑过去,夹爪稳稳合上,把方块搬到指定盒子里,全程大约8秒。换来换去,整个过程流畅得像一条流水线。
5. 调试实录:颜色识别与抓取环节的常见坑
5.1 颜色识别不稳定的排查顺序
这东西看起来简单,实际调试时要踩的坑一个不少。我遇到过几次典型的颜色识别翻车现场,整理了一个排查表:
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 目标颜色一会儿识别到一会儿消失 | 光照变化,H值漂移 | 固定窗户遮光帘,或加补光灯保持亮度恒定 |
| 识别区域比物体大一圈 | 阈值范围过宽,把阴影也算进去了 | 缩小S和V范围,尤其是V的下限 |
| 物体表面出现黑色空洞 | 反光过强,高光处S和V偏低 | 调整摄像头角度避免直射光源,或调低V下限 |
| 误识别桌面上其他物体 | 目标色与环境色接近 | 加入形状筛选,比如只保留矩形轮廓 |
| 画面整体闪烁 | 自动白平衡在作怪 | 在代码里强制关闭摄像头的自动白平衡 |
这里最容易被忽略的是自动白平衡。默认USB摄像头会自动调节色温,环境一变,整个画面的颜色就偏了,H值跟着漂。处理方式是:初始化摄像头时直接设置参数:
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_AUTO_WB, 0) # 关闭自动白平衡 cap.set(cv2.CAP_PROP_BRIGHTNESS, 128)关掉自动白平衡后,颜色识别稳定性一下提升了不止一个档次。
5.2 抓取偏差的排查顺序
如果颜色识别没问题,但机械臂总是抓偏,排查顺序要从外到内:
- 先确认摄像头支架有没有松动。我有一次调了半天发现是支架螺丝松了,画面整体平移了几毫米。
- 再验证单应性矩阵是否还准确。机械臂基座位置动了、摄像头动了,都要重新标定。
- 看夹爪中心是否和机械臂末端坐标对齐。很多机械臂的夹爪是偏置安装的,末端坐标指向法兰中心,但夹爪中心在另一侧。我在夹爪上贴了一个十字标记,用示教模式验证了偏置量,然后在代码里把这个固定偏置值加进去,抓取精度明显改善。
- 最后检查机械臂的回差。便宜机械臂往往在换向时存在明显间隙,如果两次抓取方向不同,末端位置会有偏差。解决方法是尽量保持同方向接近目标。
5.3 通讯与供电的隐蔽坑
串口通讯的丢包问题也很常见。上位机发动作指令,机械臂没响应,程序就傻等在那里。我的做法是在指令里加上帧头和校验位,通信协议类似:
0xAA 0x55 <长度> <指令类型> <目标坐标> <校验和>机械臂收到指令后返回确认帧,上位机2秒内没收到确认就自动重发。这样能保证每次动作都被可靠执行。
供电方面,机械臂在夹爪闭合瞬间电流最大,电源如果余量不足,电压跌落会导致控制板重启,表现就是机械臂动作到一半突然停止。我换过几次电源后总结的经验是:额定电流至少是峰值电流的1.5倍,并且电源线要够粗,细长线缆本身就有压降。
6. 进阶玩法:同一套框架还能往哪些方向延伸
这套代码的架构其实留了很大的扩展空间,不需要推翻重来,改几个模块就能玩出新花样。
第一个肯定要做的是多颜色分拣。当前代码已经能识别红绿蓝三种颜色,只要再加一个目标队列,把识别到不同颜色的物体分别送到不同放置区,就变成一个简单的颜色分拣工作站。我后来加了一个逻辑:优先抓取视野内距离机械臂最近的物体,这样能避免夹爪跨越整个桌面去抓远处的目标,工作效率高不少。
第二个方向是动态追踪。如果把识别频率提高到每帧都处理,再通过预测物体的运动轨迹,机械臂甚至能抓正在传送带上缓慢移动的物体。难点在于运动预测的实时性,但思路完全可以复用。
第三个方向是用深度学习替换颜色识别。比如因为传统HSV对光影变化敏感,我在户外试过一次,效果就比较糟糕。后来尝试用一个轻量的目标检测模型替代颜色识别模块,输入输出接口不变,照样输出中心点像素坐标,坐标系标定和控制逻辑完全不用动。这种“换模块不换架构”的好处,一开始设计时可能意识不到,真正遇到场景变化时才知道省了多少事。
我还试过把识别到的坐标通过TCP协议发给另一个局域网里的上位机,让远端电脑控制机械臂动作,这就有点工业远程操作的意思了。摄像头画面叠加识别框后推流到手机,在现场调试时很实用,不用一直盯着工位。
7. 收个尾:几点个人体会
项目做到这里,最大的感受不是“颜色识别有多难”,而是“坐标系变换和机械臂的脾气有多重要”。颜色识别只要环境和参数对了,几乎不用操心;真正的功夫全在把图像坐标准确映射到机械臂坐标系,以及让机械臂的动作足够稳定。如果你也准备照这个思路做一遍,我建议先花半天时间把标定做扎实,多做几组验证点,别急着让机械臂动起来。标定不稳,后面所有抓取都是碰运气。
最后分享一个实用小技巧:把所有标定参数、HSV阈值、物体高度表、夹爪偏置量统一存在一个配置文件里,每次开机直接加载。这能帮你省掉大量重复调参的时间,也方便随时调整。我就是把homography.npy和颜色阈值存成JSON文件后,整套系统在后续几次换场地调试时,只用改配置文件就能立刻跑起来。