Qt多线程编程:继承QObject与moveToThread的正确实践
1. 为什么“继承QObject”才是Qt多线程的正统解法很多人第一次接触Qt多线程脑子里蹦出来的第一个方案往往是继承QThread然后重写run()。这个思路不能说错但它有个致命问题QThread对象本身活在创建它的那个线程里只有run()函数体才跑在新线程中。这意味着你在QThread子类里定义的槽函数默认还是在旧线程执行一不小心就踩了线程亲和性的坑。我见过太多项目因为这个问题导致界面卡死、数据竞争排查半天才发现是线程归属搞混了。Qt官方推荐的模式其实很明确继承QObject把业务逻辑封装成槽函数再用moveToThread()把对象搬到工作线程。这套方案的核心优势在于对象的线程亲和性清晰可控信号槽连接自动变成队列连接跨线程调用天然安全。你不需要手动加锁不需要操心事件循环Qt的元对象系统帮你把线程间通信安排得明明白白。这篇文章适合两类人一是刚接触Qt多线程、被QThread各种坑折磨过的开发者二是已经用过moveToThread但对其底层机制一知半解的进阶者。我会从线程亲和性原理讲起拆解moveToThread的完整流程给出可直接复用的代码骨架再分享几个我在实际项目中踩过的坑和总结的技巧。读完你至少能搞清楚三件事为什么槽函数必须定义在QObject子类里、moveToThread到底做了什么、以及如何优雅地停止一个工作线程。提示本文所有代码基于Qt 5.15和Qt 6通用写法涉及差异处会单独标注。示例中的类名、项目名均为虚构代称读者可根据自己项目替换。2. 线程亲和性与事件循环理解moveToThread的前置知识2.1 每个QObject都“属于”某个线程Qt里有个概念叫线程亲和性Thread Affinity说的是每个QObject实例都归属于某一个线程。这个归属关系决定了三件事对象的槽函数在哪个线程执行、对象收到的事件在哪个线程处理、对象的定时器在哪个线程触发。默认情况下对象归属于创建它的那个线程。你可以用QObject::thread()查看对象当前归属用QThread::currentThread()查看代码正在哪个线程运行。这两个值不一致的时候跨线程操作就发生了。Qt明确规定不能直接跨线程调用对象的非线程安全方法否则行为未定义。信号槽是例外因为Qt对跨线程信号槽做了特殊处理。打个比方线程亲和性就像员工的工位归属。你人在A工位却要去操作B工位的设备中间必须走正规流程信号槽不能直接伸手去按B工位的按钮。moveToThread做的就是给对象换工位把它从A线程的管辖范围挪到B线程。2.2 事件循环是槽函数执行的“发动机”工作线程要能响应信号槽必须运行事件循环。QThread::run()的默认实现就是调用exec()启动事件循环。如果你重写了run()却没调exec()那这个线程就不会处理任何事件moveToThread过去的对象也收不到槽函数调用。事件循环的本质是一个消息泵不断从队列里取出事件分发给对应的对象处理。跨线程的信号槽连接会被Qt自动转换成QMetaCallEvent投递到目标线程的事件队列由目标线程的事件循环取出并执行槽函数。这就是为什么跨线程信号槽是线程安全的——它本质上是把调用请求排队让目标线程自己执行。理解这一点很关键槽函数不是在发射信号的线程执行而是在接收者所属线程执行。所以你把工作对象moveToThread到工作线程后它的槽函数就自动在工作线程跑了完全不需要手动创建线程或加锁。2.3 为什么继承QThread重写run()容易出问题继承QThread重写run()的模式问题出在对象归属的割裂上。假设你写了一个MyThread : public QThread在run()里做耗时操作。这个MyThread对象本身归属于创建它的线程通常是主线程只有run()函数体跑在新线程。如果你在MyThread里定义了一个槽函数doWork()然后从主线程发信号触发它这个槽函数会在主线程执行而不是新线程——因为对象归属没变。很多人没意识到这点以为只要继承了QThread所有成员函数都在新线程跑结果槽函数阻塞了主线程界面卡死。要修正这个问题你得在run()里手动创建对象、手动调exec()代码变得别扭。相比之下moveToThread模式把“线程管理”和“业务逻辑”彻底分离QThread只负责提供线程和事件循环业务对象通过moveToThread搬进去职责清晰不容易出错。3. 从零搭建一个可复用的QObject工作线程骨架3.1 定义工作类把耗时逻辑封装成槽函数先定义一个继承QObject的工作类。关键点构造函数里不要做耗时操作耗时逻辑全部放在槽函数里且槽函数要能被信号触发。// worker.h #ifndef WORKER_H #define WORKER_H #include QObject #include QDebug #include QThread class Worker : public QObject { Q_OBJECT public: explicit Worker(QObject *parent nullptr); ~Worker(); public slots: void doHeavyWork(); // 耗时任务入口 void stopWork(); // 停止标志设置 signals: void workFinished(); // 任务完成通知 void progressChanged(int percent); // 进度上报 private: bool m_stopped false; }; #endif // WORKER_H实现文件里doHeavyWork模拟一个可中断的耗时循环// worker.cpp #include worker.h Worker::Worker(QObject *parent) : QObject(parent) { qDebug() Worker constructed in thread: QThread::currentThread(); } Worker::~Worker() { qDebug() Worker destroyed in thread: QThread::currentThread(); } void Worker::doHeavyWork() { m_stopped false; qDebug() doHeavyWork running in thread: QThread::currentThread(); for (int i 0; i 100; i) { if (m_stopped) { qDebug() Work cancelled by stop flag; return; } // 模拟一段耗时操作 QThread::msleep(50); emit progressChanged(i); } emit workFinished(); } void Worker::stopWork() { m_stopped true; }注意构造函数和析构函数里打印了当前线程方便你验证对象归属。doHeavyWork里每轮循环检查m_stopped标志这是实现可中断任务的标准做法。QThread::msleep是静态函数让当前线程睡眠不会阻塞其他线程。3.2 在主线程中组装moveToThread的完整调用链主线程这边创建一个QThread和一个Worker把Worker搬到新线程连接信号槽启动线程最后发信号触发任务。// mainwindow.cpp 片段 void MainWindow::startWork() { m_thread new QThread(this); m_worker new Worker(); // 注意不指定parent m_worker-moveToThread(m_thread); // 关键一步搬移对象 // 线程启动后自动触发任务 connect(m_thread, QThread::started, m_worker, Worker::doHeavyWork); // 任务完成通知主线程 connect(m_worker, Worker::workFinished, this, MainWindow::onWorkFinished); connect(m_worker, Worker::progressChanged, this, MainWindow::onProgress); // 线程结束时清理worker connect(m_thread, QThread::finished, m_worker, QObject::deleteLater); m_thread-start(); // 启动事件循环 }这里有几个细节值得展开。第一Worker创建时不指定parent因为带parent的对象不能moveToThread——Qt会直接报错。第二connect(m_thread, QThread::started, ...)确保线程事件循环启动后才执行任务避免对象还没搬完就被调用。第三QThread::finished连到deleteLater让worker在线程结束后自动销毁避免内存泄漏。3.3 停止线程别用terminate用标志位加quit停止线程是新手最容易翻车的地方。QThread::terminate()是核武器它会强制杀死线程可能导致锁未释放、资源未清理、数据损坏。正确做法是设置停止标志让任务自然退出然后调用quit()结束事件循环最后wait()等待线程真正结束。void MainWindow::stopWork() { if (!m_thread || !m_thread-isRunning()) return; // 通过信号槽让worker设置停止标志跨线程安全 QMetaObject::invokeMethod(m_worker, stopWork, Qt::QueuedConnection); m_thread-quit(); // 请求事件循环退出 m_thread-wait(3000); // 最多等3秒 if (m_thread-isRunning()) { qWarning() Thread did not stop in time!; } }QMetaObject::invokeMethod配合Qt::QueuedConnection可以跨线程调用槽函数效果等同于发信号。quit()只是请求退出事件循环不会立即返回所以后面要跟wait()。wait()的参数是超时毫秒数超时后返回false你可以据此判断是否需要进一步处理。注意如果doHeavyWork里没有检查停止标志quit()也无法中断正在执行的槽函数。事件循环只在槽函数返回后才处理退出请求。所以可中断设计是必须的。4. 信号槽跨线程连接的底层机制与常见误判4.1 队列连接是怎么自动生效的当你用connect连接两个对象时Qt会根据发送者和接收者的线程亲和性自动决定连接类型。如果两者在同一线程默认是Qt::DirectConnection槽函数同步执行如果分属不同线程默认是Qt::QueuedConnection槽函数被封装成事件投递到接收者线程的事件队列。这个自动判断发生在connect调用时依据的是当时的线程归属。所以moveToThread必须在connect之前调用否则连接类型会判断错误。我见过有人先connect再moveToThread结果槽函数还是在旧线程执行排查半天才发现顺序反了。队列连接的参数传递也有讲究。信号参数会被拷贝到事件对象里所以自定义类型必须用qRegisterMetaType注册否则运行时会报“Cannot queue arguments of type”错误。基本类型和Qt内置类型已经注册过不用操心。4.2 一个反直觉的现象槽函数里的this指向有个常见误解以为槽函数在哪个线程执行this就属于哪个线程。实际上this始终是接收者对象本身它的线程亲和性由moveToThread决定跟槽函数执行线程一致。所以你在槽函数里调用this-thread()得到的就是工作线程。但如果你在槽函数里访问了主线程的UI对象那就危险了。Qt的UI类只能在主线程操作跨线程访问会导致崩溃或未定义行为。正确做法是通过信号把数据传回主线程由主线程的槽函数更新UI。这也是为什么工作类里不应该持有UI指针。4.3 直接连接跨线程会发生什么如果你手动指定Qt::DirectConnection连接两个不同线程的对象槽函数会在发射信号的线程同步执行相当于直接跨线程调用。这破坏了线程安全因为接收者对象可能正在被它所属线程使用两个线程同时访问同一对象数据竞争就来了。除非你非常清楚自己在做什么并且自己加了锁保护否则永远不要跨线程用直接连接。默认的自动判断已经足够智能没必要手动干预。我个人的原则是跨线程连接一律用默认的AutoConnection让Qt自己决定需要同步的地方用信号槽排队需要共享数据的地方用互斥锁保护。5. 实战中容易踩的五个坑与排查思路5.1 坑一moveToThread失败却不报错moveToThread有个前提对象不能有parent。如果对象有parent调用会失败但Qt不会抛异常只是静默返回。你可能会发现槽函数还在旧线程执行却找不到原因。排查方法调用moveToThread后检查worker-thread() m_thread如果不相等说明搬移失败。另外moveToThread只能在对象所属线程调用从其他线程调用也会失败。确保在主线程创建对象、在主线程调用moveToThread。5.2 坑二线程启动了但槽函数不执行这种情况通常是事件循环没跑起来。如果你继承QThread重写了run()却没调exec()事件循环就不存在队列连接的槽函数永远不会被处理。用moveToThread模式时QThread用默认的run()事件循环自动启动一般不会有这个问题。另一个可能是连接类型判断错了。检查connect是否在moveToThread之后调用以及发送者和接收者的线程归属是否符合预期。可以在connect后打印QObject::thread()确认。5.3 坑三程序退出时崩溃或卡死退出时崩溃多半是对象销毁顺序问题。如果Worker还在工作线程跑主线程就销毁了QThread会导致悬空指针。正确顺序是先停止任务再quit()加wait()等线程结束最后让deleteLater清理worker。如果wait()一直不返回说明槽函数没有响应停止标志。检查耗时循环里是否有m_stopped判断以及stopWork槽是否真的被调用了。可以在stopWork里加日志确认。5.4 坑四自定义信号参数导致运行时错误跨线程信号如果携带自定义类型必须注册元类型。比如你定义了一个struct Result信号是void resultReady(Result r)那在connect之前要调用qRegisterMetaTypeResult(Result)。否则运行时会输出“Cannot queue arguments of type Result”槽函数不会被调用。注册一次即可通常放在main函数或类的静态初始化里。Qt 6对自动注册做了改进但显式注册仍然是最稳妥的做法。5.5 坑五多个任务共用一个Worker导致状态混乱如果一个Worker对象被多个任务复用成员变量会互相干扰。比如m_stopped标志被上一个任务设置后没重置下一个任务一启动就退出了。我的做法是每个任务创建独立的Worker实例任务结束后销毁避免状态残留。如果确实需要复用确保每个任务入口槽函数开头重置所有状态变量。更好的设计是把状态封装在任务参数里通过信号传递而不是存在成员变量中。6. 进阶线程池、任务队列与优雅退出的工程化方案6.1 用QThreadPool管理短任务如果任务很多但每个都很短频繁创建销毁线程开销太大。QThreadPool提供了线程复用机制配合QRunnable使用。不过QRunnable不是QObject不能用信号槽需要额外包装。一个常见做法是写一个QObject适配器在run()里调用工作对象的槽函数。class Task : public QObject, public QRunnable { Q_OBJECT public: void run() override { // 耗时操作 emit finished(); } signals: void finished(); };注意QRunnable默认不自动删除要调setAutoDelete(true)。信号槽跨线程仍然走队列连接前提是接收者在有事件循环的线程。6.2 任务队列模式一个线程串行处理多个任务有时候多个任务必须串行执行比如操作同一个硬件设备。这时可以用一个工作线程加一个任务队列。主线程把任务封装成对象投递到队列工作线程从队列取任务执行。Qt的信号槽本身就是队列机制你可以用信号传递任务对象工作线程的槽函数依次处理。关键点是任务对象要能被队列连接传递所以必须是注册过元类型的可拷贝类型或者用QSharedPointer包装。工作线程处理完一个任务后发信号通知主线程主线程再投递下一个。6.3 优雅退出的完整流程工程化的退出流程应该包含停止接收新任务、等待当前任务完成、退出事件循环、等待线程结束、清理资源。我通常封装一个ThreadController类提供start()、stop()、isRunning()接口内部管理QThread和Worker的生命周期。class ThreadController : public QObject { Q_OBJECT public: void start() { m_thread new QThread; m_worker new Worker; m_worker-moveToThread(m_thread); connect(m_thread, QThread::started, m_worker, Worker::doHeavyWork); connect(m_worker, Worker::workFinished, m_thread, QThread::quit); connect(m_thread, QThread::finished, m_worker, QObject::deleteLater); connect(m_thread, QThread::finished, m_thread, QObject::deleteLater); m_thread-start(); } void stop() { if (m_thread m_thread-isRunning()) { QMetaObject::invokeMethod(m_worker, stopWork, Qt::QueuedConnection); m_thread-quit(); m_thread-wait(5000); } } private: QThread *m_thread nullptr; Worker *m_worker nullptr; };这个封装把线程管理细节藏起来调用方只需要start和stop。注意m_thread和m_worker都用deleteLater清理避免在信号处理过程中直接delete导致崩溃。7. 我在实际项目里总结的几条经验第一条永远不要在Worker构造函数里做耗时操作。构造函数在创建对象的线程执行也就是主线程。耗时逻辑必须放在槽函数里等对象搬到工作线程后再执行。我见过有人在构造函数里初始化大量数据结果主线程卡了好几秒。第二条信号参数尽量用值传递避免传指针。跨线程传指针接收者拿到指针时发送者可能已经销毁了对象导致悬空指针。如果必须传大对象用QSharedPointer或std::shared_ptr包装让引用计数保证生命周期。第三条给每个耗时循环加停止检查。不管任务多短都留一个m_stopped判断。用户关闭窗口时程序要能快速响应不能等任务跑完。检查频率不用太高每轮循环一次就够开销可以忽略。第四条调试多线程问题时打印线程ID。QThread::currentThread()返回指针可以转成字符串打印。在构造函数、槽函数入口、析构函数里都打上一眼就能看出对象在哪个线程创建、哪个线程执行、哪个线程销毁。这个习惯帮我省了无数排查时间。第五条不要迷信“线程越多越快”。线程切换有开销共享资源有竞争。IO密集型任务可以适当多开线程CPU密集型任务线程数不要超过核心数。大多数场景下一个工作线程加一个任务队列就够用了简单可靠。最后分享一个小技巧如果你不确定某个操作是否线程安全就假设它不安全用信号槽排队处理。Qt的信号槽机制虽然有一点性能开销但换来的是代码清晰、不易出错。在绝大多数应用场景下这点开销完全可以接受。真正需要极致性能的地方再考虑手动加锁和无锁数据结构但那是另一个话题了。

相关新闻

操作系统存储管理考点全解析:地址变换、页面置换与虚拟内存

操作系统存储管理考点全解析:地址变换、页面置换与虚拟内存

1. 存储管理这一章为什么总在考试里反复出现如果你正在啃操作系统这门课,翻到存储管理这一章的时候大概率会有一种感觉:概念不算难,但一到做题就懵。分页、分段、段页式、虚拟内存、页面置换算法、缺页中断、地址变换……单拎出来每个词都认识…

2026/10/14 0:58:02 阅读更多 →
纯Java实现离线二维码识别与生成(PC端无依赖方案)

纯Java实现离线二维码识别与生成(PC端无依赖方案)

简介:这是一款基于Java开发的PC端二维码识别与生成桌面软件,面向Java初学者及图形界面开发学习者,解决日常工作中二维码图像读取、数据解析与自定义生成的实际需求。资源包共24个文件,含8个核心Java源码文件(涵盖GUI交…

2026/10/14 0:58:02 阅读更多 →
Quartz 定时任务到点不执行?misfire 策略与并发锁复盘

Quartz 定时任务到点不执行?misfire 策略与并发锁复盘

Quartz 定时任务到点不执行?misfire 策略与并发锁复盘 导读:早上九点运营等着结算任务跑完,结果任务没触发,日志里一片安静。查了半天,问题不在代码,在 Quartz 的 misfire 策略和线程池配置。定时任务"…

2026/10/14 0:57:02 阅读更多 →

最新新闻

组合导航中的杆臂效应与外参标定:从原理到工程实践

组合导航中的杆臂效应与外参标定:从原理到工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:43:13 阅读更多 →
单相逆变器下垂控制原理与工程实践

单相逆变器下垂控制原理与工程实践

1. 项目概述:下垂控制在单相逆变器并机中的核心价值在分布式发电系统中,多台逆变器并联运行是提升系统容量和可靠性的关键技术。传统控制方法在并联运行时往往面临环流问题、功率分配不均等挑战。下垂控制通过模拟同步发电机的调频特性,实现了…

2026/10/14 1:43:13 阅读更多 →
医美CRM客户生命周期管理:从咨询到复购的数据主干网

医美CRM客户生命周期管理:从咨询到复购的数据主干网

简介:宏脉医美系统使用手册是一份面向医疗美容机构运营管理人员、前台接待、咨询顾问及信息化实施人员的标准化操作指南,聚焦客户数据规范化采集、新老客户精准识别与全流程业务协同管理,切实解决医美行业客户信息分散、填写不全、跨部门衔接…

2026/10/14 1:43:13 阅读更多 →
串口通信稳定性实践:分帧、超时与断线重连机制详解

串口通信稳定性实践:分帧、超时与断线重连机制详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:43:13 阅读更多 →
支腿反力与静态稳定性:吊装安全计算模块全解析

支腿反力与静态稳定性:吊装安全计算模块全解析

干吊装这行越久,越明白一个道理:绝大多数事故都不是“胆大胡来”造成的,而是“以为没问题”造成的。尤其是支腿反力和整车静态稳定性这两件事,现场最容易漏算,也最容易在临界工况上出问题。支腿反力大一点、重心偏一点…

2026/10/14 1:43:13 阅读更多 →
北航机试工程思维:输入解析、缓存优化与格式鲁棒性

北航机试工程思维:输入解析、缓存优化与格式鲁棒性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:42:11 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →