自动对焦功能在当下已经成为了相机的必备功能。实现自动对焦可以通过主动或被动感知的方法。在主动式感知中,摄像机发出的单束/多束红外光或超声波信号被成像物体反射,物体到摄像机的距离是通过三角测量或者信号返回到摄像机所需要的时间来确定的。然后,根据物体到相机的距离,从一个查找表中设置焦距。主动传感的优势在于它能够在不同的光照条件下聚焦,特别是在暗光条件下。然而,在通过窗户或玻璃进行聚焦时,由于较高的红外或超声反射率,这种感知机制会遇到一定的困难。此外,聚焦区域仅限于相机上的光源或声源所指向的位置也是其另一短板。
主流的被动式自动对焦基于对比度或锐度感知,这种方法因系统简单成本低而被中低端消费级相机广泛使用。其缺点就是需要图像具有足够的对比度/锐度,否则容易设错焦距,另外就是具有更大的时间延迟。一些较昂贵的单反中可能会有专用于对焦的额外光学系统,这不在当前的讨论之列。这篇文章要讲的是基于图像的被动式自动对焦。
基于图像的被动式自动对焦技术有两个关键点,一个是图像锐度的评价,另一个是焦点(最大锐度)搜索策略。传统的锐度评价方法可以分为四种:基于梯度的评价(如绝对梯度和、平方梯度和、最大梯度和)、基于统计信息的评价(如绝对中心距、方差、局部方差直方图、灰度熵)、基于相关的评价(如滑动自相关度差值)和基于频域信息的评价(如频域幅值和、频域熵)。具体定义参见“Evaluation of Sharpness Measures and Search Algorithms for the Auto-Focusing of High Magnification Images”(记为“文献1”,论文主要为了高放大倍数的场景进行锐度指标和搜索策略的对比评价,但通常可以视作一般情况下的对比)。
上述论文对不同锐度评价函数进行了测试,得出的结论如下:
可以看出,结论是基于梯度的评价方法和基于相关的评价方法在精度和速度上具有更好的性能,且基于相关的方法响应曲线(锐度--位置曲线)斜率可调。
焦点搜索策略是自动对焦技术的另一个重点。主流的有二分法搜索(BS)、斐波那契搜索(FS)、爬山法搜索(CS)、基于规则的搜索(RS)等。二分法搜索相比大家都明白是什么意思,下面主要讲讲其他几种搜索方法。
斐波那契搜索是二分法的变种,当斐波那契数列的项数趋于无穷时,,所以斐波那契搜索又叫黄金分割搜索。它每次大概将搜索区间缩小1/3,也因此迭代次数稍高于二分法搜索,但或许也因此而获得了更好的稳定性与搜索精度。斐波那契搜索的伪代码见下图:
一般认为爬山法源于论文“An Advanced Auto-Focus System for Video Camera Using Quasi Condition Reasoning”,其基于焦点评价值(即锐度响应值)FV和死亡带宽(dead band width,d)的关系对爬山状态进行解释:
除此之外,还定义了两个指标对场景进行推理以导出不同的步进策略(式中FVO为最新FV值,ENO为FV的滑动平均值,FVD为time shifted FV,因FVD与ENO的差值 “has the same group delay as ENO”,因此推测FVD为中间时刻的FV值):
根据ENV和FL的大小可以对场景进行推测:
进而制定不同的步进策略:
其中,死亡带宽d需要同时考虑镜头焦距和镜头F数,其确定比较困难,这也是爬山法的一个缺点。
基于规则的方法源于论文“Development and real-time implementation of a rule-based auto-focus algorithm” ,其主要思想就是每次搜索前先回到起始位置开始搜索,根据锐度指标的变化情况将整个搜索区域分为起始搜索区、粗搜索区、中间搜索区和精细搜索区,如下图:
其搜索规则的伪代码如下(从头搜索到尾,伪代码只展示一次迭代的处理流程):
其中各参量的含义如下:
文献1中给出了不同搜索方法搭配基于梯度的不同锐度评价指标的性能,最后综合性能评价如下(其中BF和FF分别是BS、FS与曲线拟合结合的搜索策略,HC为Hill Climbing的缩写,即为爬山法):
可以看出,基于规则的搜索策略和基于斐波那契搜索的策略以及爬山法都具有很好的综合性能,相对而言,基于斐波那契搜索的在总步进值和稳定性上表现最好,其次是基于规则的搜索策略。
综上所述,基于梯度/基于相关的锐度评价函数搭配基于规则的搜索或者斐波那契搜索应能获得最佳的综合性能。
另外,值得一提的是,文献1中实现的是论文“Modified Fast Climbing Search Auto-focus Algorithm with Adaptive Step Size Searching Technique for Digital Camera”中的改进版爬山法搜索策略。该算法将图像分为对焦区域和失焦区域分别用于粗搜索和精细搜索,分别记为FRS和OFRS。其中,失焦区域AREA1和对焦区域AREA2(AREA1包含AREA2)示意如下:
失焦区域仅简单统计梯度超过阈值的像素个数,当像素个数增加时就继续往前搜索,减少时退回前一步转入基于对焦区域锐度评价的精细搜索。同样地,当锐度指标减小时退回前一步作为最终对焦位置。基于改进版爬山法的搜索过程示意如下:
现给出搜索部分的c++代码(相关变量与函数的声明省略。框架刚搭好,还没有硬件配合验证,欢迎捉虫):
void CAutoFocus::FocusSearch(SearchAlgo searchAlgo, SharpFuction sharpFunc)
{
MotorPosInit(); //每次对焦前,电机位置初始化
switch (searchAlgo)
{
case RS:
RSsearch();
break;
case FS:
FSsearch();
break;
case HC:
HCsearch();
break;
default:
RSsearch();
break;
}
}
void CAutoFocus::RSsearch(SharpFuction sharpFunc)
{
int iIniStep; //对应于初始区域的单次步进值
int iCoarseStep;//对应于粗搜索区的单次步进值
int iMidStep; //对应于中间搜索区的单次步进值
int iFineStep; //对应于精细搜索区的单次步进值
float fFcur; //当前锐度
float fFpre; //前一帧锐度
float fFmax; //当前最大锐度
Status stat; //搜索区状态
int iCdown;//峰点计数
int iIter; //迭代次数
int iStep = 0; //当前步进数
vector<float> vecFcur; //记录当前锐度,用于寻找锐度最大值索引
vector<int> vecStep; //记录各位置的步进值,用于寻找锐度最大值对应的步进值
//int iAddedStep; //下一步要叠加的步进值
BYTE* img=new BYTE[ImgHeight*ImgWidth];
while (iStep < m_iEndStep)
{
iIter++;
vecStep.push_back(iStep);
img = getImage();
fFcur = SharpnessMeas(img, sharpFunc);
vecFcur.push_back(fFcur);
if (iIter < 5)
{
stat = INI;
int iNextStep = min(m_iEndStep, iStep + iIniStep);
MotorMove(iNextStep - iStep);
iStep = iNextStep;
}
else
{
if (fFcur <= 0.25*fFmax)
{
stat = COARSE; //锐度过低,转入粗搜索状态
iCdown = 0;
int iNextStep = min(m_iEndStep, iStep + iCoarseStep);
MotorMove(iNextStep - iStep);
iStep = iNextStep;
}
else
{
float fDiff = fFcur - fFpre;
if (fDiff > 0.25*fFpre)
{
stat = FINE; //锐度突增,状态切换到精细搜索
iCdown = 0;
int iNextStep = min(m_iEndStep, iStep + iFineStep);
MotorMove(iNextStep - iStep);
iStep = iNextStep;
}
else if (stat == FINE && fDiff > 0)
{
iCdown = 0;
int iNextStep = min(m_iEndStep, iStep + iFineStep);
MotorMove(iNextStep - iStep);
iStep = iNextStep;
}
else if (fDiff < 0)
{
if (stat == FINE)
{
iCdown++;//维持精细搜索,下山次数加1
int iNextStep = min(m_iEndStep, iStep + iFineStep);
MotorMove(iNextStep - iStep);
iStep = iNextStep;
}
if (iCdown == 3) //下山次数达到3次,转入中段搜索状态
{
stat = MID;
iCdown = 0;
}
}
else
{
stat = MID;
iCdown = 0;
int iNextStep = min(m_iEndStep, iStep + iMidStep);
MotorMove(iNextStep - iStep);
iStep = iNextStep;
}
}
}
fFmax = max(fFmax, fFcur);
fFpre = fFcur;
}
//寻找最大锐度对应索引和步进值
int idx = 0;
for (int i = 0; i < iIter; i++)
{
if (vecFcur[i] == fFmax)
{
idx = i;
break;
}
}
int iFmaxStep = vecStep[idx];
MotorMove(iFmaxStep - m_iEndStep); //返回到最大锐度对应位置
delete[] img;
}
void CAutoFocus::FSsearch(SharpFuction sharpFunc)
{
int a=0;
int b=m_iEndStep;
int x1;
int x2;
float y1;
float y2;
float L = b - a;
int deltaL;
int flag; //1代表预计峰值在左侧区间,2代表预计峰值在右侧区间
int xCur; //当前位置的步进值
BYTE* img=new BYTE[ImgHeight*ImgWidth];
//构造斐波那契数列
float fibArr[iterFS];
fibArr[1] = fibArr[0] = 1;
for (int i=2;i<iterFS;i++)
{
fibArr[i] = fibArr[i - 1] + fibArr[i - 2];
}
//开始迭代搜索
for (int k=1;k<=iterFS;k++)
{
if (k>1)
{
L = int(L*fibArr[iterFS - k + 1] / fibArr[iterFS - k + 2]);
}
deltaL = round(L*fibArr[iterFS - k - 1] / fibArr[iterFS - k + 1]);
if (k==1)
{
x1 = deltaL;
x2 = m_iEndStep - deltaL;
//起点终点各测量一次获得初始值
MotorMove(deltaL);
img = getImage();
y1 = SharpnessMeas(img, sharpFunc);
MotorMove(x2-x1);
img = getImage();
y2 = SharpnessMeas(img, sharpFunc);
xCur = x2;
flag = 2;
}
else if (flag==1)
{
x1 = a + deltaL;
MotorMove(x1 - xCur);
xCur = x1;
img = getImage();
y1 = SharpnessMeas(img, sharpFunc);
}
else
{
x2 = b - deltaL;
MotorMove(x2 - xCur);
xCur = x2;
img = getImage();
y2 = SharpnessMeas(img, sharpFunc);
}
if (y1>y2)
{
b = x2;
x2 = x1;
flag = 1;
}
else
{
a = x1;
x1 = x2;
y1 = y2;
flag = 2;
}
}
if (flag==1)
{
MotorMove(x1 - xCur);
}
else
{
MotorMove(x2 - xCur);
}
delete[] img;
}
void CAutoFocus::HCsearch(SharpFuction sharpFunc)
{
int iTotalLevel = 4; //总共4个步进等级
int iCurLevel = 1;//当前步进等级
float fRDR_ARR[5] = { 0,1 / 8,1 / 4,1 / 2,1 }; //相对差比
int iStepArr[4] = { 1, 2, 3, 4 };//对应不同RDR的步长
int iStep=1; //对焦区域步进值
int iEPNold; //上一次的边缘点数目
int iEPNnew; //新的边缘点数目
int th = 20; //确定边缘点的阈值
BYTE* img=new BYTE[ImgHeight*ImgWidth];
img= getImage();
iEPNnew = EdgePointsNumber(img, th);
//失焦区域搜索
while (1)
{
iEPNold = iEPNnew;
MotorMove(iStepArr[iCurLevel]);
img = getImage();
iEPNnew = EdgePointsNumber(img, th);
if (iEPNnew<iEPNold)
{
MotorMove(-iStepArr[iCurLevel]); //回退
break;
}
float fRDR = float(abs(iEPNnew - iEPNold)) / max(iEPNold, iEPNnew);
int idx = 0;
for (idx = 0; idx < iTotalLevel; idx++)
{
if (fRDR>=fRDR_ARR[idx]&&fRDR<=fRDR_ARR[idx+1])
{
break;
}
}
iCurLevel = idx;
}
//对焦区域搜索
img = getImage();
float fFVold; //旧锐度值
float fFVnew=SharpnessMeas(img,sharpFunc); //新锐度值
while (true)
{
fFVold = fFVnew;
MotorMove(-iStep); //反向步进
fFVnew = SharpnessMeas(img, sharpFunc);
if (fFVnew<fFVold)
{
MotorMove(iStep); //回退
break;
}
}
delete[] img;
}