📺 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 不要混在一起
在广角摄像头项目里,常见几个词:
| 名称 | 常见理解 | 主要作用 |
|---|---|---|
| FEC | Fish Eye Correction | 鱼眼/超广角几何校正 |
| LDC | Lens Distortion Correction | 镜头畸变校正 |
| GDC | Geometric Distortion Correction | 几何畸变校正 |
| IDC | Image Distortion Correction | 图像畸变校正,通常基于 LUT / mesh |
| LSC | Lens 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 LDC | ISP / rkaiq | 和 ISP 图像质量链路结合好 | 依赖 tuning 和标定工具 | 适合正规 ISP 调参路线 |
| Rockit GDC | Rockit / MPI | 接口工程化,适合媒体链路 | 参数模型不一定适合精确标定 | 可探索,但需要工具链 |
| IDC LUT | ISP 后处理 | 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
159

被折叠的 条评论
为什么被折叠?



