13、多摄像头协同优化:多摄同步问题、主摄与广角/长焦切换延迟、多路Stream并发性能、深度摄像头与ToF性能优化
多摄像头,现在已经是手机的标配了。但说实话,多摄带来的麻烦,远比想象中多。
我最早接触多摄项目时,以为就是多挂几个 sensor 而已。结果一跑起来,各种同步错位、切换卡顿、带宽爆炸。嗯,今天我们就来聊聊这些坑,以及怎么填。
13.1 多摄同步问题:帧同步与曝光同步
多摄协同,第一个要解决的就是同步。你想想看,主摄和广角同时拍一张照片,如果两边的帧没对齐,那合成出来的画面就是重影的。
13.1.1 硬件同步方案
最靠谱的方式,是用硬件信号来同步。我习惯让主摄 sensor 输出 VSYNC 信号,作为从摄的触发源。这样两边就能严格对齐。
// 硬件同步配置示例
static void configure_sync_master(sensor_t *sensor) {
sensor->hw_sync_mode = HW_SYNC_MASTER;
sensor->vsync_output_enable = true;
sensor->vsync_output_pin = GPIO_MASTER_VSYNC;
}
static void configure_sync_slave(sensor_t *sensor) {
sensor->hw_sync_mode = HW_SYNC_SLAVE;
sensor->vsync_input_pin = GPIO_SLAVE_VSYNC;
sensor->trigger_mode = TRIGGER_EXTERNAL;
}
13.1.2 软件同步方案
如果硬件不支持,那就用软件来凑。核心思路是:每个 sensor 都打上硬件时间戳,然后在 HAL 层做对齐。
// 软件时间戳对齐
typedef struct {
int64_t timestamp_ns;
buffer_handle_t buffer;
int camera_id;
} frame_entry_t;
static int64_t align_frames(frame_entry_t *frames, int count) {
// 找到时间戳最接近的一组帧
int64_t min_diff = INT64_MAX;
int best_idx = 0;
for (int i = 0; i < count; i++) {
int64_t diff = abs(frames[i].timestamp_ns - frames[0].timestamp_ns);
if (diff < min_diff) {
min_diff = diff;
best_idx = i;
}
}
return frames[best_idx].timestamp_ns;
}
13.2 主摄与广角/长焦切换延迟
切换延迟,说白了就是用户从 1x 变到 2x 时,画面卡住的那一下。这个延迟如果超过 200ms,用户就能明显感觉到。
13.2.1 延迟来源分析
| 延迟环节 | 典型耗时 | 优化方向 |
|---|---|---|
| sensor 启动 | 50-100ms | 预启动、保持 stream |
| ISP 初始化 | 30-50ms | 复用 pipeline |
| AE/AWB 收敛 | 100-200ms | 预置参数、快速收敛 |
| 画面平滑过渡 | 50-100ms | 帧插值、渐变 |
13.2.2 预启动策略
我个人的做法是:在用户手指滑动变焦条时,就提前把目标 camera 启动起来。不要等到切换那一刻才去开 sensor。
// 预启动策略
void on_zoom_gesture_start(float target_zoom) {
int target_camera = get_camera_for_zoom(target_zoom);
if (target_camera != current_camera) {
// 提前启动目标 camera,但不输出帧
camera_prestart(target_camera);
}
}
void on_zoom_gesture_end(float final_zoom) {
int target_camera = get_camera_for_zoom(final_zoom);
if (target_camera != current_camera) {
// 此时目标 camera 已经 ready,直接切换
camera_switch(target_camera);
}
}
13.2.3 快速 AE/AWB 收敛
切换后画面闪烁,多半是 AE/AWB 在重新收敛。我建议的做法是:在切换前,把当前 camera 的 AE/AWB 参数直接传给目标 camera。
// 快速 AE 参数传递
typedef struct {
int exposure_time;
int analog_gain;
int digital_gain;
} ae_params_t;
void transfer_ae_params(int src_camera, int dst_camera) {
ae_params_t params;
camera_get_ae_params(src_camera, ¶ms);
camera_set_ae_params(dst_camera, ¶ms);
// 让目标 camera 从接近的参数开始收敛
camera_ae_lock(dst_camera, false);
}
13.3 多路 Stream 并发性能
多摄同时跑,最头疼的就是带宽和内存。你想想看,主摄 4K 30fps,广角 1080p 30fps,再加上预览和拍照,带宽一下子就爆了。
13.3.1 带宽预算
我习惯先算一笔账。以某平台为例,ISP 总带宽是 2.4GB/s,每个 stream 的带宽需求如下:
| Stream 类型 | 分辨率 | 帧率 | 带宽需求 |
|---|---|---|---|
| 主摄预览 | 1920x1080 | 30fps | ~180MB/s |
| 主摄拍照 | 4000x3000 | 15fps | ~360MB/s |
| 广角预览 | 1920x1080 | 30fps | ~180MB/s |
| ToF 深度 | 640x480 | 30fps | ~40MB/s |
13.3.2 动态带宽分配
当带宽不够时,就得做取舍。我的策略是:优先保证预览 stream 的流畅度,拍照 stream 可以降帧。
// 动态带宽分配
void dynamic_bandwidth_alloc() {
int total_bandwidth = get_isp_bandwidth();
int used_bandwidth = 0;
// 先分配预览 stream
for (int i = 0; i < preview_stream_count; i++) {
int bw = get_stream_bandwidth(preview_streams[i]);
if (used_bandwidth + bw <= total_bandwidth) {
used_bandwidth += bw;
} else {
// 降帧处理
set_stream_fps(preview_streams[i], 15);
}
}
// 再分配拍照 stream
for (int i = 0; i < capture_stream_count; i++) {
int bw = get_stream_bandwidth(capture_streams[i]);
if (used_bandwidth + bw <= total_bandwidth) {
used_bandwidth += bw;
} else {
// 拍照 stream 可以延迟
queue_capture_request(capture_streams[i]);
}
}
}
13.4 深度摄像头与 ToF 性能优化
ToF 摄像头,说白了就是靠发射红外光,测量反射时间来计算深度。它的性能瓶颈主要在两个方面:帧率和精度。
13.4.1 ToF 帧率优化
ToF 的帧率受限于调制频率和积分时间。我建议的做法是:根据场景动态调整积分时间。
// 动态积分时间调整
void tof_adaptive_integration() {
int current_distance = get_tof_distance();
int integration_time;
if (current_distance < 1000) {
// 近距离,减少积分时间
integration_time = 100; // us
} else if (current_distance < 3000) {
integration_time = 200;
} else {
// 远距离,增加积分时间
integration_time = 400;
}
set_tof_integration_time(integration_time);
}
13.4.2 深度图后处理
ToF 出来的深度图,噪声很大。我习惯做三步处理:去噪、填充空洞、边缘增强。
// 深度图后处理
void depth_post_process(uint16_t *depth_map, int width, int height) {
// 1. 中值滤波去噪
median_filter(depth_map, width, height, 3);
// 2. 空洞填充
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (depth_map[y * width + x] == 0) {
depth_map[y * width + x] = interpolate_depth(depth_map, x, y, width);
}
}
}
// 3. 边缘增强
sobel_edge_enhance(depth_map, width, height);
}
13.4.3 ToF 与 RGB 对齐
ToF 和 RGB 的分辨率不同,视角也不同。对齐是个麻烦事。我建议用标定好的内外参来做投影。
// ToF 到 RGB 的投影
void tof_to_rgb_projection(uint16_t *tof_depth, uint8_t *rgb,
int tof_w, int tof_h, int rgb_w, int rgb_h) {
// 使用标定好的投影矩阵
float proj_matrix[3][3] = {
{0.98, 0.01, 12.5},
{0.01, 0.99, 8.3},
{0.0, 0.0, 1.0}
};
for (int ty = 0; ty < tof_h; ty++) {
for (int tx = 0; tx < tof_w; tx++) {
// 投影到 RGB 坐标
float rx = proj_matrix[0][0] * tx + proj_matrix[0][1] * ty + proj_matrix[0][2];
float ry = proj_matrix[1][0] * tx + proj_matrix[1][1] * ty + proj_matrix[1][2];
// 双线性插值
int rx_int = (int)rx;
int ry_int = (int)ry;
if (rx_int >= 0 && rx_int < rgb_w && ry_int >= 0 && ry_int < rgb_h) {
rgb[ry_int * rgb_w + rx_int] = tof_depth[ty * tof_w + tx];
}
}
}
}
- 多摄同步:硬件同步优先,软件同步做补偿
- 切换延迟:预启动 + 参数传递,把延迟压到 100ms 以内
- 多路并发:算好带宽,动态分配,优先保预览
- ToF 优化:动态积分时间 + 后处理 + 对齐