第三章 软件架构与算法:AF算法框架
自动对焦算法,说白了就是让相机自己找到最清晰的那张照片。我做了这么多年模组,见过太多硬件调得不错、算法一跑就糊的案例。嗯,这一章我们就把AF算法的骨架拆开看看。
核心观点:AF算法 = 搜索策略 + 评价函数 + 电机控制。三者缺一不可,顺序错了也不行。
3.1 搜索策略:去哪找最清晰的位置
搜索策略决定了镜头该往哪个方向走、走多远。我见过不少工程师一上来就调PID,结果搜索策略选错了,怎么调都白搭。
| 策略名称 | 原理 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| 爬山法 | 沿评价函数上升方向移动,直到峰值 | 连续对焦、视频录制 | 容易陷入局部峰值,尤其是有纹理干扰时 |
| 全局搜索 | 遍历整个对焦范围,找最大值 | 初始对焦、拍照模式 | 速度慢,功耗高,但最可靠 |
| 混合搜索 | 先全局粗搜,再爬山精搜 | 通用场景(我最推荐) | 粗搜步长要合理,否则精搜也救不回来 |
我的习惯:量产项目中我基本都用混合搜索。粗搜步长设为总行程的1/8,精搜步长设为1/64。这个比例是我试了十几个项目总结出来的,你可以先拿这个参数跑起来。
3.2 评价函数:怎么判断清不清晰
评价函数是AF算法的眼睛。它算出一个数值,数值越大代表图像越清晰。常用的有三种,我一个个说。
3.2.1 Tenengrad
基于Sobel算子,计算水平和垂直方向的梯度。效果最稳定,但计算量也最大。
// Tenengrad评价函数(C语言伪代码)
float tenengrad_eval(uint8_t *img, int w, int h) {
float sum = 0;
for (int y = 1; y < h-1; y++) {
for (int x = 1; x < w-1; x++) {
int gx = -img[(y-1)*w+(x-1)] + img[(y-1)*w+(x+1)]
-2*img[y*w+(x-1)] + 2*img[y*w+(x+1)]
-img[(y+1)*w+(x-1)] + img[(y+1)*w+(x+1)];
int gy = -img[(y-1)*w+(x-1)] -2*img[(y-1)*w+x] -img[(y-1)*w+(x+1)]
+img[(y+1)*w+(x-1)] +2*img[(y+1)*w+x] +img[(y+1)*w+(x+1)];
sum += sqrtf(gx*gx + gy*gy);
}
}
return sum / (w * h);
}
3.2.2 Laplacian
用二阶导数检测边缘。对噪声敏感,但响应速度快。我一般在低光照场景下用它。
// Laplacian评价函数
float laplacian_eval(uint8_t *img, int w, int h) {
float sum = 0;
for (int y = 1; y < h-1; y++) {
for (int x = 1; x < w-1; x++) {
int lap = 4*img[y*w+x]
- img[(y-1)*w+x] - img[(y+1)*w+x]
- img[y*w+(x-1)] - img[y*w+(x+1)];
sum += abs(lap);
}
}
return sum / (w * h);
}
3.2.3 Brenner
只计算水平方向相隔两个像素的差值。计算量最小,适合嵌入式平台。
// Brenner评价函数
float brenner_eval(uint8_t *img, int w, int h) {
float sum = 0;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w-2; x++) {
int diff = img[y*w+x] - img[y*w+x+2];
sum += diff * diff;
}
}
return sum / (w * h);
}
| 评价函数 | 计算量 | 抗噪性 | 峰值锐度 | 我推荐场景 |
|---|---|---|---|---|
| Tenengrad | 高 | 强 | 高 | 高分辨率拍照 |
| Laplacian | 中 | 弱 | 中 | 低光照视频 |
| Brenner | 低 | 中 | 低 | 嵌入式实时对焦 |
我曾经踩过的坑:在一个项目中我用了Brenner,结果在低对比度场景下评价函数曲线太平,找不到峰值。后来换成Tenengrad,问题就解决了。选评价函数不能只看计算量,要看你的目标场景。
3.3 相位检测数据处理
相位检测(PDAF)是高端模组的标配。它通过像素上的微透镜对,直接算出相位差,再换算成对焦方向。但这里有个关键——PD Pixel校准。
为什么需要校准?因为每个PD像素的灵敏度、位置都有偏差。不校准的话,算出来的相位差全是错的。
// PD Pixel校准流程(简化版)
// 1. 采集均匀光照下的PD像素响应
// 2. 计算每个PD像素的增益补偿系数
// 3. 存储校准表到模组OTP
// 相位差计算
float pd_phase_diff(uint16_t *left_pd, uint16_t *right_pd, int len) {
float sum_diff = 0;
for (int i = 0; i < len; i++) {
// 先应用校准系数
float l = left_pd[i] * gain_left[i];
float r = right_pd[i] * gain_right[i];
sum_diff += l - r;
}
return sum_diff / len; // 正值表示需要向前对焦
}
我的经验:校准表至少要在3个色温下采集(D65、A光源、TL84)。只做一个色温的话,换场景就偏了。这个坑我帮好几个客户填过。
3.4 闭环控制:PID控制器在AF中的应用
电机控制是AF的最后一公里。搜索策略告诉你往哪走,评价函数告诉你走到了没有,但真正让镜头精准停下来的,是PID控制器。
PID在AF中的角色很简单:让镜头快速、平稳地到达目标位置,不超调、不震荡。
// 位置式PID在AF中的应用
typedef struct {
float kp; // 比例系数
float ki; // 积分系数
float kd; // 微分系数
float integral;
float prev_error;
} PID_Controller;
int pid_move(PID_Controller *pid, int target_pos, int current_pos) {
float error = target_pos - current_pos;
// 比例项
float p_out = pid->kp * error;
// 积分项(带限幅)
pid->integral += error;
if (pid->integral > 1000) pid->integral = 1000;
if (pid->integral < -1000) pid->integral = -1000;
float i_out = pid->ki * pid->integral;
// 微分项
float d_out = pid->kd * (error - pid->prev_error);
pid->prev_error = error;
// 总输出
int output = (int)(p_out + i_out + d_out);
// 输出限幅(电机最大步进)
if (output > 200) output = 200;
if (output < -200) output = -200;
return output;
}
| PID参数 | 作用 | 调大后的效果 | 我常用的初始值 |
|---|---|---|---|
| Kp(比例) | 快速响应误差 | 响应快,但容易超调 | 0.5 ~ 1.0 |
| Ki(积分) | 消除稳态误差 | 消除静差,但可能震荡 | 0.01 ~ 0.05 |
| Kd(微分) | 抑制超调 | 阻尼增加,响应变慢 | 0.1 ~ 0.3 |
特别注意:积分项一定要加限幅!我见过一个项目,积分项无限累积,镜头直接冲到机械限位,咔咔响。限幅值设为电机最大行程的10%比较安全。
3.5 实战建议:从零搭建AF算法
- 先选评价函数——用Tenengrad起步,稳定后再优化计算量
- 再定搜索策略——混合搜索最稳妥,粗搜步长1/8,精搜步长1/64
- 然后做PD校准——至少3个色温,校准表存OTP
- 最后调PID——先调Kp让镜头能动,再加Kd抑制超调,最后加Ki消除静差
记住:AF算法不是调出来的,是测出来的。每个参数都要在真实场景下验证。我习惯在暗光、强光、纹理、纯色四个场景下各跑100次对焦,统计成功率和时间。达标了再量产。