RDK X5 MIPI摄像头+AI检测+MIPI屏幕调试踩坑实录

前言:一个看似简单,实则坑多的任务

上一篇文章讲完了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 解决思路

有两个选择:

  1. 继续使用硬件层显示:需要深入了解 srcampy 如何指定DSI输出
  2. 改用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转换错误-条纹花屏

这是典型的 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)

关键修复点

  1. 获取图像后先检查 len(img) 的实际大小
  2. 如果是 3110400 字节(1920×1080×1.5),按 1920×1080 解析,然后缩放到 800×480
  3. 如果是 576000 字节(800×480×1.5),直接按 800×480 解析
  4. 处理 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)

运行结果:

OpenCV画框测试 - 红蓝绿三个测试框

可以看到:

  • 左上角红色框,标注 “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字段用于存储量化偏移信息。当类型定义错误时:

  1. quantiType=2(有量化)时,后处理库尝试读取shift数据
  2. 由于类型不匹配,读取到的数据是错误的
  3. 导致反量化计算时出现NaN
  4. 最终检测分数和坐标都是无效的

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,可以看到屏幕画面显示:

AI检测最终结果

  • 实时视频流畅显示(约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 键退出程序。

参考链接

2 个赞