基于大疆MSDK实现的无人机视觉引导自适应降落功能

基于大疆MSDK实现的无人机视觉引导自适应降落功能

概述

最初需求:想要无人机在执行完航线任务后,一键落到一个指定的位置,简化人工控制。

实现一套完整的无人机自主降落功能,通过虚拟摇杆控制使无人机飞向指定位置,再利用视觉识别引导无人机精确降落到具体位置。本文中采用自适应降落策略,根据高度动态调整精度要求和下降速度,以实现安全、精确的降落。

核心点:

  • 虚拟摇杆导航替代FlyTo功能
  • 双轴(X/Y)位置偏移实时调整
  • 高度自适应降落策略
  • 视觉识别引导定位
  • 智能避障管理

系统架构

整体流程

高于50m

20-50m

5-20m

低于5m

用户触发Return to Vehicle

获取无人机GPS位置

计算与目标点距离

启动虚拟摇杆导航

飞向目标位置 5m/s

距离小于10m?

开始自适应降落

视觉识别系统

计算X/Y偏移量

更新偏移量到ViewModel

自适应降落循环

高度分段判断

高空模式

中空模式

低空模式

极低空模式

计算调整速度和下降速度

偏移大于阈值2倍?

停止下降只调整

边调整边下降

高度小于5m?

关闭下视避障

高度小于等于0.1m?

着陆完成清理资源

技术实现思路

第一步:让无人机飞到目标位置?

问题分析

遥控器控制的无人机在执行完航线任务之后,飞到给定降落点(汽车或其他载具上)。最初的想法是使用DJI SDK提供的FlyTo功能,直接指定目标GPS坐标让无人机飞过去。但在实际测试中,发现部分机型(如M3E)并不支持FlyTo功能。

机型是否支持FlyTo功能参考文档https://developer.dji.com/doc/mobile-sdk-tutorial/cn/tutorials/intelligent-flight.html

解决方案:虚拟摇杆导航

既然FlyTo功能不可用,那就用虚拟摇杆功能进行模拟。

思路:

  1. 计算当前位置到目标位置的方位角(bearing)
  2. 将方位角转换为速度分量(南北/东西)
  3. 持续发送虚拟摇杆指令,让无人机朝目标飞行
  4. 实时监测距离,接近目标时停止

方位角计算:

privatefuncalculateBearing(latA: Double, lonA: Double, latB: Double, lonB: Double): Double {val lat1 = Math.toRadians(latA)val lat2 = Math.toRadians(latB)val dLon = Math.toRadians(lonB - lonA)val y = Math.sin(dLon)* Math.cos(lat2)val x = Math.cos(lat1)* Math.sin(lat2)- Math.sin(lat1)* Math.cos(lat2)* Math.cos(dLon)var bearing = Math.toDegrees(Math.atan2(y, x)) bearing =(bearing +360)%360// 归一化到0-360度return bearing // 0°=正北, 90°=正东, 180°=正南, 270°=正西}

速度分量计算:

val bearing =calculateBearing(currentLat, currentLon, targetLat, targetLon)val bearingRad = Math.toRadians(bearing)// 使用GROUND坐标系(地面坐标系)val navParam =VirtualStickFlightControlParam().apply{ rollPitchCoordinateSystem = FlightCoordinateSystem.GROUND verticalControlMode = VerticalControlMode.POSITION yawControlMode = YawControlMode.ANGLE rollPitchControlMode = RollPitchControlMode.VELOCITY // 将速度分解为南北和东西分量 pitch = NAVIGATION_SPEED * Math.cos(bearingRad)// 南北分量(5m/s) roll = NAVIGATION_SPEED * Math.sin(bearingRad)// 东西分量(5m/s) yaw = bearing // 让机头指向目标 verticalThrottle = targetAlt }
  • GROUND坐标系是绝对方向,不受无人机朝向影响
  • pitch控制南北,roll控制东西。

虚拟摇杆参数含义https://developer.dji.com/doc/mobile-sdk-tutorial/cn/basic-introduction/basic-concepts/flight-controller.html#虚拟摇杆


第二步:判断何时到达目标点上方附近

持续监测距离

每100ms检查一次当前位置与目标的距离,距离小于预期值ARRIVAL_THRESHOLD,就认为无人机已到达目标点上方附近,停止导航,开始降落:

val navTask =object: Runnable {overridefunrun(){val currentLoc =getAircraftLocation()val remainingDistance =calculateDistance( currentLoc.latitude, currentLoc.longitude, targetLat, targetLon )if(remainingDistance < ARRIVAL_THRESHOLD){// 10米内// 到达目标,停止导航,开始降落 isNavigating =falsestartDynamicAdjustment()}else{// 继续飞行sendNavigationCommand() virtualStickHandler?.postDelayed(this,100)}}}

第三步:精确降落到指定点

无人机虽然到了目标附近(10米内),但有以下问题:

  1. GPS精度有限(±3米),不够精确。
  2. 风力影响,有时候受风的影响,无人机会偏离。
解决方案:视觉识别+位置调整

工作原理:

  1. 无人机摄像头识别地面的特定图像(如二维码、标记点)
  2. 视觉算法计算偏移量(X轴左右,Y轴前后,Z轴距图像距离)
  3. 将偏移量传给无人机
  4. 无人机调整位置,边降落边对准

数据结构:

privatevar xOffset: Double =0.0// X轴偏移(米),正=右,负=左privatevar yOffset: Double =0.0// Y轴偏移(米),正=前,负=后privatevar zDistance: Double =0.0// Z轴距离(米),距降落点高度

外部接口:

// 视觉识别系统调用这些方法更新偏移量(~1Hz)funsetXOffset(offset: Double){ xOffset = offset }funsetYOffset(offset: Double){ yOffset = offset }funsetZDistance(distance: Double){ zDistance = distance }
采用自适应策略,一边降落一遍调整

关键点:
在不同的高度,我们允许的偏移量阈值不同的,高度较高的时候,偏移量就算比较大也可以下降,随着高度降低,我们允许的偏移量阈值会不断缩小(要求越来越向中间对齐)

真实偏移超出偏移量阈值的2倍就停止下降,只进行对齐调整;
真实偏移超出偏移量的1倍,就以0.1m/s的慢速一边降落一边调整;
在偏移量范围内,且高度> 20m,以0.5m/s的速度快速下降;
在偏移量范围内,且高度在5m-20m之间,以0.2m/s的速度下降;
在偏移量范围内,且高度< 5m,以0.2m/s速度下降;

实现:

// 1. 根据高度动态计算允许的误差privatefungetOffsetThreshold(altitude: Double): Double {returnwhen{ altitude >50.0->1.0// 高空:允许1米偏移误差 altitude >20.0->0.5// 中空:允许0.5米偏移误差 altitude >5.0->0.3// 低空:允许0.3米偏移误差else->0.2// 极低空:要求0.2米精度}}// 2. 根据高度和偏移量动态计算下降速度privatefungetDescentSpeed(altitude: Double, xOffset: Double, yOffset: Double): Double {val threshold =getOffsetThreshold(altitude)returnwhen{ xOffset > threshold *2|| yOffset > threshold *2->0.0// 偏移太大:停止下降 xOffset > threshold || yOffset > threshold ->0.1// 偏移较大:慢降 altitude >20.0->0.5// 中高空:快降 altitude >5.0->0.2// 低空:慢降else->0.2// 极低空:极慢降}}

控制逻辑:

大于50m

20-50m

5-20m

小于5m

偏移大于阈值的2倍

偏移大于阈值

偏移小于阈值

获取当前高度和偏移量

高度判断

偏离阈值1m

偏离阈值0.5m

偏离阈值0.3m

偏离阈值0.2m

偏移判断

停止下降,只调整

慢降0.1m/s并且调整

快降并且微调

发送虚拟摇杆指令

高度小于等于0.1m?

着陆完成

第四步:处理避障,降落后停桨。

问题:下视避障会阻止降落

无人机的下视避障系统会将地面识别为障碍物,在接近地面时自动停止下降,我们在高度为5m的时候关闭下视避障,落到地面后调用KeyStartAutoLanding进行停桨。
参考文档:https://sdk-forum.dji.net/hc/zh-cn/articles/14578693771033-如何使用虚拟摇杆降落

低空时关闭下视避障
var downwardObstacleDisabled =false//确保关闭下视避障操作只成功执行一次// 高度<5m时关闭下视避障if(currentAltitude <=5.0&&!downwardObstacleDisabled){ downwardObstacleDisabled =truesetObstacleAvoidanceEnable(false, PerceptionDirection.DOWNWARD)}//关闭下视避障调用方法privatefunsetObstacleAvoidanceEnable(enabled: Boolean,direction: PerceptionDirection){if(direction ==null){ Log.e("Perception","方向参数为空,无法设置避障")return} PerceptionManager.getInstance().setObstacleAvoidanceEnabled(//调用大疆MSDK方法关闭下视避障 enabled, direction,object: CommonCallbacks.CompletionCallback{overridefunonSuccess(){ toastResult?.postValue(DJIToastResult.success("成功设置【${direction.name}】方向的避障为:${if(enabled)"开启"else"关闭"}")) Log.i("Perception","成功设置【${direction.name}】方向的避障为:${if(enabled)"开启"else"关闭"}")}overridefunonFailure(error: IDJIError){ downwardObstacleDisabled =false toastResult?.postValue(DJIToastResult.failed("设置【${direction.name}】方向的避障失败:$error")) Log.e("Perception","设置【${direction.name}】方向的避障失败:$error")}})}

第五步:降落循环完整逻辑

privatefunstartDynamicAdjustment(){ isAdjusting =true virtualStickHandler =Handler(Looper.getMainLooper())val adjustTask =object: Runnable {overridefunrun(){if(!isAdjusting)return// 1. 获取当前状态val currentAltitude = FlightControllerKey.KeyAltitude.create().get(0.0)val currentXOffsetAbs = Math.abs(xOffset)val currentYOffsetAbs = Math.abs(yOffset)// 2. 检查是否着陆if(currentAltitude <=0.1){stopLanding()return}// 3. 低空时关闭下视避障if(currentAltitude <=5.0&&!downwardObstacleDisabled){ downwardObstacleDisabled =truesetObstacleAvoidanceEnable(false, PerceptionDirection.DOWNWARD)}// 4. 计算自适应参数val offsetThreshold =getOffsetThreshold(currentAltitude)val descentSpeed =getDescentSpeed(currentAltitude, currentXOffsetAbs, currentYOffsetAbs)// 5. 构建虚拟摇杆指令val adjustParam =VirtualStickFlightControlParam().apply{ rollPitchCoordinateSystem = FlightCoordinateSystem.BODY verticalControlMode = VerticalControlMode.VELOCITY rollPitchControlMode = RollPitchControlMode.VELOCITY // 水平调整 roll =if(currentXOffsetAbs > offsetThreshold){if(xOffset >0) ADJUSTMENT_SPEED else-ADJUSTMENT_SPEED }else0.0 pitch =if(currentYOffsetAbs > offsetThreshold){if(yOffset >0) ADJUSTMENT_SPEED else-ADJUSTMENT_SPEED }else0.0// 垂直下降 verticalThrottle =-descentSpeed }// 6. 发送指令 VirtualStickManager.getInstance().sendVirtualStickAdvancedParam(adjustParam)// 7. 100ms后再次执行(10Hz) virtualStickHandler?.postDelayed(this,100)}} virtualStickHandler?.post(adjustTask)}

以上,就实现了一整套视觉引导的自适应降落方案

安全注意事项

WARNING

  1. 必须在空旷、安全环境测试
  2. 建议先用DJI模拟器测试
  3. 视觉识别必须持续更新(~1Hz)
  4. 准备好随时手动接管
代码
/** * One-key return to vehicle function (using Virtual Stick instead of FlyTo) * 1. Get aircraft current location * 2. Calculate distance to vehicle using Haversine formula * 3. If distance > 500m, reject with error * 4. Use Virtual Stick to navigate to vehicle location * 5. Switch to precision adjustment when close enough */funreturnToVehicle(callback: CommonCallbacks.CompletionCallback){// Get aircraft current locationval aircraftLocation =getAircraftLocation()if(aircraftLocation ==null||!isLocationValid(aircraftLocation.latitude, aircraftLocation.longitude)){ callback.onFailure(DJICommonError.FACTORY.build("无法获取无人机位置信息"))return}// Vehicle coordinates (hardcoded for now, will be replaced with API later)// TODO: Replace with actual vehicle GPS coordinates from APIval vehicleLatitude =22.579// Example coordinatesval vehicleLongitude =113.941// Example coordinates// Calculate distance using Haversine formulaval distance =calculateDistance( aircraftLocation.latitude, aircraftLocation.longitude, vehicleLatitude, vehicleLongitude )// Distance validation: reject if > 500mif(distance >500){ callback.onFailure(DJICommonError.FACTORY.build("距离过远: ${String.format("%.2f", distance)}m, 超出 500m 限制"))return}// Start virtual stick navigation to vehicle location toastResult?.postValue(DJIToastResult.success("开始飞向车辆位置"))//TODO 这个targetAlt需要后期经过计算算出来。navigateToTarget(vehicleLatitude, vehicleLongitude,100.0, callback)}/** * Navigate to target location using Virtual Stick */privatefunnavigateToTarget( targetLat: Double, targetLon: Double, targetAlt: Double, callback: CommonCallbacks.CompletionCallback ){ VirtualStickManager.getInstance().enableVirtualStick(object: CommonCallbacks.CompletionCallback{overridefunonSuccess(){ VirtualStickManager.getInstance().setVirtualStickAdvancedModeEnabled(true) isNavigating =truestartNavigation(targetLat, targetLon, targetAlt, callback)}overridefunonFailure(error: IDJIError){ callback.onFailure(error)}})}/** * Start navigation loop using Virtual Stick */privatefunstartNavigation( targetLat: Double, targetLon: Double, targetAlt: Double, callback: CommonCallbacks.CompletionCallback ){ virtualStickHandler =Handler(Looper.getMainLooper())val navTask =object: Runnable {overridefunrun(){if(!isNavigating){return}val currentLoc =getAircraftLocation()if(currentLoc ==null){ virtualStickHandler?.postDelayed(this,100)return}// Calculate remaining distanceval remainingDistance =calculateDistance( currentLoc.latitude, currentLoc.longitude, targetLat, targetLon )println("targetLat:"+targetLat+" targetLon:"+targetLon+" currentLoc.latitude:"+currentLoc.latitude+" currentLoc.longitude:"+currentLoc.longitude+" remainingDistance:"+remainingDistance)// Check if arrivedif(remainingDistance < ARRIVAL_THRESHOLD){// Arrived at target, stop navigation isNavigating =false virtualStickHandler?.removeCallbacksAndMessages(null) callback.onSuccess() toastResult?.postValue(DJIToastResult.success("已到达车辆位置,开始精确定位"))//开始调节云台角度,俯仰角为-90°,旋转时间1sstartGimbalAngleRotation(GimbalAngleRotationMode.ABSOLUTE_ANGLE,-90.0,0.0,0.0,1.0)// Start precision adjustmentstartDynamicAdjustment()}else{// Continue navigationval bearing =calculateBearing( currentLoc.latitude, currentLoc.longitude, targetLat, targetLon )val navParam =VirtualStickFlightControlParam().apply{ rollPitchCoordinateSystem = FlightCoordinateSystem.GROUND // Use ground coordinate system verticalControlMode = VerticalControlMode.POSITION yawControlMode = YawControlMode.ANGLE rollPitchControlMode = RollPitchControlMode.VELOCITY // Calculate velocity components based on bearingval bearingRad = Math.toRadians(bearing) pitch = NAVIGATION_SPEED * Math.sin(bearingRad)// North-South component roll = NAVIGATION_SPEED * Math.cos(bearingRad)// East-West component yaw = bearing // Point towards target verticalThrottle = targetAlt // Target altitude} VirtualStickManager.getInstance().sendVirtualStickAdvancedParam(navParam) virtualStickHandler?.postDelayed(this,100)}}} virtualStickHandler?.post(navTask)}funstartGimbalAngleRotation(mode: GimbalAngleRotationMode,pitch: Double,yaw: Double,roll: Double,duration: Double){val rotation =GimbalAngleRotation().apply{setMode(mode)setPitch(pitch)setYaw(yaw)setRoll(roll)setDuration(duration)} KeyManager.getInstance().performAction( KeyTools.createKey(GimbalKey.KeyRotateByAngle), rotation,object: CommonCallbacks.CompletionCallbackWithParam<EmptyMsg>{overridefunonSuccess(result: EmptyMsg?){ toastResult?.postValue(DJIToastResult.success("云台旋转成功")) Log.i("Gimbal","云台旋转成功:yaw:${rotation.yaw},pitch:${rotation.pitch},roll:${rotation.roll}")}overridefunonFailure(error: IDJIError){ toastResult?.postValue(DJIToastResult.failed("云台旋转失败,$error")) Log.e("Gimbal","云台旋转失败,$error")}})}/** * Start dynamic position adjustment loop with adaptive descent * Adjusts position while descending, with stricter requirements at lower altitudes */privatefunstartDynamicAdjustment(){ isAdjusting =true virtualStickHandler =Handler(Looper.getMainLooper())// Send adjustment commands at 10Hzval adjustTask =object: Runnable {overridefunrun(){if(!isAdjusting){return}// TODO 获取脚本检测出的z轴距离val currentAltitude = FlightControllerKey.KeyAltitude.create().get(0.0)val currentXOffsetAbs = Math.abs(xOffset)val currentYOffsetAbs = Math.abs(yOffset)//关闭降落保护,下视避障失效if(currentAltitude <=5&&!downwardObstacleDisabled){ downwardObstacleDisabled =truesetObstacleAvoidanceEnable(false, PerceptionDirection.DOWNWARD)}// 检查是否落地if(currentAltitude <=0.1){stopLanding()return}// Get adaptive thresholds based on altitudeval offsetThreshold =getOffsetThreshold(currentAltitude)val descentSpeed =getDescentSpeed(currentAltitude, currentXOffsetAbs,currentYOffsetAbs)// Log for debuggingprintln("自动调整 - 高度:%.2fm, x偏移:%.2fm,y偏移:%.2fm, 阈值:%.2fm, 下降速度:%.2fm/s".format( currentAltitude, currentXOffsetAbs,currentYOffsetAbs,offsetThreshold, descentSpeed ))// Calculate adjustment parametersval adjustParam =VirtualStickFlightControlParam().apply{ rollPitchCoordinateSystem = FlightCoordinateSystem.BODY verticalControlMode = VerticalControlMode.VELOCITY yawControlMode = YawControlMode.ANGULAR_VELOCITY rollPitchControlMode = RollPitchControlMode.ANGLE // Calculate roll value based on offset// Positive offset (need to move forward) -> positive roll// Negative offset (need to move backward) -> negative rollif(currentXOffsetAbs > offsetThreshold){// Need adjustment roll =if(xOffset >0) ADJUSTMENT_SPEED else-ADJUSTMENT_SPEED }else{// Within threshold, no adjustment needed roll =0.0}if(currentYOffsetAbs > offsetThreshold){ pitch =if(yOffset >0) ADJUSTMENT_SPEED else-ADJUSTMENT_SPEED }else{ pitch =0.0} yaw =0.0 verticalThrottle =-descentSpeed // Descend at adaptive speed} VirtualStickManager.getInstance().sendVirtualStickAdvancedParam(adjustParam) virtualStickHandler?.postDelayed(this,100)}} virtualStickHandler?.post(adjustTask) toastResult?.postValue(DJIToastResult.success("开始动态位置调整"))}/** * Stop landing and cleanup */privatefunstopLanding(){ virtualStickHandler?.removeCallbacksAndMessages(null)//调用KeyStartAutoLanding进行停桨 FlightControllerKey.KeyStartAutoLanding.create().action({ toastResult?.postValue(DJIToastResult.success("桨叶动力关闭")) Log.i("stopLanding","桨叶动力关闭成功")},{ toastResult?.postValue(DJIToastResult.failed("桨叶动力关闭失败")) Log.i("stopLanding","桨叶动力关闭失败!!")})cleanupVirtualStick() toastResult?.postValue(DJIToastResult.success("降落完成"))}/** * Get offset threshold based on current altitude * Higher altitude allows larger offset, lower altitude requires stricter precision */privatefungetOffsetThreshold(altitude: Double): Double {returnwhen{ altitude > HIGH_ALTITUDE ->1.0// High altitude: allow 1m offset altitude > MID_ALTITUDE ->0.5// Mid altitude: allow 0.5m offset altitude > LOW_ALTITUDE ->0.4// Low altitude: allow 0.3m offsetelse->0.3// Very low altitude: require 0.2m precision}}/** * Get descent speed based on current altitude and offset * Larger offset or lower altitude results in slower descent */privatefungetDescentSpeed(altitude: Double, xOffset: Double,yOffset: Double): Double {val threshold =getOffsetThreshold(altitude)returnwhen{ xOffset > threshold *2|| yOffset > threshold *2->0.0// Offset too large: stop descending xOffset > threshold || yOffset > threshold ->0.1// Offset large: slow descent altitude > MID_ALTITUDE ->0.5// Mid-high altitude: fast descent altitude > LOW_ALTITUDE ->0.2// Low altitude: slow descentelse->0.1// Very low altitude: very slow descent}}

Read more

Magic API:低代码接口开发平台完全指南

Magic API:低代码接口开发平台完全指南

Magic API:低代码接口开发平台完全指南 🌟 你好,我是 励志成为糕手 ! 🌌 在代码的宇宙中,我是那个追逐优雅与性能的星际旅人。 ✨ 每一行代码都是我种下的星光,在逻辑的土壤里生长成璀璨的银河; 🛠️ 每一个算法都是我绘制的星图,指引着数据流动的最短路径; 🔍 每一次调试都是星际对话,用耐心和智慧解开宇宙的谜题。 🚀 准备好开始我们的星际编码之旅了吗? 目录 * Magic API:低代码接口开发平台完全指南 * 摘要 * 1. Magic API概述与核心概念 * 1.1 什么是Magic API * 1.2 Magic API的核心特性 * 1.3 Magic API的设计理念 * 2. Magic API架构设计与组件分析 * 2.1 整体架构概览 * 2.2 API引擎工作原理 * 2.3 脚本引擎与SQL执行机制 * 3. Magic API核心功能实现

By Ne0inhk
【火】Spatial Joy 2025 全球 AR&AI 赛事:开发者要的资源、玩法、避坑攻略都在这

【火】Spatial Joy 2025 全球 AR&AI 赛事:开发者要的资源、玩法、避坑攻略都在这

Spatial Joy 2025 Rokid乐奇 全球 AR&AI 开发大赛 值不值得参加?不少参加过连续两届 Rokid乐奇 赛事的老兵,纷纷表示非常值得参加。 先说最实在的——奖金。 AR赛道分为应用和游戏两个赛道,金奖各20万人民币,而且是现金!交完税全是你自己的!这还不够,AR赛道总共设了27个奖项,据我打听到的往年数据,能正常跑进初赛的作品大概就60-70个,这意味着获奖比例相当高。 20万就封顶了吗?远远没有!亚马孙科技给使用Kiro并获奖的开发者,在原奖金基础上再加20%现金奖励! AI赛道同样设置了27个奖项,奖金从1万到5万不等,主要以智能体开发为主,支持市面上所有智能体平台的适配。也就是说,你之前做的智能体微调一下就能参赛! 更重要的是,现在正是智能眼镜行业爆发前夜。据我观察,未来2-3年将是空间计算应用落地的关键窗口期,提前布局的开发者将占据绝对先发优势。 好了,重磅消息说完,下面是我为大家整理的详细参赛指南: 先给开发者交个底:这赛事值得花时间吗? 对技术人来说,一场赛事值不值得冲,就看三点:资源给不给力、

By Ne0inhk
【离散化 线段树 二分查找】3661可以被机器人摧毁的最大墙壁数目|2525

【离散化 线段树 二分查找】3661可以被机器人摧毁的最大墙壁数目|2525

本文涉及知识点 【C++】树状数组的使用、原理、封装类、样例 C++线段树 C++二分查找 3661. 可以被机器人摧毁的最大墙壁数目 一条无限长的直线上分布着一些机器人和墙壁。给你整数数组 robots ,distance 和 walls: robots[i] 是第 i 个机器人的位置。 distance[i] 是第 i 个机器人的子弹可以行进的 最大 距离。 walls[j] 是第 j 堵墙的位置。 每个机器人有 一颗 子弹,可以向左或向右发射,最远距离为 distance[i] 米。 子弹会摧毁其射程内路径上的每一堵墙。机器人是固定的障碍物:如果子弹在到达墙壁前击中另一个机器人,它会 立即 在该机器人处停止,无法继续前进。

By Ne0inhk