前言:一个看似简单,实则坑多的任务
上一篇文章讲完了5寸DSI屏幕的适配,接下来自然是要让这块屏幕发挥点实际作用。作为一个支持视觉AI开发板,最经典的应用场景莫过于:MIPI摄像头采集 → AI模型推理 → 结果显示。听起来很简单对吧?官方也有现成的HDMI示例 /app/pydev_demo/03_mipi_camera_sample/mipi_camera.py,参考一下改改分辨率应该就行了。
但事实证明,从"能跑"到"能用在DSI屏幕上",中间隔着无数个坑。
这篇文章记录了我从下午折腾到凌晨的完整调试过程,包括三次大的方向调整、无数次小错误修正,以及最终那个让人抓狂的 score: nan 问题的解决。希望这些踩坑经历能帮到你。
一、环境准备
1.1 硬件环境
- 开发板:RDK X5(已适配5寸DSI屏)
- 摄像头:IMX219 MIPI CSI 摄像头(1920×1080,30fps)
- 屏幕:DFRobot 5寸 DSI触摸屏(800×480)
- 连接方式:SSH远程登录(
ssh rdkx5.local)
1.2 软件环境
系统镜像:RDK X5 Ubuntu 20.04
预装库:
hobot_vio/hobot_vio_rdkx5:视频输入输出(libsrcampy)hobot_dnn/hobot_dnn_rdkx5:深度学习推理(pyeasy_dnn)libpostprocess.so:FCOS模型后处理
AI模型:
- 路径:
/app/pydev_demo/models/fcos_512x512_nv12.bin - 类型:FCOS目标检测(512×512输入,80类COCO)
1.3 开发工具
- 本地Mac电脑:代码编辑、文件同步
- RDK X5开发板:程序运行、截图
- 同步命令:
rsync -avz ... rdk_x5.ssh:/home/sunrise/Projects/
二、第一次尝试:参考官方HDMI示例
2.1 官方示例分析
官方提供的HDMI版本位于 /app/pydev_demo/03_mipi_camera_sample/mipi_camera.py,核心逻辑:
# 1. 摄像头初始化
cam = srcampy.Camera()
cam.open_cam(0, -1, -1, [w, disp_w], [h, disp_h], sensor_height, sensor_width)
# 2. 显示初始化(HDMI)
disp = srcampy.Display()
disp.display(0, disp_w, disp_h)
# 3. 绑定摄像头到显示(硬件直通)
srcampy.bind(cam, disp)
# 4. 再初始化一层用于画框
disp.display(3, disp_w, disp_h)
# 5. 主循环:获取图像→AI推理→在层3画框
while True:
img = cam.get_img(2, w, h) # 获取512×512图像给AI
outputs = models[0].forward(img)
# ... 后处理 ...
disp.set_graph_rect(x1, y1, x2, y2, 3, 1, color) # 在层3画框
disp.set_graph_word(x, y, text, 3, 1, color) # 在层3写字
关键点:
- 使用
srcampy.bind()实现摄像头到显示的硬件直通 - 使用显示层3(
disp.display(3, ...))作为OSD层画检测框 - AI推理使用512×512的图像,显示使用1920×1080
2.2 适配DSI分辨率
我一开始的想法很简单:把官方代码复制过来,修改分辨率参数适配5寸屏(800×480),应该就能跑。
创建了 mipi_camera_dsi_ai_hdmi_style.py,主要修改:
disp_w = 800,disp_h = 480- 摄像头输出改为
[w, 800], [h, 480]
然后运行…失败了。
三、第一次踩坑:照搬HDMI代码行不通
3.1 问题现象
运行后报错,主要是显示初始化失败。查看日志发现:
ERROR: Display resolution 800x480 not supported by HDMI
等等,我明明用的是DSI屏,为什么还在检查HDMI支持的分辨率?
3.2 原因分析
仔细看了 srcampy.Display() 的实现,发现问题:
disp.display(0, ...)默认绑定到主显示输出- RDK X5默认HDMI为主输出,即使DSI屏已连接
- 官方代码中
disp_w, disp_h = get_display_res()获取的是HDMI分辨率列表
DSI屏的800×480不在HDMI支持的分辨率列表里,所以初始化失败。
3.3 解决思路
有两个选择:
- 继续使用硬件层显示:需要深入了解
srcampy如何指定DSI输出 - 改用OpenCV显示:绕过硬件层,用OpenCV创建窗口显示
考虑到时间成本,我决定先尝试方案2——用OpenCV显示。这个方案在树莓派和其他嵌入式平台上都很常用,应该可行。
四、第二次尝试:OpenCV显示方案
4.1 方案设计
改用OpenCV后的架构:
MIPI摄像头 → VIO获取NV12帧 → 转换为BGR → OpenCV显示
↓
AI推理(512×512)
↓
后处理得检测框坐标
↓
OpenCV画框 → imshow显示
4.2 NV12转BGR的关键代码
MIPI摄像头输出的是NV12格式,需要转换成BGR才能用OpenCV显示:
def nv12_to_bgr(nv12_data, width, height):
"""NV12转BGR"""
try:
yuv420sp = np.frombuffer(nv12_data, dtype=np.uint8).reshape((height + height//2, width))
return cv2.cvtColor(yuv420sp, cv2.COLOR_YUV2BGR_NV12)
except:
return None
这里有一个坑:摄像头返回的图像可能有stride对齐。官方IMX219配置下,1920×1080的图像实际数据大小是3110400字节(1920×1080×1.5),但800×480的图像可能是576000字节(800×480×1.5)。需要根据实际数据大小判断分辨率。
4.3 第一次运行OpenCV版本
创建了 mipi_camera_dsi_opencv_draw.py,最开始的时候,遇到过运行时可能会遇到画面条纹/花屏的问题:
这是典型的 NV12 转换错误现象。
4.3.1 问题分析与修复
问题原因:
- 程序期望获取 800×480 的图像用于显示
- 但实际上摄像头返回的是 1920×1080 的原始分辨率
- NV12 数据大小 = 3110400 字节(1920×1080×1.5)
- 如果按 800×480 解析,会导致数据错位,产生条纹/花屏
解决方案:根据实际数据大小动态判断分辨率
def nv12_to_bgr(nv12_data, width, height):
"""NV12 转 BGR - 处理步长对齐"""
try:
total_size = len(nv12_data)
expected_size = int(width * height * 1.5)
if total_size == expected_size:
# 无对齐,直接解析
stride = width
else:
# 有步长对齐,计算实际步长
stride = (width + 63) // 64 * 64
y_size_aligned = stride * height
uv_size_aligned = stride * height // 2
# 如果大小不匹配,尝试32字节对齐
if total_size != y_size_aligned + uv_size_aligned:
stride = (width + 31) // 32 * 32
# 创建YUV420sp数组并转换
if stride == width:
yuv420sp = np.frombuffer(nv12_data, dtype=np.uint8).reshape((height + height//2, width))
else:
# 有对齐,需要重新排列数据...
yuv_data = np.frombuffer(nv12_data, dtype=np.uint8)
y_size_aligned = stride * height
uv_size_aligned = stride * height // 2
y_full = yuv_data[:y_size_aligned].reshape((height, stride))
y = y_full[:, :width]
uv_full = yuv_data[y_size_aligned:y_size_aligned + uv_size_aligned].reshape((height//2, stride))
uv = uv_full[:, :width]
yuv420sp = np.zeros((height + height//2, width), dtype=np.uint8)
yuv420sp[:height, :] = y
yuv420sp[height:, :] = uv
return cv2.cvtColor(yuv420sp, cv2.COLOR_YUV2BGR_NV12)
except Exception as e:
print(f"[ERROR] NV12转换错误: {e}")
return None
# 主循环中根据实际数据大小判断分辨率
img = cam.get_img(use_channel, DISP_W, DISP_H)
actual_size = len(img)
if actual_size == 3110400: # 1920x1080 NV12
frame = nv12_to_bgr(img, 1920, 1080)
frame = cv2.resize(frame, (DISP_W, DISP_H))
elif actual_size == 576000: # 800x480 NV12
frame = nv12_to_bgr(img, DISP_W, DISP_H)
关键修复点:
- 获取图像后先检查
len(img)的实际大小 - 如果是 3110400 字节(1920×1080×1.5),按 1920×1080 解析,然后缩放到 800×480
- 如果是 576000 字节(800×480×1.5),直接按 800×480 解析
- 处理 stride 对齐问题(通常对齐到 64 或 32 字节)
修复后画面恢复正常:
4.4 OpenCV画框测试
在确认视频显示正常后,需要验证 OpenCV 的画框功能是否正常。这个测试程序会在画面上绘制三个不同颜色的矩形框:
# 画红色矩形框 (左上角)
cv2.rectangle(frame, (50, 50), (250, 250), (0, 0, 255), 3)
cv2.putText(frame, "Red Box", (50, 40),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
# 画绿色矩形框 (右下角)
cv2.rectangle(frame, (550, 280), (750, 430), (0, 255, 0), 3)
cv2.putText(frame, "Green Box", (550, 270),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
# 画蓝色矩形框 (中间)
cv2.rectangle(frame, (300, 150), (500, 350), (255, 0, 0), 3)
cv2.putText(frame, "Blue Box", (300, 140),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
运行结果:
可以看到:
- 左上角红色框,标注 “Red Box”
- 中间蓝色框,标注 “Blue Box”
- 右下角绿色框,标注 “Green Box”
- 左上角显示实时 FPS(约30帧)
画框功能正常,说明 OpenCV 显示方案完全可行。接下来加入AI检测代码…
五、第三次踩坑:AI后处理返回 score: nan
5.1 问题现象
加入AI推理代码后,运行结果让人崩溃:
[DEBUG] Result: "fcos_result": [{"bbox":[6.25,3.75,6.25,3.75],"score":nan,"id":0,"name":"person"},...
score: nan—— 置信度是NaN(不是数字)bbox坐标异常 —— 6.25, 3.75, 6.25, 3.75(几乎是一个点)
AI模型明明加载成功了,推理也没有报错,为什么后处理结果不对?
5.2 排查过程
第一步:检查模型输出
# 调试模型输出数据类型
for i, out in enumerate(outputs):
arr = np.array(out.buffer)
print(f"Output {i}: dtype={arr.dtype}, shape={arr.shape}")
输出:
Output 0: dtype=int32, shape=(1, 64, 64, 80), quantiType=2
Output 1: dtype=int32, shape=(1, 32, 32, 80), quantiType=2
...
模型输出是int32类型,量化类型为2(有量化),这符合预期。数据本身看起来正常。
第二步:检查后处理代码
FCOS后处理需要把模型输出的15个tensor(5个stride × 3个分支)传递给libpostprocess.FcosdoProcess函数。
关键点在于如何设置output_tensors结构体:
output_tensors[i].sysMem[0].virAddr = ctypes.cast(
outputs[i].buffer.ctypes.data_as(ctypes.POINTER(ctypes.c_int32)),
ctypes.c_void_p)
这里我犯了一个错误:一开始直接用了outputs[i].ctypes.data_as(...),但outputs[i]是PyDNNTensor对象,没有ctypes属性。正确的做法是访问outputs[i].buffer。
修正后问题依然存在。
第三步:对比官方代码
仔细看官方mipi_camera.py的处理方式:
# 官方代码
output_array = []
for item in outputs:
output_array.append(item.buffer)
# 然后传递output_array[i]到后处理
我之前的代码直接使用了outputs[i].buffer,理论上应该一样。但官方代码把buffer提取到了单独的列表中,我尝试同样处理…
问题依然存在!
第四步:深夜灵光一闪——结构体定义
凌晨时分,我注意到官方代码中定义的结构体:
class hbDNNTensorProperties_t(ctypes.Structure):
_fields_ = [
("validShape", hbDNNTensorShape_t),
("alignedShape", hbDNNTensorShape_t),
("tensorLayout", ctypes.c_int),
("tensorType", ctypes.c_int),
("shift", hbDNNQuantiShift_t), # 注意这里!
("scale", hbDNNQuantiScale_t),
...
]
而我的代码中:
class hbDNNTensorProperties_t(ctypes.Structure):
_fields_ = [
...
("shift", ctypes.c_void_p), # 错误!
...
]
问题找到了! shift字段的类型应该是hbDNNQuantiShift_t,而不是ctypes.c_void_p!
5.3 根本原因
shift字段用于存储量化偏移信息。当类型定义错误时:
quantiType=2(有量化)时,后处理库尝试读取shift数据- 由于类型不匹配,读取到的数据是错误的
- 导致反量化计算时出现NaN
- 最终检测分数和坐标都是无效的
5.4 修复方案
添加正确的hbDNNQuantiShift_t结构体定义:
class hbDNNQuantiShift_t(ctypes.Structure):
_fields_ = [("shiftLen", ctypes.c_int), ("shiftData", ctypes.c_char_p)]
class hbDNNTensorProperties_t(ctypes.Structure):
_fields_ = [
("validShape", hbDNNTensorShape_t),
("alignedShape", hbDNNTensorShape_t),
("tensorLayout", ctypes.c_int),
("tensorType", ctypes.c_int),
("shift", hbDNNQuantiShift_t), # 修正!
("scale", hbDNNQuantiScale_t),
("quantiType", ctypes.c_int),
("quantizeAxis", ctypes.c_int),
("alignedByteSize", ctypes.c_int),
("stride", ctypes.c_int * 8)
]
修复后重新运行…
六、柳暗花明:AI检测终于正常工作
6.1 修复后的输出
[DEBUG] Result: b'"fcos_result": [{"bbox":[15.25,38.48,361.22,508.54],"score":0.6977,"id":0,"name":"person"},...'
score: 0.6977—— 正常的置信度!bbox坐标合理 —— 有效检测框!
6.2 完整的检测流程
修复后的完整数据流:
1. 获取摄像头帧(NV12格式)
img = cam.get_img(2, 512, 512)
2. AI推理
img_np = np.frombuffer(img, dtype=np.uint8)
outputs = models[0].forward(img_np)
3. 后处理(关键修复后正常工作)
for i in range(5): # 5个stride
libpostprocess.FcosdoProcess(...)
result_str = get_Postprocess_result(...)
4. 解析结果并画框
data = json.loads(result_str[14:])
for result in data:
bbox = scale_bbox(result['bbox'], 512, 512, 800, 480)
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
cv2.putText(frame, label, (x1, y1-5), ...)
5. OpenCV显示
cv2.imshow("AI Detection", frame)
6.3 最终效果
运行 mipi_camera_dsi_ai_final.py,可以看到屏幕画面显示:
- 实时视频流畅显示(约30fps)
- 检测到的人像用红色框标注
- 检测到的领带用绿色框标注
- 左上角显示FPS和检测数量
- 5寸DSI屏上显示清晰
七、技术总结
7.1 关键API说明
hobot_vio (libsrcampy) 多媒体接口:
from hobot_vio import libsrcampy as srcampy
# Camera对象:MIPI摄像头采集
cam = srcampy.Camera()
cam.open_cam(pipe_id, sensor_id, fps,
[width_ch0, width_ch1], # 通道0/1输出宽度
[height_ch0, height_ch1], # 通道0/1输出高度
sensor_height, sensor_width) # 传感器原始分辨率
# 获取图像
img_ch0 = cam.get_img(0, width, height) # 通道0(通常给显示)
img_ch2 = cam.get_img(2, width, height) # 通道2(通常给AI,512x512)
# Display对象:显示输出
disp = srcampy.Display()
disp.display(layer, width, height) # layer: 0=视频层, 3=OSD层
hobot_dnn (pyeasy_dnn) 推理接口:
from hobot_dnn import pyeasy_dnn as dnn
# 加载模型
models = dnn.load('/path/to/model.bin')
# 获取模型信息
h, w = models[0].inputs[0].properties.shape[2:4] # 输入尺寸
quanti_type = models[0].outputs[0].properties.quanti_type # 量化类型
# 推理
outputs = models[0].forward(img_np) # 返回PyDNNTensor列表
7.2 结构体定义要点
与libpostprocess.so交互时,C结构体定义必须完全匹配:
// 官方C结构体(简化)
struct hbDNNTensorProperties_t {
hbDNNTensorShape_t validShape;
hbDNNTensorShape_t alignedShape;
int tensorLayout;
int tensorType;
hbDNNQuantiShift_t shift; // 不是void*!
hbDNNQuantiScale_t scale;
int quantiType;
...
};
Python ctypes对应:
class hbDNNQuantiShift_t(ctypes.Structure):
_fields_ = [("shiftLen", ctypes.c_int),
("shiftData", ctypes.c_char_p)]
class hbDNNTensorProperties_t(ctypes.Structure):
_fields_ = [
("validShape", hbDNNTensorShape_t),
("alignedShape", hbDNNTensorShape_t),
("tensorLayout", ctypes.c_int),
("tensorType", ctypes.c_int),
("shift", hbDNNQuantiShift_t), # 关键!
("scale", hbDNNQuantiScale_t),
("quantiType", ctypes.c_int),
...
]
7.3 踩坑清单
| 坑点 | 现象 | 解决 |
|---|---|---|
| 照搬HDMI代码 | 分辨率不支持错误 | 改用OpenCV显示 |
| NV12分辨率判断 | 转换失败/花屏 | 根据数据大小判断实际分辨率 |
| 结构体shift字段 | score: nan | 使用hbDNNQuantiShift_t而非void* |
| 数据类型传递 | AttributeError | 使用outputs[i].buffer而非outputs[i] |
| 坐标缩放 | 检测框位置不对 | 512×512缩放到800×480 |
八、开源代码
完整的调试过程和最终代码已整理:HonestQiao/rdk-x5-mipi_camera_dsi
mipi_camera_dsi/
├── mipi_camera_dsi_ai_final.py # ✅ 最终可用版本(OpenCV显示)
├── mipi_camera_dsi_complete.py # 硬件层显示版本(备用)
├── mipi_camera_dsi_opencv_draw.py # OpenCV画框测试
├── mipi_camera_dsi_v2.py # 基础视频显示
├── run_ai_final.sh # 启动脚本
└── README.md # 使用说明
快速使用
# 在RDK X5上
cd /home/sunrise/Projects/mipi_camera_dsi
./run_ai_final.sh
按 q 键退出程序。
参考链接
- RDK X5 多媒体接口说明:https://developer.d-robotics.cc/rdk_doc/Basic_Application/multi_media_sp_dev_api/multi_media_api/pydev_multimedia_api_ultra/
- MIPI摄像头使用:3.4.1 MIPI摄像头使用 | RDK DOC
- hobot-spdev源码:GitHub - HorizonRDK/hobot-spdev · GitHub
- FCOS论文:[1904.01355] FCOS: Fully Convolutional One-Stage Object Detection
- RDK X5 MIPI摄像头+AI检测+MIPI屏幕调试踩坑实录 电子工程世界:https://bbs.eeworld.com.cn/thread-1344144-1-1.html



