纹理特征中HOG和LBP是两大很重要的特征,利用好这两个特征的一些性质可以很好提高后续识别精度


一、原理的简单解释

LBP,是局部二值模式,最原始的LBP模式,可以很明显用下面的图描述

OPencv 获取特点定 RGB_计算机视觉

对于中心像素点,一共有8个邻域值,大于中心则取值为1,小于中心则取值为0,只有0,1两种取值,一共有2^8=256种模式,可以反映这一块区域的纹理信息;

值得注意的是,它具有旋转不变性(针对于顺时针的),也就是说无论从那个邻域值开始记录都是同样的结果;

一般操作是将所有的8位二进制数旋转直至最小值,取最小值为最终模式,这样能抗图像旋转带来的影响

原始的LBP便是通过这种模式实现的,代码比较简单,可以供参考

#include <iostream>
#include <opencv2/opencv.hpp>
#include "math.h"

using namespace std;
using namespace cv;

Mat getLBP(Mat &img){
    Mat gray_src;
    gray_src = img;
//    cvtColor(img, gray_src, COLOR_BGR2GRAY);

    //定义LBP图像的长宽,由于最外围一圈无8领域,所以长宽相比于原图各减少2
    int width = img.cols - 2;
    int hight = img.rows - 2;

    //初始化一全为0的矩阵
    Mat lbpImg = Mat::zeros(hight, width, CV_8UC1);

        for (int row = 1; row < img.rows - 1; row++)
        {
            for (int col = 1; col < img.cols - 1; col++)
            {
                uchar c = gray_src.at<uchar>(row, col);
                uchar code = 0;

                //对于八个邻域值做处理
                //|= 按位或,a |= b 和 a = a | b 等价;
                //左移<<就是将二进制的每一个数都往左移动一位,高位舍去,低位补0(超过存储上限8位的属于高位,需舍去)
                code |= (gray_src.at<uchar>(row - 1, col - 1) > c) << 7;
                code |= (gray_src.at<uchar>(row - 1, col ) > c) << 6;
                code |= (gray_src.at<uchar>(row - 1, col + 1) > c) << 5;
                code |= (gray_src.at<uchar>(row, col + 1) > c) << 4;
                code |= (gray_src.at<uchar>(row + 1, col +1) > c) << 3;
                code |= (gray_src.at<uchar>(row + 1, col ) > c) << 2;
                code |= (gray_src.at<uchar>(row + 1, col - 1) > c) << 1;
                code |= (gray_src.at<uchar>(row , col ) > c) << 0;
                //赋值操作,注意row和col是从0开始的;
                lbpImg.at<uchar>(row-1, col-1) = code;

            }
        }

    return lbpImg;
}

int main(){
    Mat src;
    src = imread("D:\\001_10\\311.bmp", 0);
    if (src.empty()) {
        printf("could not load image...\n");
        return -1;
    }

    imshow("input image", src);
    waitKey(0);
    Mat dst;
    dst = getLBP(src);
    imshow("output image", dst);
    waitKey(0);
    return 0;
}

原图进行灰度处理,才能使用imshow输出

OPencv 获取特点定 RGB_直方图_02

输出结果为(大概能看出鱼的轮廓)

OPencv 获取特点定 RGB_直方图_03

补充 CV_8UC1 的含义

CV_<bit_depth>(S|U|F)C<number_of_channels>

//每一项含义解释
bit_depth (比特数) 一般取值有8bite,16bites,32bites,64bites
					若为8,表示对于图片的每个像素点在内存空间占8位
        
S|U|F  S代表signed int,有符号整型
       U代表unsigned int,无符号整型
       F代表float,单精度浮点型
C<number_of_channels>  一张图片的通道数
       1--灰度图片grayImg,是单通道图像
       2--RGB彩色图像,是3通道图像
       3--带Alph通道的RGB图像,是4通道图像

而这256种模式中存在大量冗余,所以寻找了一种等价模式LBP来解决这种情况,
也就是把所有模式分为两类,一类位均匀化模式,一类为非均匀化模式;

均匀化模式:8位二进制数(首尾相连)中存在0到1、1到零的数目小于等于两个
例如:全0,全1,1个1、2个1相连、3个1相连直至7个1相连各8种均匀化模式,加起来就是1+1+7*8=58种;

非均匀化模式:除均匀化模式的所有的统计为1种;

故一共59种模式。


二、代码及解释

大致思路:
1.先得到等价模式LBP;
2.计算一个LBP特征图像块的直方图;
3.计算LBP特征图像的直方图LBPH;

4.经过上述三步得到一个图像的LBP特征;
5.调用4中单个图像LBP,实现所有样本的LBP特征提取。

先放上代码:

//等价模式LBP特征计算
template <typename _tp>
void getUniformPatternLBPFeature(InputArray _src,OutputArray _dst,int radius,int neighbors)
{
    Mat src = _src.getMat();
    //LBP特征图像的行数和列数的计算要准确
    _dst.create(src.rows-2*radius,src.cols-2*radius,CV_8UC1);
    Mat dst = _dst.getMat();
    dst.setTo(0);
    //LBP特征值对应图像灰度编码表,直接默认采样点为8位
    uchar temp = 1;
    uchar table[256] = {0};

    for(int i=0;i<256;i++)
    {
        if(getHopTimes(i)<3)
        {
            table[i] = temp;
            temp++;
        }
    }
    //是否进行UniformPattern编码的标志
    bool flag = false;
    //计算LBP特征图
    for(int k=0;k<neighbors;k++)
    {
        if(k==neighbors-1)
        {
            flag = true;
        }
        //计算采样点对于中心点坐标的偏移量rx,ry
        float rx = static_cast<float>(radius * cos(2.0 * CV_PI * k / neighbors));
        float ry = -static_cast<float>(radius * sin(2.0 * CV_PI * k / neighbors));
        //为双线性插值做准备
        //对采样点偏移量分别进行上下取整
        int x1 = static_cast<int>(floor(rx));
        int x2 = static_cast<int>(ceil(rx));
        int y1 = static_cast<int>(floor(ry));
        int y2 = static_cast<int>(ceil(ry));
        //将坐标偏移量映射到0-1之间
        float tx = rx - x1;
        float ty = ry - y1;
        //根据0-1之间的x,y的权重计算公式计算权重,权重与坐标具体位置无关,与坐标间的差值有关
        float w1 = (1-tx) * (1-ty);
        float w2 =    tx  * (1-ty);
        float w3 = (1-tx) *    ty;
        float w4 =    tx  *    ty;
        //循环处理每个像素
        for(int i=radius;i<src.rows-radius;i++)
        {
            for(int j=radius;j<src.cols-radius;j++)
            {
                //获得中心像素点的灰度值
                _tp center = src.at<_tp>(i,j);
                //根据双线性插值公式计算第k个采样点的灰度值
                float neighbor = src.at<_tp>(i+x1,j+y1) * w1 + src.at<_tp>(i+x1,j+y2) *w2 \
                    + src.at<_tp>(i+x2,j+y1) * w3 +src.at<_tp>(i+x2,j+y2) *w4;
                //LBP特征图像的每个邻居的LBP值累加,累加通过与操作完成,对应的LBP值通过移位取得
                dst.at<uchar>(i-radius,j-radius) |= (neighbor>center) <<(neighbors-k-1);
                //进行LBP特征的UniformPattern编码
                if(flag)
                {
                    dst.at<uchar>(i-radius,j-radius) = table[dst.at<uchar>(i-radius,j-radius)];
                }
            }
        }
    }
}

//计算跳变次数
int getHopTimes(int n)
{
    int count = 0;
    bitset<8> binaryCode = n;
    for(int i=0;i<8;i++)
    {
        if(binaryCode[i] != binaryCode[(i+1)%8])
        {
            count++;
        }
    }
    return count;
}
//计算一个LBP特征图像块的直方图
Mat getLocalRegionLBPH(const Mat& src, int minValue, int maxValue, bool normed)
{
    //定义存储直方图的矩阵
    Mat result;
    //计算得到直方图bin的数目,直方图数组的大小
    int histSize = maxValue - minValue + 1;
    //定义直方图每一维的bin的变化范围
    float range[] = { static_cast<float>(minValue),static_cast<float>(maxValue + 1) };
    //定义直方图所有bin的变化范围
    const float* ranges = { range };
    //计算直方图,src是要计算直方图的图像,1是要计算直方图的图像数目,0是计算直方图所用的图像的通道序号,从0索引
    //Mat()是要用的掩模,result为输出的直方图,1为输出的直方图的维度,histSize直方图在每一维的变化范围
    //ranges,所有直方图的变化范围(起点和终点)
    calcHist(&src, 1, 0, Mat(), result, 1, &histSize, &ranges, true, false);
    //归一化
    if (normed)
    {
        result /= (int)src.total();
    }
    //结果表示成只有1行的矩阵
    return result.reshape(1, 1);
}
//计算LBP特征图像的直方图LBPH
Mat getLBPH(Mat src, int numPatterns, int grid_x, int grid_y, bool normed)
{
    int width = src.cols / grid_x;
    int height = src.rows / grid_y;
    //定义LBPH的行和列,grid_x*grid_y表示将图像分割成这么些块,numPatterns表示LBP值的模式种类
    Mat result = Mat::zeros(grid_x * grid_y, numPatterns, CV_32FC1);
    if (src.empty())
    {
        return result.reshape(1, 1);
    }
    int resultRowIndex = 0;
    //对图像进行分割,分割成grid_x*grid_y块,grid_x,grid_y默认为8
    for (int i = 0; i<grid_x; i++)
    {
        for (int j = 0; j<grid_y; j++)
        {
            //图像分块
            Mat src_cell = Mat(src, Range(i*height, (i + 1)*height), Range(j*width, (j + 1)*width));
            //计算直方图
            Mat hist_cell = getLocalRegionLBPH(src_cell, 0, (numPatterns - 1), true);
            //将直方图放到result中
            Mat rowResult = result.row(resultRowIndex);
            hist_cell.reshape(1, 1).convertTo(rowResult, CV_32FC1);
            resultRowIndex++;
        }
    }
    return result.reshape(1, 1);
}

对于单个图像进行处理,也就是调用前面的代码

//单个图像
Mat get_single_pic_lbp(Mat& src){
    Mat src = imread("D:\\2.bmp", 0);
    Mat dst;
    //先得到等价模式LBP结果
    getUniformPatternLBPFeature<uchar>(src, dst, 1, 8); //采样点为8个
    //得到LBP的特征向量
    Mat result = getLBPH(dst, 59, 8, 8, true);  //单个图像特征由64*256=16384降为64*59=3778维特征
//    cout << result << endl;
    return result;
}

所有图片一起调用上面单个图片的LBP特征,并将所有返回值存起来,就可以得到一个超级大的矩阵,所以一般要进行降维处理,然而会发现PCA降维效果一点也不行,所以要想想别的办法处理;

主函数中进行测试

int main(){
    //lbp_pca
    MatrixXd all_pic_lbp;
    all_pic_lbp = get_all_lbp_feature();
    pca_1(all_pic_lbp, 3776, 348);
    return 0;
}
三、总结

LBP中的特征等价于看待不同模式,也需要划分为不同几个范围,得到直方图结果,进行统计分析,根据不同的实际情况,可以选择不同的LBP得到更好的结果。网上原理以及代码很多,只要懂得原理,就可以任意修改为自己想要的模式了。