1. 为什么选Jetson Nano跑Dofbot的视觉分拣
1.1 这套组合到底解决什么问题
Dofbot是一台桌面级六轴机械臂,官方标配的玩法通常是手柄示教或者固定轨迹抓放。但真正让它“长眼睛”的,是给它接上一路摄像头,让机械臂自己判断目标在哪、该往哪抓。我这次做的色块分拣,就是最典型的入门级视觉抓取场景:桌面上散落红、绿、蓝三种颜色的方块,摄像头拍一张图,程序算出每个色块的像素坐标,换算成机械臂底座坐标系下的实际位置,然后依次抓起来丢进对应颜色的盒子里。
整套系统的核心硬件就三样:Dofbot机械臂本体、Jetson Nano开发板、一个普通USB摄像头。软件栈是Ubuntu 18.04 + OpenCV + Python。选Jetson Nano的原因很直接——它自带GPU,能跑轻量级神经网络,虽然色块分拣用传统图像处理就够了,但后续如果想升级成YOLO识别任意物体,Nano的算力还能撑一撑。而且Nano的GPIO和USB接口跟Dofbot的舵机控制板对接很顺,不需要额外转接。
适合看这篇的人:手上有Dofbot或者类似总线舵机机械臂、想入门视觉抓取的学生和爱好者;正在做毕业设计需要机械臂+视觉方案的;以及已经跑通官方例程、想自己改点东西出来的朋友。如果你连Ubuntu都没装过,建议先补一下Linux基础操作,不然中间会遇到不少卡壳的地方。
1.2 色块分拣的技术路线选择
视觉抓取的技术路线大致分两种:一种是端到端的深度学习方案,直接输入图像输出抓取位姿;另一种是传统的“检测-定位-映射-执行”流水线。我选的是后者,原因有三个。
第一,色块分拣的目标特征极其明确——颜色。HSV色彩空间下,红绿蓝三色的阈值范围很窄,用cv2.inRange()就能干净地分割出来,不需要训练数据,不需要标注,改颜色只要调几个数值。第二,Jetson Nano跑深度学习推理虽然可行,但帧率有限,而传统图像处理在Nano上跑640x480的图像可以轻松到30fps以上,响应更快。第三,也是最重要的一点——可解释性。当抓取失败时,我能明确知道是颜色没分割出来、还是坐标换算错了、还是机械臂运动学参数不对。深度学习方案出问题时,排查起来就麻烦得多。
提示:如果你的场景里光照变化剧烈,或者色块表面有反光,HSV阈值法会很不稳定。这种情况要么加补光灯固定光照,要么换用Lab色彩空间,要么上深度学习。入门阶段先把光照控制住,能省掉80%的麻烦。
1.3 整体流程拆解
从摄像头拍到图像到机械臂完成抓取,中间要经过这些步骤:
- 图像采集:USB摄像头通过V4L2接口读取一帧图像
- 颜色分割:BGR转HSV,用阈值提取目标颜色区域
- 轮廓处理:找轮廓、算面积、过滤噪点、求最小外接矩形
- 坐标计算:取轮廓中心像素坐标,通过标定矩阵换算到机械臂坐标系
- 逆运动学求解:根据目标三维坐标算出六个舵机的角度
- 轨迹规划与执行:控制机械臂移动到目标上方,下降抓取,抬升,移动到对应颜色盒子,释放
这里面第4步和第5步是最容易出问题的。像素坐标到机械臂坐标的映射不是简单的线性关系,因为摄像头有畸变、安装有倾角、机械臂底座和摄像头不在同一位置。我后面会详细讲怎么用仿射变换做近似标定,以及为什么在桌面平面上这个近似足够用。
2. Ubuntu 18.04环境搭建与OpenCV安装的坑
2.1 Jetson Nano的系统准备
Jetson Nano官方推荐的就是Ubuntu 18.04 LTS,NVIDIA提供了专门的镜像文件。烧录过程不复杂,用Etcher把镜像写到SD卡里就行,但有几个细节要注意。
SD卡至少32GB,Class 10以上。我一开始用了一张16GB的卡,系统跑起来后剩余空间不到2GB,装完OpenCV直接满了。另外,烧录完成后第一次启动要接HDMI显示器、键盘鼠标,因为需要走一遍初始设置(语言、时区、用户名密码)。设置完成后建议立刻插网线或者配好WiFi,因为后面装东西全靠网络。
系统起来后第一件事是更新源和升级:
sudo apt update sudo apt upgrade -y这个过程在Nano上会比较慢,因为eMMC或者SD卡的读写速度有限。我实测大概要20到30分钟,耐心等。
注意:Jetson Nano的默认电源模式是5W,性能受限。如果你用的是5V 4A的DC供电,可以切换到10W模式:
sudo nvpmodel -m 0,然后把风扇打开:sudo jetson_clocks。这能让后续图像处理和机械臂通信都更流畅。
2.2 OpenCV在Jetson Nano上的安装策略
这是整个环境搭建里最容易踩坑的地方。Jetson Nano是ARM架构,pip install opencv-python大概率会失败或者装上一个没有CUDA加速的版本。正确的做法有两种:
方案一:用apt装系统包
sudo apt install python3-opencv这个最省事,但版本比较老(Ubuntu 18.04自带的是OpenCV 3.2),而且不带CUDA。对于色块分拣来说,3.2的API完全够用,cv2.inRange、cv2.findContours这些函数都有。如果你不打算用GPU加速图像处理,这个方案最稳。
方案二:源码编译带CUDA的OpenCV
如果你后续想用GPU跑深度学习推理,或者需要OpenCV的CUDA模块(比如cv2.cuda),那就得源码编译。这个过程在Nano上大概要2到4个小时,而且中间容易因为内存不足而编译失败。
编译前先扩大swap:
sudo fallocate -l 4G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile然后装依赖:
sudo apt install build-essential cmake git pkg-config libgtk-3-dev \ libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \ libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \ gfortran openexr libatlas-base-dev python3-dev python3-numpy \ libtbb2 libtbb-dev libdc1394-22-dev下载OpenCV源码,我用的版本是4.5.4,这个版本在Nano上编译成功率比较高:
git clone https://github.com/opencv/opencv.git git clone https://github.com/opencv/opencv_contrib.git cd opencv && git checkout 4.5.4 cd ../opencv_contrib && git checkout 4.5.4cmake配置的时候关键参数:
cmake -D CMAKE_BUILD_TYPE=RELEASE \ -D CMAKE_INSTALL_PREFIX=/usr/local \ -D WITH_CUDA=ON \ -D CUDA_ARCH_BIN=5.3 \ -D WITH_CUDNN=ON \ -D OPENCV_DNN_CUDA=ON \ -D ENABLE_FAST_MATH=ON \ -D CUDA_FAST_MATH=ON \ -D WITH_V4L=ON \ -D WITH_QT=OFF \ -D WITH_OPENGL=ON \ -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \ -D BUILD_EXAMPLES=OFF ..CUDA_ARCH_BIN=5.3是Jetson Nano的算力版本,写错了CUDA加速会失效。WITH_QT=OFF是因为Qt在Nano上编译容易出问题,用GTK就行。
编译用make -j4,四个核全开。如果中途报内存错误,改成make -j2甚至make -j1。编译完成后sudo make install,然后sudo ldconfig更新动态链接库。
验证安装:
python3 -c "import cv2; print(cv2.__version__); print(cv2.cuda.getCudaEnabledDeviceCount())"如果输出版本号且CUDA设备数大于0,说明带CUDA的OpenCV装好了。
2.3 摄像头和串口权限配置
USB摄像头在Ubuntu下通常是/dev/video0,但Jetson Nano上可能枚举成/dev/video1或更高。用ls /dev/video*确认。测试摄像头:
sudo apt install v4l-utils v4l2-ctl --list-devicesDofbot的舵机控制板通过串口通信,通常是/dev/ttyUSB0或/dev/ttyACM0。普通用户默认没有串口读写权限,每次都要sudo很麻烦。把用户加到dialout组:
sudo usermod -a -G dialout $USER然后注销重新登录生效。这个坑我踩过——明明代码没问题,就是打不开串口,查了半天才发现是权限。
提示:如果你用的是虚拟机里的Ubuntu做开发,USB设备透传经常出问题。建议要么直接装在物理机上,要么用WSL2但注意WSL2的USB支持需要额外配置。机械臂控制对实时性有要求,虚拟机方案只适合前期调试代码逻辑,最终跑还是要物理机。
3. 色块识别:从BGR到HSV的完整处理链
3.1 为什么必须转HSV
OpenCV读进来的图像默认是BGR格式。BGR三个通道的值跟光照强度强相关——同一个红色块,在亮处R=200,在暗处R=120,你没法用一个固定的R值范围把它框出来。HSV就不一样了:H(色调)表示颜色种类,S(饱和度)表示颜色深浅,V(明度)表示亮度。红色块的H值不管在亮处还是暗处都在0到10或者170到180之间,这就稳定多了。
转换代码就一行:
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)但要注意,OpenCV的H范围是0到179,不是0到360。S和V都是0到255。红色在H轴上跨越了0和180两端,所以要分两段取:
# 红色 lower_red1 = np.array([0, 100, 100]) upper_red1 = np.array([10, 255, 255]) lower_red2 = np.array([170, 100, 100]) upper_red2 = np.array([180, 255, 255]) mask_red = cv2.inRange(hsv, lower_red1, upper_red1) + cv2.inRange(hsv, lower_red2, upper_red2) # 绿色 lower_green = np.array([40, 80, 80]) upper_green = np.array([80, 255, 255]) mask_green = cv2.inRange(hsv, lower_green, upper_green) # 蓝色 lower_blue = np.array([100, 100, 100]) upper_blue = np.array([130, 255, 255]) mask_blue = cv2.inRange(hsv, lower_blue, upper_blue)这些阈值不是固定的,跟你的光照环境、色块材质都有关系。我建议先用一个调试脚本,把摄像头的HSV值实时打印出来,然后手动调。具体做法:用cv2.setMouseCallback绑定鼠标事件,点击图像上某个像素,打印它的HSV值。这样你点一下红色块,就知道当前光照下红色的H、S、V大概是多少,然后据此设定阈值范围。
3.2 形态学处理去噪
inRange出来的mask往往有噪点——桌面上可能有反光点、阴影边缘、或者色块表面的高光区域被排除后留下的空洞。形态学操作就是用来清理这些的。
kernel = np.ones((5, 5), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 开运算:先腐蚀后膨胀,去小白点 mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 闭运算:先膨胀后腐蚀,填小黑洞开运算的kernel大小很关键。太小了噪点去不干净,太大了色块本身会被腐蚀掉。5x5是个比较通用的值,但如果你的色块在图像里很小(比如小于50个像素),就要用3x3。反过来如果色块很大,可以用7x7。
我实际调试的时候发现,闭运算比开运算更重要。因为色块表面经常有反光,导致mask中间出现空洞,如果不填上,后面算轮廓中心的时候会偏。但闭运算也不能太狠,否则相邻的两个色块可能被连在一起。
3.3 轮廓筛选与中心点计算
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area = cv2.contourArea(cnt) if area < 500: # 过滤太小的噪点 continue M = cv2.moments(cnt) if M["m00"] == 0: continue cx = int(M["m10"] / M["m00"]) cy = int(M["m01"] / M["m00"]) # cx, cy 就是色块中心的像素坐标cv2.RETR_EXTERNAL只取最外层轮廓,避免色块内部如果有纹理产生嵌套轮廓。CHAIN_APPROX_SIMPLE压缩轮廓点,省内存。
面积阈值500是我实测下来比较合适的值。640x480的图像里,一个3cm见方的色块在30cm距离上大概占2000到3000个像素。设500能过滤掉大部分噪点,又不会误杀真正的色块。但如果你的色块更小或者摄像头更远,这个值要相应调低。
注意:
cv2.moments算出来的中心是轮廓的几何中心,不是最小外接矩形的中心。对于正方形色块两者差不多,但如果色块被部分遮挡或者形状不规则,几何中心可能偏。更稳的做法是用cv2.minAreaRect取最小外接矩形,然后取矩形中心。我在实际项目里两种都用过,色块完整的情况下差别不大,但遮挡场景下minAreaRect更可靠。
3.4 多色块同时识别的处理逻辑
桌面上同时有红绿蓝三个色块时,程序需要知道每个色块的颜色和位置。我的做法是分别对三个颜色生成mask,各自找轮廓,然后把结果汇总到一个列表里:
targets = [] for color_name, mask in [("red", mask_red), ("green", mask_green), ("blue", mask_blue)]: contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: if cv2.contourArea(cnt) < 500: continue M = cv2.moments(cnt) cx = int(M["m10"] / M["m00"]) cy = int(M["m01"] / M["m00"]) targets.append({"color": color_name, "px": cx, "py": cy})然后按某种策略排序——比如按y坐标从近到远,或者按颜色优先级。我一般按距离机械臂底座最近的先抓,这样运动路径最短,效率最高。
这里有个细节:如果同一个色块在红色mask和蓝色mask里都出现了(比如紫色块),会被识别两次。实际用纯红绿蓝三色块不会遇到这个问题,但如果你的色块颜色接近,就要加一个互斥判断——同一个像素区域只归为一种颜色。
4. 像素坐标到机械臂坐标的标定方法
4.1 为什么不能直接用像素坐标
摄像头看到的是二维图像,机械臂工作在三维空间。像素坐标(cx, cy)只告诉你在图像里的位置,不告诉你这个点在机械臂坐标系下的(x, y, z)。要把两者对应起来,需要知道摄像头的内参(焦距、主点、畸变系数)和外参(摄像头相对于机械臂底座的位置和姿态)。
完整的标定流程是:用棋盘格标定板做相机标定,得到内参矩阵和畸变系数;然后做手眼标定,得到摄像头到机械臂基座的变换矩阵。这套流程很标准,但比较繁琐,而且需要精确的标定板。
对于桌面色块分拣这个场景,我有一个简化方案:假设所有色块都在同一个平面上(桌面),那么像素坐标到机械臂坐标的映射就是一个平面到平面的仿射变换(或者透视变换)。这个假设在色块分拣里完全成立——色块就放在桌面上,高度固定。
4.2 仿射变换标定的实操步骤
仿射变换需要至少三对对应点。我的做法是:
- 在桌面上选三个点,用机械臂末端去触碰,记录下这三个点在机械臂坐标系下的坐标(通过舵机角度和正运动学算出来,或者直接用示教模式读出来)
- 把摄像头固定好,在图像里找到这三个点对应的像素坐标
- 用
cv2.getAffineTransform算出变换矩阵
但实际操作中,用机械臂末端去精确触碰桌面上的点很麻烦。我换了一个更简单的方法:用色块本身做标定。
具体做法:把红色色块放在桌面上一个已知位置(比如机械臂底座正前方20cm处),记录像素坐标;然后移动色块到另一个已知位置,再记录像素坐标。重复三次,得到三对(像素坐标, 机械臂坐标)。然后用cv2.getAffineTransform:
pts_pixel = np.float32([[cx1, cy1], [cx2, cy2], [cx3, cy3]]) pts_robot = np.float32([[x1, y1], [x2, y2], [x3, y3]]) M = cv2.getAffineTransform(pts_pixel, pts_robot)之后对于任何像素坐标(cx, cy),机械臂坐标就是:
robot_pt = cv2.transform(np.array([[[cx, cy]]], dtype=np.float32), M)这个方法的精度取决于三个标定点的选取。我建议三个点尽量分散,不要挤在一起,而且不要共线。三角形的面积越大,标定精度越高。
提示:仿射变换只能处理平移、旋转、缩放和剪切,不能处理透视畸变。如果你的摄像头是斜着装的(光轴不垂直于桌面),仿射变换会有误差。这时候要用透视变换
cv2.getPerspectiveTransform,需要四对对应点。我实测下来,摄像头光轴与桌面夹角在70度以上时,仿射变换的误差在5mm以内,对于3cm的色块来说完全够用。
4.3 高度补偿与抓取姿态
色块有厚度,机械臂抓取时末端要下降到色块表面,而不是桌面。所以z坐标要加上色块的高度。标准色块高度是3cm,那么抓取时z = 桌面高度 + 3cm。
但这里有个问题:仿射变换只给出了x和y,z是固定的(桌面高度)。如果色块高度不同,z就要相应调整。我的做法是在程序里维护一个颜色到高度的映射表,红色块3cm,绿色块3cm,蓝色块3cm——都一样,所以z固定。如果你的色块高度不同,就要在标定时把高度也考虑进去,或者用两个摄像头做立体视觉。
抓取姿态方面,Dofbot的末端夹爪默认是垂直向下的。对于桌面上的色块,垂直向下抓是最稳的。但如果色块靠近机械臂底座,垂直向下可能会碰到机械臂本体。这时候需要调整末端姿态,让夹爪稍微倾斜。Dofbot的逆运动学求解器通常支持指定末端姿态,我一般设成垂直向下,遇到干涉再手动调。
4.4 标定误差的验证与修正
标定完成后一定要验证。我的验证方法是:把色块放在桌面上五个不同位置,程序算出机械臂坐标后,控制机械臂移动到该坐标上方,然后看末端是否对准色块中心。
如果误差在1cm以内,基本可以接受,因为夹爪的开口有2cm左右,有容错空间。如果误差超过2cm,就要检查:
- 标定点是否准确?重新标定
- 摄像头是否移动过?重新标定
- 仿射变换是否适用?改用透视变换
- 机械臂运动学参数是否准确?检查Dofbot的DH参数
我遇到过一种情况:标定完了误差很小,但运行一段时间后误差变大。后来发现是摄像头支架松了,轻微移位导致标定失效。所以摄像头一定要固定牢,最好用螺丝锁死,不要用夹子。
5. 机械臂运动控制与抓取流程编排
5.1 Dofbot的通信协议与Python控制
Dofbot的舵机控制板通过串口接收指令,协议通常是自定义的帧格式。官方Python SDK封装了底层通信,直接调用就行。核心API大概是这样:
from dofbot import Dofbot arm = Dofbot(port='/dev/ttyUSB0', baudrate=1000000) arm.set_servo_angle(servo_id=1, angle=90, speed=50)servo_id从1到6对应六个关节,angle是目标角度,speed是运动速度。速度参数很重要——设太快机械臂会抖,设太慢效率低。我一般用50到70之间的值。
逆运动学求解方面,Dofbot的SDK通常提供了set_position(x, y, z)这样的接口,内部自动算逆解。但要注意,逆解可能有多组解,SDK一般会选一组“最自然”的。如果遇到奇异位形(比如目标点在机械臂工作空间边缘),逆解可能无解或者解出来的角度超出舵机范围。这时候要检查目标坐标是否在工作空间内。
5.2 抓取动作的状态机设计
抓取流程不是简单的“移动-抓-放”,中间有很多状态转换和异常处理。我用一个状态机来管理:
| 状态 | 动作 | 转换条件 |
|---|---|---|
| IDLE | 等待 | 检测到目标 |
| APPROACH | 移动到目标上方5cm | 到达位置 |
| DESCEND | 下降到抓取高度 | 到达位置 |
| GRASP | 闭合夹爪 | 夹爪闭合完成 |
| LIFT | 抬升到安全高度 | 到达位置 |
| MOVE_TO_BOX | 移动到对应颜色盒子 | 到达位置 |
| RELEASE | 打开夹爪 | 夹爪打开完成 |
| RETURN | 回到IDLE | 到达初始位置 |
每个状态都有超时检测。如果某个状态超过预期时间还没完成,就报错并回到IDLE。比如APPROACH状态如果5秒内没到达,可能是逆解失败或者舵机堵转,需要人工检查。
注意:夹爪闭合后不要立刻抬升,要等一小段时间(比如0.3秒)让夹爪完全夹紧。我一开始没加这个延时,结果机械臂抬升时色块掉了。另外,夹爪的闭合力度也要调——太松夹不住,太紧可能把色块压变形。Dofbot的夹爪力度是通过舵机扭矩控制的,一般设成中等偏上就行。
5.3 多目标抓取的顺序规划
桌面上有多个色块时,抓取顺序会影响效率。最简单的策略是按距离排序——离机械臂底座最近的先抓。但还要考虑颜色盒子的位置。如果红色盒子在左边,蓝色盒子在右边,而桌面上红色块在右边、蓝色块在左边,那先抓红色块就要横跨整个桌面,效率低。
我的做法是算一个代价函数:代价 = 到色块的距离 + 从色块到对应盒子的距离。然后按代价从小到大排序。这样能减少机械臂的总运动距离。
但实际测试下来,对于三四个色块的小场景,顺序优化的收益不大,因为机械臂运动速度是瓶颈。我一般就用简单的距离排序,够用了。
5.4 异常处理与安全机制
机械臂运动过程中可能遇到各种异常:逆解失败、舵机堵转、串口通信超时、摄像头掉线。每一种都要有对应的处理。
逆解失败时,程序应该跳过这个目标,继续处理下一个,而不是卡死。舵机堵转时,要立刻停止所有运动,因为继续通电可能烧舵机。串口超时时,重试三次,还不行就报错退出。摄像头掉线时,暂停抓取流程,等待摄像头恢复。
还有一个重要的安全机制:急停。我在程序里绑定了一个键盘按键(比如空格键),按下后立刻发送停止指令给所有舵机。调试阶段这个功能救了我好几次——机械臂有时候会往奇怪的方向运动,没有急停就只能拔电源。
import keyboard if keyboard.is_pressed('space'): arm.emergency_stop() print("急停触发")6. 实测中遇到的典型问题与解决思路
6.1 颜色识别受光照影响严重
这是最常见的问题。白天阳光从窗户照进来,桌面上的色块颜色会偏;晚上开日光灯,颜色又不一样。我试过几种解决方案:
方案一:固定光源。在摄像头旁边装一个LED补光灯,让桌面光照恒定。这个最有效,成本也低。我用的是一个USB供电的环形灯,装在摄像头周围,亮度可调。
方案二:自动白平衡。OpenCV可以通过cv2.xphoto.createSimpleWB()做白平衡,但Jetson Nano上这个模块不一定编译进去了。更简单的做法是手动调摄像头的白平衡参数:
v4l2-ctl -d /dev/video0 --set-ctrl=white_balance_temperature_auto=0 v4l2-ctl -d /dev/video0 --set-ctrl=white_balance_temperature=4500方案三:动态阈值。每次抓取前,先拍一张桌面背景图,算出背景的HSV均值,然后根据背景调整色块阈值。这个方法比较复杂,但适应性最强。
我最终用的是方案一加方案二。固定光源保证了光照稳定,手动白平衡消除了摄像头自动调整带来的波动。这两招下来,颜色识别基本没再出过问题。
6.2 机械臂抓取位置偏差
标定完了,程序也算出了机械臂坐标,但抓的时候就是偏。可能的原因有几个:
原因一:标定点不准。用色块做标定时,色块本身的中心位置可能跟记录的位置有偏差。解决方法是标定时用尖头物体(比如笔尖)代替色块,更精确。
原因二:机械臂重复定位精度不够。Dofbot用的是总线舵机,重复定位精度大概在1到2度。对于20cm的臂展,1度误差对应末端3到4mm的偏差。这个没办法完全消除,只能通过软件补偿——记录每次抓取的实际偏差,下次抓同一个位置时预先补偿。
原因三:逆运动学求解误差。Dofbot的DH参数如果标定不准,逆解出来的角度会有偏差。这个需要重新标定DH参数,比较麻烦。我一般先用软件补偿顶着,有时间再重新标。
原因四:色块高度没算对。如果色块实际高度是3.5cm,程序里按3cm算,下降抓取时夹爪就会撞到色块或者抓空。用卡尺量一下色块实际高度,更新到程序里。
6.3 串口通信不稳定
Jetson Nano的USB口供电能力有限,如果同时接了摄像头和机械臂控制板,可能出现供电不足导致串口掉线。我的解决方法是给Jetson Nano用独立的DC供电(5V 4A),不要只靠USB供电。另外,串口线要用带屏蔽的,长度不要超过1米,太长容易受干扰。
软件层面,每次发送指令后加一个确认机制——机械臂控制板收到指令后返回一个ACK,程序收到ACK才发下一条。如果超时没收到ACK,重发。这个机制能过滤掉大部分偶发的通信错误。
def send_command_with_retry(cmd, retries=3): for i in range(retries): arm.send(cmd) if arm.wait_ack(timeout=0.5): return True return False6.4 摄像头帧率与处理速度的平衡
Jetson Nano跑640x480的HSV转换加轮廓查找,单帧处理时间大概在15到20ms,也就是50到60fps。但USB摄像头在Nano上经常只能跑到30fps。如果处理速度跟不上采集速度,图像会堆积,延迟越来越大。
我的做法是:采集线程和处理线程分开,采集线程只负责从摄像头读帧并放入队列,处理线程从队列取帧处理。队列长度设为1,如果处理线程还没处理完上一帧,采集线程就丢弃旧帧,只保留最新帧。这样保证处理的永远是最新图像,不会累积延迟。
import queue import threading frame_queue = queue.Queue(maxsize=1) def capture_thread(): cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: continue if frame_queue.full(): frame_queue.get() # 丢弃旧帧 frame_queue.put(frame) def process_thread(): while True: frame = frame_queue.get() # 处理frame这个模式在Jetson Nano上跑下来很稳,延迟控制在50ms以内,对于色块分拣来说完全够用。
6.5 机械臂运动时的振动问题
Dofbot的机械臂比较轻,运动速度快的时候末端会抖动。抖动会导致两个问题:一是抓取时位置不准,二是摄像头如果装在机械臂上,图像会模糊。
解决方法是降低运动速度,特别是在接近目标位置的时候。我一般把运动分成两段:粗定位用高速(speed=80),精定位用低速(speed=30)。这样既保证了效率,又保证了精度。
另外,机械臂底座一定要固定牢。我用的是螺丝把底座锁在桌面上,如果只是放在桌上,机械臂运动时底座会跟着晃,精度根本没法保证。
7. 从色块分拣延伸到更复杂的视觉抓取
7.1 换成YOLO识别任意物体
色块分拣跑通之后,下一步很自然就是想识别更复杂的物体。Jetson Nano跑YOLOv5s是可行的,用TensorRT加速后大概能到10到15fps。流程是把YOLO的输出框中心作为抓取点,替代原来的颜色轮廓中心。
但这里有个问题:YOLO只能给出物体的二维框,不知道物体的抓取姿态。对于规则物体(比如杯子、盒子),可以假设抓取点在框中心,末端垂直向下。对于不规则物体,就需要更复杂的抓取检测网络,比如GG-CNN或者GR-ConvNet。这些在Nano上跑会比较吃力,建议升级到Jetson Orin Nano。
7.2 加入深度相机做三维抓取
USB摄像头只有二维信息,z坐标是假设固定的。如果换成深度相机(比如Intel RealSense D435i),就能直接得到每个像素的深度值,z坐标不用假设。这样就能处理高度不同的物体,也能做避障。
RealSense在Jetson Nano上需要装librealsense SDK,然后通过pyrealsense2读取深度图和对齐的彩色图。深度图跟彩色图对齐后,每个彩色像素都有对应的深度值,直接查表就能得到三维坐标。
但深度相机也有坑:透明物体、反光表面、黑色物体会导致深度值缺失。色块分拣用深度相机有点杀鸡用牛刀,但如果你要做更复杂的场景,深度相机是值得投资的。
7.3 用ROS做系统集成
如果项目变大,比如多个传感器、多个执行器、需要远程控制,那就该上ROS了。Dofbot有ROS驱动包,可以把机械臂控制、摄像头采集、视觉处理都做成ROS节点,通过topic通信。
ROS的好处是模块化——视觉节点只负责发布目标位置,运动规划节点订阅目标位置并控制机械臂。两个节点可以独立调试,互不影响。而且ROS有rviz可视化工具,能实时看到机械臂姿态和摄像头图像,调试起来方便很多。
但ROS也有学习成本,而且Jetson Nano上跑ROS会占用不少资源。如果你的项目就是简单的色块分拣,没必要上ROS。等你要做多机械臂协作或者移动抓取的时候再考虑。
7.4 标定精度的进一步提升
仿射变换标定的精度对于色块分拣够用,但如果你要做精密装配(比如把销钉插入孔里),就需要更高的精度。这时候要做完整的相机标定加手眼标定。
相机标定用棋盘格,OpenCV有现成的cv2.calibrateCamera函数。手眼标定分两种:eye-in-hand(摄像头装在机械臂末端)和eye-to-hand(摄像头固定在外部)。色块分拣用的是eye-to-hand,标定方法是让机械臂末端移动到多个位置,同时记录末端在机械臂坐标系的坐标和摄像头坐标系下的坐标,然后用cv2.calibrateHandEye求解变换矩阵。
这套流程我跑过,精度能到1mm以内。但标定过程比较耗时,需要采集20到30组数据。如果你的应用对精度要求不高,仿射变换就够了。
7.5 抓取策略的优化方向
现在的抓取策略是“看到什么抓什么”,没有考虑抓取的成功率。实际场景中,有些色块可能被遮挡、有些可能靠近桌面边缘不好抓。更智能的策略是给每个目标算一个抓取成功率,优先抓成功率高的。
抓取成功率的评估因素包括:色块是否完整(轮廓面积是否接近标准值)、是否在机械臂工作空间中心区域、周围是否有障碍物。这些可以用简单的规则实现,也可以用机器学习的方法训练一个分类器。
另外,抓取失败后的重试策略也很重要。如果第一次抓取失败(夹爪闭合后色块没被夹起来),程序应该能检测到并重试。检测方法可以是:夹爪闭合后,再看一眼摄像头,如果色块还在原地,说明没抓起来。然后调整位置重试,或者换个角度抓。
8. 写在最后的一些实操体会
这套Dofbot加Jetson Nano加OpenCV的色块分拣系统,我从零搭起来大概花了三周时间,其中环境搭建占了一周,视觉处理占了一周,机械臂控制和联调占了一周。中间踩的坑主要集中在OpenCV编译、串口权限、标定精度这三个方面。
如果让我给后来者建议,我会说:先把环境搭稳,再调视觉,最后联调机械臂。不要一上来就想着把整个流程跑通,那样出了问题都不知道是哪一环的错。每一步都单独验证——摄像头能出图、颜色能分割、坐标能算对、机械臂能走到指定位置——然后再串起来。
还有一个很实用的技巧:在程序里加一个调试模式,把每一步的中间结果都保存成图片或者打印出来。比如HSV转换后的图、mask图、轮廓标注图、标定后的坐标值。出问题的时候,看一眼中间结果就知道是哪一步不对。这个习惯帮我省了大量排查时间。
最后,机械臂的调试一定要慢。速度设低一点,急停键放在手边,工作空间里不要放易碎物品。安全永远是第一位的。