4. 视觉感知与渲染:OpenGL ES管线、摄像头图像畸变校正、车道线/障碍物叠加渲染

这一章,我们终于要开始把摄像头看到的真实世界,和ADAS感知到的虚拟信息,在屏幕上融合到一起了。

说白了,就是让驾驶员看到“带特效的现实”。这个特效不是美颜,是车道线、障碍物框、危险预警这些关键信息。

我个人习惯把这一章叫做“ADAS显示的灵魂”。因为不管你算法多牛,如果显示出来延迟高、畸变大、叠加错位,用户只会觉得这系统不靠谱。

4.1 OpenGL ES渲染管线——你画的每一帧,都经过这条流水线

先聊聊OpenGL ES。嵌入式设备上做图形渲染,基本绕不开它。Vulkan虽然新,但OpenGL ES的生态和兼容性,在ADAS领域还是主流。

渲染管线,你可以想象成一条工厂流水线。原始数据从一头进去,经过几道工序,最后变成屏幕上的像素。

核心管线阶段(简化版):

  • 顶点着色器:处理每个顶点的位置。比如一个矩形框的四个角。
  • 图元装配:把顶点连成三角形。OpenGL ES里万物皆三角形。
  • 光栅化:把三角形变成屏幕上的像素点。这一步是硬件完成的。
  • 片段着色器:决定每个像素的颜色。纹理贴图、光照计算都在这里。
  • 测试与混合:深度测试、透明度混合。决定谁在前谁在后。

嗯,这里要注意。ADAS渲染对延迟极其敏感。我曾经在一个项目里,就因为片段着色器里多算了一个光照,导致帧率从60fps掉到45fps。驾驶员一转头,画面就感觉卡顿。所以,着色器能简则简。

下面这张图,是我自己项目里常用的渲染管线流程,你可以对照着看。

OpenGL ES 渲染管线(ADAS场景) 应用层数据 顶点着色器 图元装配 光栅化 片段着色器 测试与混合 帧缓冲区 💡 注意:ADAS渲染中,片段着色器是性能瓶颈高发区。尽量减少纹理采样次数, 避免在着色器内做复杂的三角函数运算。

4.2 摄像头图像畸变校正——让鱼眼镜头变回“人眼”

车载摄像头,尤其是广角镜头,拍出来的画面边缘是弯曲的。这就是畸变。如果不校正,你把车道线叠上去,会发现线是歪的。

畸变主要分两种:径向畸变和切向畸变。径向畸变是镜头本身造成的,像鱼眼效果。切向畸变是镜头和传感器没装平行,有点歪。

校正思路:

不是去“拉直”原始图像,而是建立一个映射表。对于输出图像的每个像素,去原始图像里找它应该取哪个位置的颜色。这个映射表,我们叫它“查表法”。

我建议你在初始化时算好这个表,运行时直接查。别在每一帧里重新算畸变公式,太慢了。

数学上,校正公式长这样(以径向畸变为例):

// 归一化坐标
x_distorted = x * (1 + k1 * r^2 + k2 * r^4 + k3 * r^6)
y_distorted = y * (1 + k1 * r^2 + k2 * r^4 + k3 * r^6)
// 其中 r^2 = x^2 + y^2

实际项目中,我们一般用OpenCV的cv::initUndistortRectifyMap先算好映射矩阵,然后传到GPU,在片段着色器里用纹理采样完成校正。

我曾经踩过一个坑:

在低端芯片上,直接在GPU里逐像素算畸变公式,结果帧率直接腰斩。后来改成预计算查表法,性能瞬间就回来了。记住,查表法是嵌入式优化的老朋友。

4.3 车道线/障碍物叠加渲染——把数据变成“看得懂”的图形

感知算法给了你一堆数据:车道线的点集、障碍物的包围盒、目标的速度和类型。你的任务是把这些数据,用图形的方式,准确、实时地叠在视频画面上。

我一般把叠加渲染分成三层:

渲染层 内容 渲染方式
底层 摄像头原始画面(已校正) 纹理贴图到全屏四边形
中间层 车道线、路沿、地面标记 线段/曲线绘制(GL_LINE_STRIP)
顶层 障碍物框、图标、文字、预警 四边形+纹理(GL_TRIANGLES)

车道线渲染,我建议用线段列表。感知算法输出的车道线通常是一系列点,你把这些点连起来就行。但要注意,车道线有虚实线之分。实线画实心,虚线画间隔。

// 伪代码:绘制车道线
glBindVertexArray(vao_lane);
glLineWidth(3.0f);  // 线宽
glDrawArrays(GL_LINE_STRIP, 0, point_count);

障碍物渲染,核心是包围盒。但别只画一个框。我习惯在框的上方加一个“头顶标签”,显示目标类型(轿车/行人/自行车)和速度。这样驾驶员一眼就能看懂。

叠加渲染的关键原则:

  • 对齐精度:虚拟物体必须和真实画面中的位置严格对齐。偏移超过5个像素,人眼就能察觉。
  • 透明度:不要遮挡太多视野。障碍物框用半透明填充,车道线用亮色但不刺眼。
  • 动态更新:目标移动时,框要平滑跟随。不要跳变,否则驾驶员会晕。

你想想看,如果一辆车从侧面过来,你的框却还在上一帧的位置,那这个显示就毫无意义了。所以,渲染线程和感知线程的数据同步,是另一个需要精心设计的地方。我一般用双缓冲队列,避免锁竞争。

好了,这一章的内容就是这些。从管线到校正,再到叠加,每一步都有坑,但也都有解法。动手写代码的时候,多想想“这个像素到底该从哪里来”,很多问题就迎刃而解了。