多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

视觉机器人抓取全流程:从物体定位到抓取估计的Python实现

视觉机器人抓取全流程:从物体定位到抓取估计的Python实现 视觉机器人抓取这几年是越做越常见了从工业上下料、分拣码垛到服务机器人的“拿起杯子”本质都是在解决同一个问题让机器人知道物体在哪儿、怎么伸手去拿。我最初接触这个方向时最头疼的不是深度学习模型也不是机械臂控制而是“把像素坐标变成机器人能执行的抓取指令”这一整条链路。今天这篇就把视觉抓取里的两个核心环节——物体定位和抓取估计从原理到Python代码实现完整过一遍都是我实际调试中验证过可行的方案适合玩机械臂、做毕设、或者刚入门机器视觉抓取的朋友参考。这类项目的关键点从来不在某个单点技术上而是坐标系变换、图像处理、通信协议这些环节如何串起来。下面我会从方案选型开始再到标定、定位、抓取估计最后给出可直接跑的示例代码和排查经验。跟着走一遍你能搭出一个最简但完整的视觉抓取闭环。1. 视觉抓取的完整链路与方案选型1.1 视觉抓取到底在解决什么问题先说个最朴素的问题没有视觉的机械臂能不能抓东西能但前提是物体位置固定不变。实际产线里工件位置会有偏差传送带在动料框里的零件堆叠错乱这时候靠“示教点固定轨迹”就不行了。视觉抓取的核心价值就是让机械臂具备“看见物体、算出位置、调整动作”的能力。拆开来看视觉抓取通常由四个模块组成物体感知通过摄像头捕捉图像或点云把目标物体从场景中“找出来”。坐标映射把图像中的像素坐标或相机坐标系下的三维坐标转换到机器人基座标系。抓取估计根据物体的位置、姿态判断机械臂末端执行器该以什么样的位姿去接近物体。运动执行把抓取位姿发送给机器人触发执行动作。这四个模块里最容易让人卡住的是“坐标映射”和“抓取估计”。坐标映射如果搞不清视觉输出的坐标和机械臂实际到达的位置会偏差很大抓取估计如果只给位置不给姿态夹爪往往会在碰撞或抓空边缘疯狂试探。1.2 2D视觉和3D视觉怎么选我最早做视觉抓取时纠结过是用普通RGB相机加颜色分割还是直接上深度相机。后来发现这个选择主要取决于物体和场景的复杂度。二维视觉方案适合场景固定、物体颜色与背景对比明显的场景。比如分拣蓝色盒子、识别固定形状的工件。实现简单用普通USB相机加OpenCV就能做成本几十到几百块。缺点是只能给出二维像素坐标深度要额外估算或者依赖物体放置平面高度基本恒定这个假设。三维视觉方案适合物体形状不规则、堆叠、需要精确位姿的场景。用深度相机比如常见的工业级深度相机、消费级深度相机直接获取点云或深度图再配合点云处理方法定位抓取点。信息更全但对算法和算力的要求也更高。从原型验证角度我强烈建议先用2D方案跑通整个流程再把感知模块换成3D。原因很简单视觉抓取的难点不在感知而在“坐标系变换”和“抓取姿态估计”。用2D方案快速验证标定和通信把整条链路打通了后面换感知算法只是替换上游模块而已。我在实际项目中不少需求用2D方案就能稳定交付不一定非要上3D。1.3 为什么用Python做原型与调试有人会问工业上很多视觉系统用C为什么这里推荐Python我的回答是做原型验证和算法迭代Python的效率高太多了。OpenCV、NumPy、PyTorch这些库在Python里都是开箱即用写标定脚本、快速画框看效果、做数据可视化Python都是最顺手的选择。而且机械臂厂商现在基本都提供了Python SDK或ROS接口通信这块也不缺支持。Python的执行效率问题在视觉抓取原型的场景下通常不是瓶颈。定位算法如果是颜色分割加几何计算单帧处理在普通笔记本上也就几毫秒到十几毫秒。真正需要高性能时可以把核心算法用C重写或者用Numba、Cython做加速但那是后话。先把流程跑通比一上来追求“极致性能”重要得多。2. 相机标定与手眼标定机器人“看得准”的前提2.1 相机内参标定的作用与方法视觉定位的第一步是把像素坐标转换成相机坐标系下的三维坐标。这个转换依赖相机内参焦距、主点、畸变系数。如果内参不准后续的坐标映射全是空中楼阁。内参标定最常用的方法是棋盘格标定。核心思路是拍摄不同姿态下的棋盘格通过特征角点检测解算相机内参和畸变系数。OpenCV里提供了现成的接口cv2.findChessboardCorners负责找角点cv2.calibrateCamera负责标定求解使用门槛很低。实际操作时有几个细节非常重要棋盘格要贴平不能有折痕不然角点坐标会有系统误差。拍摄10到20张不同角度、不同距离的图片覆盖画面的中心和边缘。标定板不要刚好平行于相机平面要有明显的倾角否则解算的畸变系数会不稳定。我标定完会看一个关键指标重投影误差reprojection error。一般小于0.3像素就算比较理想。如果误差偏大优先检查角点检测有没有错位以及图片数量是否足够。2.2 手眼标定眼在手外和眼在手上相机内参解决的是“像素点对应相机坐标系下的哪条射线”但机器人要执行抓取必需的是物体在机器人基座标系或世界坐标系下的坐标。这一步就得靠手眼标定。手眼标定分为两种情况眼在手外eye-to-hand相机固定安装在场景中不随机械臂移动。标定求解的是“相机坐标系”到“机器人基座标系”的变换矩阵。眼在手上eye-in-hand相机安装在机械臂末端法兰上随机械臂一起运动。标定求解的是“相机坐标系”到“机械臂末端坐标系”的变换矩阵再结合机器人正向运动学换算到基座。对初学者来说眼在手外更容易理解和调试因为相机不动只要一次标定就能一直用。下面我以眼在手外为例展开讲解。2.3 眼在手外标定的实用流程眼在手外标定的经典方法是“利用标定板来回变换求解AXXB类方程”。实现上有现成工具库比如常用的手眼标定库支持cv2.solvePnP与多组位姿联合求解。这里我推荐一个偏工程的做法把标定板固定放在机械臂工作空间内的某个位置。让机械臂末端或工具依次移动多个不同的位姿记录下每个位姿下机器人控制器输出的末端位姿。在每一个机械臂位姿下用相机拍摄标定板通过cv2.solvePnP求出标定板坐标系到相机坐标系的变换矩阵。将若干组“机械臂末端位姿”和“标定板在相机下的位姿”输入标定算法求解出手眼变换矩阵cam_T_base或base_T_cam看定义方向。一步到位的自动化标定比较复杂但工程上可以提前记录多组数据用标定库离线计算。这样做的好处是容易排查问题如果标定结果不对可以检查是哪一组数据异常重新拍摄或剔除后重算。需要记住的是标定的精度直接决定了抓取的精度。我在项目里一般要求手眼标定后的“视觉引导点到实际机器人到达点”的误差在2毫米到5毫米以内具体取决于相机分辨率和机械臂重复定位精度。如果标定结果不稳定最常见的原因是标定板平面与相机光轴夹角太小求得的姿态分量退化。3. 物体定位从像素到机器人坐标的关键一跳3.1 基于颜色分割的物体检测在视觉抓取中物体定位的方式取决于场景复杂度。先讲最常用也最容易上手的方案颜色分割。颜色分割的思路很直接把RGB图像转换到HSV颜色空间用阈值把目标颜色的像素提取出来再做形态学处理去掉噪声然后找轮廓计算物体的中心点和朝向角。为什么用HSV而不用RGB做阈值因为HSV把色调H、饱和度S、明度V分开受光照影响远小于RGB。例如检测一个蓝色盒子可以设定H在95到125之间S和V在一个合适的范围内这样即使有阴影也能较稳定地分割出目标。示例代码如下import cv2 import numpy as np def detect_object_color(image_path, hsv_lower, hsv_upper): # 读取图像并转换到HSV颜色空间 img cv2.imread(image_path) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 颜色阈值分割 mask cv2.inRange(hsv, hsv_lower, hsv_upper) # 用形态学操作去除小噪点 kernel np.ones((5, 5), np.uint8) 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_contour max(contours, keycv2.contourArea) return img, mask, largest_contour这段代码只是一个基础框架。实际调试时建议先用一个滑块工具动态调整HSV阈值观察哪些值能把目标完整分割出来又不带太多背景。网上有很多现成的HSV调试工具脚本调好后再把参数固化到代码里。3.2 计算物体的中心坐标与朝向角拿到目标轮廓之后需要计算出它的几何特征。这里有一个经验不要直接使用轮廓的原始像素点计算可以先对轮廓点做多边形逼近再用最小外接矩形或图像矩来求中心和角度。中心坐标的计算有几种方式用图像矩cv2.moments(contour)计算质心这是最常用的方法对常见形状都稳定。用最小外接矩形cv2.minAreaRect(contour)返回的中心点更贴近实际工件的中心。如果目标是圆形也可以用霍夫圆检测直接拿圆心坐标。朝向角通常是抓取估计的关键输入。对于矩形或长条形目标cv2.minAreaRect返回的角度需要特别小心因为它返回的角度范围是[-90, 0]度而且正负方向与图像坐标系的y轴方向有关图像y轴向下。很多初学者在这里被坑过算出来的角度一一正一反。我通常会把角度统一换算到0到180度范围并且明确它是绕图像Z轴垂直于图像平面的旋转角。有了这个角度后续才能换算到机器人坐标系下的绕Z轴旋转角通常用rz / yaw 表示。3.3 深度信息与相机坐标系下的三维坐标2D图像本身只有像素坐标要变成三维坐标需要深度信息。深度来源有两种使用深度相机直接读取像素点对应的深度值。使用固定平面假设例如物体放在已知高度的桌面上通过相机内参和平面高度反算Z坐标。用固定平面的方式举个例子。假设相机安装在桌面上方光轴基本垂直于桌面物体在桌面上的Z_camera相机坐标系下近似为一个常数即桌面到相机的距离。那么像素坐标(u, v)与相机坐标系下坐标(X_c, Y_c, Z_c)的关系为import numpy as np def pixel_to_camera(u, v, depth, camera_matrix): # 相机内参矩阵fx, fy为焦距cx, cy为主点 fx camera_matrix[0, 0] fy camera_matrix[1, 1] cx camera_matrix[0, 2] cy camera_matrix[1, 2] X (u - cx) * depth / fx Y (v - cy) * depth / fy Z depth return np.array([X, Y, Z, 1.0])如果用的是深度相机depth直接取目标区域像素深度的中值或平均值。为什么要取中值而不是单点因为单点可能是深度图的坏点或边缘杂讯。取目标区域中间部分的中值抗干扰能力会好很多。3.4 从相机坐标到机器人基座标系拿到相机坐标系下的三维点之后下一步就是通过手眼标定得到的变换矩阵把它映射到机器人基座标系。这一步是视觉抓取中最容易出“坐标乱飞”的地方。假设手眼标定得到矩阵T_base_cam表示“相机坐标系到机器人基座标系的齐次变换”那么物体在机器人基座标系下的坐标就是def camera_to_base(point_cam, T_base_cam): # point_cam: 相机坐标系下的齐次坐标 [X, Y, Z, 1] point_base_homo T_base_cam point_cam point_base point_base_homo[:3] / point_base_homo[3] # 归一化齐次坐标 return point_base这里齐次坐标的除法很重要否则坐标数值会被缩放导致机械臂去到一个错误的位置。我之前调试时遇到过一个问题抓取坐标总是“偏大”了几倍排查到最后发现就是齐次坐标没有除以w分量。映射完成后需要把打印到终端验证一下移动机械臂末端到一个已知点用视觉定位该点坐标看和机器人控制器显示的实际坐标是否一致。如果不一致优先检查手眼矩阵的方向定义是从相机到基座还是从基座到相机以及矩阵是否有转置或求逆搞反。4. 抓取估计不只是找到圆心那么简单4.1 抓取估计要估算什么很多人以为定位到物体的中心就万事大吉其实还差得远。抓取估计需要同时回答三个问题抓哪里末端工具夹爪的姿态包括位置和角度。怎么接近沿着哪个方向靠近物体避免碰撞。能不能抓夹爪能否在物体周围留出足够的空间抓取方向是否与物体几何冲突。对于简单的规则物体盒子、圆柱、平板件抓取估计通常可以简化为以物体中心为抓取点以物体的主方向为抓取方向让夹爪的两个手指沿着物体短边方向张开并夹紧。4.2 一个实用的抓取位姿生成方法下面以一个常见的矩形状物体为例说明抓取位姿的计算方法。假设视觉系统估计出物体的中心在机器人基座标系下的坐标为 (X_base, Y_base, Z_base)并估计出物体主轴在水平面内的旋转角为 yaw弧度绕机器人Z轴。那么机械臂末端执行器的目标位姿可以设置为位置物体中心上方一个安全高度offset处例如 Z_base 0.05米根据夹爪尺寸调整。姿态绕Z轴旋转yaw角末端执行器默认的夹爪张开方向与物体短边对齐。在代码中可以用欧拉角构建旋转矩阵再用XYZ格式组成位姿。下面给一个用Python和NumPy实现旋转矩阵与四元数转换的片段def compute_grasp_pose(x, y, z, yaw_rad, z_offset0.05): # 先生成一个简单的绕Z轴旋转矩阵 Rz np.array([ [np.cos(yaw_rad), -np.sin(yaw_rad), 0], [np.sin(yaw_rad), np.cos(yaw_rad), 0], [0, 0, 1] ]) # 有些机器人控制器需要四元数这里可以直接用旋转矩阵或转四元数 # 位置在物体正上方偏移z_offset position [x, y, z z_offset] return position, Rz有的机械臂SDK支持直接用位姿x, y, z, rx, ry, rz下发有的需要四元数x, y, z, w。如果用的是ROS通常用geometry_msgs/Pose消息里面有position和orientation四元数字段。四元数可以用工具库从旋转矩阵转换比如scipy.spatial.transform.Rotation。4.3 抓取姿态角和接近方向的处理细节抓取姿态的估计说到底是在“物体主轴方向”和“机械臂手腕可达性”之间做权衡。如果只追求物体主轴对齐机械臂关节可能在某个姿态下达到极限导致无法执行。我的做法是先按物体主轴生成一组候选抓取姿态再从中筛选出机械臂可达且不与周边障碍碰撞的。接近方向也很重要。对于夹爪接近方向一般是沿着物体表面法线也就是从正上方往下抓。如果物体侧面有把手或异形结构就需要调整接近方向。对于绝大多数桌面抓取场景“竖直向下抓取”是最稳妥的初版策略调通了再考虑多角度抓取。还有一个细节抓取点并不一定在物体中心。比如方形盒子当夹爪从正上方夹持时中心点没问题但如果是圆形工件夹爪接触点应该在过圆心的直径方向上等距的两点此时抓取点仍然是圆心但夹爪的闭合方向必须与直径方向一致。这就需要结合夹爪参数指爪宽度做碰撞检查。5. 完整Python实现从图像输入到下发抓取指令5.1 环境准备与依赖在动手写完整代码前先确认环境没问题。我这里用的是Python 3.8以上的版本依赖库如下OpenCV读取图像、图像处理pip install opencv-pythonNumPy矩阵运算pip install numpySciPy四元数与旋转矩阵转换pip install scipyPySerial串口通信可选用于与机械臂通信pip install pyserial如果是用深度相机还需要安装对应厂商的SDK或Python接口。如果只是做纯视觉演示可以使用一张仿真图像来跑通流程。5.2 完整的定位与抓取估计代码示例下面这段代码整合了物体定位、坐标变换和抓取位姿估计。为了让代码具备可读性我做了模块化设计每一步的输入输出都很清晰。import cv2 import numpy as np class VisionGraspEstimator: def __init__(self, camera_matrix, T_base_cam, hsv_lower, hsv_upper): self.camera_matrix camera_matrix self.T_base_cam T_base_cam # 相机坐标系 - 机器人基座标系 self.hsv_lower np.array(hsv_lower) self.hsv_upper np.array(hsv_upper) def detect_contour(self, frame): hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, self.hsv_lower, self.hsv_upper) kernel np.ones((5, 5), np.uint8) 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, None largest max(contours, keycv2.contourArea) return mask, largest def get_2d_pose(self, contour): moments cv2.moments(contour) if moments[m00] 0: return None, None cx moments[m10] / moments[m00] cy moments[m01] / moments[m00] rotation_rect cv2.minAreaRect(contour) angle rotation_rect[2] # 统一角度到 [0, 180) 度 angle angle % 180 return (cx, cy), angle def compute_3d_position(self, cx, cy, depth): x (cx - self.camera_matrix[0, 2]) * depth / self.camera_matrix[0, 0] y (cy - self.camera_matrix[1, 2]) * depth / self.camera_matrix[1, 1] z float(depth) return x, y, z def transform_to_base(self, point_cam): point_cam_homo np.array([point_cam[0], point_cam[1], point_cam[2], 1.0]) point_base_homo self.T_base_cam point_cam_homo return point_base_homo[:3] / point_base_homo[3] def estimate_grasp_pose(self, frame, depth_value): mask, contour self.detect_contour(frame) if contour is None: return None (cx, cy), angle_2d self.get_2d_pose(contour) x_cam, y_cam, z_cam self.compute_3d_position(cx, cy, depth_value) x_base, y_base, z_base self.transform_to_base((x_cam, y_cam, z_cam)) # 把2D角度转换成机器人坐标系下的偏航角。 # 注意如果相机与机器人X轴方向有固定夹角需要额外补偿。 yaw np.deg2rad(angle_2d) return (x_base, y_base, z_base, yaw)这段代码里T_base_cam是通过手眼标定得到的齐次变换矩阵通常是一个4x4的矩阵。如果你标定的矩阵方向是反的注意求逆后再使用。可以先用一个已知点验证矩阵方向是否一致。5.3 与机械臂的通信和动作执行拿到抓取位姿后需要把它发送给机械臂执行。不同机械臂的通信方式差异很大常见的有基于TCP/IP的协议用Python的socket库连接控制器发送JSON或定制格式的指令。基于串口的协议例如常见的桌面机械臂用串口发送G代码或专用指令。基于ROS的接口发布move_group或actionlib目标适合研究环境。这里给一个基于JSON的TCP通信伪代码示例实际使用时需要根据你的机械臂协议调整import json import socket class RobotController: def __init__(self, ip, port): self.sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((ip, port)) def send_grasp_pose(self, x, y, z, yaw_rad): # 假设控制器接收JSON格式{x, y, z, rx, ry, rz} msg { cmd: move_linear, target: { x: x, y: y, z: z, rx: 0.0, ry: 0.0, rz: yaw_rad, } } data json.dumps(msg).encode(utf-8) self.sock.sendall(data) response self.sock.recv(1024) return response通信这块最容易踩的坑是坐标系和单位不一致。有的机器人控制器期望的角度是弧度制有的是度制有的位置单位是米有的是毫米。下发前一定要确认清楚。我在联调时吃过一次亏视觉系统输出的位置是米控制器按毫米解析机器人直接奔着10米外去了。这个教训让我在每次通信前都会加一次单位检查。5.4 主流程串联与简单测试把所有模块串联起来一个最简的视觉抓取循环大概是这样的相机采集一帧图像。VisionGraspEstimator检测目标计算抓取位姿。如果物体存在将位姿发送给机器人控制器。机器人移动到物体上方执行下探、夹爪闭合、抬升动作。在还没有真实机械臂的时候你也可以用仿真环境或直接把位姿打印出来验证。很多机械臂厂商提供仿真器可以先在仿真里跑通再接实体机器人。我建议新手一定要先在仿真里试一次因为实体机器人的限位、奇异点、速度设置等稍微一个错误指令就可能造成碰撞。6. 常见问题与排查技巧实录6.1 定位偏差过大机械臂抓偏这是最常遇到的问题。定位偏差来源主要分三类相机标定不准重投影误差大导致像素到相机的坐标转换就有问题。手眼矩阵不准确变换矩阵方向定义错、标定数据质量差。深度值不准用固定平面假设时桌面高度变化或者深度相机在这个距离下的精度本来就不高。排查顺序也很重要。我一般先做“静态验证”放一个已知位置的物体视觉定位后把坐标打印出来再手动让机械臂末端移动到那个坐标看看偏差有多大。如果偏差在几毫米内说明标定和变换基本没问题如果偏差大到几厘米优先检查手眼标定的数据是否有明显跳变再检查相机安装是否松动。6.2 抓取角度不稳定或偶尔存在跳变minAreaRect返回的角度在物体接近正方形时容易发生跳变。因为正方形的主方向没有唯一性稍微旋转一点角度就会从接近0度跳到接近90度。解决办法有以下几种给角度加时序滤波例如用滑动平均或一阶低通滤波让角度平滑变化。对角度跳变设置阈值如果两帧之间的角度差大于45度认为发生了跳变用前一帧的角度。如果物体有明显特征比如一个有贴纸或标记的面可以改用特征点匹配来估计姿态。角度滤波的代码如下思路很简单class AngleFilter: def __init__(self, alpha0.6): self.alpha alpha self.last_angle None def update(self, angle_deg): if self.last_angle is None: self.last_angle angle_deg else: # 处理角度环绕避免0度与180度之间的跳变 diff (angle_deg - self.last_angle 180) % 360 - 180 self.last_angle self.alpha * diff return self.last_angle这个滤波器能有效减少单帧噪声带来的角度抖动但它也有滞后性如果物体运动速度很快要根据实际情况调整alpha。6.3 机械臂通信超时或指令格式错误通信层的问题往往在联调阶段集中爆发。常见的有端口或IP配置错误连接不上。机械臂处于报错/急停状态不接收新指令。指令字段名与机械臂SDK期望的不一致导致解析失败。位置或角度单位错误机械臂执行了异常轨迹。我的建议是写一个“通信自检脚本”手动指定一个已知的抓取位姿发给机械臂看它能否正确运动到对应位置。如果这一步通过了再接入视觉输出。这样能有效区分是视觉问题还是通信问题。6.4 避坑清单与调试心得下面这份避坑清单是从多个项目里总结出来的每一条都是实际踩过的坑用固定平面假设时确保相机安装角度没有大倾斜否则像素坐标在Z方向上的换算会失真。最小外接矩形的角度方向要结合相机的安装姿态统一换算到机器人坐标系不能直接拿图像的angle作为机器人偏航角。手眼标定时机械臂末端位姿和相机拍摄必须同步记录任何一移动后忘记更新记录标定结果就会错。深度相机要留意反光和透明物体这类材料深度图往往有空洞或错误值最好在分割阶段就排除掉这些区域。抓取下探的安全高度要给够尤其是使用深度阈值平面时物体高度波动大容易发生夹爪撞到物体的风险。调试这类系统我个人的经验是“先静态、后动态、先开环、后闭环”。先把物体摆在固定位置跑通定位、坐标变换和抓取动作然后再让物体小幅移动看视觉能不能跟上最后再加入传送带或随机放置的场景。每一步都确认无误了再进下一步能省下大量排查问题的时间。结语与下一步扩展如果你照着上面的流程完整跑通了一遍你会发现视觉机器人抓取并不是一个遥不可及的黑魔法它本质上就是“感知-标定-映射-控制”四个环节的工程串联。Python在这条链路里提供了极其顺畅的调试体验尤其适合做原型验证。接下来你可以往这些方向继续深入把颜色分割换成基于深度学习的实例分割让系统适应更多种物体加入抓取质量评价网络对候选抓取点进行打分筛选引入力觉反馈让夹爪在接触物体后自适应调整夹紧力。我自己也是在把基础流程跑通之后才开始逐个替换模块的每次替换都会对整个系统的稳定性有更深入的理解。希望这篇文章能帮你顺利迈出第一步。
返回列表