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;
}
我的经验: 硬件同步虽然可靠,但有些 sensor 不支持。这时候就得靠软件补偿了。我曾在某款国产 sensor 上遇到过 VSYNC 信号抖动,最后不得不加了一层时间戳对齐。

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;
}
注意: 软件同步的精度受限于时间戳的精度。如果驱动层的时间戳不准,那上层再怎么对齐也没用。我曾经踩过这个坑,最后发现是 sensor 驱动里用了 jiffies 而不是 ktime。

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);
    }
}
避坑指南: 预启动会额外消耗功耗。我曾经在某个项目里预启动了所有 camera,结果手机发热严重。后来改成只预启动相邻的一个 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, &params);
    camera_set_ae_params(dst_camera, &params);
    // 让目标 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]);
        }
    }
}
注意: 降帧不能降得太狠。我曾经把预览降到 15fps,结果用户反馈画面卡顿。后来改成动态调整,只在带宽紧张时短暂降帧,带宽恢复后立刻升回去。

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);
}
我的经验: 空洞填充不能填太多。我曾经把大面积空洞都填了,结果深度图看起来平滑,但实际物体的边缘全糊了。后来改成只填充小空洞(面积小于 10 个像素),大空洞留给上层算法处理。

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 优化:动态积分时间 + 后处理 + 对齐
多摄像头协同优化知识体系 多摄同步 • 硬件 VSYNC 同步 • 软件时间戳对齐 • 曝光同步控制 • 帧对齐补偿 切换延迟优化 • 预启动策略 • 参数快速传递 • ISP pipeline 复用 • 画面平滑过渡 多路 Stream 并发 • 带宽预算计算 • 动态带宽分配 • 降帧策略 • 内存复用 ToF 深度优化 • 动态积分时间 • 深度图后处理 • 空洞填充 • RGB 对齐投影 优化目标与指标 🎯 帧同步误差 < 1ms(硬件) / < 5ms(软件) 🎯 切换延迟 < 100ms(用户无感知) 🎯 多路并发带宽利用率 > 85% 🎯 ToF 帧率 > 25fps(动态场景)
公众号:蓝海资料掘金营,微信deep3321