大家好,我是个大三的学生。最近因为导师的一个课题,需要把一个我自己训练的、用于识别玉米穗的 YOLO11n 模型,部署到一块 Rockchip RK3588 开发板上。
这其实还有个“前传”:为了让板子支持最新的模型,我已经折腾了好一阵子,把整个 Linux 内核连带 RKNPU 驱动都给交叉编译升级了一遍。所以,当底层环境就绪,模型也转换成.rknn
格式后,我当时觉得,接下来应该就是顺理成章的“应用开发”了。
很自然地,我找到了野火社区的“鲁班猫”AI 教程,他们提供了一个yolo11
的 C++示例。我寻思着,既然有现成的,改改用就行了呗?
事实证明,我还是太年轻了。
当我把官方提供的yolo11.rknn
模型(COCO 数据集,80 个类别)跑起来时,一切正常。但当我换上我自己的、只有一个类别的玉米穗模型时,程序就陷入了沉默的崩溃,连个像样的错误日志都没有。
这次失败,让我下定决心:放弃修改,从零重构。
这个决定看似冲动,但其实背后有一个在我脑子里盘旋了好几个月的执念:一个 AI 模型推理能力,如果不能被轻松地集成到其他应用里,那它的价值就大打折扣。 无论是导师的农业项目,还是我一直想玩的、带目标检测的 ROS 机器人,它们需要的都不是一个只能在终端里敲命令运行的独立程序,而是一个稳定、可靠、可以被轻松调用的“能力模块”。
这篇博客,就是想聊聊我看到的那个 C++示例是如何打破了我这个“集成梦”,以及为什么最终选择了 Rust 作为我的“破局之刃”。
解剖 C++“动物园”:一些让我无法妥协的设计
首先要说明一点,野火的这个示例项目(LubanCat AI Manual Code),它的项目结构很大程度上继承自 Rockchip 的官方模型仓库(rknn_model_zoo)。所以,我接下来要吐槽的,并不仅仅是某一个示例项目的问题,而是一种在嵌入式 C++开发中相当普遍的、让我难以忍受的设计惯性。
1. 硬编码的“想当然”
当我开始寻找程序崩溃的原因时,postprocess.h
里的这几行代码让我瞬间明白了问题所在:
1 | // yolo11/cpp/postprocess.h |
OBJ_CLASS_NUM
,这个代表物体类别总数的宏,被“想当然”地写死成了80
。整个后处理代码,包括各种循环和内存计算,都依赖这个写死的值。我的玉米穗模型只有一个类别,当后处理逻辑试图去访问它根本不存在的第 2 到第 80 个类别的预测数据时,不出错才怪了。
这还只是冰山一-角。代码里还假设模型永远有 3 个输出分支,用app_ctx->io_num.n_output / 3
来计算每个分支的张量数。这种写法,让代码变得极其脆弱。这已经不是一个“框架”了,它更像一个“一次性脚本”,只能和特定模型绑定。每次模型结构发生丁点变化,都得回来改代码,然后重新编译,太麻烦了。
2. 紧耦合的“大杂烩”——我的“集成梦”是如何被打破的
这,才是我最无法容忍的一点。看看它的构建系统,CMakeLists.txt
里赫然写着:
1 | // yolo11/cpp/CMakeLists.txt |
这意味着,为了编译这一个yolo11
的小小示例,我需要把整个包含3rdparty
和utils
的上层代码库——也就是那个庞大的“动物园”(Zoo)——完整地下载下来。
集成个屁!
我魂牵梦绕了几个月,想的是怎么把 YOLO 能力作为一个干净的库,轻松地链接到其他项目里。结果这个 Demo 告诉我,想用我?行啊,先把我的整个“动物园”亲戚都请过来。这还怎么集成?难道我的 ROS 项目也要被迫依赖这一大堆它根本用不上的utils
和3rdparty
吗?
这种设计,彻底扼杀了我将其作为“能力模块”进行二次开发的可能性。
3. 手动管理的“安全隐患”
最后,是老生常谈的 C++资源管理问题。代码里充斥着手动malloc
/free
,以及经典的goto cleanup;
模式。
这种写法本身没错,但在复杂的逻辑中,它完全依赖开发者的记忆力和纪律性。少写一个free
,或者在某个错误处理分支忘了goto
,就意味着一次隐蔽的内存泄漏。在需要 7x24 小时稳定运行的嵌入式设备上,这种隐患是致命的。
我的破局之道:选择 Rust 的三重考量
面对以上种种,我意识到“缝缝补补”是没有出路的。我需要的是一个安全、独立、现代化的解决方案。于是,我决定用 Rust 来重写这一切。
1. 从“记住释放”到“自动管理”——RAII 带来的心智解放
我最看重 Rust 的一点,就是它的 RAII(资源获取即初始化)机制。我做的第一件事,就是创建一个专门的rknn-ffi
Crate,然后把需要手动管理的rknn_context
句柄用一个 Rust 结构体包起来:
1 | // rkyolo/rknn-ffi/src/context.rs |
就这么简单。Drop
trait 就像一个和编译器签下的“契约”,保证了只要RknnContext
对象消亡,rknn_destroy
就一定会被调用。我再也不用担心忘记释放资源了,因为编译器会帮我记住。这种心智上的解放,让我可以更专注于业务逻辑本身。
2. 从“依赖泥潭”到“清晰边界”——Cargo 带来的工程尊严
告别了复杂的CMakeLists.txt
,Rust 的包管理器 Cargo 让项目工程变得前所未有的清爽。
我的rkyolo
项目通过 Cargo 的workspace
功能,从一开始就被划分为职责分明的几个部分:rknn-ffi
(负责与 C 库交互)、rkyolo-core
(核心算法)、rkyolo-app
(应用程序)。每个部分都是一个独立的 Crate,依赖关系在各自的Cargo.toml
里一目了然。
这种设计带来了极大的灵活性,完美地回应了我最初的“集成梦”。未来任何项目想复用我的核心推理能力,只需要依赖rkyolo-core
这个 Crate 就行,完全不会被其他不相关的东西所拖累。
3. 从“处处unsafe
”到“安全封装”——FFI 带来的健壮抽象
Rust 并没有回避与 C 交互时unsafe
的必要性,但它提供了一套机制,让我们能把这些“危险”的操作关进一个可控的“笼子”里。
我的rknn-ffi
Crate 就是这样一个“笼子”。所有与 C 库的裸指针交互、内存操作,都被严格限制在其中,并被封装成安全的、返回Result
类型的 Rust 函数。这样一来,上层的rkyolo-core
和rkyolo-app
就可以在 100% safe 的 Rust 环境中开发,享受编译器提供的全部安全检查。
结论:一项值得的战略投资
是的,用 Rust 重写这一切花了我两天时间。但这两天并不是“重复造轮子”,而是在打地基。
我投入时间,换来的是一个在编译时就杜绝了内存泄漏、依赖清晰、易于集成、核心逻辑与应用完全解耦的坚固地基。我相信,这项前期的“战略投资”,为我后续快速、高质量地完成导师的任务,甚至探索更有趣的机器人项目,铺平了道路。
当然,地基打好只是开始。很快,我就在这块坚实的地基上,遇到了第一个真正的拦路虎——模型能跑了,但结果却“指鹿为-马”。
下一篇,我们就来聊聊我是如何侦破这个棘手的数值迷案的。