纹理特征提取(1):LBP
近期专注于图像分类任务的研究,并希望将纹理特征作为输入数据进行分析。已有较为系统的相关研究已经出现,然而为了深入理解这一领域,系统梳理一遍仍能带来新的收获。
LBP(Local Binary Patterns):
基本算法 即对一个像素及其周边像素的值进行比较处理:若其周边像素的值小于中心像素的值,则将其设为0;反之,则设为1。进而根据这些0-1分布情况生成二进制特征矩阵。最终会得到一个纹理特征图。然而通常不会直接使用该纹理图像作为训练数据源而是采用该区域的灰度直方图进行统计分析。具体而言我们需要计算每个灰度级别的出现频率并基于此构建特征向量。在实现过程中我们会将纹理图像分割成多个小网格区域对每个区域内的图像特性进行分析假设在x和y方向上分别划分为3段和4段,则整个纹理图像被划分为12个小网格区域。在此基础上每种小网格区域内的灰度信息会被单独提取并用于构建完整的训练数据集
在实现过程中通常采用圆形邻域。然而,在其中的像素位置上可能存在缺失值的情况,因此不得不采用双线性插值方法进行计算。代码:
// 圆形邻域LBP
Mat Pre::circleNeighbor() {
Mat result;
result.create(img.rows - radius * 2, img.cols - radius * 2, img.type());
result.setTo(0);
for (int n = 0; n<neighbors; n++)
{
// sample points
float x = static_cast<float>(radius * cos(2.0*CV_PI*n / static_cast<float>(neighbors)));//这一步没有细看 找采样点坐标
float y = static_cast<float>(-radius * sin(2.0*CV_PI*n / static_cast<float>(neighbors)));
// relative indices 下面是双线性插值操作
int fx = static_cast<int>(floor(x));
int fy = static_cast<int>(floor(y));
int cx = static_cast<int>(ceil(x));
int cy = static_cast<int>(ceil(y));
// fractional part
float ty = y - fy;
float tx = x - fx;
// set interpolation weights
float w1 = (1 - tx) * (1 - ty);
float w2 = tx * (1 - ty);
float w3 = (1 - tx) * ty;
float w4 = tx * ty;
// iterate through your data
for (int i = radius; i < img.rows - radius; i++)
{
for (int j = radius; j < img.cols - radius; j++)
{
// calculate interpolated value
float t = static_cast<float>(w1*img.at<uchar>(i + fy, j + fx) + \
w2*img.at<uchar>(i + fy, j + cx) + \
w3*img.at<uchar>(i + cy, j + fx) + \
w4*img.at<uchar>(i + cy, j + cx));
// floating point precision, so check some machine-dependent epsilon
result.at<uchar>(i - radius, j - radius) += \
((t > img.at<uchar>(i, j)) || (std::abs(t - img.at<uchar>(i, j)) < std::numeric_limits<float>::epsilon())) << n;
}
}
}
return result;
}
显然可以看出该方法并不具备旋转不变性。然而,在实际应用中我们通常采用一种称为旋转无关定位比对法(Rotation Invariant LBP), 其核心思想是通过循环右移运算(>>)来获取特征向量,并利用其方向特性消除由于图像旋转带来的影响。具体来说,在计算过程中我们首先对输入图像进行归一化处理以消除尺度变化的影响随后通过计算相邻区域之间的对比度特征并结合这些特征进行匹配从而实现对目标物体位置和形状信息的有效提取与识别
// 将圆形邻域LBP应用到旋转不变
void Pre::RILBP() {
imgLbp = circleNeighbor();
int val=0, temp=0;
numPatterns = pow(2, neighbors);//灰度值的个数,与邻域中采样点的个数有关,8则为256
uchar RITable[numPatterns];
for (int i = 0; i<numPatterns; i++)
{
val = i;
for (int j = 0; j<neighbors-1; j++)
{
temp = i >> 1;//循环右移,直到找到最小值
if (val>temp)
{
val = temp;
}
}
RITable[i] = val;//将新的LBP存在一个数组里面
}
for (int i = 1; i<imgLbp.rows - 1; i++)
{
for (int j = 1; j<imgLbp.cols - 1; j++)
{
imgLbp.at<uchar>(i, j) = RITable[imgLbp.at<uchar>(i, j)];
}
}
return;
}
此外, 还有一种称为 等价模式 LBP 的方法. 考虑到 8 位二进制码元能产生高达 256 种不同的灰度值, 这样的数量实在太多以至于难以直接处理. 通过等价转换方法, 我们能够将复杂情况简化为仅包含 58 种模式类型. 该算法的核心在于计算跳跃次数, 其中两次跳跃及以下被视为同一类模式. 跳跃次数计算从最后一位开始至首位结束. 总共有 P(P-1) + 2 种可能的组合方式, 其中 P 表示采样点的数量. 为什么是这个式子呢?
Ojala等人研究表明,在真实场景中,绝大多数LBP(Local Binary Patterns)模式其转换次数不超过两次相邻数值的变化。为此,Ojala将其定义为:当某个LBP所对应的循环二进制数在其循环过程中从低值跃升至高值或反之亦不超过两次变化时,该二进制序列即被定义为一个等价模态类别。例如,序列全零状态(即全零序列)具有零次变化;而像序列五位转置后的情况则包含一次上升变化及由于循环关系导致的一次下降变化;类似地,像序列七位转置后的情况则包含了两次上下交替的变化。值得注意的是,除了上述两类模态之外,其余则被归入另一类别,统称为混合模态类别
