拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

香橙派RK3588上yolov5s取流循环分段计时与X11画面回传实战

香橙派RK3588上yolov5s取流循环分段计时与X11画面回传实战

1. 从"能跑"到"能看":为什么取流循环必须加计时和画面回传

很多人把 yolov5s 在香橙派 RK3588 上跑通之后,就停在"终端里能看到检测框坐标"这一步。说实话,这个阶段只能算"模型能推理",离"这套系统能用来做判断"还差得远。原因很简单:你根本不知道每一帧到底花了多少时间,也不知道画面到底长什么样。终端里刷出来的person 0.87这种日志,既不能告诉你画面有没有偏色、有没有撕裂,也不能告诉你取流这一环是不是拖了后腿。

我在实际调试 RK3588 上的视觉管线时,最深的体会是:性能问题从来不是靠猜的,是靠计时器量出来的。取流循环里如果不埋时间戳,你永远分不清瓶颈在 RTSP 解码、在 NPU 推理、还是在后处理画框。而画面回传(把 RK3588 上处理后的画面弹回电脑显示)解决的是另一个问题——可视化验证。没有画面,你连"模型到底有没有正确读到这一帧"都无法确认。

这一篇要干的事情很具体:给取流循环加上分段计时,把每一帧的耗时拆开看清楚;然后用 X11 转发把处理后的画面从香橙派弹回本地电脑显示。关键词里的香橙派、RK3588、yolov5s、X11、SSH就是这条链路的五个支点。适合已经能在板子上跑通 yolov5s 推理、但还没建立起"可观测性"的读者。如果你还在纠结模型怎么转 RKNN,那是前几篇的事,这篇默认你已经有一个能出结果的推理脚本了。

提示:计时和画面回传是两件独立的事,但建议先做计时、再做回传。因为画面回传本身会引入额外开销,如果先做回传,你测出来的耗时里混进了 X11 传输的时间,数据就不干净了。

2. 取流循环的分段计时:把一帧拆成四段来量

2.1 为什么是四段而不是一个总时间

一个典型的取流推理循环,粗看就是"读一帧、推理一下、画个框、显示出来"。但真正定位问题时,一个总耗时数字毫无意义——它只能告诉你"慢",不能告诉你"哪里慢"。我习惯把它拆成四段:

  • 取流段:从视频源(RTSP 流、USB 摄像头、本地视频文件)拿到一帧并解码成可用图像的时间
  • 预处理段:resize、归一化、颜色空间转换(BGR 转 RGB)、转成 RKNN 需要的输入格式
  • 推理段:rknn.inference()这一次调用本身的耗时,这是 NPU 真正干活的时间
  • 后处理段:解析输出张量、做 NMS、把框画到图上

这四段加起来才是单帧总耗时。分开量之后,你会发现很多"玄学卡顿"其实有非常明确的归属。比如我遇到过取流段占了 80% 的时间,推理段反而很快——那问题根本不在模型,在网络或者解码器。

2.2 用 perf_counter 而不是 time.time

计时函数的选择有讲究。time.time()返回的是墙上时钟,受系统时间调整影响,精度也一般。Python 里做性能计时应该用time.perf_counter(),它是单调递增的高精度计数器,不受系统时间跳变影响。

import time # 在循环开始前初始化累加器 t_capture = 0.0 t_preprocess = 0.0 t_inference = 0.0 t_postprocess = 0.0 frame_count = 0 while True: t0 = time.perf_counter() frame = capture_frame() # 取流 + 解码 t1 = time.perf_counter() input_data = preprocess(frame) # 预处理 t2 = time.perf_counter() outputs = rknn.inference(inputs=[input_data]) # NPU 推理 t3 = time.perf_counter() result = postprocess(outputs, frame) # NMS + 画框 t4 = time.perf_counter() t_capture += (t1 - t0) t_preprocess += (t2 - t1) t_inference += (t3 - t2) t_postprocess += (t4 - t3) frame_count += 1

这里有个细节:不要在循环里每帧都 print。print 本身是 IO 操作,会显著拖慢循环,尤其是通过 SSH 连接时,终端输出要走网络,开销更大。正确做法是累加,然后每隔 N 帧(比如 30 帧)输出一次平均值。

2.3 分段计时的输出格式与判读

每 30 帧输出一次,格式建议做成这样,一眼就能看出各段占比:

if frame_count % 30 == 0: avg_cap = t_capture / frame_count * 1000 avg_pre = t_preprocess / frame_count * 1000 avg_inf = t_inference / frame_count * 1000 avg_post = t_postprocess / frame_count * 1000 total = avg_cap + avg_pre + avg_inf + avg_post print(f"[{frame_count}帧] 取流 {avg_cap:.1f}ms | " f"预处理 {avg_pre:.1f}ms | 推理 {avg_inf:.1f}ms | " f"后处理 {avg_post:.1f}ms | 合计 {total:.1f}ms | " f"FPS {1000/total:.1f}")

判读的时候有个经验阈值可以参考。在 RK3588 上跑 yolov5s(640x640 输入),各段的合理范围大致是:

阶段合理耗时偏慢的信号常见原因
取流5-30ms超过 50ms网络抖动、解码器没走硬件、分辨率过高
预处理3-15ms超过 30ms用了 PIL 而不是 OpenCV、反复内存拷贝
推理20-60ms超过 100ms模型没量化、NPU 频率没拉满、输入尺寸过大
后处理5-20ms超过 40msNMS 用了纯 Python 实现、候选框太多

这张表不是标准答案,是帮你建立"直觉"的参考。真正有价值的是你自己板子上的基线——先跑一次记录下正常值,之后任何异常都能对比出来。

2.4 一个容易忽略的坑:预热帧

第一次推理往往特别慢,因为 NPU 驱动要初始化、模型权重要从内存加载、各种缓存要建立。如果你从第一帧就开始计时,平均值会被严重拉高。我的做法是前 10 帧只跑不计时,当作预热,从第 11 帧开始正式统计。

WARMUP_FRAMES = 10 if frame_count > WARMUP_FRAMES: # 正常累加计时 ...

这个细节看起来小,但如果你不做,测出来的 FPS 可能比真实值低 20% 以上,然后你会花大量时间去优化一个根本不存在的问题。我踩过这个坑,当时以为推理要 120ms,折腾半天发现预热之后稳定在 45ms。

3. X11 转发:让香橙派的画面出现在你电脑屏幕上

3.1 X11 转发到底在转发什么

先说清楚原理,不然配置出问题你都不知道从哪查。X11 是一套客户端-服务器架构的图形系统。这里的"服务器"指的是运行在香橙派上的 X Server,它负责实际的绘制;而"客户端"是你电脑上运行的程序,它发出绘制指令。X11 转发做的事情,是通过 SSH 隧道把香橙派上 X Server 的画面数据传回你本地电脑的 X Server 显示。

听起来绕,用一句话概括:程序在香橙派上跑,窗口显示在你电脑上。这跟 VNC 那种"传整个桌面"的方案不一样,X11 转发是"按需传单个窗口",开销小得多,特别适合我们这种只需要看一个检测结果窗口的场景。

注意:X11 转发传的是绘制指令和位图,如果画面里有大量实时变化的图像(比如视频流),带宽消耗会明显上升。在局域网内问题不大,跨网络就要掂量一下。

3.2 SSH 服务端与客户端的配置要点

香橙派这边(服务端)需要确保 SSH 允许 X11 转发。检查/etc/ssh/sshd_config:

X11Forwarding yes X11DisplayOffset 10 X11UseLocalhost yes

改完记得重启 SSH 服务:sudo systemctl restart sshd。这里X11UseLocalhost yes是让 X11 的转发端口只绑定在本地回环,更安全,配合 SSH 隧道使用没问题。

客户端这边,连接时加-X参数开启转发:

ssh -X user@192.168.x.x

-X是"可信转发",-Y是"受信任转发"(跳过部分安全检查)。日常调试用-X就够了,如果遇到某些程序因为安全策略拒绝显示,再考虑-Y。连上之后,在香橙派终端里执行echo $DISPLAY,正常应该输出类似localhost:10.0的内容。如果输出为空,说明转发没生效,回去检查服务端配置。

3.3 验证 X11 是否真的通了

别急着跑你的检测程序,先用一个小工具验证链路。香橙派上如果装了xeyes或者xclock,直接运行:

xeyes

如果本地电脑上弹出一对跟着鼠标转的眼睛,说明 X11 转发完全打通。如果报Error: Can't open display,那就是DISPLAY环境变量没设置或者转发没开。

我建议把这个验证步骤固定下来,每次换连接方式(比如从 WindTerm 换到命令行 SSH)都先跑一次xeyes。因为不同 SSH 工具的 X11 转发配置方式不一样,有的默认不开,有的需要手动勾选。用xeyes做冒烟测试,比直接上检测程序再排查要快得多。

3.4 OpenCV 窗口在 X11 下的显示问题

这里有个非常经典的坑:OpenCV 的imshow依赖 GUI 后端。如果你在香橙派上装的 OpenCV 是 headless 版本(很多 pip 安装的默认就是),cv2.imshow会直接报错或者什么都不显示。检查方法:

import cv2 print(cv2.getBuildInformation())

在输出里找GUI那一行。如果是GUI: NONE,那就是 headless 版本,需要换成带 GUI 支持的版本。在香橙派上,更省事的做法是用系统包管理器装:

sudo apt install python3-opencv

系统源里的 OpenCV 通常带 GTK 后端,配合 X11 转发能正常显示。如果你非要用 pip 版本,得找带 GUI 的 wheel,或者自己编译,那就麻烦了。

另一个坑是cv2.waitKey(1)的返回值。在 X11 转发环境下,窗口的响应会有一点延迟,waitKey的返回值处理不当会导致窗口卡死。建议至少给 1ms 的等待,不要用waitKey(0),那会阻塞整个循环。

4. 把计时和回传整合进同一个循环

4.1 整合后的循环骨架

现在把前面两块拼起来。核心思路是:计时照常累加,但显示部分做成"可开关"的,方便你对比开显示和不开显示的性能差异。

import cv2 import time SHOW_WINDOW = True # 通过命令行参数或环境变量控制 STAT_INTERVAL = 30 while True: t0 = time.perf_counter() ret, frame = cap.read() if not ret: break t1 = time.perf_counter() input_data = preprocess(frame) t2 = time.perf_counter() outputs = rknn.inference(inputs=[input_data]) t3 = time.perf_counter() frame = postprocess(outputs, frame) t4 = time.perf_counter() if frame_count > WARMUP_FRAMES: t_capture += (t1 - t0) t_preprocess += (t2 - t1) t_inference += (t3 - t2) t_postprocess += (t4 - t3) frame_count += 1 if SHOW_WINDOW: cv2.imshow("RK3588 YOLOv5s", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break if frame_count % STAT_INTERVAL == 0: # 输出统计 ...

4.2 开显示与不开显示的耗时对比

这是整合之后最有价值的一个实验。同一段视频、同一个模型,分别跑一次开显示和不开显示,对比数据。我实测下来的典型结果是这样的:

配置推理段总耗时FPS
不开显示45ms78ms12.8
开 X11 显示45ms95ms10.5

可以看到推理段完全没变,多出来的 17ms 全在显示环节。这 17ms 里包含了图像编码、通过 SSH 隧道传输、本地解码显示的全过程。这个数据告诉你:X11 转发是有成本的,但成本可控。如果你追求极致帧率,可以只在需要观察的时候开显示;如果只是调试阶段,10 FPS 完全够用。

提示:如果显示环节耗时超过 50ms,检查一下传输的画面分辨率。把显示用的帧单独 resize 到 640 宽再 imshow,能显著降低传输量,而检测本身还是在原分辨率上做的。

4.3 用环境变量控制显示开关

硬编码SHOW_WINDOW = True不灵活。更好的做法是用环境变量,这样同一条 SSH 命令可以切换模式:

import os SHOW_WINDOW = os.environ.get("SHOW_WINDOW", "1") == "1"

运行时:

# 开显示 SHOW_WINDOW=1 python3 detect.py # 不开显示,纯测性能 SHOW_WINDOW=0 python3 detect.py

这样你在做性能基准测试的时候,一条命令就能关掉显示,不用改代码。

5. 实测中那些文档不会告诉你的坑

5.1 SSH 断线导致程序中断

这是最烦人的问题之一。你跑着一个长循环,SSH 一断,程序跟着就死了。原因是程序挂在了 SSH 的会话上,会话结束收到 SIGHUP 信号就退出了。解决办法是用nohup或者tmux:

# 方式一:nohup + 重定向 nohup python3 detect.py > detect.log 2>&1 & # 方式二:tmux(推荐) tmux new -s detect python3 detect.py # Ctrl+B 然后 D 脱离会话

用 tmux 的好处是随时能tmux attach -t detect回去看实时输出,而且 X11 转发在 tmux 里也能正常工作(前提是连接时带了-X)。但要注意,如果你脱离 tmux 之后重新 attach,DISPLAY变量可能已经失效,需要重新设置。

5.2 X11 转发下的画面撕裂与延迟

X11 转发传视频画面,偶尔会出现撕裂或者明显延迟。这通常不是代码问题,而是传输链路的问题。几个缓解方向:

  • 降低显示帧率:不必每帧都 imshow,可以每 2 帧显示一次
  • 缩小显示尺寸:检测在原图做,显示用缩略图
  • 用cv2.resizeWindow固定窗口大小,避免窗口自适应带来的重绘开销
if SHOW_WINDOW and frame_count % 2 == 0: display = cv2.resize(frame, (640, 480)) cv2.imshow("RK3588 YOLOv5s", display) cv2.waitKey(1)

5.3 计时数据被日志输出污染

前面提过 print 的开销,这里再强调一次。如果你在循环里每帧都 print 检测结果,那计时数据基本不可信。我建议把检测结果的输出也做成"每 N 帧一次",或者干脆写到文件里,不要走终端。

# 不好的做法:每帧都 print # print(f"检测到 {len(boxes)} 个目标") # 好的做法:累积后定期输出 if frame_count % STAT_INTERVAL == 0: print(f"最近 {STAT_INTERVAL} 帧平均检测到 {avg_boxes:.1f} 个目标")

5.4 不同 SSH 工具的 X11 配置差异

命令行ssh -X是最标准的。但很多人用图形化 SSH 工具(比如 WindTerm、MobaXterm 之类),这些工具的 X11 转发配置位置各不相同,有的默认开启,有的藏在设置深处。我的建议是:调试 X11 相关问题时,先用命令行 ssh -X 确认链路本身没问题,再去折腾图形工具的配置。这样能把"链路问题"和"工具配置问题"分开排查,效率高很多。

5.5 板子上的 DISPLAY 变量在 sudo 下丢失

如果你需要用sudo运行程序(比如访问某些硬件设备),会发现DISPLAY变量没了,X11 转发失效。原因是 sudo 默认不继承环境变量。解决办法:

sudo -E python3 detect.py

-E保留环境变量。或者显式传递:

sudo DISPLAY=$DISPLAY python3 detect.py

这个坑很隐蔽,因为程序能跑,只是窗口不显示,你会以为是 X11 配置问题,其实是 sudo 把变量吃了。

6. 从计时数据反推优化方向

6.1 取流段慢:先查解码方式

如果计时显示取流段占了总时间的一半以上,优先怀疑解码没走硬件。RK3588 有专门的视频解码单元(VPU),用ffmpeg或者 GStreamer 的硬件解码管线能大幅降低 CPU 占用和延迟。用 OpenCV 的VideoCapture读 RTSP 流时,默认可能走的是软件解码。可以尝试指定后端:

cap = cv2.VideoCapture(rtsp_url, cv2.CAP_GSTREAMER)

配合 GStreamer 的mppvideodec(RK3588 的硬件解码插件)效果更好。具体管线字符串要根据你的流格式调整,这块内容比较多,值得单独一篇来讲。

6.2 推理段慢:检查 NPU 频率和模型量化

推理段如果超过 80ms,先确认模型是不是 INT8 量化的。FP16 甚至 FP32 的模型在 NPU 上跑会慢很多。然后检查 NPU 的工作频率:

cat /sys/class/devfreq/fdab0000.npu/cur_freq

如果频率偏低,可能是温控降频或者 governor 设置保守。可以手动拉高:

echo performance | sudo tee /sys/class/devfreq/fdab0000.npu/governor

注意:拉满频率会增加功耗和发热,长时间跑要注意散热。香橙派加个散热片或者小风扇是基本操作。

6.3 后处理段慢:NMS 是重灾区

后处理里最耗时的通常是 NMS。如果你用的是纯 Python 循环实现的 NMS,候选框一多就会爆炸。换成 OpenCV 的cv2.dnn.NMSBoxes或者 numpy 向量化实现,速度能提升一个数量级。另外,yolov5s 的输出候选框数量可以通过置信度阈值提前过滤,减少进入 NMS 的框数。

6.4 预处理段慢:避免不必要的拷贝

预处理里常见的浪费是反复的内存拷贝和颜色空间转换。比如先把 BGR 转 RGB,又转回来,或者用 PIL 处理完再转 numpy。统一用 OpenCV + numpy 处理,减少中间对象。resize 的时候用cv2.INTER_LINEAR就够,别用INTER_CUBIC,后者慢不少而且对检测精度提升有限。

7. 一个完整的可复现实验流程

把上面所有东西串起来,给你一个可以直接照着做的流程:

  1. 确认基础环境:香橙派上 SSH 服务正常,X11Forwarding yes已配置,OpenCV 带 GUI 支持
  2. 验证 X11 链路:本地ssh -X连上,跑xeyes确认窗口能弹出来
  3. 跑基线:SHOW_WINDOW=0 python3 detect.py,记录四段耗时和 FPS,这是你的性能基线
  4. 开显示对比:SHOW_WINDOW=1 python3 detect.py,对比显示环节的额外开销
  5. 定位瓶颈:看哪一段占比最高,对照第 6 节的排查方向逐个验证
  6. 优化后复测:每次只改一个变量,改完重新跑基线,确认优化有效

这个流程的关键是一次只改一个东西。我见过太多人一口气改五个地方,结果性能提升了也不知道是哪个改动起的作用,下次遇到问题还是不会排查。

8. 关于这套组合的一些个人体会

X11 转发这个方案,说实话在局域网内用着很舒服,配置简单、不用装额外软件、延迟也能接受。但它的定位是"调试工具",不是"生产方案"。如果你要做的是一个长期运行的检测系统,最终还是要走 Web 推流(比如 MJPEG 或者 WebRTC)或者本地 HDMI 输出,X11 转发只适合开发阶段快速看效果。

计时这块,我现在的习惯是任何新管线第一步就埋计时,哪怕当时不优化。因为性能数据是有时效性的,你今天不测,明天改了代码就再也回不到那个状态了。而且分段计时的代码写一次就能复用,成本极低,收益极高。

最后分享一个小技巧:把计时统计结果同时写一份到 CSV 文件里,跑完之后用 pandas 画个趋势图,能看出耗时随时间的漂移。有时候性能问题是渐进式的(比如内存泄漏、温度上升降频),单看平均值发现不了,看趋势就一目了然。这个习惯帮我抓到过好几次隐蔽的降频问题。

返回列表