0%

RKYOLO诞生记 (五):从静态图片到实时视频——全功能应用的构建之旅

在前几轮的扎实工作后,RKYOLO 的核心推理引擎已经变得相当可靠:安全、自适应且性能良好。但它当时更像一个“库”,离一个开箱即用的“工具”还有一步之遥。我的下一个目标,就是为这个强大的引擎,打造一个完整的外壳,让它能灵活地处理现实世界中的各种视觉输入。

功能一:构建统一的输入源处理

我希望这个应用能足够通用。用户可能需要处理一张图片、一个包含多张图片的文件夹、一个视频文件,或者直接连接一个像 /dev/video0 这样的摄像头设备。为每种情况都写一个单独的程序显然不够优雅。

解决方案的核心在于架构设计:如何用统一的接口来处理这些不同的输入类型?在 Rust 中,枚举(enum) 完美地表达了“多种可能类型中的一种”这个概念。于是,InputSource 这个核心设计便应运而生。

1
2
3
4
5
6
pub enum InputSource {
SingleImage(PathBuf),
ImageDirectory(PathBuf),
VideoFile(PathBuf),
CameraDevice(i32), // 存储摄像头ID
}

有了这个枚举,main.rs 的逻辑变得清晰而简洁:

  1. 统一入口:只保留一个 --source 参数来接收用户输入的字符串。
  2. 智能解析:在应用启动时,一段解析逻辑会检查这个字符串:
    • /dev/video 开头?解析出设备 ID,包装成 InputSource::CameraDevice(id)
    • .mp4 等视频扩展名结尾?包装成 InputSource::VideoFile(path)
    • 路径指向一个目录?包装成 InputSource::ImageDirectory(path)
    • 否则,视为 InputSource::SingleImage(path)
  3. 清晰分发:解析完成后,一个简洁的 match 语句就能处理所有情况,每个分支调用相应的处理函数,如 process_video_sourceprocess_directory。这个设计的优雅之处在于它的可扩展性——未来若要支持网络流(RTSP),只需在枚举中添加一个新变体并在 match 中添加一个分支即可,编译器会确保所有情况都得到处理。

功能二:集成 OpenCV 处理视频流

处理视频和摄像头自然离不开 OpenCV。在 Rust 中集成这个庞大的 C++库,尤其是在交叉编译到 ARM64 开发板时,并非总是那么顺利。opencv-rust 这个 crate 有时会因找不到头文件或链接库而编译失败。

初始编译确实遇到了问题。基于以往的经验,我添加了 features = ["clang-runtime"] 这个特性,它使得 opencv-rust 的构建脚本在运行时使用 libclang 来解析头文件,而不是依赖可能不完整的系统预设路径。这个调整顺利解决了编译问题。

数据流转的过程比预想的要顺畅。关键在于颜色空间转换。摄像头通常输出 BGR 格式的 cv::Mat,而我们的 NPU 模型需要 RGB 格式。因此,处理循环的第一步总是调用 imgproc::cvt_color(...) 进行转换。转换后,从 RGB 格式的 Mat 中获取原始字节流就非常直接了,mat.data_bytes()? 返回一个 &[u8] 切片,这正是我们 _from_buffer 系列预处理函数所需的完美输入。

功能三:打造流畅的用户体验

对于一个实时视频应用,用户体验至关重要。一个光秃秃的视频窗口是远远不够的。

  • FPS 计数器:需要一个直观的性能指标。实现起来简单有效:在循环外记录一个起始时间,在循环内累加帧数。每当经过一秒,就用“帧数 / 经过的秒数”来计算平均 FPS,更新显示字符串,并重置计数器和计时器。这样得到的读数稳定,不会像瞬时 FPS 那样剧烈跳动,看起来非常专业。

  • **无头模式 (--headless)**:这个功能完全是出于通用性的考虑。rkyolo 不应该只是一个交互式工具。如果需要在服务器上运行批处理脚本或作为后台服务,弹出 GUI 窗口将是灾难性的。--headless 参数使得应用能够一键切换身份,融入任何自动化流程。

  • 优雅退出:通过 waitKey(1) 检测 ESC 键,允许用户随时优雅地终止程序。这是一个合格实时应用的基本素养。

功能四:探索硬件加速编码

这是整个视频功能开发中一次令人兴奋的探索!

在实现了视频录制功能(--output-video)后,我通过 htop 观察到 CPU 占用率较高,原因是 OpenCV 在使用 libx264 进行软件编码。我知道 RK3588 拥有强大的 RKMPP 硬件视频编码器,便思考如何让 OpenCV 利用它。

最初的想法比较复杂,考虑修改 opencv crate 的源码或自行封装 FFmpeg 接口。随后,我记起了一个经典的 Linux/Unix 哲学:“一切皆可配置”。是否存在一种方式,能够在不修改代码的情况下,“影响”底层库的行为?

果然,我找到了!OpenCV 的 VideoWriter 底层调用 FFmpeg,而 FFmpeg 的行为可以通过环境变量来注入参数。这个发现非常巧妙。

接下来的实现充满了创造性。我没有在程序启动前全局设置环境变量,那样过于粗暴。相反,我使用了 std::env::set_var,在调用 VideoWriter::new() 之前,临时将环境变量 OPENCV_FFMPEG_WRITER_OPTIONS 设置为 -codec:v h264_rkmpp。这个操作被封装在一个 Guard 对象中,确保在该对象离开作用域时,环境变量会自动恢复原样。这是一种极其精准、非侵入式的方法,成功地“引导”了 OpenCV 去尝试调用 RKMPP 硬件编码器。

(实事求是的说明)

需要坦诚的是,在后续的深入测试中,由于 OpenCV、FFmpeg 版本与底层驱动之间复杂的交互,当前设置的环境变量并未能 100%成功激活RKMPP 硬件编码。但这并不能否定该方法所展现的思路的巧妙性。它证明了我们有能力在不修改第三方库源码的情况下,对其行为进行精准干预。我相信,随着对底层细节的进一步探索,完全激活硬件编码是可行的。

当我成功录制视频并构想出未来 CPU 占用率大幅降低的场景时,我知道,我们又在扩展功能边界上前进了一步。从一个处理静态图片的库,到一个能够应对多种输入、提供良好体验的全功能应用,整个构建过程充满了探索和实现的乐趣。