ARTICLE DETAIL

资讯详情

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

基于Apriltag视觉定位的无人机海上精准降落方案与代码实现

基于Apriltag视觉定位的无人机海上精准降落方案与代码实现 海上无人机降落这件事折腾过的人都知道有多闹心。尤其是降落目标不是一个固定平台而是一块随着浪涌上下起伏的甲板时GPS那点精度根本不够看——误差动不动一两米运气好落在停机圈旁边运气不好就直接洗了个海水澡。我之前在近海测试时试过纯RTK方案基站架在岸上无人机到了目标点上方之后飘得倒是稳但甲板是会动的光靠绝对坐标锁定一个移动或不规则晃动的目标本质上就是碰运气。后来把Apriltag引入到降落流程里事情才有了本质变化。Apriltag本质上是一类方形二维码标记但它不是用来存信息的而是用来做空间定位的。无人机通过机载相机拍到标签后可以实时计算出自己相对于标签的距离、俯仰角、偏航角精度能做到厘米级而且计算量非常小在嵌入式平台上跑也不会成为瓶颈。把Apriltag贴到降落平台中央无人机就能像一只看到窝的老鹰一样稳稳地修正位置并降落在理想落点。这篇文章我把我实际跑通的一套方案完整拆出来从系统架构、相机标定到代码实现、海上实测踩坑全部讲清楚代码部分可以直接拿走做二次开发。1. 项目整体设计与思路拆解1.1 为什么传统的GPS/RTK方案在海上会失效很多刚接触无人机船舶降落的朋友会先想到RTK。RTK确实能把定位精度做到厘米级但它解决的是“无人机在世界坐标系中的绝对位置”问题而海上降落要求的是“无人机相对于降落平台的相对位置”。平台本身在水面上是不断运动的横摇、纵摇、垂荡哪怕GPS告诉你无人机已经精准到了平台正上方平台可能已经漂开了一两米。加上海面多径效应明显GPS信号在水面上反射非常严重固定解经常意外丢失一旦RTK掉到浮点解或者单点解定位精度直接崩盘。视觉方案则完全不同。Apriltag检测不依赖任何外部基站或卫星信号它通过相机图像直接解算无人机与标签之间的相对位姿。物理世界中的一个已知尺寸的标签经过内参矩阵和畸变校正后投影到图像平面上只要能从图像中提取到标签的四个角点就可以通过PnP算法解算出相机相对于标签的旋转矩阵和平移向量。这意味着无人机不需要知道自己在世界坐标系的哪里只需要知道“标签在我前方多少米、偏左多少厘米”就能完成精准降落。这个逻辑放到船甲板上依然成立因为标签跟着船一起晃动无人机跟随的是标签坐标系而不是一个固定的经纬度点。1.2 Apriltag相比ArUco等其他视觉标记的优势市面上可用于视觉定位的标记并不少OpenCV自带的ArUco用得也挺多那为什么我最终选了Apriltag主要有三个原因。第一是检测稳定性。Apriltag家族在编码设计上有更严格的位姿校验机制对遮挡、模糊、光照变化的容忍度比ArUco高很多。海上场景里甲板反光、水花飞溅、阳光直射都是家常便饭标签稍微被遮挡一点,ArUco可能就直接丢帧了Apriltag还能通过剩余可用的角点信息坚持输出结果。第二是亚像素级角点提取。Apriltag的检测库尤其是pupil-apriltags和apriltag-imread在角点提取时做了亚像素优化这对远距离时减小位姿抖动非常重要。降落过程是一个渐进逼近的过程无人机从十几米高度开始下降时标签在画面中只有一小块角点提取精度直接决定了早期的修正方向是否正确。第三是标签家族多样。Apriltag提供了tag36h11、tag25h9、tag16h5等不同密度的标签家族密度越高的家族同一尺寸下可用的独立ID越多但单个模块在图像中占的像素也越小。海上场景中一个标签就够用了选择低密度的tag36h11反而能在更远的距离上被稳定检测到。1.3 方案架构概述整套系统分三大部分感知端机载相机采集图像Apriltag检测算法实时解算无人机与标签的相对位姿。决策端根据相对位姿生成降落控制指令包括水平位置修正量、下降速度控制、偏航校正。执行端通过大疆的SDK接口将控制指令发送给飞控驱动无人机执行降落动作。这三个部分可以全部跑在机载电脑上也可以拆分成机载感知地面站决策。考虑到海上通信链路不稳定我强烈建议所有感知和决策逻辑都放在机载端地面站只做监控和应急接管。2. 系统硬件与软件环境准备2.1 无人机选型与改装建议我用的是大疆M300 RTK主要看中的是它的开放性和载重能力。M300本身支持OSDKOnboard SDK可以挂载第三方机载电脑通过串口或网络与飞控通信实时发送控制指令。它的IP45防护等级在海上相对安全不至于被盐雾直接腐蚀掉关键部件。如果你手头是消费级的Mavic系列也不是完全没办法。Mavic 3 Enterprise的Mobile SDK支持航线飞行和自定义控制但控制频率和指令类型受限做不到真正的实时位姿反馈闭环。用我后面给的代码框架做二次开发的话建议至少从M300或者M350这个级别起步否则降落逻辑写得再好飞控端响应不了也是白搭。机载电脑我用的是NVIDIA Jetson Xavier NX8GB内存版本。Jetson平台跑Apriltag检测非常轻松Jetson Nano都能跑到30fps以上Xavier NX跑全套视觉控制逻辑还有大量富余算力。当然如果你不想在机载端写代码也可以把机载电脑换成大疆的Manifold 2兼容性更好但Manifold 2的性能够用但不算富余跑Apriltag检测需要优化图像尺寸。2.2 相机选型与安装位置相机我用了普通的USB工业相机分辨率1920x1080全局快门焦距6mm。这里有两个关键参数我要特别强调。一个是快门类型。海上光照强、目标运动快全局快门能避免卷帘快门带来的运动模糊和果冻效应。云台相机虽然画质好但要通过DJI SDK取流再处理延迟高、集成复杂。独立USB相机挂在无人机底部云台架上直接接入Jetson取流和处理都在本地延迟可以控制在50ms以内。另一个是焦距。焦距决定了视场角FOV而视场角决定了无人机在不同高度能看到多大的地面范围。6mm镜头在1/2.7英寸传感器上大概有60度左右的视场角这个数值比较均衡太高了看不到正下方附近的区域太低了又让远处的标签太小无法检测。实测下来在10米高度能稳定检测到一个50cm见方的Apriltag再高就需要更大尺寸的标签或者更长焦距的镜头这个后面我会给出详细的计算过程。安装位置方面相机要尽量正对下方光轴与无人机机体Z轴平行。会有一定安装角度误差但这不影响功能因为我们可以通过后续的标定流程把这个角度误差标定出来并补偿到位姿解算结果中。2.3 软件框架与依赖库软件方面我使用的是Ubuntu 20.04 ROS Noetic但代码逻辑本身不依赖ROS完全可以用纯Python脚本跑。核心依赖库就三个opencv-python图像预处理、PnP位姿解算、可视化。pupil-apriltagsApriltag检测库对Python支持好返回的检测结果包含了四个角点的图像坐标和ID。pyserial或socket与飞控通信发送控制指令。ROS在这个项目里只起到数据分发的作用把相机图像、位姿结果、控制指令统一管理起来。如果你想跳过ROS完全可以每个模块之间的数据流用Python的全局变量甚至共享内存就能解决。2.4 Apriltag标签制作与尺寸选择标签不是普通打印纸就行。海上甲板反光强普通纸张在强光下容易反白导致检测丢失。我用的是防水的哑光PVC材料打印后用双面胶固定在甲板中央。哑光面减少镜面反射PVC材料耐盐雾和潮湿不会因为受潮起皱变形而影响角点提取精度。标签尺寸的选择要结合降落高度来算。假设相机在距离标签H米的高度上悬停相机焦距为f单位像素标签边长为L单位米传感器像素宽度为W视场角为FOV那么标签在图像上占的像素数p可以用下面这个公式估算p W * L / (2 * H * tan(FOV/2))以我用的相机为例W1920FOV60度降落过程中无人机在10米高度时一个边长0.5m的标签占p 1920 * 0.5 / (2 * 10 * tan(30°)) 960 / (20 * 0.577) ≈ 83像素AprilTag检测库一般要求标签在图像中至少占30到50像素才能稳定检测83像素算是比较充裕的。到了1米高度时p 1920 * 0.5 / (2 * 1 * 0.577) ≈ 832像素标签已经会超出画面了。所以降落策略并不是让标签始终保持完整出现在画面里而是要在标签超出画面之前切换到“记忆位置盲降”模式。具体来说当检测到的标签面积过大或者角点超出图像边界时就冻结最后一次有效的位置修正数据保持当前对地速度缓慢下降直到触地。实际操作中从标签刚好填满画面到触地大概有1-1.5米的距离下降速度控制在0.3m/s以内靠飞控自身的落地检测就能安全触地。3. 核心算法与完整代码实现3.1 Apriltag检测参数调优细节pupil-apriltags库的检测参数里有几个关键选项在实际使用中必须根据海上场景调整否则检测率会大打折扣。from pupil_apriltags import Detector import cv2 import numpy as np # 创建检测器 detector Detector( familiestag36h11, nthreads4, quad_decimate1.0, # 关键参数图像缩放比例 quad_sigma0.0, # 边缘检测前的模糊系数 refine_edgesTrue, # 亚像素边缘细化 decode_sharpening0.25, # 解码锐化强度 ) # 相机内参和畸变系数需要根据自己相机标定结果填入 camera_params [758.34, 759.21, 963.52, 523.87] # fx, fy, cx, cy dist_coeffs [0.021, -0.043, 0.0012, -0.0028, 0.0] # k1,k2,p1,p2,k3quad_decimate这个参数很值得说。它大于1时算法会先缩小图像再检测四边形速度可以提升非常多但代价是远距离小标签更容易漏检。我一开始图快设为2.0结果在7米以上高度时经常出现连续几帧检测不到的情况。后来改成1.0关闭缩放检测率就稳定了。Jetson Xavier NX跑1080P图像不做缩放的检测耗时大概是15-20ms一帧完全够用。decode_sharpening是解码时对标签内部黑白格进行锐化的强度。海上光线强、图像对比度高可以稍微调低一点如果是阴天或者黄昏调高到0.5能有效提升识别率。3.2 完整检测与位姿解算代码下面这段代码是整个视觉感知端的核心包含了从图像采集到位姿输出的全流程。它不依赖ROS直接用OpenCV读摄像头输出的是相机也就是无人机相对于标签的旋转矩阵和平移向量。import cv2 import numpy as np from pupil_apriltags import Detector class AprilTagLocalizer: def __init__(self, camera_params, dist_coeffs, tag_size0.5): self.detector Detector( familiestag36h11, nthreads4, quad_decimate1.0, quad_sigma0.0, refine_edgesTrue, decode_sharpening0.25, ) self.camera_params camera_params self.dist_coeffs dist_coeffs # 标签的物理尺寸单位米 self.tag_size tag_size def detect_pose(self, frame): 输入一帧BGR图像返回标签ID、旋转向量、平移向量、四个角点坐标 如果未检测到标签返回None gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # AprilTag检测 detections self.detector.detect( gray, estimate_tag_poseTrue, camera_paramsself.camera_params, tag_sizeself.tag_size, ) results [] for det in detections: # 旋转向量罗德里格斯形式和平移向量 # 注意pupil-apriltags返回的平移向量单位与tag_size一致米 rvec det.pose_R tvec det.pose_t # 将旋转矩阵转为旋转向量方便后续与OpenCV函数对接 rvec_cv, _ cv2.Rodrigues(rvec) # 四个角点图像坐标顺序是左上、右上、右下、左下 corners det.corners results.append({ id: det.tag_id, rvec: rvec_cv, tvec: tvec.flatten(), corners: corners, }) return results # 使用示例 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少图像缓存延迟 # 相机内参请替换为你自己标定的数值 camera_params [758.34, 759.21, 963.52, 523.87] dist_coeffs [0.021, -0.043, 0.0012, -0.0028, 0.0] localizer AprilTagLocalizer(camera_params, dist_coeffs, tag_size0.5) while True: ret, frame cap.read() if not ret: continue poses localizer.detect_pose(frame) for pose in poses: print(fTag ID: {pose[id]}) print(f平移向量 (x, y, z): {pose[tvec]}) # 可视化 cv2.drawContours(frame, [pose[corners].astype(int)], -1, (0, 255, 0), 2) cv2.imshow(AprilTag Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()3.3 从坐标系解算到无人机控制指令拿到tvec之后不能直接把相机系下的坐标发给飞控需要做一个坐标变换再生成控制指令。这里有一个重要细节大疆OSDK里的速度控制指令是基于无人机的机体坐标系body frame的而Apriltag解算出来的位姿是相机坐标系相对标签坐标系的。无人机降落时我们希望的是“无人机水平移动到标签正上方”这意味着要在机体水平面内解算偏差。假设相机安装时与机体坐标系完全对齐忽略安装角偏差或已通过标定补偿那么tvec的三个分量分别表示tvec[0]相机系下标签在X方向的偏移。标签在右值为正。tvec[1]相机系下标签在Y方向的偏移。标签在下值为正。tvec[2]相机系下标签到相机的距离近似为无人机高度。控制目标是让tvec[0]和tvec[1]趋近于0。在悬停或缓降状态下可以直接把这两个量作为水平速度控制的误差项做一个简单的P控制器# 控制参数海上环境实测好用的数值 Kp_x 0.6 Kp_y 0.6 # 安全阈值当水平偏差小于5cm时不再修正避免频繁震荡 position_threshold 0.05 def compute_velocity_command(tvec): 根据相机系下的平移向量生成机体坐标系下的水平速度控制指令 返回值: (vx, vy, vz, yaw_rate)单位 m/s 和 rad/s if tvec is None: # 失去目标时悬停等待 return 0.0, 0.0, 0.0, 0.0 # 提取水平偏差 dx tvec[0] # 左右偏差单位米 dy tvec[1] # 前后偏差单位米 # 死区处理 vx -Kp_x * dx if abs(dx) position_threshold else 0.0 vy -Kp_y * dy if abs(dy) position_threshold else 0.0 # 垂直速度根据高度设计这里先固定一个安全下降速度 h tvec[2] if h 3.0: vz -0.5 # 高度较高时快速下降 elif h 0.8: vz -0.3 # 接近标签时缓慢下降 else: vz -0.15 # 已经很低了再慢一点 # 偏航角不控制保持当前朝向 yaw_rate 0.0 return vx, vy, vz, yaw_rate这段代码的P控制器增益为什么选0.6因为海上风浪环境下过大的增益会让无人机在接近标签时出现来回震荡增益太小又修正太慢容易被风吹偏。我实测调整了多组参数最终0.6在3级风以内都能保持比较好的跟随性能。当然你需要根据自己的飞机重量和响应特性重新调参建议从0.3起步逐步加大观察无人机是否出现振荡。3.4 与大疆OSDK的通信实现控制指令算出来了接下来要解决的是怎么把它发给飞控。M300的OSDK已经更名为PSDK提供了丰富的控制API可以通过机载电脑串口直接调用。// 伪代码示例展示OSDK控制接口的核心逻辑 // 使用DJI OSDK的VelocityControl类 #include dji_vehicle.hpp using namespace DJI::OSDK; using namespace DJI::OSDK::Telemetry; // 定义控制指令 typedef struct { float vx; // 前向速度单位m/s float vy; // 右侧速度单位m/s float vz; // 垂直速度单位m/s float yawRate; // 偏航角速度单位rad/s } VelocityCommand; void sendVelocityControl(Vehicle* vehicle, VelocityCommand cmd) { // 创建速度控制指令 VelocityControlCmd velocityCmd; velocityCmd.ctrlMode VelocityControlCmd::HORIZONTAL_ATTITUDE_VERTICAL_THRUST; velocityCmd.x cmd.vx; // 水平前向速度 velocityCmd.y cmd.vy; // 水平横向速度 velocityCmd.z cmd.vz; // 垂直升降速度 // 发送控制指令频率建议10-20Hz vehicle-control-velocityAndYawRateCtrl(velocityCmd); }如果你不想写C大疆也提供了ROS环境下的OSDK包可以直接监听ROS topic把Python计算出的vx、vy、vz发布出去再通过OSDK节点转发给飞控。这种做法在工程上最容易调试我就是先用ROS的rqt工具可视化速度指令确认逻辑没问题后才固话成独立模块。整套闭环控制的核心在于控制频率。我建议视觉检测和位姿解算跑在20Hz以上控制指令下发频率不要低于10Hz。低于10Hz时无人机在海上风场中修正速度跟不上扰动会出现明显的滞后和偏移积累。4. 海上实战的坑与排查技巧4.1 强光照与水面反光干扰第一次带到海上测试遇到了一个非常棘手的问题正午阳光直射时Apriltag标签的白色区域反光严重整个标签在图像里变成一整块亮斑角点提取直接失败。后来我换了哑光PVC材料问题改善了不少但依然不能完全避免某个特定角度下标签反光。排查思路是这样的如果发现检测率骤降先别急着改代码把当前帧保存下来直接看。用下面这段代码把检测失败的帧存成图片# 在循环中检测失败时保存当前帧 if len(poses) 0: cv2.imwrite(ffailed_frame_{cv2.getTickCount()}.jpg, frame) print(检测失败帧已保存)实际保存下来的图片显示反光区域的灰度值超过了250而标签黑色区域在40左右整张图的对比度接近爆表。这种情况下的解决方案是开启相机自动曝光并把曝光上限压低cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 开启自动曝光部分驱动用不同枚举值 cap.set(cv2.CAP_PROP_EXPOSURE, 200) # 限制最大曝光时间在光照极强的情况下直接把ISO压低效果更明显。测试时我把ISO固定在100曝光时间限制在1/2000秒以内标签检测稳定性大幅提升。4.2 标签与甲板对比度不足海上甲板通常是浅灰色或者白色如果标签的白色区域和甲板颜色太接近边缘检测会出问题。我第一版标签是白底黑格放在白色甲板上检测率非常差。后来换成了黄底黑格的标签检测率一下子恢复了。黄底的好处在于和大多数人造甲板的蓝色、灰色、白色差距大即使图像再做一次灰度转换黄色在灰度图上的亮度也明显高于甲板边缘对比度依然存在。如果你也碰到标签和背景颜色接近的情况更换标签底色是最快的解法不需要动任何代码。4.3 甲板晃动导致的位姿跳变海上平台是运动的拍到的标签每帧位置都在变化直接用原始tvec做控制会导致无人机的修正指令忽大忽小。我加了一个简单的滑动平均滤波效果立竿见影。class MovingAverageFilter: def __init__(self, window_size5): self.window_size window_size self.buffer [] def update(self, value): if len(self.buffer) self.window_size: self.buffer.pop(0) self.buffer.append(value) return np.mean(self.buffer, axis0) # 使用示例 filter_x MovingAverageFilter(5) filter_y MovingAverageFilter(5) # 在控制循环中 raw_dx tvec[0] filtered_dx filter_x.update(raw_dx)窗口大小5对应的延迟大约0.25秒在20Hz控制频率下这个延迟对降落过程来说是完全可以接受的。窗口再大的话虽然更平滑但会明显滞后于甲板的快速运动反而导致控制发散。4.4 远距离检测不到标签的终极方案起飞后到下降到一定高度之前标签可能处于画面边缘或者过小检测不到。这个阶段不能干等要让无人机先依靠GPS或者RTK粗定位到降落区域上空再开始视觉搜索。具体做法是当视觉检测持续丢失超过2秒时切换回位置控制模式保持当前位置悬停每1秒旋转一个固定角度比如15度直到重新检测到标签。这套“GPS引导视觉精修”的降落流程本质上是一个由粗到精的渐进式引导策略。先解决是否在目标附近的问题再解决是否正中目标的问题逐步提高精度这也是整个海上降落设计的核心思路。5. 常见问题速查表与调试工具推荐下面这个表格是我实际项目中整理的排查清单按照频率排序基本覆盖了海上降落遇到的大部分问题。问题现象可能原因解决方案检测不到标签标签过小/距离过远加大标签尺寸或提高相机分辨率检测不到标签标签反光或对比度不足更换哑光材料选择与背景色差大的底色检测到但位姿抖动大相机曝光参数不合理压低ISO限制曝光时间位姿跳变明显甲板晃动导致角点提取不稳定增加滑动平均滤波窗口无人机接近标签时来回震荡控制器增益过大降低Kp_x和Kp_y标签快触地时位姿丢失标签超出画面边界设置盲降阈值切换盲降模式高帧率下控制卡顿图像处理耗时过长降低分辨率到1280x720开启多线程海上通信中断图传和数传受干扰所有关键逻辑放机载端地面站仅备用调试过程中我强烈推荐在降落地面上架一台摄像机从侧面完整记录整个降落过程。这样当降落失败时你能从外部视角看到无人机的姿态变化和标签的相对位置关系比看飞控日志直观得多。我就通过这种方法发现了一个隐蔽问题在接近触地的最后0.3米无人机的下视传感器会介入辅助降落这导致飞控优先使用超声波测距而不是视觉信号两者切换之间出现了一个小的抖动。理论上默认策略是视觉优先但海上甲板晃动会让超声波数据出现异常最终通过调整飞控配置解决了这个问题。5.1 推荐使用的调试工具rqt_image_view实时查看图像和检测结果是排查标签检测问题的第一工具。plotjuggler把tvec的x、y、z数据实时画成曲线可以直观看到无人机接近标签过程中的收敛曲线。DJI Assistant 2读取飞控日志排查控制指令是否正确下发。6. 项目扩展与后续思考这套方案跑通后可扩展的方向很多。如果你面对的不是固定的小甲板而是一艘在航行中的船需要跟踪运动的降落目标那就需要引入目标预测算法。最简单的方法是给tvec序列加一个卡尔曼滤波估计出目标的水平运动速度和加速度然后把速度补偿叠加到控制指令中。如果是大甲板或者需要更高降落精度可以增加标签数量和排列方式。多个标签的位姿结果做加权融合可以有效减少单个标签漏检时的抖动。我做过一个小规模实验用两个标签对角放置控制精度比单标签提升了大约20%。视觉与RTK融合是另一个值得尝试的方向。在信号良好的条件下RTK提供绝对位置做粗定位视觉提供相对位置做精修正两者通过扩展卡尔曼滤波融合理论上可以同时获得稳定性和精度。我目前正在测试这套融合方案初步数据看起来很不错偏僻地区跳变问题明显减少但因为场景复杂还没完全准备好在船上进行大量测试。说回到海上降落这个项目本身我的最大体会是精准降落的关键不只在于算法精度更在于整个系统在真实环境中的鲁棒性。一个能在仿真里跑到厘米级误差的算法拿到海上可能连标签都检测不到。多实拍、多保存失败帧、多分析失败原因这才是让系统真正可靠的路径。代码只是个起点场景里的细节才是真正拉开差距的地方。最后再分享一个很小的细节却是我踩过最大的坑出海测试前一定要多准备几张防水标签以备不时之需。溅上咸水后标签表面的盐渍在阳光照射下会形成一层白霜严重影响检测效果。用清水冲一下就能恢复但如果你只带了一张又已经固定在甲板上了整个测试计划就得中断。这种看似不起眼的准备工作往往决定了你一天测试的成败。
返回列表