11、摄像头与视觉系统:车载摄像头架构、Camera HAL 适配、环视系统原理
车载摄像头,是智能座舱和自动驾驶的“眼睛”。
我最早接触车载摄像头时,觉得它跟手机摄像头差不多。后来踩了不少坑才发现——完全不是一回事。手机摄像头拍糊了可以重拍,车载摄像头如果延迟一帧,可能就撞上了。
这一章,我们聊聊Android Automotive里的摄像头架构、Camera HAL怎么适配,以及环视系统到底是怎么拼出来的。
11.1 车载摄像头架构概览
先看整体架构。车载摄像头系统,从硬件到应用,大致分四层:
- 物理摄像头:包括鱼眼、广角、长焦、红外等。每颗摄像头都有自己的ISP(图像信号处理器)。
- Camera HAL:硬件抽象层,负责把不同厂商的摄像头驱动统一成Android Camera API。
- Camera Service:Android系统的相机服务,管理摄像头生命周期、数据流。
- 应用层:环视、DMS、行车记录仪等。
我画了一张架构图,帮你理清关系:
嗯,这张图你保存好。后面讲HAL适配和环视原理时,都会用到这个分层。
11.2 Camera HAL 适配
Android Automotive的Camera HAL,核心是HAL3接口。它跟手机上的Camera HAL3基本一致,但有几个关键差异点。
11.2.1 多摄像头同步
车载场景下,经常需要同时打开多颗摄像头。比如环视系统,要同时采集前后左右四路视频。
手机上的Camera HAL,同时打开两路摄像头已经很吃力了。车载上要同时开4路、6路甚至8路。这对HAL的buffer管理、ISP带宽都是巨大考验。
我建议你在HAL层实现一个同步帧回调机制。具体做法:
// 伪代码:多摄像头同步帧回调
class MultiCameraSyncCallback {
std::map<int, CameraMetadata> frameMap;
int expectedCount = 4; // 四路摄像头
void onFrameReceived(int cameraId, CameraMetadata metadata) {
frameMap[cameraId] = metadata;
if (frameMap.size() == expectedCount) {
// 所有摄像头帧都到齐了,统一回调
notifyAllFramesReady(frameMap);
frameMap.clear();
}
}
}
我在项目中遇到过一个问题:某款车的前置摄像头帧率是30fps,后置是25fps。同步时总是丢帧。后来发现是ISP的时钟源不一致。解决办法是让所有摄像头共用同一个PLL时钟源。
11.2.2 物理摄像头ID映射
Android系统通过cameraId来识别摄像头。但车载上,摄像头的位置是固定的——前视、后视、左视、右视。
我建议在HAL层建立一个物理位置到cameraId的映射表:
| 物理位置 | cameraId | FOV | 分辨率 | 帧率 |
|---|---|---|---|---|
| 前视 | 0 | 120° | 1920x1080 | 30fps |
| 后视 | 1 | 140° | 1920x1080 | 30fps |
| 左视 | 2 | 190°(鱼眼) | 1280x720 | 25fps |
| 右视 | 3 | 190°(鱼眼) | 1280x720 | 25fps |
这样做的好处是,上层应用不需要关心硬件细节。环视系统只需要说“我要左视的画面”,HAL自动映射到cameraId 2。
11.2.3 性能优化
车载摄像头对延迟非常敏感。从摄像头采集到画面显示,延迟不能超过100ms,否则驾驶员会感觉画面“卡顿”。
我常用的优化手段:
- 零拷贝buffer:使用dma-buf在HAL和显示层之间直接传递,避免内存拷贝。
- 降低分辨率:环视系统不需要4K,720p足够。分辨率越低,处理越快。
- 硬件加速:把图像拼接、畸变校正放到GPU或DSP上做。
11.3 环视系统原理
环视系统,也叫360°全景影像。它把车身四周的摄像头画面拼接成一个鸟瞰图,让驾驶员在车内就能看到车周围的情况。
11.3.1 核心流程
环视系统的处理流程,我总结为四步:
- 图像采集:从4颗(或更多)鱼眼摄像头获取原始画面。
- 畸变校正:鱼眼镜头有严重的桶形畸变,需要校正成平面图像。
- 透视变换:把校正后的图像投影到鸟瞰视角。
- 图像拼接:把四幅鸟瞰图拼成一幅完整的360°画面。
我画了一个流程图:
11.3.2 畸变校正原理
鱼眼摄像头的畸变校正,说白了就是“把弯曲的直线拉直”。
数学上,它需要一个畸变映射表。这个表记录了原始图像每个像素点在校正后图像中的位置。
// 畸变校正映射表生成(简化版)
for (int y = 0; y < dstHeight; y++) {
for (int x = 0; x < dstWidth; x++) {
// 将目标像素映射到归一化坐标
float nx = (x - cx) / fx;
float ny = (y - cy) / fy;
// 应用鱼眼畸变模型
float r = sqrt(nx*nx + ny*ny);
float theta = atan(r);
float theta_d = theta * (1 + k1*theta*theta + k2*theta*theta*theta*theta);
// 计算原始图像中的坐标
float srcX = cx + fx * (theta_d / r) * nx;
float srcY = cy + fy * (theta_d / r) * ny;
mapX[y][x] = srcX;
mapY[y][x] = srcY;
}
}
这段代码看起来有点复杂,但核心思想很简单:每个目标像素,都能在原始图像中找到对应的位置。如果找不到,就插值。
11.3.3 图像拼接与融合
四幅鸟瞰图拼在一起,接缝处会有明显的亮度差异和几何错位。这就需要图像融合。
我常用的融合方法有两种:
- 线性融合:在重叠区域,按距离加权平均。简单,但效果一般。
- 多频段融合:把图像分解成低频和高频,分别融合。效果好,但计算量大。
实际项目中,我建议用线性融合 + 亮度均衡。先对每路图像做全局亮度调整,让四幅图的亮度一致,再用线性融合处理接缝。这样计算量小,效果也够用。
11.4 避坑指南
最后,分享几个我踩过的坑:
- 摄像头标定:每颗摄像头的内参和外参必须精确标定。我曾经因为标定板没贴平,导致拼接画面错位了5个像素。后来重新标定才解决。
- 帧同步:四路摄像头如果帧不同步,拼接画面会出现“撕裂”。我建议用硬件触发信号(如PWM)来同步所有摄像头的曝光时刻。
- 热管理:车载摄像头在夏天暴晒后,温度可能达到85°C以上。ISP的性能会下降,甚至丢帧。一定要做降频保护。
嗯,这一章的内容就到这里。摄像头和视觉系统是智能座舱的核心,也是坑最多的地方。希望我的经验能帮你少走弯路。
公众号:蓝海资料掘金营,微信deep3321