ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PX4 Gazebo无人机视觉降落实战:摄像头配置与OpenCV闭环

PX4 Gazebo无人机视觉降落实战:摄像头配置与OpenCV闭环 1. 从纯飞控仿真到视觉闭环为什么非要给Gazebo无人机加摄像头很多人做PX4仿真停留在“飞机能起飞、能悬停、能按航点飞”就收手了。这其实只验证了飞控的位置环和姿态环离真正的自主任务还差得远。一旦你想做视觉降落、目标跟踪、二维码识别这类任务就必须让仿真环境里的无人机“长眼睛”——也就是在Gazebo模型上挂一个摄像头传感器把图像通过ROS话题吐出来再用OpenCV做处理最后把识别结果转成位置指令喂回飞控。这条链路听起来简单实际配置时坑非常多Gazebo里的摄像头话题名和真实设备不一致、MAVROS转发图像延迟、OpenCV版本和ROS自带的cv_bridge打架、相机内参对不上导致测距偏差……我前后搭过三套不同版本的PX4GazeboROS环境每次都要重新踩一遍。这篇就把视觉降落这条完整链路拆开讲从模型加相机、话题桥接、OpenCV识别到MAVROS发指令每一步都给出可复现的操作和背后的原因。适合的读者是已经能跑通PX4基础仿真SITLGazebo能起飞想进一步做视觉相关二次开发的人。如果你连PX4源码都没编译过建议先把仿真环境跑通再来看这篇否则会卡在环境问题上浪费大量时间。提示本文基于PX4 v1.14.x固件、Gazebo Classic 11、ROS Noetic、OpenCV 4.x的组合。如果你用的是ROS2或Gazebo SimIgnition话题名和启动方式会有差异但核心思路一致。2. 给无人机模型挂摄像头的三种方式与选型逻辑2.1 为什么不能直接用Gazebo自带的相机模型Gazebo自带一个camera传感器很多人第一反应是直接在world文件里加一个。但问题在于这个相机是挂在world坐标系下的不会跟着无人机动。你要的是机载相机必须把它作为无人机模型的一部分跟着机体一起运动。所以正确做法是修改无人机的SDF/URDF模型文件在机体link上添加sensor typecamera。另一个常见误区是直接改PX4源码里的模型文件。PX4的模型定义在Tools/sitl_gazebo/models/下改这里确实能生效但每次更新PX4源码都会被覆盖。更稳妥的做法是在自己的ROS包或独立模型目录里做一份拷贝通过环境变量GAZEBO_MODEL_PATH指向自己的模型路径。2.2 三种挂载方式的对比方式操作位置优点缺点适用场景直接改PX4自带模型Tools/sitl_gazebo/models/iris/改完即用无需配路径更新源码被覆盖临时测试拷贝模型到独立目录自定义GAZEBO_MODEL_PATH不污染源码可版本管理需配环境变量长期开发用xacro动态生成URDFxacro参数化多相机方便学习成本高复杂多传感器我个人的选择是第二种把iris模型整个拷贝到~/my_models/iris_cam/改完在.bashrc里加一行export GAZEBO_MODEL_PATH$GAZEBO_MODEL_PATH:~/my_models。这样PX4升级不影响我的模型而且模型文件可以单独用git管理。2.3 相机参数怎么定别拍脑袋填在模型文件里加相机传感器时这几个参数必须认真填sensor typecamera namedownward_camera update_rate30.0/update_rate camera namedown_cam horizontal_fov1.047/horizontal_fov !-- 60度 -- image width640/width height480/height formatR8G8B8/format /image clip near0.05/near far50.0/far /clip noise typegaussian/type mean0.0/mean stddev0.007/stddev /noise /camera plugin namecamera_controller filenamelibgazebo_ros_camera.so robotNamespace//robotNamespace cameraNamedown_cam/cameraName imageTopicNameimage_raw/imageTopicName cameraInfoTopicNamecamera_info/cameraInfoTopicName frameNamedown_cam_link/frameName hackBaseline0.07/hackBaseline /plugin /sensorhorizontal_fov决定了视野范围视觉降落一般用60度左右比较合适——太窄了降落时目标容易跑出画面太宽了图像畸变大、测距精度下降。update_rate设30Hz足够设太高会拖慢Gazebo仿真速度。noise建议加上纯理想图像会让你的算法在仿真里表现完美一到真机就崩加一点高斯噪声更接近真实。注意libgazebo_ros_camera.so这个插件是ROS和Gazebo的桥接核心它负责把Gazebo内部的图像数据转成ROS的sensor_msgs/Image话题。如果这个插件加载失败你在rostopic list里根本看不到图像话题。2.4 相机安装位置和朝向的坑视觉降落通常用下视相机朝下看地面。在模型里相机的pose决定了它的位置和朝向。这里有个容易搞错的地方Gazebo的坐标系是X朝前、Y朝左、Z朝上相机默认朝向是沿Z轴负方向朝下还是沿X轴正方向朝前取决于你的pose里有没有加旋转。如果你要下视相机pose应该写成类似0 0 -0.1 0 1.5708 0最后的1.5708是绕Y轴旋转90度把相机从朝前掰成朝下。我第一次配的时候忘了这个旋转结果图像里全是天空排查了半天才发现是朝向问题。3. 从Gazebo话题到OpenCV图像链路的打通与验证3.1 启动顺序错了话题就找不到很多人启动仿真后rostopic list看不到图像话题八成是启动顺序问题。正确的顺序是先启动Gazebomake px4_sitl gazebo或roslaunch方式等Gazebo完全加载出模型再启动MAVROS最后启动你的图像处理节点如果Gazebo还没加载完就启动MAVROS有时会导致插件初始化失败。我习惯在启动脚本里加一个sleep 10虽然笨但有效。启动后先用rostopic list | grep image确认话题存在。正常情况下你应该看到类似/down_cam/image_raw和/down_cam/camera_info两个话题。如果没有检查模型文件里插件的cameraName和imageTopicName拼写这两个拼起来就是话题名。3.2 用image_view快速验证图像是否正常在写任何OpenCV代码之前先用ROS自带的工具确认图像能出来rosrun image_view image_view image:/down_cam/image_raw如果弹出一个窗口显示Gazebo里的画面说明链路通了。如果窗口是黑的或者报错先解决这个问题再往下走。这一步能帮你排除掉80%的配置问题。3.3 cv_bridgeROS图像和OpenCV图像的翻译官ROS的图像消息格式是sensor_msgs/ImageOpenCV用的是cv::Mat两者之间需要一个转换层这就是cv_bridge。用起来很简单import rospy from sensor_msgs.msg import Image from cv_bridge import CvBridge bridge CvBridge() def image_callback(msg): try: cv_image bridge.imgmsg_to_cv2(msg, desired_encodingbgr8) # 到这里cv_image就是标准的OpenCV图像了 except Exception as e: rospy.logerr(转换失败: %s % e) rospy.init_node(vision_landing) rospy.Subscriber(/down_cam/image_raw, Image, image_callback) rospy.spin()desired_encodingbgr8这个参数很关键。Gazebo默认输出的可能是rgb8而OpenCV习惯用bgr8如果不指定颜色通道会反红色和蓝色对调。做颜色识别的时候这个错误很隐蔽——你会发现自己调的红色阈值怎么都识别不到目标。3.4 版本兼容性cv_bridge和OpenCV的恩怨这是最容易让人崩溃的坑。ROS Noetic自带的cv_bridge是编译时链接到系统OpenCV的如果你用pip或conda装了另一个版本的OpenCV运行时就会出现符号冲突报错类似undefined symbol: _ZN2cv...。我的建议是不要用conda环境跑ROS节点。conda的OpenCV和ROS的cv_bridge几乎必然冲突。如果非要用conda做算法开发就把算法部分写成独立的Python脚本通过文件或socket和ROS节点通信而不是直接在ROS节点里import conda的cv2。验证cv_bridge是否正常可以跑这个最小测试python3 -c from cv_bridge import CvBridge; import cv2; print(cv2.__version__)如果这行能正常输出版本号说明基本没问题。如果报错先sudo apt install ros-noetic-cv-bridge重装ROS版本的cv_bridge。4. 视觉降落的核心算法从图像到降落指令4.1 降落靶标的选择为什么用ArUco而不是纯颜色视觉降落需要一个地面靶标。常见方案有两种纯颜色块和ArUco二维码。纯颜色块实现简单但受光照影响大而且只能给出位置不能给出姿态。ArUco码能同时给出位置和姿态鲁棒性也好得多。在Gazebo里你可以用一个带纹理的平面模型作为降落靶标纹理图片就是ArUco码。生成ArUco码可以用OpenCV自带的cv2.aruco模块import cv2 import numpy as np aruco_dict cv2.aruco.Dictionary_get(cv2.aruco.DICT_6X6_250) marker_image cv2.aruco.drawMarker(aruco_dict, 0, 400) cv2.imwrite(landing_marker.png, marker_image)生成的图片放到Gazebo模型的material里作为纹理。注意Gazebo里纹理的尺寸要和实际物理尺寸对应否则测距会偏。比如你希望靶标实际是0.5米见方那在模型里就要把平面设成0.5x0.5米。4.2 相机标定仿真里也不能省很多人觉得仿真里相机是理想的不需要标定。但实际上Gazebo的相机有内参焦距、主点你不标定的话用solvePnP算出来的距离和实际距离对不上。Gazebo的相机内参可以从camera_info话题里直接读它已经帮你算好了。但如果你想用OpenCV的标定流程走一遍也可以拿一个棋盘格在Gazebo里拍几张图做标定。我一般直接用camera_info里的参数省事且准确def camera_info_callback(msg): global camera_matrix, dist_coeffs camera_matrix np.array(msg.K).reshape(3, 3) dist_coeffs np.array(msg.D)msg.K是3x3内参矩阵msg.D是畸变系数。Gazebo默认畸变是0但内参矩阵里的焦距是有效的必须用。4.3 用solvePnP算相对位置拿到ArUco码的四个角点在图像中的像素坐标后配合已知的靶标物理尺寸和相机内参就能用solvePnP算出相机相对于靶标的位置和姿态# 靶标四个角点的3D坐标靶标坐标系单位米 marker_size 0.5 obj_points np.array([ [-marker_size/2, marker_size/2, 0], [ marker_size/2, marker_size/2, 0], [ marker_size/2, -marker_size/2, 0], [-marker_size/2, -marker_size/2, 0] ], dtypenp.float32) # img_points是从aruco.detectMarkers拿到的角点 success, rvec, tvec cv2.solvePnP(obj_points, img_points, camera_matrix, dist_coeffs) if success: # tvec就是相机在靶标坐标系下的位置 x, y, z tvec.flatten()这里z就是相机离地面的高度假设靶标贴地x和y是水平偏移。视觉降落的目标就是让x和y趋近于0同时控制z缓慢减小。注意tvec的单位是米但它的准确性高度依赖marker_size填得对不对。如果你在Gazebo里把靶标设成0.5米但代码里写0.3米算出来的距离会差将近一倍。这个错误非常隐蔽因为图像上看起来一切正常。4.4 从位置偏差到速度指令拿到x, y, z之后不能直接把它们当位置指令发给飞控。原因是视觉检测有噪声和延迟直接发位置会导致飞机抖动。通常的做法是做一个比例控制把位置偏差转成速度指令kp 0.5 # 比例增益 max_vel 0.5 # 最大速度限制 vel_x np.clip(kp * x, -max_vel, max_vel) vel_y np.clip(kp * y, -max_vel, max_vel) vel_z -0.3 # 固定下降速度接近地面时再减小kp的选取很讲究太大飞机会震荡太小响应太慢。我一般从0.3开始试根据实际响应调整。max_vel是安全限制防止偏差过大时飞机猛冲。当z小于某个阈值比如0.3米时切换到更慢的下降速度最后当z小于0.1米时发送降落指令MAV_CMD_NAV_LAND。5. MAVROS指令下发让飞控听懂视觉结果5.1 用setpoint_position还是setpoint_velocityMAVROS提供了多种setpoint接口。视觉降落常用的是setpoint_velocity速度控制或setpoint_position位置控制。我推荐用速度控制因为视觉检测的频率30Hz和飞控的控制频率通常250Hz以上不匹配直接发位置容易出现指令跳变。用速度控制的代码大概长这样from geometry_msgs.msg import TwistStamped vel_pub rospy.Publisher(/mavros/setpoint_velocity/cmd_vel, TwistStamped, queue_size10) def send_velocity(vx, vy, vz): cmd TwistStamped() cmd.twist.linear.x vx cmd.twist.linear.y vy cmd.twist.linear.z vz vel_pub.publish(cmd)注意坐标系MAVROS默认用的是ENU东-北-天而相机算出来的x, y是相机坐标系下的。如果相机是下视且机头朝前相机坐标系的x对应机体的前方y对应左方。你需要根据相机的安装朝向做一次坐标变换否则飞机往反方向飞。5.2 模式切换的时机视觉降落不是一上来就切OFFBOARD模式。正确的流程是飞机先起飞到一定高度比如2米悬停启动视觉节点确认能稳定检测到靶标切换飞控到OFFBOARD模式开始发送速度指令当高度低于阈值发送降落指令并切回AUTO.LAND模式切OFFBOARD之前必须先持续发送setpoint否则飞控会因为收不到指令而拒绝切换。我一般让视觉节点在启动后就以10Hz的频率发送零速度指令等确认收到后再切模式。5.3 安全兜底检测丢失怎么办视觉检测不可能100%稳定靶标被遮挡或者光照突变都会导致丢失。必须有兜底逻辑连续N帧比如10帧检测不到靶标立即发送零速度指令悬停如果超过一定时间比如3秒还没恢复切回AUTO.LOITER或直接降落高度低于0.3米时如果丢失目标直接发送降落指令不要再尝试视觉修正这些逻辑看起来简单但实际飞行中能救命。我在仿真里测试时故意遮挡靶标没有兜底逻辑的版本直接飞出了画面。6. 实测中那些文档不会告诉你的坑6.1 Gazebo仿真速度跟不上图像处理Gazebo默认是实时仿真但如果你的图像处理算法太耗时会导致Gazebo的仿真步进变慢整个系统时间膨胀。表现是飞机动作变慢、图像延迟增大。解决办法有两个一是降低图像分辨率640x480降到320x240二是把图像处理放到独立线程里不要阻塞ROS的回调。我试过在回调里直接做ArUco检测结果Gazebo的实时因子掉到0.5以下飞机像慢动作一样。6.2 相机话题的frame_id和TF树MAVROS和视觉节点都需要TF变换。如果相机的frame_id没有正确配置到TF树里solvePnP算出来的位置就没法转到机体坐标系。Gazebo的相机插件会发布frameName指定的frame但你需要确保这个frame和飞控的base_link之间有TF关系。最省事的做法是在模型文件里把相机的frameName设成和机体base_link一致或者用一个static_transform_publisher手动建立关系rosrun tf static_transform_publisher 0 0 -0.1 0 1.5708 0 base_link down_cam_link 1006.3 OpenCV的aruco模块在ROS里可能缺失ROS Noetic自带的OpenCV是4.2版本aruco模块是包含的。但如果你系统里装了多个OpenCVPython import的时候可能加载到没有aruco的版本。验证方法import cv2 print(cv2.__version__) print(hasattr(cv2, aruco))如果hasattr返回False说明你加载的OpenCV没有aruco。解决办法是统一OpenCV版本或者用cv2.aruco的替代实现。6.4 降落最后阶段的“地效”问题仿真里当飞机降到很低0.1米以下时Gazebo的碰撞检测可能会让飞机弹跳。这是因为起落架和地面的碰撞模型不够精细。解决办法是在降落最后阶段直接切AUTO.LAND让飞控自己处理而不是继续用视觉速度控制。7. 把这套流程复用到其他视觉任务上视觉降落只是机载视觉的一个应用。这套“Gazebo加相机 → ROS话题 → OpenCV处理 → MAVROS控制”的链路稍作修改就能用在其他任务上目标跟踪把ArUco检测换成颜色跟踪或YOLO检测输出目标在图像中的位置控制飞机保持目标在画面中心避障用双目相机或深度相机检测前方障碍物距离发送侧向速度指令绕开二维码巡检飞机飞到指定位置用相机扫描二维码识别后记录关键是把视觉处理和控制解耦视觉节点只负责输出“目标相对于相机的位置”控制节点负责把这个位置转成飞控指令。这样换视觉算法的时候不用动控制代码换控制策略的时候也不用动视觉代码。我在实际项目里把这套架构封装成了一个ROS package视觉部分做成插件式换检测算法只需要改一个配置文件。这样每次做新任务环境配置和通信链路都是现成的只需要专注算法本身。最后分享一个调试技巧在Gazebo里跑视觉算法时把处理后的图像用image_transport发布出来用rqt_image_view同时看原始图和处理图。这样能直观看到检测是否准确比看日志高效得多。
返回列表