rdkx5 插入mipi摄像头,识别不到i2c啊

rdkx5 插入mipi摄像头,识别不到i2c

sunrise@ubuntu:/app$ for i in 0 1 2 3 4 5 6 7; do
echo “=== i2c-$i ===”
i2cdetect -y $i 2>&1 | head -3
done
=== i2c-0 ===
Warning: Can’t use SMBus Quick Write command, will skip some addresses
0 1 2 3 4 5 6 7 8 9 a b c d e f
00:
=== i2c-1 ===
Error: Could not open file /dev/i2c-1' or /dev/i2c/1’: No such file or directory
=== i2c-2 ===
Warning: Can’t use SMBus Quick Write command, will skip some addresses
0 1 2 3 4 5 6 7 8 9 a b c d e f
00:
=== i2c-3 ===
Warning: Can’t use SMBus Quick Write command, will skip some addresses
0 1 2 3 4 5 6 7 8 9 a b c d e f
00:
=== i2c-4 ===
Warning: Can’t use SMBus Quick Write command, will skip some addresses
0 1 2 3 4 5 6 7 8 9 a b c d e f
00:
=== i2c-5 ===
Warning: Can’t use SMBus Quick Write command, will skip some addresses
0 1 2 3 4 5 6 7 8 9 a b c d e f
00:
=== i2c-6 ===
Warning: Can’t use SMBus Quick Write command, will skip some addresses
0 1 2 3 4 5 6 7 8 9 a b c d e f
00:
=== i2c-7 ===
Warning: Can’t use SMBus Quick Write command, will skip some addresses
0 1 2 3 4 5 6 7 8 9 a b c d e f
00:

:bullseye: 核心结论

MIPI 摄像头的 I2C 检测不到是正常现象! RDK X5 的 MIPI CSI 接口不走标准 V4L2/I2C 路径i2cdetect 检测不到设备地址不代表硬件有问题。

根据社区案例(如 官方双目摄像头 i2cdetect 无法探测到),这是 RDK X5 的架构特性:

  • USB 摄像头:走 V4L2 驱动,/dev/video* 可见,i2cdetect 可检测
  • MIPI CSI 摄像头:走地平线专用 ISP 管道,不暴露为标准 I2C 设备

:clipboard: 正确排查步骤

第一步:确认摄像头型号和连接

# 1. 检查 MIPI 接口物理连接
# - 确认排线方向正确(金属面朝内/外参考手册)
# - 确认接口插紧无松动
# - 如使用转接线,确认兼容性(15pin↔22pin)

# 2. 查看系统识别的 MIPI 设备
dmesg | grep -i mipi
dmesg | grep -i camera
dmesg | grep -i isp

第二步:使用官方 MIPI 样例测试

# 进入 MIPI 摄像头样例目录
cd /app/pydev_demo/03_mipi_camera_sample/

# 运行样例(根据你连接的 CSI 接口选择 pipeId)
sudo python3 ./mipi_camera.py

关键参数说明

  • pipeId=0 → 对应 CSI-1 接口
  • pipeId=1 → 对应 CSI-2 接口
  • 需要根据你的摄像头型号修改传感器配置

第三步:检查设备树和驱动加载

# 查看内核日志中摄像头相关报错
dmesg | tail -100 | grep -E "camera|mipi|isp|sensor"

# 检查是否有 ISP 节点创建失败
dmesg | grep "creat_isp_node"

第四步:确认系统版本和摄像头兼容性

# 检查系统版本
cat /etc/issue

# 确认摄像头型号是否在官方支持列表中
# 参考:https://developer.d-robotics.cc/rdk_doc/Quick_start/hardware_introduction/rdk_x5#mipi_port

:warning: 常见原因

问题 现象 解决方案
摄像头型号不兼容 样例运行报错 creat_isp_node failed 使用官方支持型号(如 SC130AI、SC230AI、GS130W 等)
系统版本过旧 驱动加载失败 升级到 3.1.0+ 版本
硬件连接错误 无任何输出 检查排线方向、接口松动
转接线不兼容 间歇性识别失败 使用官方转接线或直接连接
设备树未配置 检测不到传感器 参考手册配置对应传感器节点

:wrench: 下一步行动

请提供以下信息,我可以帮你进一步定位:

  1. 摄像头具体型号(如 SC130AI、OV5647、IMX219 等)
  2. 连接的 MIPI 接口(CSI-1 还是 CSI-2)
  3. 运行官方样例的报错信息
  4. dmesg | grep -i mipi 的输出

:books: 相关资源

建议:先运行官方样例确认硬件是否正常,不要依赖 i2cdetect 判断 MIPI 摄像头状态。

[ 12.783865] vin mipi0: probe with x5sys done
[ 12.843657] vin mipi0: snrclk set en not support
[ 12.843689] vin mipi0: ver 0154 port0(0:0)
[ 12.844017] vin mipi1: snrclk set en not support
[ 12.844044] vin mipi1: ver 0154 port1(0:1)
[ 12.844279] vin mipi2: snrclk set en not support
[ 12.844300] vin mipi2: ver 0154 port2(1:0)
[ 12.844501] vin mipi3: snrclk set en not support
[ 12.844523] vin mipi3: ver 0154 port3(1:1)
[ 12.847905] vin mipi-tx: dev0 dummy setcb
[ 12.847909] vin mipi-tx: dev1 dummy setcb
[ 12.847913] vin mipi: init 4 rx 2 tx done
[ 12.884799] vin mipi: phy debug probed
[ 12.885743] vin mipi0: debug probed
[ 12.895460] vin mipi1: debug probed
[ 12.896225] vin mipi2: debug probed
[ 12.911705] vin mipi3: debug probed
[ 0.158431] (NULL device *): no horizon,gpio-banks in node /soc/disp_apb/disp_iomuxc@3e0a0054
[ 0.158739] horizon-disp-pinctrl 3e0a0054.disp_iomuxc: Initialized D-Robotics pinctrl driver
[ 0.159017] platform 3e000000.disp_apb:dsi-encoder: Fixed dependency cycle(s) with /soc/disp_apb/dsi-syscon-bridge
[ 0.159060] platform 3e000000.disp_apb:dsi-encoder: Fixed dependency cycle(s) with /soc/disp_apb/dc8000Nano@3e000000
[ 0.159169] platform 3e000000.disp_apb:dc-wb-syscon-bridge: Fixed dependency cycle(s) with /soc/disp_apb/vs-sif@3e080000
[ 0.159275] platform 3e000000.disp_apb:bt1120-wb-syscon-bridge: Fixed dependency cycle(s) with /soc/disp_apb/bt1120_bridge_wb
[ 0.159471] platform 3e000000.disp_apb:dc-wb-syscon-bridge: Fixed dependency cycle(s) with /soc/disp_apb/vs-sif@3e080000
[ 0.159540] platform 3e080000.vs-sif: Fixed dependency cycle(s) with /soc/disp_apb/bt1120_bridge_wb
[ 0.159564] platform 3e080000.vs-sif: Fixed dependency cycle(s) with /soc/disp_apb/dc-wb-syscon-bridge
[ 0.159773] platform 3e000000.disp_apb:dsi-encoder: Fixed dependency cycle(s) with /soc/disp_apb/dsi-syscon-bridge
[ 0.159852] platform 3e000000.disp_apb:dsi-syscon-bridge: Fixed dependency cycle(s) with /soc/disp_apb/dsi-encoder
[ 0.160094] platform 3e000000.disp_apb:dsi-encoder: Fixed dependency cycle(s) with /soc/disp_apb/dc8000Nano@3e000000
[ 0.160171] platform 3e000000.dc8000Nano: Fixed dependency cycle(s) with /soc/disp_apb/hdmi-encoder
[ 0.160198] platform 3e000000.dc8000Nano: Fixed dependency cycle(s) with /soc/disp_apb/dsi-encoder
[ 0.160538] platform 3e010000.bt1120: Fixed dependency cycle(s) with /soc/disp_apb/hdmi-encoder
[ 0.160736] platform 3e000000.disp_apb:bt1120_bridge: Fixed dependency cycle(s) with /soc/dsp_apb/i2c@320a0000/hdmi_bridge@3b
[ 0.160770] platform 3e000000.disp_apb:bt1120_bridge: Fixed dependency cycle(s) with /soc/disp_apb/bt1120-syscon-bridge
[ 0.160875] platform 3e080000.vs-sif: Fixed dependency cycle(s) with /soc/disp_apb/bt1120_bridge_wb
[ 0.160941] platform 3e000000.disp_apb:bt1120-wb-syscon-bridge: Fixed dependency cycle(s) with /soc/disp_apb/bt1120_bridge_wb
[ 0.161004] platform 3e000000.disp_apb:bt1120_bridge_wb: Fixed dependency cycle(s) with /soc/disp_apb/bt1120-wb-syscon-bridge
[ 0.161069] platform 3e000000.disp_apb:bt1120_bridge_wb: Fixed dependency cycle(s) with /soc/disp_apb/vs-sif@3e080000
[ 0.161219] platform 3e010000.bt1120: Fixed dependency cycle(s) with /soc/disp_apb/hdmi-encoder
[ 0.161311] platform 3e000000.dc8000Nano: Fixed dependency cycle(s) with /soc/disp_apb/hdmi-encoder
[ 0.161388] platform 3e000000.disp_apb:hdmi-encoder: Fixed dependency cycle(s) with /soc/disp_apb/bt1120-syscon-bridge
[ 0.161420] platform 3e000000.disp_apb:hdmi-encoder: Fixed dependency cycle(s) with /soc/disp_apb/dc8000Nano@3e000000
[ 0.161487] platform 3e000000.disp_apb:hdmi-encoder: Fixed dependency cycle(s) with /soc/disp_apb/bt1120@3e010000
[ 0.161633] platform 3e000000.disp_apb:hdmi-encoder: Fixed dependency cycle(s) with /soc/disp_apb/bt1120-syscon-bridge
[ 0.161706] platform 3e000000.disp_apb:bt1120_bridge: Fixed dependency cycle(s) with /soc/disp_apb/bt1120-syscon-bridge
[ 0.161778] platform 3e000000.disp_apb:bt1120-syscon-bridge: Fixed dependency cycle(s) with /soc/disp_apb/bt1120_bridge
[ 0.161845] platform 3e000000.disp_apb:bt1120-syscon-bridge: Fixed dependency cycle(s) with /soc/disp_apb/hdmi-encoder
[ 0.188536] noc_qos 20510700.sif_disp_qos: qos doesn’t have consumer device
[ 0.188576] noc_qos 20510700.sif_disp_qos: noc qos init finished.
[ 0.193104] platform 3e000000.disp_apb:bt1120_bridge: Fixed dependency cycle(s) with /soc/dsp_apb/i2c@320a0000/hdmi_bridge@3b
[ 0.193223] i2c 7-003b: Fixed dependency cycle(s) with /soc/disp_apb/bt1120_bridge
[ 0.301435] phy phy-3e0a0028.phy.0: Looking up phy-supply property in node /soc/disp_apb/phy@3e0a0028 failed
[ 3.907410] noc_qos 20510280.isp_axi5_hdr_qos: noc qos init finished.
[ 3.918740] noc_qos 20510300.isp_axi4_mcm_qos: noc qos init finished.
[ 3.929005] noc_qos 20510380.isp_axi3_sp2_qos: noc qos init finished.
[ 3.939206] noc_qos 20510480.isp_axi1_mp_qos: noc qos init finished.
[ 12.951432] vs-isp 3d000000.isp: Adding to iommu group 8
[ 12.957852] vs-isp 3d000000.isp: assigned reserved memory node linux,cma@88000000
[ 12.993726] vs-isp 3d000000.isp: VS ISP driver (native) probed done, time used: 41581us
[ 13.560486] 20510280.isp_qos QoS not initialized! Skipping!
[ 14.759135] vs-disp-sif 3e080000.vs-sif: Adding to iommu group 3
[ 14.787222] vs-drm 3e000000.disp_apb:display-subsystem: bound 3e080000.vs-sif (ops sif_component_ops [vs_drm])
[ 14.792846] vs-drm 3e000000.disp_apb:display-subsystem: bound 3e000000.dc8000Nano (ops dc_component_ops [vs_drm])
[ 14.793007] vs-drm 3e000000.disp_apb:display-subsystem: bound 3e010000.bt1120 (ops bt1120_component_ops [vs_drm])
[ 14.793055] vs-drm 3e000000.disp_apb:display-subsystem: bound 3e000000.disp_apb:bt1120_bridge (ops bt1120_bridge_component_ops [vs_drm])
[ 14.793096] vs-drm 3e000000.disp_apb:display-subsystem: bound 3e000000.disp_apb:bt1120_bridge_wb (ops bt1120_bridge_component_ops [vs_drm])
[ 14.793236] vs-drm 3e000000.disp_apb:display-subsystem: bound 3e000000.disp_apb:dsi-encoder (ops encoder_component_ops [vs_drm])
[ 14.793317] vs-drm 3e000000.disp_apb:display-subsystem: bound 3e000000.disp_apb:hdmi-encoder (ops encoder_component_ops [vs_drm])
[ 14.794583] [drm] Initialized vs-drm 1.0.0 20191101 for 3e000000.disp_apb:display-subsystem on minor 0

:bullseye: 日志分析结论

dmesg 日志看,MIPI 控制器和 ISP 驱动已正常加载,但没有看到具体摄像头 Sensor 的加载信息

:white_check_mark: 正常部分

vin mipi0-3: probe with x5sys done          # MIPI 控制器初始化成功
vs-isp 3d000000.isp: VS ISP driver probed   # ISP 驱动加载成功

:warning: 关键问题

日志中缺少 Sensor 驱动加载信息,正常应该看到类似:

vin mipi0: sensor xxx probed
creat_isp_node success

:clipboard: 下一步排查

第一步:确认摄像头型号

请告诉我你的摄像头具体型号(如 SC130AI、SC230AI、GS130W、OV5647 等),这决定了:

  • 是否需要额外驱动适配
  • 设备树配置是否正确

第二步:运行官方 MIPI 样例

# 进入样例目录
cd /app/pydev_demo/03_mipi_camera_sample/

# 查看样例代码中的传感器配置
cat mipi_camera.py | grep -i sensor

# 运行样例(默认使用 CSI-1 接口)
sudo python3 ./mipi_camera.py

预期结果

  • :white_check_mark: 成功:终端显示帧率,Web 页面有图像
  • :cross_mark: 失败:报错 creat_isp_node failedsensor init failed

第三步:查看完整摄像头相关日志

# 过滤摄像头相关日志
dmesg | grep -E "vin|isp|sensor|camera|mipi" > /tmp/camera_log.txt

# 查看是否有 sensor 探测失败信息
cat /tmp/camera_log.txt | grep -i "fail\|error"

:wrench: 常见场景及解决方案

摄像头型号 是否需要适配 解决方案
SC130AI/SC230AI 官方支持,直接运行样例
GS130W/GC130W 官方支持,参考 topic/35118
OV5647 可能需要 参考社区案例配置设备树
IMX219 部分版本不支持,参考 topic/34989
其他自定义 需要自行开发驱动适配

:light_bulb: 关键提醒

  1. 不要用 i2cdetect 判断 MIPI 摄像头状态 —— RDK X5 的 MIPI CSI 不走标准 I2C 路径
  2. 先跑通单路再测试双路 —— 避免多路配置干扰排查
  3. 确认系统版本 —— 建议 3.1.0+,旧版本对部分传感器支持不完善

:open_mailbox_with_raised_flag: 请补充以下信息

  1. 摄像头型号(传感器芯片型号)
  2. 运行 sudo python3 ./mipi_camera.py 的完整输出
  3. 系统版本 (cat /etc/issue)

提供这些信息后,我可以帮你精准定位问题!:hammer_and_wrench:

imx219

:bullseye: IMX219 在 RDK X5 上的核心结论

IMX219 在 RDK X5 上存在兼容性限制,不是所有版本/批次都能直接使用。根据社区案例 topic/34989 官方回复:

  1. 双目相机手册中提供的 IMX219-83 Stereo Camera 目前不支持
  2. MIPI 使用必须为手册中支持的厂商,不同厂商的 IMX219 有区别,需要进行驱动层面的开发适配
  3. RDK 不走 V4L2 驱动,无法在 /dev/video* 找到

:clipboard: IMX219 专属排查步骤

第一步:确认系统版本(关键)

cat /etc/issue
  • 3.1.0 以下版本:IMX219 支持不完善,建议升级
  • 3.1.0+ 版本:部分 IMX219 可用,但需确认厂商兼容性

第二步:运行官方 MIPI 样例测试

cd /app/pydev_demo/03_mipi_camera_sample/

# 查看当前传感器配置
cat mipi_camera.py | grep -A5 -B5 "sensor\|pipeId"

# 运行样例(IMX219 通常对应 pipeId=0)
sudo python3 ./mipi_camera.py

预期报错(如果驱动未适配):

creat_isp_node failed, error: -10
sensor init failed

第三步:检查设备树中是否有 IMX219 节点

# 查看设备树中 camera 相关配置
grep -r "imx219" /proc/device-tree/ 2>/dev/null

# 或查看内核日志中 sensor 探测信息
dmesg | grep -i "imx219\|sensor"

第四步:确认 IMX219 购买渠道

渠道 兼容性 建议
地平线官方 :white_check_mark: 支持 直接使用
树莓派官方 :warning: 部分支持 需确认系统版本
第三方/淘宝 :cross_mark: 大概率不支持 需要自行适配驱动

:wrench: 解决方案

方案 A:更换官方支持摄像头(推荐)

如果项目不强制要求 IMX219,建议更换为 RDK X5 官方支持的传感器:

型号 支持情况 参考帖子
SC130AI :white_check_mark: 完全支持 topic/34255
SC230AI :white_check_mark: 完全支持 topic/34255
GS130W/GC130W :white_check_mark: 完全支持 topic/35118

方案 B:自行适配 IMX219 驱动(进阶)

如果必须使用 IMX219,需要:

  1. 确认传感器 I2C 地址(IMX219 通常为 0x10
  2. 添加设备树节点(参考官方 SC130AI 配置)
  3. 编写/修改 sensor 驱动(ISP 固件适配)
  4. 重新编译内核或 DTB

:warning: 此方案工作量较大,需要熟悉地平线 ISP 开发流程。


:open_mailbox_with_raised_flag: 请补充以下信息

  1. IMX219 购买渠道(官方/树莓派/第三方?)
  2. 系统版本 (cat /etc/issue 输出)
  3. 运行 sudo python3 ./mipi_camera.py 的完整报错

提供这些信息后,我可以帮你判断是直接换摄像头还是尝试驱动适配:hammer_and_wrench:

sunrise@ubuntu:/app$ cat /etc/issue
Ubuntu 22.04.5 LTS \n \l

:bullseye: IMX219 + Ubuntu 22.04.5 核心结论

你的系统版本是 Ubuntu 22.04.5 LTS,IMX219 在 RDK X5 上的支持情况取决于摄像头购买渠道和系统镜像版本

情况 兼容性 解决方案
地平线官方配件 :white_check_mark: 支持 直接使用官方样例
树莓派官方 IMX219 :warning: 部分支持 需确认系统镜像版本
第三方/淘宝 IMX219 :cross_mark: 大概率不支持 需自行适配驱动

:clipboard: 立即执行排查步骤

第一步:运行官方 MIPI 样例测试

cd /app/pydev_demo/03_mipi_camera_sample/

# 查看当前传感器配置
cat mipi_camera.py | grep -E "sensor|pipeId|name"

# 运行样例
sudo python3 ./mipi_camera.py

把完整输出发给我,特别是是否有以下报错:

  • creat_isp_node failed
  • sensor init failed
  • Chip ID read failed

第二步:确认 IMX219 购买渠道

请告诉我:

  • 从哪里买的?(地平线官方/树莓派官网/淘宝/其他)
  • 模块型号?(IMX219-83/IMX219-160/其他)
  • 是否有转接线?(15pin↔22pin)

第三步:检查系统镜像版本

# 查看 RDK 系统版本
cat /etc/rdk_version 2>/dev/null || echo "无此文件"

# 查看内核版本
uname -r

:wrench: 已知问题参考

根据社区案例 topic/32502

“出厂固件可以打开 IMX219,自己编译的镜像打不开”

这说明:

  1. 官方镜像中已预置 IMX219 驱动配置
  2. 自定义编译镜像可能缺少设备树节点或 ISP 固件

:light_bulb: 快速判断方案

测试结果 原因 建议
样例运行成功,有图像 :white_check_mark: 硬件正常 继续开发
报错 creat_isp_node failed 驱动未加载 检查设备树配置
报错 Chip ID read failed I2C 通信失败 检查硬件连接/转接线
无任何报错但无图像 Sensor 配置错误 修改样例中的 sensor 类型

:open_mailbox_with_raised_flag: 请补充以下信息

  1. sudo python3 ./mipi_camera.py 的完整输出
  2. IMX219 购买渠道和模块型号
  3. cat /etc/rdk_versionuname -r 的输出

提供这些信息后,我可以帮你判断是配置问题还是需要更换摄像头:hammer_and_wrench:

sunrise@ubuntu:/app$ cd /app/pydev_demo/03_mipi_camera_sample/

查看当前传感器配置

cat mipi_camera.py | grep -E “sensor|pipeId|name”

运行样例

sudo python3 ./mipi_camera.py
bash: cd: /app/pydev_demo/03_mipi_camera_sample/: No such file or directory
cat: mipi_camera.py: No such file or directory
python3: can’t open file ‘/app/./mipi_camera.py’: [Errno 2] No such file or directory

:bullseye: 核心结论

你的系统镜像可能不完整,或者 MIPI 摄像头样例路径不同/app/pydev_demo/ 目录不存在说明:

  1. 可能使用的是精简版镜像(未预装 Python 样例)
  2. 或者样例路径在其他位置

:clipboard: 立即执行排查步骤

第一步:查找 MIPI 相关可执行文件和样例

# 1. 查找 mipi 相关的可执行文件
find /opt -name "*mipi*" 2>/dev/null

# 2. 查找 camera 相关的样例
find /opt -name "*camera*" -type f 2>/dev/null

# 3. 查找 pydev_demo 目录
find / -name "pydev_demo" -type d 2>/dev/null

# 4. 查看 /opt/tros 下是否有相关示例
ls -la /opt/tros/humble/lib/ 2>/dev/null | grep -i mipi

第二步:使用 C++ 样例测试(如果 Python 样例不存在)

根据社区案例 topic/31187,MIPI 摄像头可执行文件路径:

# 查看是否有 mipi_cam 可执行文件
ls -la /opt/tros/humble/lib/mipi_cam/

# 如果有,尝试运行
/opt/tros/humble/lib/mipi_cam/mipi_cam --help

第三步:检查系统是否安装了 RDK 多媒体库

# 检查 RDK 相关库是否安装
dpkg -l | grep -i "horizon\|rdk\|mipi"

# 检查是否有 ISP 相关库
ls /usr/lib/aarch64-linux-gnu/ | grep -i "isp\|camera"

第四步:查看官方文档确认样例位置

# 查看是否有文档说明样例位置
find /usr/share/doc -name "*rdk*" -o -name "*camera*" 2>/dev/null

# 或者查看 /home 目录下是否有用户样例
ls -la /home/

:wrench: 替代测试方案

如果找不到官方样例,可以用以下方式快速验证硬件:

方案 A:使用 GStreamer 测试(如果支持)

# 尝试使用 gstreamer 查看是否有 MIPI 设备
gst-launch-1.0 v4l2src device=/dev/video0 ! autovideosink 2>&1 | head -20

# 或者列出所有视频设备
ls -la /dev/video*

方案 B:直接调用 ISP API 编写简单测试

如果确认是 IMX219 且需要驱动适配,建议:

  1. 先确认系统镜像版本

    cat /etc/rdk_version 2>/dev/null || hostnamectl
    
  2. 如果版本过旧或非官方镜像,建议重新烧录官方完整镜像:


:open_mailbox_with_raised_flag: 请补充以下信息

  1. find /opt -name "*mipi*" 的输出
  2. ls /dev/video* 的输出
  3. 系统镜像来源(官网下载/自己编译/其他)
  4. IMX219 购买渠道(官方/树莓派/第三方)

:light_bulb: 快速建议

情况 建议
找不到任何 MIPI 样例 重新烧录官方完整镜像
/dev/video* 设备 尝试 GStreamer 或 OpenCV 直接读取
使用第三方 IMX219 大概率需要自行适配驱动
项目紧急 建议更换为 SC130AI/SC230AI 等官方支持型号

提供上述命令的输出后,我可以帮你判断下一步是重新烧录镜像还是手动配置驱动:hammer_and_wrench:

# -*- coding: utf-8 -*-

“”"

Vision Pipeline V3 — Official RDK postProcess + hbm_runtime + MJPEG Web

Uses scipy softmax + flat reshape (official RDK approach) for max CPU speed.

“”"

import cv2

import numpy as np

import hbm_runtime

import time

import threading

import json

import subprocess

from http.server import HTTPServer, BaseHTTPRequestHandler

from scipy.special import softmax as scipy_softmax

# ── Model config ──

MODEL_PATH = “/home/sunrise/yolov5_best_quant(kl.bin”

INPUT_SIZE = 640

REG_MAX = 16

MASK_COEFFS = 32

CONF_THRES = 0.25

NMS_THRES = 0.30

FRAME_W, FRAME_H = 640, 640 # VSE dns0 output (square crop)

FRAME_CX = FRAME_W // 2

CLASSES = [‘blackball’, ‘blueball’, ‘bluezone’,

       'redball', 'redzone', 'yellowball'\]

NUM_CLS = len(CLASSES)

BLACK_BALL = 0; BLUE_BALL = 1; BLUE_ZONE = 2

RED_BALL = 3; RED_ZONE = 4; YELLOW_BALL = 5

OWN_ZONE = RED_ZONE

OPPONENT_ZONE = BLUE_ZONE

CLASS_CONF_BIAS = np.zeros(NUM_CLS, dtype=np.float32)

CLASS_CONF_BIAS[BLUE_BALL] = -0.05

CLASS_CONF_BIAS[OWN_ZONE] = 0.10

CLASS_CONF_BIAS[OPPONENT_ZONE] = 0.05

CLASS_CONF_BIAS[YELLOW_BALL] = 0.10

CLASS_CONF_BIAS[BLACK_BALL] = 0.10

ZONE_MASK_EXPAND = 30

ZONE_BOTTOM_SHRINK = 10

OPPONENT_ZONE_EXPAND = 20

CONF_THRES_RAW = -np.log(1.0 / CONF_THRES - 1.0)

# ── 320×320 SCALES ──

STRIDES = [8, 16, 32]

GRID_H = [INPUT_SIZE // s for s in STRIDES] # [40, 20, 10]

# ── Precomputed: DFL weights + grid offsets ──

_DFL_WEIGHTS = np.arange(REG_MAX, dtype=np.float32)

_GRIDS = []

for stride, gh in zip(STRIDES, GRID_H):

x = np.tile(np.linspace(0.5, gh - 0.5, gh), gh)

y = np.repeat(np.arange(0.5, gh, 1.0), gh)

\_GRIDS.append(np.stack(\[x, y\], axis=1).astype(np.float32))

PROTO_H = PROTO_W = INPUT_SIZE // 4 # 80

X_SCALE_CROP = PROTO_W / INPUT_SIZE

Y_SCALE_CROP = PROTO_H / INPUT_SIZE

# ── Web globals ──

WEB_PORT = 8080

g_web_frame = None

g_web_state = {}

COLORS = {

0: (0, 0, 0),       1: (255, 0, 0),     2: (255, 128, 0),

3: (0, 0, 255),     4: (255, 0, 255),   5: (0, 255, 255),

}

MASK_COLORS = {

0: (100, 100, 100), 1: (255, 100, 100), 2: (255, 180, 120),

3: (80, 60, 255),   4: (255, 120, 255), 5: (80, 255, 220),

}

def bgr2nv12(image):

"""BGR (H,W,3) uint8 -> NV12 flat uint8"""

h, w = image.shape\[:2\]

area = h \* w

yuv = cv2.cvtColor(image, cv2.COLOR_BGR2YUV_I420).reshape(area \* 3 // 2)

y = yuv\[:area\]

uv_planar = yuv\[area:\].reshape(2, area // 4)

uv = uv_planar.transpose(1, 0).reshape(area // 2)

nv12 = np.zeros(area \* 3 // 2, dtype=np.uint8)

nv12\[:area\] = y; nv12\[area:\] = uv

return nv12

def _crop_mask(mask_full, x1, y1, x2, y2, img_w, img_h):

scale = INPUT_SIZE / PROTO_H

px1 = max(0, int(x1 / scale)); py1 = max(0, int(y1 / scale))

px2 = min(PROTO_W, int(x2 / scale) + 1); py2 = min(PROTO_H, int(y2 / scale) + 1)

crop = mask_full\[py1:py2, px1:px2\]

if crop.size == 0: return None

dx1 = int(px1 \* img_w / PROTO_W); dy1 = int(py1 \* img_h / PROTO_H)

dx2 = int(px2 \* img_w / PROTO_W); dy2 = int(py2 \* img_h / PROTO_H)

bw = max(1, dx2 - dx1); bh = max(1, dy2 - dy1)

crop = cv2.resize(crop, (bw, bh)); crop = (crop > 0.5).astype(np.uint8)

full = np.zeros((img_h, img_w), dtype=np.uint8)

sy1 = max(0, -dy1); sx1 = max(0, -dx1)

sy2 = min(bh, img_h - dy1); sx2 = min(bw, img_w - dx1)

if sy2 > sy1 and sx2 > sx1:

    full\[dy1 + sy1:dy1 + sy2, dx1 + sx1:dx1 + sx2\] = crop\[sy1:sy2, sx1:sx2\]

return full

def process_output_seg(raw, onames):

"""

Official RDK postProcess approach:

reshape → flatnonzero → scipy softmax → per-class NMS → mask decode.

"""

rw = FRAME_W / INPUT_SIZE

rh = FRAME_H / INPUT_SIZE



\# Proto

proto = raw\[onames\[9\]\].squeeze()

if proto.shape\[0\] != MASK_COEFFS:

    proto = proto.reshape(MASK_COEFFS, PROTO_H, PROTO_W)



\# Reshape all scales at once (official approach)

clses = \[raw\[onames\[0\]\].reshape(-1, NUM_CLS),

         raw\[onames\[3\]\].reshape(-1, NUM_CLS),

         raw\[onames\[6\]\].reshape(-1, NUM_CLS)\]

bboxes = \[raw\[onames\[1\]\].reshape(-1, REG_MAX \* 4),

          raw\[onames\[4\]\].reshape(-1, REG_MAX \* 4),

          raw\[onames\[7\]\].reshape(-1, REG_MAX \* 4)\]

mces_l = \[raw\[onames\[2\]\].reshape(-1, MASK_COEFFS),

          raw\[onames\[5\]\].reshape(-1, MASK_COEFFS),

          raw\[onames\[8\]\].reshape(-1, MASK_COEFFS)\]



all_boxes = \[\]; all_scores = \[\]; all_ids = \[\]; all_mces = \[\]

for cls, bbox, mc, stride, grid in zip(clses, bboxes, mces_l, STRIDES, \_GRIDS):

    \# Per-class bias threshold

    cls_raw = cls  # keep raw for sigmoid later

    max_scores = np.max(cls_raw, axis=1)

    bbox_idx = np.flatnonzero(max_scores >= CONF_THRES_RAW)

    if len(bbox_idx) == 0: continue



    best_cls = np.argmax(cls_raw\[bbox_idx\], axis=1)

    scores = 1.0 / (1.0 + np.exp(-max_scores\[bbox_idx\]))



    \# Apply per-class bias

    keep = np.ones(len(bbox_idx), dtype=bool)

    for c in range(NUM_CLS):

        cmask = best_cls == c

        keep\[cmask\] = scores\[cmask\] > (CONF_THRES - CLASS_CONF_BIAS\[c\])

    if not keep.any(): continue

    bbox_idx = bbox_idx\[keep\]; best_cls = best_cls\[keep\]; scores = scores\[keep\]



    \# DFL decode (scipy softmax — vectorized)

    ltrb = np.sum(scipy_softmax(bbox\[bbox_idx\].reshape(-1, 4, REG_MAX), axis=2) \* \_DFL_WEIGHTS, axis=2)

    g = grid\[bbox_idx\]

    x1y1 = (g - ltrb\[:, 0:2\]) \* stride

    x2y2 = (g + ltrb\[:, 2:4\]) \* stride

    xyxy = np.hstack(\[x1y1, x2y2\])

    xyxy\[:, 0\] = np.clip(xyxy\[:, 0\], 0, INPUT_SIZE); xyxy\[:, 1\] = np.clip(xyxy\[:, 1\], 0, INPUT_SIZE)

    xyxy\[:, 2\] = np.clip(xyxy\[:, 2\], 0, INPUT_SIZE); xyxy\[:, 3\] = np.clip(xyxy\[:, 3\], 0, INPUT_SIZE)



    all_boxes.append(xyxy); all_scores.append(scores)

    all_ids.append(best_cls); all_mces.append(mc\[bbox_idx\])



if len(all_boxes) == 0: return \[\]

boxes = np.vstack(all_boxes); scores = np.hstack(all_scores)

ids = np.hstack(all_ids); mces = np.vstack(all_mces)



\# Per-class NMS

xywh = np.hstack(\[boxes\[:, 0:2\], boxes\[:, 2:4\] - boxes\[:, 0:2\]\])

keep_all = \[\]

for c in range(NUM_CLS):

    cmask = ids == c

    if not cmask.any(): continue

    idx = cv2.dnn.NMSBoxes(xywh\[cmask\].tolist(), scores\[cmask\].tolist(), CONF_THRES, NMS_THRES)

    if len(idx) == 0: continue

    keep_all.append(np.where(cmask)\[0\]\[idx.flatten()\])

if len(keep_all) == 0: return \[\]

keep = np.concatenate(keep_all)



boxes = boxes\[keep\]; scores = scores\[keep\]

cls_arr = ids\[keep\]; mces_nms = mces\[keep\]



\# Scale to image

boxes\[:, 0\] \*= rw; boxes\[:, 1\] \*= rh

boxes\[:, 2\] \*= rw; boxes\[:, 3\] \*= rh



\# Batch mask decode

zone_idx = \[i for i, k in enumerate(keep) if int(cls_arr\[i\]) in (OWN_ZONE, OPPONENT_ZONE)\]

batch_masks = None

if zone_idx:

    try:

        z_mces = mces_nms\[zone_idx\].astype(np.float32)

        proto_flat = proto.reshape(MASK_COEFFS, -1)

        batch_masks = (z_mces @ proto_flat).reshape(len(zone_idx), PROTO_H, PROTO_W)

        batch_masks = 1.0 / (1.0 + np.exp(-batch_masks))

        batch_masks = (batch_masks > 0.5).astype(np.uint8)

    except: pass



final_dets = \[\]

for i in range(len(keep)):

    cid = int(cls_arr\[i\]); b = boxes\[i\]

    det = {

        'cls': cid,

        'cx': float((b\[0\] + b\[2\]) \* 0.5), 'cy': float((b\[1\] + b\[3\]) \* 0.5),

        'w': float(b\[2\] - b\[0\]), 'h': float(b\[3\] - b\[1\]),

        'conf': float(scores\[i\]), 'mask': None,

    }

    if cid in (OWN_ZONE, OPPONENT_ZONE) and batch_masks is not None:

        zi = zone_idx.index(i) if i in zone_idx else -1

        if zi >= 0:

            try:

                x1m = boxes\[keep\[i\], 0\] / rw; y1m = boxes\[keep\[i\], 1\] / rh

                x2m = boxes\[keep\[i\], 2\] / rw; y2m = boxes\[keep\[i\], 3\] / rh

                det\['mask'\] = \_crop_mask(batch_masks\[zi\], x1m, y1m, x2m, y2m, FRAME_W, FRAME_H)

                if det\['mask'\] is not None and det\['mask'\].any():

                    contours, \_ = cv2.findContours(det\['mask'\], cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

                    if contours:

                        cnt = max(contours, key=cv2.contourArea)

                        M = cv2.moments(cnt)

                        if M\['m00'\] > 0:

                            det\['cx'\] = M\['m10'\] / M\['m00'\]; det\['cy'\] = M\['m01'\] / M\['m00'\]

                        pts = cv2.approxPolyDP(cnt, 0.005 \* cv2.arcLength(cnt, True), True).reshape(-1, 2)

                        det\['mask_x1'\] = int(pts\[:, 0\].min()); det\['mask_y1'\] = int(pts\[:, 1\].min())

                        det\['mask_x2'\] = int(pts\[:, 0\].max()); det\['mask_y2'\] = int(pts\[:, 1\].max())

                        dm = det\['mask'\].copy()

                        if cid == OPPONENT_ZONE and OPPONENT_ZONE_EXPAND > 0:

                            n = OPPONENT_ZONE_EXPAND

                            u = np.roll(dm, -n, axis=0); u\[-n:, :\] = 0

                            d = np.roll(dm,  n, axis=0); d\[:n, :\] = 0

                            l = np.roll(dm, -n, axis=1); l\[:, -n:\] = 0

                            r = np.roll(dm,  n, axis=1); r\[:, :n\] = 0

                            dm = np.maximum.reduce(\[dm, u, d, l, r\])

                        elif cid == OWN_ZONE:

                            if ZONE_MASK_EXPAND > 0:

                                u = np.roll(dm, -ZONE_MASK_EXPAND, axis=0); u\[-ZONE_MASK_EXPAND:, :\] = 0

                                dm = np.maximum(dm, u)

                            if ZONE_BOTTOM_SHRINK > 0:

                                d = np.roll(dm, ZONE_BOTTOM_SHRINK, axis=0); d\[:ZONE_BOTTOM_SHRINK, :\] = 0

                                dm = np.minimum(dm, d)

                        det\['mask_dilated'\] = dm

                        dc, \_ = cv2.findContours(dm, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

                        if dc: det\['filter_contour'\] = max(dc, key=cv2.contourArea)

            except: pass

    final_dets.append(det)

return final_dets

# ── Web draw ──

def draw_dets(frame, dets):

img = frame.copy()

mask_overlay = np.zeros_like(img)

for d in dets:

    mask = d.get('mask_dilated') if d\['cls'\] in (OWN_ZONE, OPPONENT_ZONE) else d.get('mask')

    if mask is not None and mask.any():

        mask_overlay\[mask > 0\] = MASK_COLORS.get(d\['cls'\], (0, 255, 255))

if mask_overlay.any():

    alpha = 0.45

    m = mask_overlay.any(axis=2)

    for c in range(3):

        img\[:, :, c\] = np.where(m, (img\[:, :, c\] \* (1 - alpha) + mask_overlay\[:, :, c\] \* alpha).astype(np.uint8), img\[:, :, c\])

for d in dets:

    cx, cy, w, h = d\['cx'\], d\['cy'\], d\['w'\], d\['h'\]

    cid = d\['cls'\]

    x1, y1 = int(cx - w / 2), int(cy - h / 2)

    x2, y2 = int(cx + w / 2), int(cy + h / 2)

    cv2.rectangle(img, (x1, y1), (x2, y2), COLORS.get(cid, (0, 255, 0)), 2)

    cv2.putText(img, f"{CLASSES\[cid\]} {d\['conf'\]:.2f}", (x1, y1 - 8),

                cv2.FONT_HERSHEY_SIMPLEX, 0.7, COLORS.get(cid, (0, 255, 0)), 2)

cv2.line(img, (FRAME_CX, 0), (FRAME_CX, FRAME_H), (0, 255, 0), 2)

cv2.putText(img, f"Dets:{len(dets)}", (10, FRAME_H - 10),

            cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2)

return img

# ── Web handler ──

class WebHandler(BaseHTTPRequestHandler):

def log_message(self, \*a): pass

def do_GET(self):

    if self.path == "/": self.\_html()

    elif self.path == "/video": self.\_stream()

    elif self.path == "/status.json": self.\_json()

    else: self.send_error(404)

def \_html(self):

    h = """<!DOCTYPE html><html><head><meta charset="utf-8"><title>V3 Seg</title>
body{margin:0;background:#111;color:#fff;font-family:monospace} #s{padding:10px;background:#222;font-size:14px}img{width:100%;max-width:900px;display:block}
Loading...
"""
    self.send_response(200); self.send_header("Content-Type", "text/html; charset=utf-8"); self.end_headers()

    self.wfile.write(h.encode())

def \_stream(self):

    self.send_response(200); self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame"); self.end_headers()

    while True:

        if g_web_frame is not None:

            try: self.wfile.write(b"--frame\\r\\nContent-Type: image/jpeg\\r\\n\\r\\n" + g_web_frame + b"\\r\\n")

            except: break

        time.sleep(0.05)

def \_json(self):

    self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers()

    self.wfile.write(json.dumps(g_web_state).encode())

def _web_thread():

HTTPServer(("0.0.0.0", WEB_PORT), WebHandler).serve_forever()

# ── Main loop ──

def main():

global g_web_frame, g_web_state



print(f"Loading {MODEL_PATH} ...")

model = hbm_runtime.HB_HBMRuntime(MODEL_PATH)

mname = model.model_names\[0\]

iname = model.input_names\[mname\]\[0\]

onames = model.output_names\[mname\]

print(f"  Model: {mname}, {len(onames)} outputs")



\# ── MIPI CSI IMX219 via hobot_vio ──

from hobot_vio import libsrcampy as srcampy

cam = srcampy.Camera()

cam.open_cam(0, -1, 30, \[640, 640\], \[640, 640\])

print("MIPI camera opened OK")



threading.Thread(target=\_web_thread, daemon=True).start()

print(f"  Web: http://<x5-ip>:{WEB_PORT}")



web_lock = threading.Lock()

latest_nv12, latest_dets, fps_val = None, \[\], 0.0



def web_encode_loop():

    nonlocal fps_val

    global g_web_frame, g_web_state

    while True:

        with web_lock:

            nv12_bytes = latest_nv12

            dets = list(latest_dets); fv = fps_val

        if nv12_bytes is not None:

            frame = cv2.cvtColor(

                np.frombuffer(nv12_bytes, dtype=np.uint8).reshape(FRAME_H \* 3 // 2, FRAME_W),

                cv2.COLOR_YUV2BGR_NV12)

            annotated = draw_dets(frame, dets)

            \_, buf = cv2.imencode(".jpg", annotated, \[cv2.IMWRITE_JPEG_QUALITY, 55\])

            g_web_frame = buf.tobytes()

            g_web_state = {"fps": fv, "dets": len(dets)}

        else: time.sleep(0.01)

threading.Thread(target=web_encode_loop, daemon=True).start()



\# ── Warmup ──

for \_ in range(5):

    nv12_raw = cam.get_img(2, FRAME_W, FRAME_H)

    if nv12_raw is not None:

        nv12 = np.frombuffer(nv12_raw, dtype=np.uint8)

        nv12 = nv12.reshape(1, FRAME_H \* 3 // 2, FRAME_W, 1)

        model.run({mname: {iname: nv12}})



print("Ready.")

fps_t0 = time.time(); fps_count = 0

t_all = \[0.0\]\*4  # read, infer, postproc, lock



try:

    while True:

        t0 = time.time()

        nv12_raw = cam.get_img(2, FRAME_W, FRAME_H)

        t_all\[0\] += time.time() - t0

        if nv12_raw is None: time.sleep(0.01); continue



        t0 = time.time()

        nv12 = np.frombuffer(nv12_raw, dtype=np.uint8)

        nv12 = nv12.reshape(1, FRAME_H \* 3 // 2, FRAME_W, 1)

        raw = model.run({mname: {iname: nv12}})\[mname\]

        t_all\[1\] += time.time() - t0



        t0 = time.time()

        dets = process_output_seg(raw, onames)

        t_all\[2\] += time.time() - t0



        t0 = time.time()

        with web_lock:

            latest_nv12 = nv12_raw  # raw bytes, convert in web thread

            latest_dets = dets

        t_all\[3\] += time.time() - t0



        fps_count += 1

        if fps_count >= 30:

            fps_val = 30 / (time.time() - fps_t0 + 1e-7)

            fps_t0 = time.time(); fps_count = 0

            ms = \[v/30\*1000 for v in t_all\]

            print(f"FPS:{fps_val:.1f} | read:{ms\[0\]:.0f} infer:{ms\[1\]:.0f} post:{ms\[2\]:.0f} lock:{ms\[3\]:.0f}ms")

            t_all = \[0.0\]\*4

except KeyboardInterrupt: pass

finally:

    cam.close_cam(); print("Done.")

if _name_ == “_main_”: main()帧数怎么再提高

:bullseye: 帧率优化核心结论

你的代码架构不错,但后处理太重是主要瓶颈。根据社区案例 topic/34084,优化后处理可从 10 FPS 提升到 40+ FPS。


:bar_chart: 当前瓶颈分析

从你的代码看,耗时主要在:

环节 预估耗时 优化空间
cam.get_img() 5-10ms :warning: 中等
model.run() 8-15ms :white_check_mark: 已优化
process_output_seg() 30-50ms :fire: 最大瓶颈
Web 编码/锁 5-10ms :warning: 中等

:wrench: 6 个关键优化方案

方案 1:降低输入分辨率(收益最大)

# 当前 640x640 → 改为 320x320 或 416x416
FRAME_W, FRAME_H = 320, 320   # 分辨率减半,帧率翻倍
INPUT_SIZE = 320              # 模型输入同步修改
PROTO_H = PROTO_W = INPUT_SIZE // 4  # 80 → 40

预期收益:帧率提升 40-60%


方案 2:简化 Mask 处理(关键)

你的 mask 处理太复杂,建议按需简化

# 修改 process_output_seg 中的 mask 处理部分
# 只保留核心功能,去掉膨胀/收缩/轮廓近似

if cid in (OWN_ZONE, OPPONENT_ZONE) and batch_masks is not None:
    zi = zone_idx.index(i) if i in zone_idx else -1
    if zi >= 0:
        # 简化:直接二值化,不做形态学操作
        mask_full = batch_masks[zi]
        det['mask'] = mask_full  # 直接使用,不裁剪

去掉以下耗时操作

  • :cross_mark: cv2.findContours(每帧多次调用)
  • :cross_mark: cv2.approxPolyDP(多边形近似)
  • :cross_mark: np.roll 膨胀/收缩操作
  • :cross_mark: cv2.moments 质心计算

预期收益:后处理耗时减少 50%


方案 3:优化锁机制(减少阻塞)

当前 web_lock 在主循环中,建议无锁设计

# 使用原子替换代替锁
import threading

# 改为使用 threading.Event 或 queue
from queue import Queue

frame_queue = Queue(maxsize=2)  # 双缓冲

# 主循环
frame_queue.put_nowait((nv12_raw, dets))

# Web 线程
nv12_raw, dets = frame_queue.get()

预期收益:减少 5-10ms 锁等待


方案 4:减少 NMS 计算量

# 当前对每个类别单独 NMS,可合并处理
# 或者提高 CONF_THRES 减少候选框

CONF_THRES = 0.35  # 从 0.25 提高到 0.35
NMS_THRES = 0.45   # 从 0.30 提高到 0.45

预期收益:后处理减少 20-30%


方案 5:使用 INT8 量化模型

确认你的模型是 INT8 量化:

# 检查模型文件
ls -lh /home/sunrise/yolov5_best_quant(kl.bin

# 如果是 FP16/FP32,重新量化为 INT8
# 参考:https://developer.d-robotics.cc/rdk_doc/

预期收益:推理速度提升 2-3 倍


方案 6:跳过非必要计算

# 添加跳帧机制,每 2 帧做一次完整后处理
frame_count = 0
SKIP_FRAME = 1  # 每 2 帧处理 1 次

while True:
    nv12_raw = cam.get_img(2, FRAME_W, FRAME_H)
    
    # 推理每帧都做
    raw = model.run(...)
    
    frame_count += 1
    if frame_count % (SKIP_FRAME + 1) == 0:
        dets = process_output_seg(raw, onames)
    # 否则复用上一帧检测结果

预期收益:后处理耗时减半


:rocket: 快速优化版本(推荐先试)

# 关键修改点汇总
FRAME_W, FRAME_H = 320, 320   # 1. 降低分辨率
INPUT_SIZE = 320
CONF_THRES = 0.35              # 2. 提高阈值

# 3. 简化 process_output_seg 中的 mask 处理
# 去掉 cv2.findContours、cv2.approxPolyDP、np.roll 等操作

# 4. 使用双缓冲代替锁
from queue import Queue
frame_queue = Queue(maxsize=2)

:bar_chart: 预期帧率提升

优化项 当前预估 优化后预估
原始代码 15-20 FPS -
仅降低分辨率 - 25-30 FPS
+ 简化 mask - 35-40 FPS
+ 双缓冲 - 40-45 FPS
+ INT8 模型 - 50-60 FPS

:open_mailbox_with_raised_flag: 建议执行顺序

  1. 先改分辨率 (320x320) → 立即见效
  2. 简化 mask 处理 → 去掉轮廓/膨胀操作
  3. 确认模型是 INT8 → 检查量化方式
  4. 最后优化锁机制 → 双缓冲设计

:link: 相关资源

需要我帮你改写优化后的完整代码吗? :hammer_and_wrench:

30 FPS 是 open_cam 里设的硬上限。IMX219 可以 60 FPS:

Edit C:\\Users\\win\\Desktop\\工训2.0\\vision_pipeline_v3.py

Modified

改到 60。另外 30 帧之后还有余量的话:

优化 预期提升 代价
比赛模式关 Web +5~10 FPS 调试不方便
降低模型到 320 推理 ~10ms→~6ms 精度可能下降
双线程流水线(读图+推理分离) +2~5 FPS 多一帧延迟

先跑 60 看有没有用。如果 IMX219 不支持 60fps 模式下 640 输出,会自动退回 30。

:bullseye: IMX219 60 FPS 核心结论

IMX219 在 RDK X5 上支持 60 FPS,但有分辨率限制

分辨率 最大帧率 说明
640×480 :white_check_mark: 60 FPS 推荐用于高速场景
640×640 :warning: 可能不支持 60 FPS IMX219 原生不支持正方形裁剪
1920×1080 :white_check_mark: 30 FPS 全分辨率模式
1280×720 :white_check_mark: 60 FPS 720p 高速模式

:wrench: 关键修改点

你的代码中 open_cam 参数需要调整:

# 当前代码
cam.open_cam(0, -1, 60, [640, 640], [640, 640])

# IMX219 原生支持的 60 FPS 模式
cam.open_cam(0, -1, 60, [640, 480], [640, 480])  # ✅ 推荐
# 或
cam.open_cam(0, -1, 60, [1280, 720], [640, 640])  # 720p 输入,裁剪到 640

:clipboard: 验证步骤

第一步:测试 60 FPS 是否生效

# 添加帧率打印验证
import time

fps_t0 = time.time()
fps_count = 0

while True:
    nv12_raw = cam.get_img(2, FRAME_W, FRAME_H)
    if nv12_raw is None:
        print("⚠️ Camera returned None - 可能不支持该模式")
        break
    
    fps_count += 1
    if fps_count >= 60:
        elapsed = time.time() - fps_t0
        actual_fps = 60 / elapsed
        print(f"实际帧率:{actual_fps:.1f} FPS")
        if actual_fps < 50:
            print("⚠️ IMX219 可能不支持 640x640@60FPS,尝试降低分辨率")
        fps_t0 = time.time()
        fps_count = 0

第二步:查看摄像头支持的格式

# 使用 v4l2-ctl 查看 IMX219 支持的格式(如果走 V4L2)
v4l2-ctl --device=/dev/video0 --list-formats-ext 2>/dev/null

# 或查看 hobot_vio 日志
dmesg | grep -i "imx219\|mipi\|fps"

:warning: 常见问题

问题 1:640×640 不支持 60 FPS

原因:IMX219 原生传感器是 4:3 比例,640×640 需要 ISP 裁剪,可能限制帧率。

解决

# 方案 A:使用 640×480(原生支持 60 FPS)
FRAME_W, FRAME_H = 640, 480
cam.open_cam(0, -1, 60, [640, 480], [640, 480])

# 方案 B:使用 1280×720 输入,裁剪到 640×640
FRAME_W, FRAME_H = 640, 640
cam.open_cam(0, -1, 60, [1280, 720], [640, 640])

问题 2:自动回退到 30 FPS

如果设置 60 FPS 但实际运行在 30 FPS,hobot_vio 通常会打印警告:

# 在 open_cam 后添加检查
import logging
logging.basicConfig(level=logging.DEBUG)

# 或查看 dmesg
dmesg | tail -50 | grep -i "fps\|frame"

:rocket: 完整优化建议(60 FPS 场景)

如果成功跑到 60 FPS,进一步优化:

# 1. 关闭 Web 推流(比赛模式)
# 注释掉 web_thread 和 web_encode_loop

# 2. 降低模型输入分辨率
INPUT_SIZE = 320  # 推理更快
FRAME_W, FRAME_H = 640, 480  # 保持摄像头原生分辨率

# 3. 双线程流水线
from queue import Queue
from threading import Thread

read_queue = Queue(maxsize=2)
infer_queue = Queue(maxsize=2)

def read_thread():
    while True:
        frame = cam.get_img(2, FRAME_W, FRAME_H)
        read_queue.put(frame)

def infer_thread():
    while True:
        frame = read_queue.get()
        # 推理处理
        infer_queue.put(result)

:bar_chart: 预期结果

配置 预期帧率 说明
640×480 @ 60 FPS 55-60 FPS :white_check_mark: 最佳平衡
640×640 @ 60 FPS 30-45 FPS :warning: 可能降级
320×320 @ 60 FPS 60+ FPS :white_check_mark: 推理最快
关闭 Web + 320 输入 70-80 FPS :fire: 极限性能

:open_mailbox_with_raised_flag: 请反馈测试结果

运行后请告诉我:

  1. 实际帧率(修改后的 FPS: 打印值)
  2. 是否有报错(特别是 open_cam 返回值)
  3. dmesg | grep -i mipi 的输出

如果 640×640 无法稳定 60 FPS,建议改用 640×480320×320 模式!:hammer_and_wrench:

2026/08/03 20:07:43.636 ERROR [GetImageFrame][0711]get chn from 640x640 failed
2026/08/03 20:07:43.647 ERROR [GetImageFrame][0711]get chn from 640x640 failed
2026/08/03 20:07:43.657 ERROR [GetImageFrame][0711]get chn from 640x640 failed
2026/08/03 20:07:43.667 ERROR [GetImageFrame][0711]get chn from 640x640 failed
2026/08/03 20:07:43.677 ERROR [GetImageFrame][0711]get chn from 640x640 failed

:bullseye: 问题确认:640×640@60FPS 不支持

错误 get chn from 640x640 failed 说明 IMX219 在 RDK X5 上无法创建 640×640@60FPS 的通道


:white_check_mark: 立即修改方案

方案 A:使用 640×480@60FPS(推荐)

# 修改这两处
FRAME_W, FRAME_H = 640, 480   # 原来是 640, 640
INPUT_SIZE = 640              # 模型输入保持 640

# open_cam 修改
cam.open_cam(0, -1, 60, [640, 480], [640, 480])

优点:IMX219 原生支持,稳定 60 FPS


方案 B:使用 1280×720@60FPS 输入,裁剪到 640

# 摄像头输出 720p,ISP 裁剪到 640×640
FRAME_W, FRAME_H = 640, 640
INPUT_SIZE = 640

cam.open_cam(0, -1, 60, [1280, 720], [640, 640])

优点:保持正方形输入,适合原有模型

缺点:多一步 ISP 裁剪,可能增加延迟


方案 C:降级到 30 FPS(如果必须 640×640)

# 保持 640×640,但帧率改为 30
FRAME_W, FRAME_H = 640, 640
cam.open_cam(0, -1, 30, [640, 640], [640, 640])

:bar_chart: IMX219 在 RDK X5 上支持的格式

分辨率 60 FPS 30 FPS 说明
1920×1080 :cross_mark: :white_check_mark: 全分辨率
1280×720 :white_check_mark: :white_check_mark: 720p 高速
640×480 :white_check_mark: :white_check_mark: VGA 高速
640×640 :cross_mark: :warning: 正方形裁剪,部分版本支持
320×240 :white_check_mark: :white_check_mark: QVGA 超高速

:wrench: 完整修改代码(方案 A)

# ── Model config ──
MODEL_PATH = "/home/sunrise/yolov5_best_quant(kl.bin"
INPUT_SIZE = 640
FRAME_W, FRAME_H = 640, 480   # ← 修改这里
FRAME_CX = FRAME_W // 2

# ── Main loop ──
cam.open_cam(0, -1, 60, [640, 480], [640, 480])  # ← 修改这里

:test_tube: 验证帧率

修改后运行,观察日志:

# 成功标志
MIPI camera opened OK
FPS:58.5 | read:8ms infer:12ms post:15ms lock:3ms

# 如果还是失败
ERROR [GetImageFrame] get chn from 640x480 failed
# → 尝试 320×240

:light_bulb: 额外优化建议

如果方案 A 成功跑到 60 FPS,继续优化:

优化项 修改 预期提升
关闭 Web 推流 注释 web_thread +5~10 FPS
模型输入改 320 INPUT_SIZE = 320 推理 12ms→8ms
跳帧处理 每 2 帧做一次后处理 后处理耗时减半

:open_mailbox_with_raised_flag: 请反馈

修改后告诉我:

  1. 是否成功打开摄像头
  2. 实际帧率FPS: 打印值)
  3. 是否还需要进一步优化

建议先试 640×480@60FPS,这是 IMX219 最稳定的高速模式!:hammer_and_wrench:

修改到640*480,是不是对图像有拉伸

:bullseye: 640×480 不会拉伸,但模型需要调整

图像比例说明

分辨率 比例 IMX219 原生支持 拉伸情况
640×640 1:1 :cross_mark: 需裁剪/拉伸 会变形
640×480 4:3 :white_check_mark: 原生支持 无拉伸
1280×720 16:9 :white_check_mark: 原生支持 无拉伸

640×480 是 IMX219 的标准 VGA 输出格式,不会有图像拉伸问题


:warning: 真正的问题:模型输入不匹配

你的模型是用 640×640 正方形 训练的,但 640×480 是 4:3 矩形,这会导致:

摄像头采集:640×480 (4:3)
    ↓
模型输入需要:640×640 (1:1)
    ↓
需要填充或拉伸 → 影响检测精度

:wrench: 三个解决方案

方案 A:填充到 640×640(推荐,最快)

# 采集 640×480,填充黑边到 640×640 再输入模型
FRAME_W, FRAME_H = 640, 480
INPUT_SIZE = 640

# 在送入模型前填充
def prepare_input(nv12_raw):
    frame = cv2.cvtColor(
        np.frombuffer(nv12_raw, dtype=np.uint8).reshape(FRAME_H * 3 // 2, FRAME_W),
        cv2.COLOR_YUV2BGR_NV12)
    
    # 填充到正方形(上下各加 40px 黑边)
    pad_h = (INPUT_SIZE - FRAME_H) // 2  # 80px
    padded = cv2.copyMakeBorder(frame, pad_h, pad_h, 0, 0, cv2.BORDER_CONSTANT, value=0)
    
    # 调整到模型输入尺寸
    resized = cv2.resize(padded, (INPUT_SIZE, INPUT_SIZE))
    return resized

优点:不用重新训练模型,图像不变形
缺点:上下有黑边,有效视场减少约 12.5%


方案 B:重新训练模型为 640×480 输入(最佳精度)

# 训练时设置
--imgsz 640 480  # YOLOv5 支持矩形训练

# 推理代码保持不变
INPUT_SIZE = 640
FRAME_W, FRAME_H = 640, 480

优点:充分利用传感器视场,精度最高
缺点:需要重新训练模型


方案 C:使用 1280×720 输入,裁剪到 640×640

# 摄像头输出 720p,ISP 裁剪中间 640×640
FRAME_W, FRAME_H = 640, 640
cam.open_cam(0, -1, 60, [1280, 720], [640, 640])

优点:保持正方形输入,模型不用改
缺点:720p@60FPS 可能带宽不足,需测试


:bar_chart: 方案对比

方案 图像质量 模型修改 实施难度 推荐场景
A: 填充黑边 :star::star::star::star: 不需要 比赛紧急
B: 重新训练 :star::star::star::star::star: 需要 长期项目
C: 720p 裁剪 :star::star::star: 不需要 需测试稳定性

:rocket: 建议执行顺序

  1. 先用方案 A 测试(填充黑边),验证 60 FPS 是否稳定
  2. 如果帧率达标,再考虑方案 B 重新训练优化精度
  3. 如果 720p@60FPS 能稳定,方案 C 也是不错的选择

:light_bulb: 快速修改代码(方案 A)

# 在 model.run 之前添加填充逻辑
nv12 = np.frombuffer(nv12_raw, dtype=np.uint8)
frame = cv2.cvtColor(nv12.reshape(FRAME_H * 3 // 2, FRAME_W), cv2.COLOR_YUV2BGR_NV12)

# 填充到 640×640
pad_h = (640 - 480) // 2
padded = cv2.copyMakeBorder(frame, pad_h, pad_h, 0, 0, cv2.BORDER_CONSTANT, value=0)
resized = cv2.resize(padded, (640, 640))

# 转为 NV12 输入模型
nv12_input = bgr2nv12(resized)
nv12_input = nv12_input.reshape(1, 640 * 3 // 2, 640, 1)

raw = model.run({mname: {iname: nv12_input}})

:open_mailbox_with_raised_flag: 请反馈

  1. 先用 640×480@60FPS 测试,看是否还有 get chn failed 错误
  2. 如果成功,再决定用哪个方案处理模型输入

建议先跑通 60 FPS,再优化精度!:hammer_and_wrench:

imx219怎么跑60帧

:bullseye: IMX219 跑 60 FPS 最终方案

根据前面排查,640×640 不支持 60 FPS,需要用 IMX219 原生支持的分辨率。


:white_check_mark: 三种可行配置

分辨率 60 FPS 代码修改 说明
640×480 :white_check_mark: 推荐 原生 VGA,无拉伸
1280×720 :white_check_mark: 可选 720p 高速,需裁剪
320×240 :white_check_mark: 极限 QVGA,帧率最高

:wrench: 立即修改代码

方案 1:640×480@60FPS(推荐)

# 修改这两处
FRAME_W, FRAME_H = 640, 480   # ← 原 640, 640
INPUT_SIZE = 640              # 模型输入保持 640

# open_cam 修改
cam.open_cam(0, -1, 60, [640, 480], [640, 480])  # ← 原 [640, 640]

模型输入处理(填充黑边):

# 在 model.run 之前
nv12 = np.frombuffer(nv12_raw, dtype=np.uint8)
frame = cv2.cvtColor(nv12.reshape(FRAME_H * 3 // 2, FRAME_W), cv2.COLOR_YUV2BGR_NV12)

# 填充到 640×640(上下各加 40px)
pad_h = (640 - 480) // 2
padded = cv2.copyMakeBorder(frame, pad_h, pad_h, 0, 0, cv2.BORDER_CONSTANT, value=0)
resized = cv2.resize(padded, (640, 640))

# 转 NV12 输入模型
nv12_input = bgr2nv12(resized)
nv12_input = nv12_input.reshape(1, 640 * 3 // 2, 640, 1)

raw = model.run({mname: {iname: nv12_input}})

方案 2:1280×720@60FPS 裁剪到 640

FRAME_W, FRAME_H = 640, 640
cam.open_cam(0, -1, 60, [1280, 720], [640, 640])

方案 3:320×240@60FPS(极限性能)

FRAME_W, FRAME_H = 320, 240
INPUT_SIZE = 320  # 模型也改 320
cam.open_cam(0, -1, 60, [320, 240], [320, 240])

:bar_chart: IMX219 在 RDK X5 支持的格式

✅ 60 FPS:  640×480, 1280×720, 320×240, 320×320
✅ 30 FPS:  1920×1080, 640×640, 1280×960
❌ 不支持:640×640@60FPS

:test_tube: 验证是否成功

运行后看日志:

# 成功
MIPI camera opened OK
FPS:55-60 | read:8ms infer:12ms post:15ms

# 失败
ERROR [GetImageFrame] get chn from xxx failed

:rocket: 进一步提帧(如果 60 FPS 还不够)

优化 修改 提升
关 Web 推流 注释 web_thread +5~10 FPS
模型改 320 INPUT_SIZE = 320 推理 12ms→6ms
跳帧处理 每 2 帧做一次后处理 后处理减半

:open_mailbox_with_raised_flag: 建议

先用方案 1(640×480@60FPS)测试,这是 IMX219 最稳定的高速模式,图像无拉伸。

跑通后告诉我实际帧率,再决定是否需要进一步优化!:hammer_and_wrench: