RK3576 没有 FEC 也能做畸变矫正:LDC、GDC、IDC 三条路线的实践总结

AI编程·六月创作之星博客挑战赛 10w+人浏览 1.6k人参与


📺 B站 嵌入式孙老师博主个人介绍

📘 博主书籍-京东购买链接*:Yocto项目实战教程

📘 加博主微信,进技术交流群jerrydev


RK3576 没有 FEC 也能做畸变矫正:LDC、GDC、IDC 三条路线的实践总结

最近在 RK3576 平台上做 4K60 广角镜头畸变校正,过程中反复接触了几个容易混淆的概念:LDC、GDC、IDC、FEC,还有 rkaiq、Rockit、GStreamer、OpenCL、DMABUF 等等。

刚开始看文档和代码时,很容易形成一个误解:是不是必须有 FEC 硬件模块,才能做广角畸变矫正?

经过实际验证后,我的结论是:

RK3576 即使没有明确的 FEC 模块,也依然可以做畸变矫正。关键是要选对路线。

在 RK3576 上,目前可走的路线大概有三类:

1. ISP / rkaiq LDC
2. Rockit / MPI GDC
3. IDC / OpenCL LUT

它们都和“畸变校正”有关,但工作位置、适用范围、调参方式和工程落地难度并不一样。

这篇文章主要记录我这次调试 RK3576 + IMX585 + 4K60 畸变校正链路的学习和实践过程,重点总结 LDC、GDC、IDC 的区别,以及最终为什么选择 IDC 路线完成 4K60 校正和编码。


在这里插入图片描述

1. 先说清楚:FEC、LDC、GDC、IDC 不要混在一起

在广角摄像头项目里,常见几个词:

名称常见理解主要作用
FECFish Eye Correction鱼眼/超广角几何校正
LDCLens Distortion Correction镜头畸变校正
GDCGeometric Distortion Correction几何畸变校正
IDCImage Distortion Correction图像畸变校正,通常基于 LUT / mesh
LSCLens Shading Correction镜头阴影/暗角校正

这里最容易混的是 LDC、GDC、IDC 和 FEC。

从本质上说,FEC、LDC、GDC、IDC 都可以被理解为一种“几何映射”问题。它们做的事情不是简单增强画质,而是改变像素的位置。

比如原始广角图像中,墙上的直线可能是弯的:

输入图像:直线被镜头拉弯
输出图像:通过坐标映射,把线尽量拉直

其核心逻辑可以抽象为:

output(x, y) = input(map_x(x, y), map_y(x, y))

也就是说,输出图像上的每个点,都要从输入图像上的某个位置采样。这个 map_x / map_y 就是校正的核心。

而 Lens Shading Correction 不一样。LSC 解决的是暗角、亮度不均、颜色不均,它不是改像素坐标,而是改像素增益:

output_pixel(x, y) = input_pixel(x, y) * gain(x, y)

所以 LSC 不属于这篇讨论的畸变校正路线。它应该由 ISP / rkaiq 的 lens shading 模块处理,而不是用 IDC 去做。


2. RK3576 没有 FEC,是否就不能做畸变校正?

答案是否定的。

是否能做畸变校正,不能只看有没有一个叫 FEC 的硬件模块。更关键的是平台是否提供了其他可用的几何重映射能力。

在 RK3576 上,我实际接触到的路线有三条:

路线一:rkaiq / ISP LDC
路线二:Rockit / MPI GDC
路线三:IDC / OpenCL LUT

它们的目标类似,都是为了处理镜头几何畸变,但工作层级不同。

整体关系可以这样理解:

Sensor
  ↓
MIPI CSI-2
  ↓
ISP / rkaiq
  ↓
V4L2 输出 NV12
  ↓
后处理模块:GDC / IDC / GPU / OpenCL
  ↓
编码 / 显示 / 保存

其中 LDC 更靠近 ISP / rkaiq;GDC 属于 Rockit/MPI 侧的几何处理接口;IDC 则更像一个独立的后处理模块,通过 mesh/LUT 做坐标映射。


3. LDC:ISP/rkaiq 路线,适合和 ISP 调参绑定

LDC 通常可以理解为 ISP 体系中的 Lens Distortion Correction。它一般和 rkaiq 相关。

rkaiq 主要负责 ISP 初始化和 3A 控制,比如:

AE:自动曝光
AWB:自动白平衡
AF:自动对焦
LSC:镜头阴影校正
CCM:颜色矩阵
NR:降噪
Sharpness:锐化
LDC:镜头畸变校正

它工作的位置比较靠前,通常在 ISP 输出图像之前或 ISP 处理链路中。

逻辑图:

Sensor RAW
  ↓
ISP
  ↓
rkaiq 控制 AE / AWB / LSC / LDC / NR / Sharpness
  ↓
/dev/videoX 输出 NV12 / RGB

LDC 的优点是:

1. 和 ISP 图像质量流程结合紧密
2. 适合产品级调参
3. 对 AE/AWB/ISP 输出的整体一致性更友好
4. 如果官方 tuning 工具链完整,落地会比较正规

但它也有局限:

1. 参数依赖 rkaiq tuning 文件和 ISP 配置
2. 调参不够直观
3. 如果缺少标定工具,参数很难靠手工调整准确
4. 对广角大畸变场景,是否足够灵活要看具体平台支持

我在 RK3576 上接触 LDC 时,能看到相关接口和参数,例如校正强度、中心点、畸变系数等。但从工程角度看,如果没有完整标定工具,仅靠手动试参数,很容易出现“有变化,但不准确”的问题。

比如画面能被拉伸,边缘也有变化,但要做到稳定、准确、可复现的畸变矫正,还是需要标准标定流程。

所以我的理解是:

LDC 更适合走 ISP tuning 的正规路线。如果有官方标定工具和完整参数,LDC 是合理选择;如果没有工具,仅靠手动调参,效率会比较低。


4. GDC:Rockit/MPI 路线,接口清晰,但调参依赖模型

GDC 可以理解为 Geometric Distortion Correction,在 Rockchip 的 MPI / Rockit 体系里可以看到相关接口。

比如常见接口类似:

RK_MPI_GDC_AddCorrectionTask
RK_MPI_GDC_AddCorrectionExTask

GDC 的位置通常在 ISP 输出之后,属于后处理几何校正。

逻辑图:

V4L2 / VI 输出图像
  ↓
Rockit / MPI GDC
  ↓
校正后图像
  ↓
VENC / VO / File

GDC 的优点:

1. 属于 Rockit / MPI 体系,接口比较工程化
2. 可以和 Rockchip 媒体处理模块组合
3. 理论上适合 VI -> GDC -> VENC 这种硬件媒体链路
4. 不一定依赖 GStreamer

但我在实际使用中遇到的问题是:GDC 的调参模型和我当前的畸变校正目标并不是完全匹配。

我测试过类似 mount、view、radius、zoom 等参数,能明显看到画面变化:

画面缩放变化
边缘拉伸变化
视场角变化

但是要做到针对 110 度左右广角镜头的准确畸变校正,仅靠这些高层参数不够直观。

GDC 更像是提供一套预定义几何模型或几何变换接口。它可以做几何变化,但不等于可以直接替代基于标定结果的精确 mesh/LUT 校正。

所以我的总结是:

GDC 能做几何校正,但如果目标是根据具体镜头标定结果做精确畸变矫正,最好要有明确的标定工具和参数生成流程。


5. IDC:基于 mesh/LUT 的后处理路线,更适合当前验证

这次最终跑通 4K60 的路线是 IDC。

IDC 的核心思路是:先根据镜头模型或标定结果生成 mesh/LUT 文件,然后运行时根据这个 LUT 做图像重映射。

我的理解是:

mesh/LUT 文件描述了输出图像每个区域应该从输入图像哪里采样
IDC 根据 mesh/LUT 执行坐标映射
输出校正后的 NV12 图像

逻辑图:

/dev/video11 NV12 3840x2160@60
  ↓
IDC input buffer
  ↓
RKALG_IDC_LUT_DoLut()
  ↓
IDC output buffer
  ↓
编码 / 保存

这条路线最关键的优势是:

1. 校正逻辑直观
2. mesh/LUT 可以独立生成和替换
3. 不强依赖 ISP tuning
4. 适合快速验证不同 FOV / 畸变校正效果
5. 可以通过 OpenCL/GPU 加速

在我的测试中,IDC 单独处理 4K60 是可以达到接近 60fps 的。真正的问题不是 IDC 算法本身,而是后面如何把 IDC 输出接到编码器。

一开始我犯过一个典型错误:虽然 IDC 输入输出已经用了 DMA buffer,但后面编码时仍然把 IDC 输出通过 CPU memcpy 拷贝到 GStreamer 的 GstBuffer 中。

旧链路是:

V4L2
  ↓
IDC
  ↓
CPU memcpy
  ↓
GstBuffer
  ↓
mpph265enc
  ↓
MKV

这一步 IDC dst -> GstBuffer 的拷贝非常慢。4K NV12 一帧大小约为:

3840 x 2160 x 1.5 ≈ 12.4 MB

每帧拷贝一次,60fps 下就是非常大的内存压力。

当时测出来的关键耗时类似:

copy_in  = 3.61 ms
idc      = 7.38 ms
copy_out = 20.91 ms

其中最慢的是 copy_out。这说明瓶颈不是 IDC 本身,而是 IDC 输出没有直接进入编码器。


6. 最终成功路线:IDC 输出 DMABUF 直接进入 GStreamer 编码器

最终跑通的路线是:

V4L2 /dev/video11
  ↓
IDC src buffer
  ↓
IDC dst DMABUF
  ↓
GstBuffer(memory:DMABuf)
  ↓
appsrc
  ↓
mpph265enc
  ↓
h265parse
  ↓
matroskamux
  ↓
filesink

核心变化是:

不再把 IDC dstImage.virAddr 拷贝到 GstBuffer
而是把 IDC dstImage.fd 包装成 GstBuffer(memory:DMABuf)

关键代码逻辑类似:

GstAllocator *allocator = gst_dmabuf_allocator_new();

int dupFd = dup(dstImage.fd[0]);

GstMemory *mem = gst_dmabuf_allocator_alloc(
    allocator,
    dupFd,
    frameSize
);

GstBuffer *buf = gst_buffer_new();
gst_buffer_append_memory(buf, mem);

gst_app_src_push_buffer(GST_APP_SRC(appsrc), buf);

这段代码的作用不是“拷贝图像数据”,而是把已有的 dma-buf fd 包装成 GStreamer 可以识别的 DMABUF memory。

appsrc 的 caps 需要使用:

video/x-raw(memory:DMABuf),format=NV12,width=3840,height=2160,framerate=60/1

我也实际验证过,虽然 gst-inspect-1.0 mpph265enc 的 sink pad 没有直接显示 memory:DMABuf,但实际 pipeline negotiation 是成功的:

mpph265enc0.GstPad:sink: caps =
video/x-raw(memory:DMABuf), format=NV12, width=3840, height=2160, framerate=60/1

这说明:

mpph265enc 实际可以接收 appsrc 传入的 DMABUF。

最终性能从原来的 31fps 提升到 60fps 以上。

实测结果:

frames=1000
seconds=16.592
fps=60.27

关键耗时变成:

copy_in     = 11.95 ms
idc         = 4.61 ms
dmabuf_wrap = 0.02 ms
qwait       = 0.01 ms

其中 dmabuf_wrap=0.02ms 说明 IDC 输出到编码输入这一步已经基本没有整帧 CPU 拷贝。


7. 当前成功链路的完整架构图

最终成功链路可以画成:

┌────────────────────────────┐
│ IMX585 Sensor               │
│ 3840x2160@60 RAW12          │
└──────────────┬─────────────┘
               │
               ▼
┌────────────────────────────┐
│ MIPI CSI-2 / RKCIF / RKISP  │
│ ISP 输出 NV12               │
└──────────────┬─────────────┘
               │
               ▼
┌────────────────────────────┐
│ V4L2 /dev/video11           │
│ NV12 3840x2160@60           │
└──────────────┬─────────────┘
               │
               ▼
┌────────────────────────────┐
│ IDC LUT / OpenCL            │
│ meshxy.bin 几何重映射        │
└──────────────┬─────────────┘
               │
               ▼
┌────────────────────────────┐
│ IDC dst DMABUF              │
│ dstImage.fd[0]              │
└──────────────┬─────────────┘
               │ 只传 fd,不拷贝图像
               ▼
┌────────────────────────────┐
│ GstBuffer(memory:DMABuf)    │
│ appsrc                      │
└──────────────┬─────────────┘
               │
               ▼
┌────────────────────────────┐
│ mpph265enc                  │
│ H.265 hardware encode       │
└──────────────┬─────────────┘
               │
               ▼
┌────────────────────────────┐
│ h265parse + matroskamux     │
│ 输出 MKV                    │
└────────────────────────────┘

一句话总结:

IDC 输出 buffer 直接作为编码器输入 buffer,避免中间 CPU 拷贝。


8. Buffer 生命周期:不能只传 fd,还要防止过早复用

DMABUF 零拷贝有一个容易忽略的问题:buffer 生命周期。

如果 IDC 写完一个 dst buffer 后,把它交给编码器,但编码器还没读完,程序又拿同一个 buffer 给 IDC 写下一帧,就可能出现:

花屏
错帧
撕裂
编码异常

所以不能简单这样做:

slot = frame_id % slot_count

正确方式应该是维护一个 free slot 队列:

freeSlotQueue
  ↓
IDC 取一个空闲 slot
  ↓
IDC 输出到 slot.fd
  ↓
slot.fd 包装成 GstBuffer
  ↓
push 给 GStreamer
  ↓
GStreamer 用完 GstBuffer
  ↓
回调函数释放 slot
  ↓
slot 回到 freeSlotQueue

在当前代码中,使用了类似 gst_mini_object_set_qdata() 的机制给 GstBuffer 绑定回收回调。

逻辑是:

gst_mini_object_set_qdata(
    GST_MINI_OBJECT(buf),
    dmabufSlotReleaseQuark(),
    ctx,
    releaseDmabufSlotToQueue
);

这表示:

当 GstBuffer 被 GStreamer 真正释放时
调用 releaseDmabufSlotToQueue()
把对应的 DMABUF slot 放回空闲队列

这一步非常关键。因为 gst_app_src_push_buffer() 返回,只代表 buffer 交给 GStreamer 了,不代表编码器已经用完这帧。

当前使用:

dmabuf slots = 12
queue depth  = 4

实测 wait_slot=0.00,说明目前 12 个 slot 足够。


9. GStreamer 中 queue 的作用

GStreamer pipeline 中经常看到 queue

appsrc ! queue ! mpph265enc ! queue ! h265parse ! matroskamux ! filesink

queue 的作用不是提高硬件性能,而是:

1. 在两个 element 之间增加缓冲
2. 创建独立线程
3. 解耦前后模块
4. 吸收短时间抖动

比如编码器偶尔慢一下,queue 可以暂时缓存几帧,避免前面的 appsrc 立刻被阻塞。

但要注意:

queue 不能解决算法太慢,也不能解决 CPU copy 太慢。

这次从 31fps 提升到 60fps,真正关键不是 queue,而是 DMABUF 零拷贝。queue 只是保证 pipeline 更稳定。


10. GStreamer、Rockit、rkaiq 三者怎么区分?

这次调试过程中,我对 RK 平台上的三套体系有了更清楚的认识。

rkaiq:负责 ISP 和图像质量

rkaiq 主要管:

AE / AWB / AF
LSC / CCM / NR / Sharpness
ISP 初始化
部分 ISP 侧 LDC / AFEC 控制

它更靠近 sensor 和 ISP。

逻辑位置:

Sensor -> ISP -> rkaiq 控制图像质量 -> V4L2 输出

GStreamer:负责多媒体 pipeline

GStreamer 管的是应用层媒体管线:

appsrc
mpph265enc
h265parse
matroskamux
filesink

它适合快速组合编码、封装、保存、推流。

当前成功路线就是 GStreamer 路线:

DMABUF -> appsrc -> mpph265enc -> MKV

Rockit / RK_MPI:原生媒体接口

Rockit / RK_MPI 是 Rockchip 原生媒体处理接口,比如:

RK_MPI_VENC_SendFrame
RK_MPI_VENC_GetStream
RK_MPI_MB_*
RK_MPI_SYS_*

它可以走更底层的 VI / VPSS / VENC / MB buffer 管理。

如果后续要完全绕开 GStreamer,输出裸 H.265,可以考虑:

IDC dst DMA/MB buffer -> RK_MPI_VENC_SendFrame -> raw .h265

不过当前这次已经通过 GStreamer DMABUF 路线跑通 4K60 MKV,因此没有必要马上切到 Rockit。


11. 三条路线对比

路线工作位置优点局限当前结论
rkaiq LDCISP / rkaiq和 ISP 图像质量链路结合好依赖 tuning 和标定工具适合正规 ISP 调参路线
Rockit GDCRockit / MPI接口工程化,适合媒体链路参数模型不一定适合精确标定可探索,但需要工具链
IDC LUTISP 后处理mesh/LUT 直观,适合快速验证需要处理 buffer 和性能问题当前成功路线

最终我这次的经验是:

如果目标是快速验证 RK3576 4K60 广角畸变校正,IDC + DMABUF + GStreamer 是一条很有效的路线。


12. 编译和依赖注意事项

使用 GStreamer DMABUF 需要包含:

#include <gst/allocators/gstdmabuf.h>

链接时必须加:

-lgstallocators-1.0

Makefile 中不能只写:

-lgstapp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0

否则会报:

undefined reference to gst_dmabuf_allocator_new
undefined reference to gst_dmabuf_allocator_alloc

正确链接应该包含:

-lgstapp-1.0
-lgstbase-1.0
-lgstallocators-1.0
-lgstreamer-1.0
-lgobject-2.0
-lglib-2.0

对应 Yocto 包一般来自:

gstreamer1.0-plugins-base
gstreamer1.0-plugins-base-dev

13. 实测命令和结果

运行命令:

./rkalg_idc_lut_4k-60fs_idc-h265-mkv \
  --mesh /data/rkalg_idc_gpu_meshxy_3840x2160_step16x8_fovscale0911.bin \
  --frames 1000 \
  --queue-buffers 4 \
  --pool-buffers 12 \
  --bps 90000000 \
  --gop 60 \
  --output /tmp/idc_4k-60fps_rate-90k.mkv

关键结果:

IDC MKV record end:
frames=1000
seconds=16.592
fps=60.27
cap=1000
idc=1000

关键耗时:

copy_in     = 11.95 ms
idc         = 4.61 ms
wait_slot   = 0.00 ms
dmabuf_wrap = 0.02 ms
qwait       = 0.01 ms

说明:

1. V4L2 到 IDC src 仍然有一次 copy
2. IDC 处理约 4.6ms
3. IDC 输出到编码输入几乎无拷贝
4. GStreamer / 编码器没有明显堵塞
5. 整体达到 4K60

14. 我这次踩过的坑

坑 1:以为 IDC 慢,实际是 copy_out 慢

一开始看到 IDC + 编码只有 31fps,很容易以为是 IDC 处理太慢。

后来加分段耗时后发现:

idc = 7ms 左右
copy_out = 20ms 左右

真正瓶颈是输出拷贝,不是 IDC。

坑 2:用了 DMA buffer,但后面又 CPU copy

IDC 的输入输出已经是 DMA buffer,但如果后面仍然用 virAddr 做 memcpy,本质上还是普通内存路径。

只有把 fd 传给下游,才是真正利用 DMABUF。

坑 3:gst-inspect 没写 DMABUF,不代表实际不支持

gst-inspect-1.0 mpph265enc 里 sink caps 没直接显示 memory:DMABuf,但实际测试:

appsrc caps="video/x-raw(memory:DMABuf),..."

可以 negotiation 成功。

所以不能只看 gst-inspect 的静态输出,还要实际跑 pipeline 验证。

坑 4:忘记链接 gstreamer-allocators

头文件能找到,不代表链接能通过。

gst_dmabuf_allocator_new()gst_dmabuf_allocator_alloc() 来自:

libgstallocators-1.0.so

Makefile 必须加:

-lgstallocators-1.0

坑 5:buffer 不能过早复用

DMABUF 不是复制数据,而是共享同一块 buffer。
因此必须等 GStreamer 用完后再回收 slot。


15. 后续还可以优化什么?

当前已经达到 4K60,但还不是全链路零拷贝。

现在仍然存在:

V4L2 buffer -> IDC src buffer

这一步 copy_in 大约 12ms。

后续如果要进一步优化,可以考虑:

1. V4L2 capture 直接使用 DMABUF
2. IDC src 直接 import VI 输出 buffer
3. 消除 copy_in
4. 形成完整 VI -> IDC -> VENC 零拷贝链路

理想最终架构:

VI DMABUF
  ↓
IDC src import
  ↓
IDC dst DMABUF
  ↓
VENC input DMABUF
  ↓
H.265

但从当前结果看,哪怕还保留一次 copy_in,只要去掉 copy_out,RK3576 也已经可以完成 4K60 畸变校正和 H.265 MKV 录制。


16. 总结

这次 RK3576 4K60 畸变校正调试,最大的收获是:

没有 FEC,不代表不能做畸变校正。

在 RK3576 上,可以根据场景选择不同路线:

ISP 调参路线:rkaiq LDC
Rockit 媒体路线:GDC / RK_MPI
后处理验证路线:IDC LUT

从工程验证角度,我最终跑通的是:

V4L2 -> IDC LUT -> DMABUF -> GStreamer mpph265enc -> MKV

成功的关键不是简单加 queue,也不是盲目增加 buffer,而是找到真正瓶颈:

IDC dst -> GstBuffer 的 CPU copy

然后用 DMABUF 把 IDC 输出 buffer 直接交给编码器:

IDC dstImage.fd -> GstBuffer(memory:DMABuf) -> mpph265enc

最终实测:

1000 frames / 16.592s = 60.27fps

这说明 RK3576 在没有 FEC 的情况下,依然可以通过 IDC/LUT 后处理路线实现 4K60 畸变校正和 H.265 录制。

对我来说,这次调试也再次说明一个问题:
在嵌入式视频链路里,性能瓶颈往往不在“算法名字”上,而在 buffer 是否真的零拷贝、模块之间是否共享同一块内存、数据是否被无意中复制了一遍


📺 B站 嵌入式孙老师博主个人介绍

📘 博主书籍-京东购买链接*:Yocto项目实战教程

📘 加博主微信,进技术交流群jerrydev


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值