雙目匹配代碼分析
原理
通過將兩個水平放置的攝像頭獲取的圖像,匹配相應的區域視差。方式有很多種,SAD是速度最快的方式,主要原理是塊匹配,計算每個搜索塊在最大視差範圍內的sad值,選擇最小的sad值的水平位移距離作爲視差輸出。
一、區準函數
主要的算法只有一個函數,這裏輸出的是兩幅圖像在x方向上的sobel變化後的圖像。並且通過查找表歸一化數據,可以去掉一些亮度影響。
static void findStereoCorrespondence( const Mat& left, const Mat& right,
Mat& disp, Mat& cost, const CvStereoBMState& state,
uchar* buf, int _dy0, int _dy1 )
- 創建查找表tab
for( x = 0; x < TABSZ; x++ )
tab[x] = (uchar)std::abs(x - ftzero);
得到tab(0:255) = (31:0, 1:224)
- 初始化第一列的搜索窗
for( x = -wsz2-1; x < wsz2; x++ )
{
hsad = hsad0 - dy0*ndisp;
cbuf = cbuf0 + (x + wsz2 + 1)*cstep - dy0*ndisp;
// 最大視差比搜索窗大很多的情況下,不需要判斷邊界
lptr = lptr0 + x - dy0*sstep;//std::min(std::max(x, -lofs), width-lofs-1) - dy0*sstep;//
rptr = rptr0 + x - dy0*sstep;//std::min(std::max(x, -rofs), width-rofs-1) - dy0*sstep;//
//int val;
for( y = -dy0; y < height + dy1; y++,
hsad += ndisp, cbuf += ndisp,
lptr += sstep, rptr += sstep )
{
// 左邊減去右邊差的絕對值,在最大視差範圍內計算
int lval = lptr[0];
for( d = 0; d < ndisp; d++ )
{
int diff = std::abs(lval - rptr[d]);
//cbuf[dy0+height+dy1][ndisp]
cbuf[d] = (uchar)diff; // 每一列,當前像素與對應像素差值
hsad[d] = (int)(hsad[d] + diff);//搜索窗裏sad
}
htext[y] += tab[lval]; // 計算一個搜索窗裏左圖像素-31的絕對值之和
//val = lval;
}
}
計算左圖與右圖殘差值,每個像素都減去右圖水平偏移0~disp-1個像素
for x = -wsz2-1:wsz2
cbuf(x, -dy0:height+dy1, 0:disp-1) = abs(tab(Left(x, -dy0:height+dy1))-tab(right(x:x+disp, -dy0:height+dy1)))
end
先cbuf初始一個搜索窗的殘差值,一共(wsz, H, disp)
hsad[0] = \sum_{n=-wsz2-1}^{wsz2}abs(tab(left(n))-tab(right(n)))
hsad[1] = \sum_{n=-wsz2-1}^{wsz2}abs(tab(left(n))-tab(right(n+1)))
...
hsad[disp-1] = \sum_{n=-wsz2-1}^{wsz2}abs(tab(left(n))-tab(right(n+disp-1)))
上面是計算第(-wsz2-1~wsz2)列範圍內的hsad(hsad命名意思應該是水平方向上的sad)。
htext[i] = \sum_{n=-wsz2-1}^{wsz2}tab(left(n))
i=-wsz2-1:wsz2
htext計算水平方向的紋理。
3. 初始化邊界
// initialize the left and right borders of the disparity map
for( y = 0; y < height; y++ )
{
for( x = 0; x < lofs; x++ )
dptr[y*dstep + x] = FILTERED;
for( x = lofs + width1; x < width; x++ )
dptr[y*dstep + x] = FILTERED;
}
disp[0:maxDisp-1, :] = -16
disp[W-maxDisp:W, :] = -16
- 在搜索窗裏計算sad
// 計算每列sad值,左圖像素-右圖0到最大視差偏移像素
for( y = -dy0; y < height + dy1; y++,
cbuf += ndisp, cbuf_sub += ndisp,
hsad += ndisp, lptr += sstep,
lptr_sub += sstep, rptr += sstep )
{
int lval = lptr[0];
for( d = 0; d < ndisp; d++ )
{
int diff = std::abs(lval - rptr[d]);
cbuf[d] = (uchar)diff;
hsad[d] = hsad[d] + diff - cbuf_sub[d];
}
htext[y] += tab[lval] - tab[lptr_sub[0]];
}
在前面的基礎上,計算每個水平方向的sad和text,只需要去掉第一個值再加上當前值。
- 填充分界
// fill borders
for( y = dy1; y <= wsz2; y++ )
htext[height+y] = htext[height+dy1-1];
for( y = -wsz2-1; y < -dy0; y++ )
htext[y] = htext[-dy0];
當搜索窗和上下邊界不一致時候,需要擴展大小
6. 初始化sad,方便後面判斷
// initialize sums
for( d = 0; d < ndisp; d++ )
sad[d] = (int)(hsad0[d-ndisp*dy0]*(wsz2 + 2 - dy0));
hsad = hsad0 + (1 - dy0)*ndisp;
for( y = 1 - dy0; y < wsz2; y++, hsad += ndisp )
for( d = 0; d < ndisp; d++ )
sad[d] = (int)(sad[d] + hsad[d]);
計算一個搜索窗口(21x21)內的sad,第一個循環(wsz2+2-dy0)按照設定的參數計算出來爲2,不清楚爲什麼要這麼做。
sad(0:ndisp-1) = \sum_{i=1-dy0}^{wsz2}hsad(i)(0:ndisp)
- 初始化tsum
int tsum = 0;
for( y = -wsz2-1; y < wsz2; y++ )
tsum += htext[y];
tsum = \sum_{i=-wsz2-1}^{wsz2}htext[i]
計算搜索窗口裏的紋理大小
- 匹配,找到合適的匹配窗口,計算出偏移大小
// finally, start the real processing
for( y = 0; y < height; y++ )
{
int minsad = INT_MAX, mind = -1;
hsad = hsad0 + MIN(y + wsz2, height+dy1-1)*ndisp;
hsad_sub = hsad0 + MAX(y - wsz2 - 1, -dy0)*ndisp;
for( d = 0; d < ndisp; d++ )
{
int currsad = sad[d] + hsad[d] - hsad_sub[d];
sad[d] = currsad;
if( currsad < minsad )
{
minsad = currsad;
mind = d;
}
}
tsum += htext[y + wsz2] - htext[y - wsz2 - 1];
// 紋理閾值,在21x21裏計算左圖像素與31差值的sad
// 值太小,容易出現誤匹配;值太大,匹配點變少
if( tsum < textureThreshold )
{
dptr[y*dstep] = FILTERED;
continue;
}
// 唯一性判斷,次小值不能太小
if( uniquenessRatio > 0 )
{
int thresh = minsad + (minsad * uniquenessRatio/100);
for( d = 0; d < ndisp; d++ )
{
if( sad[d] <= thresh && (d < mind-1 || d > mind+1))
break;
}
if( d < ndisp )
{
dptr[y*dstep] = FILTERED;
continue;
}
}
sad[-1] = sad[1];
sad[ndisp] = sad[ndisp-2];
int p = sad[mind+1], n = sad[mind-1];
d = 0;//p + n - 2*sad[mind] + std::abs(p - n);
// 輸出值爲16bit short類型,原值先乘265再加上一個修正值,最後除16,
// 最終擴大了16倍, 修正值在正負16之間(擴大了16倍)
dptr[y*dstep] = (short)(((ndisp - mind - 1 + mindisp)*256 + (d != 0 ? (p-n)*256/d : 0) + 15) >> 4);
costptr[y*coststep] = sad[mind];
}
將上面的分支分別分析如下:
- 計算最小sad
for( d = 0; d < ndisp; d++ )
{
int currsad = sad[d] + hsad[d] - hsad_sub[d];
sad[d] = currsad;
if( currsad < minsad )
{
minsad = currsad;
mind = d;
}
}
mind = \sum_{i=0:ndisp-1}^{ndisp}sad(i)
- 計算當前窗口的紋理值
tsum += htext[y + wsz2] - htext[y - wsz2 - 1];
// 紋理閾值,在21x21裏計算左圖像素與31差值的sad
// 值太小,容易出現誤匹配;值太大,匹配點變少
if( tsum < textureThreshold )
{
dptr[y*dstep] = FILTERED;
continue;
}
tsum如果太小,當前窗口認爲是無紋理區域,disp直接填充-16。這樣平滑區域不做判斷。
- 唯一性判斷,次小值不能太小
// 唯一性判斷,次小值不能太小
if( uniquenessRatio > 0 )
{
int thresh = minsad + (minsad * uniquenessRatio/100);
for( d = 0; d < ndisp; d++ )
{
if( sad[d] <= thresh && (d < mind-1 || d > mind+1))
break;
}
if( d < ndisp )
{
dptr[y*dstep] = FILTERED;
continue;
}
}
次小值不能太接近最小值。減少誤判,但匹配點會減少,當紋理不豐富時,這兩個地方能減少誤判,紋理變化大則影響不大。
- 輸出,增加修正值,修正值對數據大小影響不大,主要是爲了增加一些紋理區別
sad[-1] = sad[1];
sad[ndisp] = sad[ndisp-2];
int p = sad[mind+1], n = sad[mind-1];
d = 0;//p + n - 2*sad[mind] + std::abs(p - n);
// 輸出值爲16bit short類型,原值先乘265再加上一個修正值,最後除16,
// 最終擴大了16倍, 修正值在正負16之間(擴大了16倍)
dptr[y*dstep] = (short)(((ndisp - mind - 1 + mindisp)*256 + (d != 0 ? (p-n)*256/d : 0) + 15) >> 4);
costptr[y*coststep] = sad[mind];