用Qt+FFmpeg+OpenCV打造Tello无人机PC地面站

发布时间:2026/10/20 2:42:50
用Qt+FFmpeg+OpenCV打造Tello无人机PC地面站 简介这是一套完整的大疆Tello无人机PC端控制工程基于Visual Studio与Qt开发适合有C基础且对无人机通信、视频流处理感兴趣的学生或开发者。它解决了在个人电脑上通过UDP协议向Tello发送起飞、降落、前后左右移动、翻滚等控制指令同时接收状态和视频数据并完成实时解码与界面显示的关键问题。压缩包共17个文件约61.98MB包含VS解决方案、Qt工程文件、C源程序、动态链接库、可执行文件和一个演示视频其中动态链接库为Qt、FFmpeg、OpenCV等依赖组件提供运行支持便于快速配置开发环境。目前已有1297人浏览学习。通过研读该工程的UDP通信、指令封装、Qt界面搭建、FFmpeg解码与OpenCV显示等模块可以系统掌握无人机PC端控制的完整实现链路演示视频和可执行程序也有助于对照学习适合在此基础上扩展自主飞行、目标检测、避障等进阶功能。1. 项目定位一台Tello、三行握手把手机遥控换成PC地面站无人机到手先飞了两圈接着就想从电脑上接管它。Tello官方App只能做基础飞行想在Windows上用键盘控制、想看实时码流、想把OpenCV算法接进画面必须走SDK协议自己搭一套地面站。这个项目就是干这个的底层用UDP发指令视频流用FFmpeg解H.264画面用OpenCV处理并显示界面用VSQt搭建。跑通之后你得到的不只是一个遥控器而是一个可继续加算法、加任务脚本的桌面控制框架。适合手里有Tello、想在PC端做二次开发的同学不管你是第一次接触无人机SDK还是想在上层挂视觉识别这份工程都能直接改着用。2. 技术选型与整体架构VSQt做壳、FFmpeg解H.264、OpenCV显示2.1 为什么是VSQt而不是纯PythonTello的协议一点都不复杂三分钟就能看懂所以选型的空间反而很大。用Python写个控制脚本确实很快socket发送字符串、OpenCV直接读流二十行代码就能起飞。但这个项目不是“让飞机动一下”而是要做成一个长期迭代的桌面控制端之后要加任务队列、加状态监控、加画面算法处理。Python写原型快但做到带界面的地面站线程管理和界面刷新就会开始别扭。VSQt的组合在这个场景下更顺。Qt的QUdpSocket封装了UDP收发信号槽机制天然适合跨线程传画面和状态QThread又可以直接用来跑视频解码这类长任务。FFmpeg的C接口在C工程里链接和调用都非常直接而OpenCV本身就是C写的把解码出的AVFrame转换成Mat几乎不需要中间层。另外Windows上VS调试多线程代码的体验比Python好不少断点打到解码线程里能看到每一帧的状态这对调视频流问题非常重要。提示如果只是临时飞一次Python是更快出结果的选择但只要你有“加界面、加任务编排、加图像算法”的计划C/Qt这套结构更稳改起来也更有底。2.2 模块划分指令、码流、状态三线程Tello和PC之间一共有三条UDP通道这是整个项目架构的根基通道端口方向内容指令8889PC → TelloASCII字符串如takeoff、land返回ok/error视频11111Tello → PCH.264编码码流状态8890Tello → PC分号分隔键值对约10Hz推送三条通道互相独立所以PC端也拆成三个独立模块。TelloCommand负责发指令是同步调用TelloVideoStream是一个QThread专跑FFmpeg解码循环TelloStatusMonitor是另一个QThread常驻读取状态消息并解析成字段。这三个线程不能合并的唯一原因是视频解码是重活一帧H.264解码可能吃掉几毫秒到几十毫秒如果放在指令线程里按下起飞键要等解完当前帧才有响应操作感会非常糟糕。状态监听则要求一直挂在8890端口上不能有间断否则会漏掉状态包。2.3 工程目录结构TelloPcControl/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── MainWindow.h / .cpp │ ├── TelloCommand.h / .cpp │ ├── TelloVideoStream.h / .cpp │ ├── TelloStatusMonitor.h / .cpp │ └── VideoWidget.h / .cpp └── docs/每个文件的职责很清晰。TelloCommand是纯指令封装不碰UITelloVideoStream只负责把UDP里的H.264解码成Mat通过信号往外发TelloStatusMonitor把状态消息解析成QMap后发射信号MainWindow做三件事的粘合把信号接到界面控件和状态栏VideoWidget负责把Mat画到界面上。这种分层让后面加算法模块时只需要在视频信号链路上插一个处理节点不影响其他部分。3. 核心代码拆解从按下空格键到Tello电机转动的完整链路3.1 指令通道先command握手再takeoff起飞Tello的SDK指令是一套ASCII字符串通过UDP发到192.168.10.1:8889。每条指令都要等待Tello返回“ok”才能继续发下一条没有等到响应就发指令Tello会大概率丢掉或报error。指令通道的代码因此是同步的发送、等待、读响应、判断结果。// TelloCommand.cpp - 同步发送一条指令并等待响应 bool TelloCommand::sendCommand(const QString cmd, int timeoutMs) { QByteArray payload cmd.toUtf8(); m_socket-writeDatagram(payload, m_telloIp, m_commandPort); if (!m_socket-waitForReadyRead(timeoutMs)) { qWarning() command timeout: cmd; return false; } QByteArray resp; resp.resize(static_castint(m_socket-pendingDatagramSize())); m_socket-readDatagram(resp.data(), resp.size()); return resp.trimmed() ok; } bool TelloCommand::enterSdkMode() { return sendCommand(command, 2000); } bool TelloCommand::takeoff() { return sendCommand(takeoff, 3000); }writeDatagram把字符串发到目标IP和端口waitForReadyRead是阻塞等待超时时间按指令类型区分。command是SDK握手Tello进入SDK模式后才能接收其他指令takeoff的超时给到3000毫秒因为起飞瞬间飞控要做姿态初始化响应比普通指令慢。所有指令统一走sendCommand这个封装后续加新指令只需要增加一行转发。提示Tello指令间隔很敏感两条指令之间至少要留200毫秒最稳妥的方式是收到上一条的ok后再发下一条。3.2 视频流线程UDP拉流 FFmpeg解码H.264视频流是项目里最容易翻车的地方处理逻辑也最多。Tello默认不主动推流需要先发streamon指令然后视觉模块才开始往11111端口吐H.264码流。解码线程用avformat_open_input打开udp:FFmpeg会把UDP上的裸H.264当作输入流处理。// TelloVideoStream.cpp - FFmpeg 解码 H.264 主循环 void TelloVideoStream::run() { avformat_network_init(); AVDictionary* opts nullptr; av_dict_set(opts, buffer_size, 65536, 0); av_dict_set(opts, fifo_size, 500000, 0); av_dict_set(opts, timeout, 3000000, 0); int ret avformat_open_input(m_formatCtx, udp://0.0.0.0:11111, nullptr, opts); if (ret 0) { emit errorOccurred(QString(open video stream failed, code%1).arg(ret)); return; } ret avformat_find_stream_info(m_formatCtx, nullptr); int videoIdx av_find_best_stream(m_formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); if (videoIdx 0) { emit errorOccurred(no video stream found); return; } AVCodecParameters* params m_formatCtx-streams[videoIdx]-codecpar; const AVCodec* codec avcodec_find_decoder(params-codec_id); m_codecCtx avcodec_alloc_context3(codec); avcodec_parameters_to_context(m_codecCtx, params); avcodec_open2(m_codecCtx, codec, nullptr); int width m_codecCtx-width; int height m_codecCtx-height; m_swsCtx sws_getContext(width, height, m_codecCtx-pix_fmt, width, height, AV_PIX_FMT_BGR24, SWS_BILINEAR, nullptr, nullptr, nullptr); AVPacket* pkt av_packet_alloc(); AVFrame* frame av_frame_alloc(); while (!m_stopFlag.load()) { ret av_read_frame(m_formatCtx, pkt); if (ret 0) { QThread::msleep(5); continue; } if (pkt-stream_index videoIdx) { ret avcodec_send_packet(m_codecCtx, pkt); while (ret 0 avcodec_receive_frame(m_codecCtx, frame) 0) { cv::Mat bgr(height, width, CV_8UC3); uint8_t* dstData[1] { bgr.data }; int dstLinesize[1] { static_castint(bgr.step) }; sws_scale(m_swsCtx, frame-data, frame-linesize, 0, height, dstData, dstLinesize); emit frameReady(bgr.clone(), width, height); av_frame_unref(frame); } } av_packet_unref(pkt); if (m_droppedFrames 15) { avcodec_flush_buffers(m_codecCtx); m_droppedFrames 0; } } av_frame_free(frame); av_packet_free(pkt); sws_freeContext(m_swsCtx); avcodec_free_context(m_codecCtx); avformat_close_input(m_formatCtx); }几个关键参数在这套场景里是调过的直接抄也可以。buffer_size设65536Tello一帧H.264数据基本在这个范围内设小了会直接丢包fifo_size设500000给UDP接收留一定缓冲太小会在解码慢时直接溢出timeout单位是微秒3秒收不到包就认为流断了。av_dict_set这三个参数必须传不传会按系统默认值Tello码流会很卡。解码部分用的是avcodec_send_packet和avcodec_receive_frame这套新版API老教程里常见的avcodec_decode_video2在新版FFmpeg里已经不能用了。每解出一帧sws_scale把YUV420p或NV12转成BGR24Mat的data直接作为目标内存不做多余拷贝。m_droppedFrames计数超过15帧时调用avcodec_flush_buffers清一次解码缓冲这是处理长时间飞行后延迟越来越大的关键操作后面避坑章节会专门说。3.3 图像显示把OpenCV Mat画到Qt控件解码线程拿到的是OpenCV Mat但Qt控件不能直接画Mat要在VideoWidget里转成QImage再做绘制。这里面有个非常隐蔽的坑OpenCV的Mat通道顺序是BGR直接转QImage会红蓝互换必须先cvtColor转成RGB。// VideoWidget.cpp - 把解码后的 Mat 绘制到 Qt 界面 void VideoWidget::setFrame(const cv::Mat frame) { cv::Mat rgb; cv::cvtColor(frame, rgb, cv::COLOR_BGR2RGB); QImage qimg(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888); m_pixmap QPixmap::fromImage(qimg.copy()); update(); } void VideoWidget::paintEvent(QPaintEvent*) { QPainter painter(this); if (!m_pixmap.isNull()) { QPixmap scaled m_pixmap.scaled(this-size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); painter.drawPixmap((width() - scaled.width()) / 2, (height() - scaled.height()) / 2, scaled); } }setFrame由解码线程调用通过信号槽跨线程触发update()会请求一次界面重绘。qimg.copy()这步不能省QImage构造时只是引用rgb.data的内存不copy的话下一帧Mat数据一变当前QImage内容就被改掉了。提示Mat的step可能带内存对齐填充把rgb.step直接传给QImage构造函数能避免图像错位或倾斜。3.4 状态监听8890端口上的键值对流状态通道是三个通道里最省事的但最容易被忽略。Tello会持续向8890端口推送状态消息内容类似“pitch:0;roll:0;yaw:0;vgx:0;vgy:0;vgz:0;templ:41;temph:42;tof:10;h:0;bat:87;press:986.9;time:1000”分号分隔键值对约10Hz一次。// TelloStatusMonitor.cpp - 解析 8890 端口状态消息 void TelloStatusMonitor::run() { QUdpSocket socket; socket.bind(QHostAddress::AnyIPv4, 8890); while (!m_stopFlag.load()) { if (!socket.waitForReadyRead(200)) { continue; } while (socket.hasPendingDatagrams()) { QByteArray datagram; datagram.resize(static_castint(socket.pendingDatagramSize())); socket.readDatagram(datagram.data(), datagram.size()); parseAndEmit(datagram); } } } void TelloStatusMonitor::parseAndEmit(const QByteArray data) { QMapQString, QString fields; const auto parts data.split(;); for (const auto part : parts) { int colon part.indexOf(:); if (colon 0) { fields[part.left(colon)] part.mid(colon 1); } } emit statusUpdated(fields); }bind(8890)固定了本地接收端口waitForReadyRead(200)的意思是每200毫秒轮询一次是否有数据到达。hasPendingDatagrams的while循环把缓冲里所有积压的包全部读完防止状态消息堆积。解析只用split和indexOf不用正则因为状态包每秒来10个正则的开销在这种高频小包场景里没必要。h字段是高度厘米数bat是电量百分比templ和temph是两个电机温度界面上显示这四五个字段就够了log里打印全部字段方便排障。4. 运行与部署从克隆代码到顺利起飞4.1 环境准备这是最容易卡住新手的一关。需要四样东西Visual Studio 2019或2022、Qt 5.15 LTS、FFmpeg 4.x开发版、OpenCV 4.x。Qt安装时勾上Widgets和Network模块OpenCV下载Windows版解压后配置环境变量FFmpeg要的是包含include和lib的开发包不是命令行用的精简版。FFmpeg的库文件要确认有avformat、avcodec、avutil、swscale四个缺一个都链接不上。OpenCV的bin目录要加入PATH否则运行时找不到opencv_world4.dll。Qt方面用MSVC编译器版本和VS保持一致不要混用MinGW编译的库否则链接时报一堆奇怪的符号错误。4.2 CMake工程配置与FFmpeg链接工程用CMake组织比.pro文件在VS里调试更顺手。关键配置是把FFmpeg、OpenCV、Qt三者正确串起来。cmake_minimum_required(VERSION 3.16) project(TelloPcControl) set(CMAKE_CXX_STANDARD 17) find_package(Qt5 COMPONENTS Widgets Network REQUIRED) find_package(OpenCV REQUIRED) set(FFMPEG_INCLUDE_DIR D:/libs/ffmpeg/include) set(FFMPEG_LIB_DIR D:/libs/ffmpeg/lib) target_link_libraries(TelloPcControl PRIVATE Qt5::Widgets Qt5::Network ${OpenCV_LIBS} ${FFMPEG_LIB_DIR}/avformat.lib ${FFMPEG_LIB_DIR}/avcodec.lib ${FFMPEG_LIB_DIR}/swscale.lib ${FFMPEG_LIB_DIR}/avutil.lib )链接顺序是有讲究的avformat依赖avcodecavcodec又依赖swscale和avutil所以顺序必须是avformat、avcodec、swscale、avutilMSVC链接器对静态库顺序很敏感顺序反了会报一堆LNK2019未解析符号。include目录和lib目录按你自己的FFmpeg实际路径替换建议把FFmpeg和OpenCV统一放在D:/libs下路径里不要带中文。4.3 连接与启动顺序Tello从开机到可控制有一串顺序不能错。先把Tello开机等指示灯变黄、Wi-Fi热点出现再让电脑连上以TELLO开头的热点。连上热点后启动程序程序会自动发command完成握手此时Tello进入SDK模式。进SDK模式后先别急着起飞点击“开始视频”按钮程序会先发streamon等返回ok后再启动解码线程。画面出来后看一眼状态栏的电量和高度字段确认状态通道正常最后才点takeoff。这段顺序的核心是Wi-Fi就绪 → command握手 → streamon开流 → 状态确认 → 起飞。颠倒任意两步都会出问题尤其是先起飞再开视频流画面会晚几秒才出现。4.4 冒烟测试飞之前的自检清单每次改完代码上真机前我习惯按下面这张表过一遍全程不超过两分钟步骤操作预期结果1连接Tello热点系统显示TELLO开头SSID2启动程序状态栏出现电量百分比3点击连接按钮日志输出command ok4点击开始视频画面窗出现实时图像5点击起飞Tello起飞并稳定悬停6点击降落Tello平稳落地这张表覆盖了三条通道的全部状态第3步验证指令通道第4步验证视频通道第2步验证状态通道。任何一步没达到预期就不要继续往下飞。真机上出了问题排查成本比桌面上高很多飞机摔了是小事桨叶伤到人就是大事了。5. 常见问题排查黑屏、指令超时、延迟翻车的血泪经验5.1 command指令一直超时现象程序启动后日志反复输出“command timeout”无论等多久Tello都不响应。原因Tello开机后要等Wi-Fi模块和摄像头初始化完成单纯看到热点出现不代表SDK已经就绪。有次我亲眼盯着指示灯热点出现后立刻发command照样超时等了5秒再发就通了。解决程序里加自动重试command重试三次每次间隔1秒基本能覆盖Tello的启动窗口期。bool TelloCommand::enterSdkModeWithRetry() { for (int i 0; i 3; i) { if (sendCommand(command, 2000)) { return true; } QThread::msleep(1000); } return false; }5.2 画面黑屏但能起飞现象Tello可以正常起飞降落但视频区域全黑FFmpeg日志报打开udp流失败或一直读不到帧。原因最常见的是没发streamon就去拉流。Tello的视觉模块默认不推流必须收到streamon才会开始往11111端口发送H.264数据。另一种可能是拉流时机太早视频编码模块还没就绪UDP前几个包错过就断了。解决在启动解码线程前先发streamon并确认返回ok然后等200毫秒再开解码。如果已经这样做了仍然黑屏检查一下本地防火墙是否拦了11111端口的UDP入站Windows防火墙经常把这种非系统进程的UDP流量静默丢弃。5.3 延迟越拉越大现象刚开始画面正常飞行一分钟后延迟从0.3秒慢慢涨到3秒以上并且持续恶化。原因解码速度跟不上推流速度AVPacket在内部排队积累越积越多画面表现就是延迟一路升高。UDP不像TCP有流控发送端不管接收端能不能消化一直按固定码率丢数据过来。解决在解码循环里统计连续丢帧数超过15帧就调用avcodec_flush_buffers清空解码器内部缓冲。这个方法会偶尔产生一个小卡顿但延迟能稳定控制在0.5秒左右比一路涨到失控强得多。另一个辅助手段是把av_dict_set里的fifo_size调小让UDP缓冲不至于积压太多旧数据。5.4 连上Tello热点后电脑上不了网现象连接Tello的Wi-Fi后浏览器打不开网页Git push超时依赖也拉不下来。原因Tello的热点是隔离网络只提供内网通信没有默认网关也没有DNS解析电脑外网自然断掉。解决这个属于设计如此不是故障。提前把依赖下载好Tello连接期间不做需要外网的操作。笔记本如果带双网卡可以插手机USB共享网络外网走USBTello走Wi-Fi互不干扰。5.5 编译报LNK2019未解析符号现象链接阶段报avformat_open_input、avcodec_find_decoder等符号找不到但代码和头文件都正常。原因FFmpeg静态库链接顺序错误。avformat引用了avcodec里的符号avcodec引用了avutil里的符号如果链接时把这些库的先后顺序排反链接器无法解析反向依赖。解决按avformat、avcodec、swscale、avutil的顺序排列把依赖最底层的库放最后。另外注意Debug和Release配置不要混用如果Debug配置链接的是Release版FFmpeg库同样会出符号问题。整理一下CMake里的target_link_libraries顺序这种报错基本都能消掉。6. 二次开发把遥控器升级成自主飞行的地面站6.1 任务队列让预设航线按顺序执行基础控制跑通后最自然的第一步是把“一次按一个键”变成“一键飞完一套动作”。做法是把指令放进队列每条指令执行成功后隔一段固定时间再取下一条。QQueueQString m_taskQueue; void MainWindow::onStartMission() { m_taskQueue.enqueue(takeoff); m_taskQueue.enqueue(up 30); m_taskQueue.enqueue(forward 100); m_taskQueue.enqueue(cw 90); m_taskQueue.enqueue(land); processNextTask(); } void MainWindow::processNextTask() { if (m_taskQueue.isEmpty()) return; QString cmd m_taskQueue.dequeue(); m_command-sendCommand(cmd, 3000); QTimer::singleShot(600, this, [this]() { processNextTask(); }); }这个实现用600毫秒固定间隔代替指令响应等待省去异步回调的复杂度。真要做得更严谨可以把sendCommand改成异步带响应回调响应ok后才推进下一条。6.2 目标追踪把OpenCV算法接进视频链路画面既然已经在Mat里了往上挂视觉算法就是顺理成章的事。最简单有效的例子是颜色目标追踪在解码线程的frameReady信号和VideoWidget之间插一个处理节点识别绿色色块并计算它与画面中心的偏移把偏移量转成旋转指令反馈给Tello。void TrackingNode::process(const cv::Mat frame) { cv::Mat hsv, mask; cv::cvtColor(frame, hsv, cv::COLOR_BGR2HSV); cv::inRange(hsv, cv::Scalar(35, 80, 80), cv::Scalar(77, 255, 255), mask); cv::Moments m cv::moments(mask); if (m.m00 800) return; // 目标面积太小忽略 int cx static_castint(m.m10 / m.m00); int cy static_castint(m.m01 / m.m00); int dx cx - frame.cols / 2; if (std::abs(dx) 25) { emit controlRequest(dx 0 ? cw 15 : ccw 15); } }HSV范围取35到77是绿色色相的典型区间m.m00是目标面积阈值小于800说明可能是噪点。controlRequest信号接到MainWindow再转成指令发送给TelloCommand。加上这个节点整个链路就闭环了图像进算法、算法产生控制量、控制量回传飞控。6.3 数据记录飞行日志与离线回放视觉闭环跑通后记录就变得很有必要。代码改动了、参数调过了没有日志就不知道飞机当时的状态对不对。我一般会把每一条发出的指令和对应的状态包追加写进CSV时间戳对齐飞行结束后用任何电子表格软件都能分析。这个做法帮我排掉过至少三次“改了视频参数导致延迟升高”的隐性回归问题。那次之后我每次改代码都强制先把“连接 → streamon → 起飞 → 降落”整条链路走一遍确认三个通道全部正常才继续调算法已经成了改这套工程前的固定习惯。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询