摘要:本文主要描述ORB算法原理以及opencv中ORB算法的实现。
关键字:ORB,FAST,BRIEF
ORB算法ICCV论文:ORB:an efficient alternative to sift or surf。
ORB(Oriented Fast and Rotated BRIEF)是一种快速局部特征点提取和快速计算局部特征点描述子算法。该算法分为两个部分:特征点提取和特征点描述子提取。其中特征掉提取是根据FAST改进而来的oFAST;而特征点描述子提取是根据BRIEF(Binary Robust IndependentElementary Feature)特征描述算法改进而来。
O R B = O r i e n t e d F a s t ( k e y p o i n t ) + R o t a t e d B R I E F ( d e s c r i p t o r ) ORB=Oriented\quad Fast(keypoint)+Rotated\quad BRIEF(descriptor) ORB=OrientedFast(keypoint)+RotatedBRIEF(descriptor)
FAST
FAST算法计算性能比较好但是不具备尺度不变性,旋转不变性以及检测到的特征点没有旋转方向。oFAST正是针对这些缺点改进FAST而来。
FAST:Features from Accelerated Segment Test
FAST:Features from Accelerated Segment Test (FAST)
FAST在进行特征点提取是根据当前点邻域内的点的差值作为特征点的筛选标准,它假定如果一个点与周围邻域内足够多的点的差值够大则认为是一个特征点:
上面的筛选方式是FAST初版的筛选方式,但是该方式有一些基本的缺陷:
问题1,2可以通过机器学习的方式来解决,问题3通过极大值抑制解决。
机器学习的解决方式是针对应用场景训练一个决策树,利用该决策树进行筛选:
Machine learning for high-speed corner detection
ID3 Descision Tree
问题3可以用非极大值抑制来解决:
oFAST
oFAST采用的是FAST-9,邻域半径为9。
oFAST使用多尺度金字塔解决FAST不具备尺度不变性的问题,使用矩来确定特征点的方向,来解决其不具备旋转不变形的问题。
多尺度金字塔就是将给定的图像以一个给定的尺度进行缩放,来生成多层不同尺度的图像 I 1 , I 2 , . . . , I n I_1,I_2,...,I_n I1,I2,...,In。这些图像每张图像的尺寸不同但是相邻两层图像间的宽高比例相同。然后针对多个尺度的图像应用FAST算法筛选特征点。
oFAST使用强度质心(intensity centroid,强度质心假设角的强度偏离其中心,并且该向量可用于估算方向)来确定特征点的方向,即计算特征点以 r r r为半径范围内的质心,特征点坐标到质心形成一个向量作为特征点的方向。矩的定义
m p q = ∑ x , y ∈ r x p y q I ( x , y ) m_{pq}=\sum_{x,y\in r}x^p y^q I(x,y) mpq=x,y∈r∑xpyqI(x,y)
矩的质心为(这里计算是限定的 r r r的邻域内):
C = ( m 10 m 00 , m 01 m 00 ) C=(\frac{m_{10}}{m_{00}},\frac{m_{01}}{m_{00}}) C=(m00m10,m00m01)
特征点的方向为:
θ = a t a n 2 ( m 01 , m 10 ) \theta=atan2(m_{01},m_{10}) θ=atan2(m01,m10)
oFast会使用Harris角点检测对筛选出的特征点进行角点度量,然后根据度量值将特征点排序,最后取出top-N得到目标特征点。
BRIEF
BRIEF: Binary Robust Independent Elementary Features⋆
BRIEF是一种对已经检测到的特征点进行描述的算法,该算法生成一种二进制描述子来描述已知的特征点。这些描述子可以用来进行特征点匹配等操作。BRIEF摒弃了利用区域直方图描述特征点的传统方法,采用二进制、位异或运算处理,大大的加快了特征点描述符建立的速度,同时也极大的降低了特征点匹配的时间,是一种非常快速的特征点描述子算法。
BRIEF的目标是得到一个二进制串,该串描述了特征点的特性。描述子生成的方式:
上面第二步,选取点的采样方式可以为:
BRIEF存在不具备旋转不变性,不具备尺度不变性和对噪声敏感的问题。
steered BRIEF
rBRIEF通过将筛选的特征点旋转一定的角度再计算对应的描述子来得到旋转不变性。比如点集 S S S:
S = ( x 1 , . . . , x n y 1 , . . . , y n ) \begin{equation} S=\left( \begin{array}{ll} x_1,...,x_n\\ y_1,...,y_n \end{array}\right) \end{equation} S=(x1,...,xny1,...,yn)
通过旋转矩阵 R θ R_{\theta} Rθ(这一角度并不是固定的而是预先以2π/30为增量构建的模式查找表获取的)旋转得到 S θ S_{\theta} Sθ然后再点集 S θ S_{\theta} Sθ上计算描述子:
S θ = R θ S S_{\theta}=R_{\theta}S Sθ=RθS
上述方式虽然能够得到旋转不变性但是这种方式计算得到的描述子在不同特征点之间的区分度不是很大。因此为了解决该问题,进一步采用统计学习的方式来重新选择点的集合。
// nfeatures 期望检测到的特征点数
// scaleFactor 多尺度金字塔的尺度
// nlevels 金字塔层数
// edgeThreshold 边界阈值,靠近边缘edgeThreshold以内的像素是不检测特征点的
// firstLevel 金字塔层的起始层
// wat_k 产生rBRIEF描述子点对的个数
// scoreType 角点响应函数,默认使用HARRIS对角点特征进行排序
// patchSize 邻域大小
// fastThreshold
Ptr<ORB> ORB::create(int nfeatures, float scaleFactor, int nlevels, int edgeThreshold, int firstLevel, int wta_k, int scoreType, int patchSize, int fastThreshold){
CV_Assert(firstLevel >= 0);
return makePtr<ORB_Impl>(nfeatures, scaleFactor, nlevels, edgeThreshold, firstLevel, wta_k, scoreType, patchSize, fastThreshold);
}
ORB的具体实现直接看detectAndCompute
就可以,FeatureExtractor
也提供了detect
和compute
的接口,但是区别不大:
/** Compute the ORB_Impl features and descriptors on an image
* @param img the image to compute the features and descriptors on
* @param mask the mask to apply
* @param keypoints the resulting keypoints
* @param descriptors the resulting descriptors
* @param do_keypoints if true, the keypoints are computed, otherwise used as an input
* @param do_descriptors if true, also computes the descriptors
*/
void ORB_Impl::detectAndCompute( InputArray _image, InputArray _mask, std::vector<KeyPoint>& keypoints, OutputArray _descriptors, bool useProvidedKeypoints )
下面的代码在opencv中的modules/feature2d/orb.cpp
中,并且删除了部分不影响实际流程的宏处理比如OpenCL,以及部分Assert等相关宏。
void ORB_Impl::detectAndCompute( InputArray _image, InputArray _mask, std::vector<KeyPoint>& keypoints,
OutputArray _descriptors, bool useProvidedKeypoints ) {
CV_Assert(patchSize >= 2);
bool do_keypoints = !useProvidedKeypoints;
bool do_descriptors = _descriptors.needed();
if( (!do_keypoints && !do_descriptors) || _image.empty() )
return;
//ROI handling
const int HARRIS_BLOCK_SIZE = 9;
int halfPatchSize = patchSize / 2;
// sqrt(2.0) is for handling patch rotation
int descPatchSize = cvCeil(halfPatchSize*sqrt(2.0));
int border = std::max(edgeThreshold, std::max(descPatchSize, HARRIS_BLOCK_SIZE/2))+1;
Mat image = _image.getMat(), mask = _mask.getMat();
if( image.type() != CV_8UC1 )
cvtColor(_image, image, COLOR_BGR2GRAY); //ORB采用点邻域半径内的像素和当前像素的灰度差来表征特征点,颜色信息对于特征描述意义不大
int i, level, nLevels = this->nlevels, nkeypoints = (int)keypoints.size();
bool sortedByLevel = true;
std::vector<Rect> layerInfo(nLevels); //当前层图像的rect相对于firstLevel层的位置,如果当前层的图像比firstLevel层大则采用当前图源尺寸
std::vector<int> layerOfs(nLevels); //当前层的数据index,用于OpenCL优化
std::vector<float> layerScale(nLevels); //保存从第0层到第nLevels层缩放的尺度
Mat imagePyramid, maskPyramid;
float level0_inv_scale = 1.0f / getScale(0, firstLevel, scaleFactor); //第一层的缩放尺度
size_t level0_width = (size_t)cvRound(image.cols * level0_inv_scale); //第一层图像的宽度
size_t level0_height = (size_t)cvRound(image.rows * level0_inv_scale); //第一层图像的高度
Size bufSize((int)alignSize(level0_width + border*2, 16), 0); //第一层图像对齐后的rowbyteperrow,+2border是为方便做HARRIS做的padding
int level_dy = (int)level0_height + border*2;
Point level_ofs(0, 0);
//这里只计算每一层的尺寸并不进行实际的金字塔层的构造工作,下面的一系列计算都是为了防止越界,以及金字塔特征层是在一张Mat上而不是一个vector
for( level = 0; level < nLevels; level++ )
{
//getScale->(float)std::pow(scaleFactor, (double)(level - firstLevel))金字塔层第0层就是原图,层数越高,图像的尺度多样性越丰富,图像越小
float scale = getScale(level, firstLevel, scaleFactor);
layerScale[level] = scale;
float inv_scale = 1.0f / scale; //从这里能够看出第一层图像是最大的然后逐层减小
Size sz(cvRound(image.cols * inv_scale), cvRound(image.rows * inv_scale)); //当前层图像的尺寸
Size wholeSize(sz.width + border*2, sz.height + border*2); //padding边界尺寸之后的实际尺寸
if( level_ofs.x + wholeSize.width > bufSize.width )
{//如果当前行存在足够的空间则优先放在当前行,由于图像是从大到小生成的y方向始终是足够的
level_ofs = Point(0, level_ofs.y + level_dy);
level_dy = wholeSize.height;
}
Rect linfo(level_ofs.x + border, level_ofs.y + border, sz.width, sz.height);
layerInfo[level] = linfo;
layerOfs[level] = linfo.y*bufSize.width + linfo.x;
level_ofs.x += wholeSize.width;
}
bufSize.height = level_ofs.y + level_dy;
imagePyramid.create(bufSize, CV_8U);
if( !mask.empty() )
maskPyramid.create(bufSize, CV_8U);
Mat prevImg = image, prevMask = mask;
// Pre-compute the scale pyramids 构造金字塔层,下
for (level = 0; level < nLevels; ++level)
{//从上面计算的尺寸中取出相应的尺寸缩放图像生成对应的金字塔
Rect linfo = layerInfo[level];
Size sz(linfo.width, linfo.height);
Size wholeSize(sz.width + border*2, sz.height + border*2);
Rect wholeLinfo = Rect(linfo.x - border, linfo.y - border, wholeSize.width, wholeSize.height);
Mat extImg = imagePyramid(wholeLinfo), extMask;
Mat currImg = extImg(Rect(border, border, sz.width, sz.height)), currMask;
if( !mask.empty() )
{
extMask = maskPyramid(wholeLinfo);
currMask = extMask(Rect(border, border, sz.width, sz.height));
}
// Compute the resized image 这里生成的图像边界padding的并不是一个固定的像素值而是原图像的镜像,这样能够最大程度的保留图像的信息避免额外引入的噪声的干扰
if( level != firstLevel )
{
resize(prevImg, currImg, sz, 0, 0, INTER_LINEAR_EXACT);
if( !mask.empty() )
{
resize(prevMask, currMask, sz, 0, 0, INTER_LINEAR_EXACT);
if( level > firstLevel )
threshold(currMask, currMask, 254, 0, THRESH_TOZERO);
}
copyMakeBorder(currImg, extImg, border, border, border, border,
BORDER_REFLECT_101+BORDER_ISOLATED);
if (!mask.empty())
copyMakeBorder(currMask, extMask, border, border, border, border,
BORDER_CONSTANT+BORDER_ISOLATED);
}
else
{
copyMakeBorder(image, extImg, border, border, border, border,
BORDER_REFLECT_101);
if( !mask.empty() )
copyMakeBorder(mask, extMask, border, border, border, border,
BORDER_CONSTANT+BORDER_ISOLATED);
}
if (level > firstLevel)
{
prevImg = currImg;
prevMask = currMask;
}
}
// Get keypoints, those will be far enough from the border that no check will be required for the descriptor
computeKeyPoints(imagePyramid, uimagePyramid, maskPyramid,
layerInfo, ulayerInfo, layerScale, keypoints,
nfeatures, scaleFactor, edgeThreshold, patchSize, scoreType, useOCL, fastThreshold);
if( do_descriptors )
{
int dsize = descriptorSize(); //固定dsize=32byte就是特征的256位的二进制位描述
nkeypoints = (int)keypoints.size();
if( nkeypoints == 0 )
{
_descriptors.release();
return;
}
_descriptors.create(nkeypoints, dsize, CV_8U); //这里创建的是一个nxndesc大小的矩阵表示n个特征点的特征描述
std::vector<Point> pattern;
const int npoints = 512; //256对点
Point patternbuf[npoints];
const Point* pattern0 = (const Point*)bit_pattern_31_; //bit_pattern_32_是通过PASCAL 2006数据集训练好的pattern,这部分数据是硬编码。是patchSize为31时,在其邻域内选取的256个点对
if( patchSize != 31 )
{
pattern0 = patternbuf;
makeRandomPattern(patchSize, patternbuf, npoints); //如果patchSize不为31的话会随机生成筛选的点对,而且随机数的种子是固定的保证筛选的位置都是固定的,不然同一张图不同时刻输入得到特征点不一样
}
CV_Assert( wta_k == 2 || wta_k == 3 || wta_k == 4 );
if( wta_k == 2 )
std::copy(pattern0, pattern0 + npoints, std::back_inserter(pattern));
else
{
int ntuples = descriptorSize()*4;
initializeOrbPattern(pattern0, pattern, ntuples, wta_k, npoints);
}
for( level = 0; level < nLevels; level++ )
{
// preprocess the resized image
Mat workingMat = imagePyramid(layerInfo[level]);
//对每一层的图像进行高斯模糊
//boxFilter(working_mat, working_mat, working_mat.depth(), Size(5,5), Point(-1,-1), true, BORDER_REFLECT_101);
GaussianBlur(workingMat, workingMat, Size(7, 7), 2, 2, BORDER_REFLECT_101);
}
{
Mat descriptors = _descriptors.getMat();
computeOrbDescriptors(imagePyramid, layerInfo, layerScale,
keypoints, descriptors, pattern, dsize, wta_k);
}
}
}
生成的金字塔是一整张大图,所有的图像会放到一张图像中如果几张小图能够放在同一行则是有限并排存放而不是总是按照顺序存放,如下:
static void computeKeyPoints(const Mat& imagePyramid, const UMat& uimagePyramid, const Mat& maskPyramid, const std::vector<Rect>& layerInfo, const UMat& ulayerInfo, const std::vector<float>& layerScale, std::vector<KeyPoint>& allKeypoints, int nfeatures, double scaleFactor, int edgeThreshold, int patchSize, int scoreType, bool useOCL, int fastThreshold ) {
int i, nkeypoints, level, nlevels = (int)layerInfo.size();
std::vector<int> nfeaturesPerLevel(nlevels);
// fill the extractors and descriptors for the corresponding scales
float factor = (float)(1.0 / scaleFactor);
float ndesiredFeaturesPerScale = nfeatures*(1 - factor)/(1 - (float)std::pow((double)factor, (double)nlevels));
int sumFeatures = 0;
//从这里能够看出检测出的n个特征点是按比例分配在多层金字塔上的,其总数为nfeatures
for( level = 0; level < nlevels-1; level++ ) {
nfeaturesPerLevel[level] = cvRound(ndesiredFeaturesPerScale);
sumFeatures += nfeaturesPerLevel[level];
ndesiredFeaturesPerScale *= factor; //第一层的特征点数最多,越往上越少
}
nfeaturesPerLevel[nlevels-1] = std::max(nfeatures - sumFeatures, 0);
// Make sure we forget about what is too close to the boundary
//edge_threshold_ = std::max(edge_threshold_, patch_size_/2 + kKernelWidth / 2 + 2);
// pre-compute the end of a row in a circular patch
int halfPatchSize = patchSize / 2;
std::vector<int> umax(halfPatchSize + 2);
int v, v0, vmax = cvFloor(halfPatchSize * std::sqrt(2.f) / 2 + 1);
int vmin = cvCeil(halfPatchSize * std::sqrt(2.f) / 2);
for (v = 0; v <= vmax; ++v)
umax[v] = cvRound(std::sqrt((double)halfPatchSize * halfPatchSize - v * v));
// Make sure we are symmetric
for (v = halfPatchSize, v0 = 0; v >= vmin; --v){
while (umax[v0] == umax[v0 + 1])
++v0;
umax[v] = v0;
++v0;
}
allKeypoints.clear();
std::vector<KeyPoint> keypoints;
std::vector<int> counters(nlevels);
keypoints.reserve(nfeaturesPerLevel[0]*2);
for( level = 0; level < nlevels; level++ )
{
int featuresNum = nfeaturesPerLevel[level];
Mat img = imagePyramid(layerInfo[level]);
Mat mask = maskPyramid.empty() ? Mat() : maskPyramid(layerInfo[level]);
// Detect FAST features, 20 is a good threshold
{使用fast进行角点检测
Ptr<FastFeatureDetector> fd = FastFeatureDetector::create(fastThreshold, true);
fd->detect(img, keypoints, mask);
}
// 移除边界附近的角点特征
KeyPointsFilter::runByImageBorder(keypoints, img.size(), edgeThreshold);
// Keep more points than necessary as FAST does not give amazing corners
KeyPointsFilter::retainBest(keypoints, scoreType == ORB_Impl::HARRIS_SCORE ? 2 * featuresNum : featuresNum);
nkeypoints = (int)keypoints.size();
counters[level] = nkeypoints;
float sf = layerScale[level];
for( i = 0; i < nkeypoints; i++ ){
keypoints[i].octave = level;
keypoints[i].size = patchSize*sf;
}
std::copy(keypoints.begin(), keypoints.end(), std::back_inserter(allKeypoints));
}
std::vector<Vec3i> ukeypoints_buf;
nkeypoints = (int)allKeypoints.size();
if(nkeypoints == 0){
return;
}
Mat responses;
// 使用Harris筛选FAST检测到的特征点
if( scoreType == ORB_Impl::HARRIS_SCORE ) {
HarrisResponses(imagePyramid, layerInfo, allKeypoints, 7, HARRIS_K);
std::vector<KeyPoint> newAllKeypoints;
newAllKeypoints.reserve(nfeaturesPerLevel[0]*nlevels);
int offset = 0;
for( level = 0; level < nlevels; level++ ) {
int featuresNum = nfeaturesPerLevel[level];
nkeypoints = counters[level];
keypoints.resize(nkeypoints);
std::copy(allKeypoints.begin() + offset,
allKeypoints.begin() + offset + nkeypoints,
keypoints.begin());
offset += nkeypoints;
//cull to the final desired level, using the new Harris scores.
KeyPointsFilter::retainBest(keypoints, featuresNum);
std::copy(keypoints.begin(), keypoints.end(), std::back_inserter(newAllKeypoints));
}
std::swap(allKeypoints, newAllKeypoints);
}
nkeypoints = (int)allKeypoints.size();
{ //通过质心计算当前特征点的角度fastAtan2((float)m_01, (float)m_10);
ICAngles(imagePyramid, layerInfo, allKeypoints, umax, halfPatchSize);
}
for( i = 0; i < nkeypoints; i++ ) { //将每一层经过缩放过的角点还原到原图上
float scale = layerScale[allKeypoints[i].octave];
allKeypoints[i].pt *= scale;
}
}
下面两张图分别是用Harris过滤前的结果,第二张图的角点比第一张图稍微少一些但是不明显:
下面是经过计算的点的角度,图中线相对于x轴正方向的夹角就是角点的角度:
计算特征点的描述子:
static void
computeOrbDescriptors( const Mat& imagePyramid, const std::vector<Rect>& layerInfo,
const std::vector<float>& layerScale, std::vector<KeyPoint>& keypoints,
Mat& descriptors, const std::vector<Point>& _pattern, int dsize, int wta_k )
{
int step = (int)imagePyramid.step;
int j, i, nkeypoints = (int)keypoints.size();
for( j = 0; j < nkeypoints; j++ ){
const KeyPoint& kpt = keypoints[j];
const Rect& layer = layerInfo[kpt.octave];
float scale = 1.f/layerScale[kpt.octave];
float angle = kpt.angle;
angle *= (float)(CV_PI/180.f);
float a = (float)cos(angle), b = (float)sin(angle);
const uchar* center = &imagePyramid.at<uchar>(cvRound(kpt.pt.y*scale) + layer.y, cvRound(kpt.pt.x*scale) + layer.x);
float x, y;
int ix, iy;
const Point* pattern = &_pattern[0];
uchar* desc = descriptors.ptr<uchar>(j);
#if 1//这个宏就是将点根据pattern旋转对应的角度得到新的xy
#define GET_VALUE(idx) \
(x = pattern[idx].x*a - pattern[idx].y*b, \
y = pattern[idx].x*b + pattern[idx].y*a, \
ix = cvRound(x), \
iy = cvRound(y), \
*(center + iy*step + ix) )
#else
//省略
#endif
if( wta_k == 2 ){
for (i = 0; i < dsize; ++i, pattern += 16){
int t0, t1, val;
t0 = GET_VALUE(0); t1 = GET_VALUE(1);
val = t0 < t1; //这里比较明显就是比较一个点对的大小关系来作为当前二进制位的具体值
t0 = GET_VALUE(2); t1 = GET_VALUE(3);
val |= (t0 < t1) << 1;
t0 = GET_VALUE(4); t1 = GET_VALUE(5);
val |= (t0 < t1) << 2;
t0 = GET_VALUE(6); t1 = GET_VALUE(7);
val |= (t0 < t1) << 3;
t0 = GET_VALUE(8); t1 = GET_VALUE(9);
val |= (t0 < t1) << 4;
t0 = GET_VALUE(10); t1 = GET_VALUE(11);
val |= (t0 < t1) << 5;
t0 = GET_VALUE(12); t1 = GET_VALUE(13);
val |= (t0 < t1) << 6;
t0 = GET_VALUE(14); t1 = GET_VALUE(15);
val |= (t0 < t1) << 7;
desc[i] = (uchar)val;
}
}
else if( wta_k == 3 ){//下面wta_k=3和4同理
for (i = 0; i < dsize; ++i, pattern += 12){
int t0, t1, t2, val;
t0 = GET_VALUE(0); t1 = GET_VALUE(1); t2 = GET_VALUE(2);
val = t2 > t1 ? (t2 > t0 ? 2 : 0) : (t1 > t0);
t0 = GET_VALUE(3); t1 = GET_VALUE(4); t2 = GET_VALUE(5);
val |= (t2 > t1 ? (t2 > t0 ? 2 : 0) : (t1 > t0)) << 2;
t0 = GET_VALUE(6); t1 = GET_VALUE(7); t2 = GET_VALUE(8);
val |= (t2 > t1 ? (t2 > t0 ? 2 : 0) : (t1 > t0)) << 4;
t0 = GET_VALUE(9); t1 = GET_VALUE(10); t2 = GET_VALUE(11);
val |= (t2 > t1 ? (t2 > t0 ? 2 : 0) : (t1 > t0)) << 6;
desc[i] = (uchar)val;
}
}
else if( wta_k == 4 ){
for (i = 0; i < dsize; ++i, pattern += 16){
int t0, t1, t2, t3, u, v, k, val;
t0 = GET_VALUE(0); t1 = GET_VALUE(1);
t2 = GET_VALUE(2); t3 = GET_VALUE(3);
u = 0, v = 2;
if( t1 > t0 ) t0 = t1, u = 1;
if( t3 > t2 ) t2 = t3, v = 3;
k = t0 > t2 ? u : v;
val = k;
t0 = GET_VALUE(4); t1 = GET_VALUE(5);
t2 = GET_VALUE(6); t3 = GET_VALUE(7);
u = 0, v = 2;
if( t1 > t0 ) t0 = t1, u = 1;
if( t3 > t2 ) t2 = t3, v = 3;
k = t0 > t2 ? u : v;
val |= k << 2;
t0 = GET_VALUE(8); t1 = GET_VALUE(9);
t2 = GET_VALUE(10); t3 = GET_VALUE(11);
u = 0, v = 2;
if( t1 > t0 ) t0 = t1, u = 1;
if( t3 > t2 ) t2 = t3, v = 3;
k = t0 > t2 ? u : v;
val |= k << 4;
t0 = GET_VALUE(12); t1 = GET_VALUE(13);
t2 = GET_VALUE(14); t3 = GET_VALUE(15);
u = 0, v = 2;
if( t1 > t0 ) t0 = t1, u = 1;
if( t3 > t2 ) t2 = t3, v = 3;
k = t0 > t2 ? u : v;
val |= k << 6;
desc[i] = (uchar)val;
}
}
else
CV_Error( Error::StsBadSize, "Wrong wta_k. It can be only 2, 3 or 4." );
#undef GET_VALUE
}
}