zoukankan      html  css  js  c++  java
  • HOG特征基础(一)

    HOG特征简介

    HOG 全称为 Histogram of Oriented Gradients ,即方向梯度的直方图。HOG 是由 Navneet Dalal & Bill Triggs 在 CVPR 2005发表的论文中提出来的,

    目的是为了更好的解决行人检测的问题。先来把这几个字拆开介绍,首先,梯度的概念和计算梯度的方法已经在前一篇文章中介绍了,方向梯度就是说梯度的方向我们也要利用上,

    在前一篇中我们只是用到了梯度大小,直方图是个新的概念,所以下面先来介绍直方图

    直方图

    假如有这么一个3x3的矩阵:

     
    示例矩阵

    要算它的直方图,我们先要选定一个取值的间隔,比如取10,那么0-9看成一类、10-19看成一类,如此类推,那么0-9有{6、5、9} 3个,10-19有{12、17}两个,
    用excel画出来看看。
     

     

    excel画的直方图

    那么方向梯度的直方图,统计的就是某个方向区间内的梯度的大小(voting vector,不知道怎么翻译,投票向量?)

    HOG特征图详解

    HOG计算图例

    左边的原图是个疑似是双马尾萝莉塔装的妹子的灰度图,也别问我为何这么熟练,最右边的就是我们上一期使用的 Sobel 算子对原图卷积以后得到的梯度图。

    先看红色的格子:
    红色的格子称为 cell(细胞、监牢),是计算直方图的基本单位,也是个固定大小的格子,典型值是 8x8 个像素。

    在这些所有的 8x8 的 cell 上单独计算直方图,我们知道直方图要自己指定 x 轴设置多少个区间,典型值是 9,x 轴表示的是梯度的方向,

    也就是把梯度方向割成 9 个区间,梯度方向的范围我们指定为 0 ~ 180°,也就是每个区间范围是 20°,像这样:

     

    计算方向梯度直方图

    第一步:预处理

    Patch可以是任意的尺寸,但是有一个固定的比列,比如当patch长宽比1:2,那patch大小可以是100*200, 128*256或者1000*2000但不可以是101*205。

    这里有张图是720*475的,我们选100*200大小的patch来计算HOG特征,把这个patch从图片里面抠出来,然后再把大小调整成64*128。

     

    第二步:计算梯度图像

    首相我们计算水平和垂直方向的梯度,再来计算梯度的直方图。可以用下面的两个kernel来计算,

    也可以直接用OpenCV里面的kernel大小为1的Sobel算子来计算。

     
     

    调用OpenCV代码如下:

    // C++ gradient calculation.
    // Read image
    Mat img = imread("bolt.png");
    img.convertTo(img, CV_32F, 1/255.0);
     
    // Calculate gradients gx, gy
    Mat gx, gy; 
    Sobel(img, gx, CV_32F, 1, 0, 1);
    Sobel(img, gy, CV_32F, 0, 1, 1);
    
    # Python gradient calculation 
     
    # Read image
    im = cv2.imread('bolt.png')
    im = np.float32(im) / 255.0
     
    # Calculate gradient 
    gx = cv2.Sobel(img, cv2.CV_32F, 1, 0, ksize=1)
    gy = cv2.Sobel(img, cv2.CV_32F, 0, 1, ksize=1)
    

    接着,用下面的公式来计算梯度的幅值g和方向theta:

    可以用OpenCV的cartToPolar函数来计算:

    // C++ Calculate gradient magnitude and direction (in degrees)
    Mat mag, angle; 
    cartToPolar(gx, gy, mag, angle, 1);
    
    # Python Calculate gradient magnitude and direction ( in degrees ) 
    mag, angle = cv2.cartToPolar(gx, gy, angleInDegrees=True)
    

    计算得到的gradient图如下:

    从上面的图像中可以看到x轴方向的梯度主要凸显了垂直方向的线条,y轴方向的梯度凸显了水平方向的梯度,

    梯度幅值凸显了像素值有剧烈变化的地方。(注意:图像的原点是图片的左上角,x轴是水平的,y轴是垂直的)

    图像的梯度去掉了很多不必要的信息(比如不变的背景色),加重了轮廓。换句话说,你可以从梯度的图像中还是可以轻而易举的发现有个人。

    在每个像素点,都有一个幅值(magnitude)和方向,对于有颜色的图片,会在三个channel上都计算梯度。

    那么相应的幅值就是三个channel上最大的幅值,角度(方向)是最大幅值所对应的角。

    第三步:在8*8的网格中计算梯度直方图

    在这一步,上面的patch图像会被分割成8*8大小的网格(如下图),每个网格都会计算一个梯度直方图。那为什么要分成8*8的呢?

    用特征描述子的一个主要原因是它提供了一个紧凑(compact)/压缩的表示。一个8*8的图像有8*8*3=192个像素值,

    每个像素有两个值(幅值magnitude和方向direction,三个channel取最大magnitude那个),加起来就是8*8*2=128,

    后面我们会看到这128个数如何用一个9个bin的直方图来表示成9个数的数组。不仅仅是可以有紧凑的表示,用直方图来表示一个patch也可以更加抗噪,

    一个gradient可能会有噪音,但是用直方图来表示后就不会对噪音那么敏感了。

     
    这个patch的大小是64*128,分割成8*8的cell,那么一共有64/8 * 128/8 = 8*16=128个网

    对于64*128的这幅patch来说,8*8的网格已经足够大来表示有趣的特征比如脸,头等等。
    直方图是有9个bin的向量,代表的是角度0,20,40,60.....160。

    我们先来看看每个8*8的cell的梯度都是什么样子:

                        中间: 一个网格用箭头表示梯度 右边: 这个网格用数字表示的梯度

    中间这个图的箭头是梯度的方向,长度是梯度的大小,可以发现箭头的指向方向是像素强度都变化方向,幅值是强度变化的大小。

    右边的梯度方向矩阵中可以看到角度是0-180度,不是0-360度,这种被称之为"无符号"梯度("unsigned" gradients)因为一个梯度和它的负数是用同一个数字表示的,

    也就是说一个梯度的箭头以及它旋转180度之后的箭头方向被认为是一样的。那为什么不用0-360度的表示呢?

    在事件中发现unsigned gradients比signed gradients在行人检测任务中效果更好。一些HOG的实现中可以让你指定signed gradients。

    下一步就是为这些8*8的网格创建直方图,直方图包含了9个bin来对应0,20,40,...160这些角度。

    下面这张图解释了这个过程。我们用了上一张图里面的那个网格的梯度幅值和方向。根据方向选择用哪个bin, 根据副值来确定这个bin的大小。

    先来看蓝色圈圈出来的像素点,它的角度是80,副值是2,所以它在第五个bin里面加了2,再来看红色的圈圈出来的像素点,它的角度是10,副值是4,

    因为角度10介于0-20度的中间(正好一半),所以把幅值一分为二地放到0和20两个bin里面去。

    这里有个细节要注意,如果一个角度大于160度,也就是在160-180度之间,我们知道这里角度0,180度是一样的,所以在下面这个例子里,

    像素的角度为165度的时候,要把幅值按照比例放到0和160的bin里面去。

     

    把这8*8的cell里面所有的像素点都分别加到这9个bin里面去,就构建了一个9-bin的直方图,上面的网格对应的直方图如下:

     
                                                   8*8网格直方图

    这里,在我们的表示中,Y轴是0度(从上往下)。你可以看到有很多值分布在0,180的bin里面,这其实也就是说明这个网格中的梯度方向很多都是要么朝上,要么朝下。

    第四步: 16*16块归一化

    上面的步骤中,我们创建了基于图片的梯度直方图,但是一个图片的梯度对于整张图片的光线会很敏感。如果你把所有的像素点都除以2,

    那么梯度的幅值也会减半,那么直方图里面的值也会减半,所以这样并不能消除光线的影响。所以理想情况下,我们希望我们的特征描述子可以和光线变换无关,

    所以我们就想让我们的直方图归一化从而不受光线变化影响。

    先考虑对向量用l2归一化的步骤是:
    v = [128, 64, 32]
    [(128^2) + (64^2) + (32^2) ]^0.5=146.64
    把v中每一个元素除以146.64得到[0.87,0.43,0.22]
    考虑另一个向量2*v,归一化后可以得到向量依旧是[0.87, 0.43, 0.22]。你可以明白归一化是把scale给移除了。

    你也许想到直接在我们得到的9*1的直方图上面做归一化,这也可以,但是更好的方法是从一个16*16的块上做归一化,也就是4个9*1的直方图组合成一个36*1的向量,

    然后做归一化,接着,窗口再朝后面挪8个像素(看动图)。重复这个过程把整张图遍历一边。

    hog-16x16-block-normalization

    第五步:计算HOG特征向量

    为了计算这整个patch的特征向量,需要把36*1的向量全部合并组成一个巨大的向量。向量的大小可以这么计算:

    1. 我们有多少个16*16的块?水平7个,垂直15个,总共有7*15=105次移动。
    2. 每个16*16的块代表了36*1的向量。所以把他们放在一起也就是36*105=3780维向量。
     
     二、HOG特征opencv代码处理和SVM训练
     
    各个区间先列出来,分别是:0-20、20-40、40-60、60-80、80-100、100-120、120-140、140-160、160-180,区间的右端点是不包含的。
    我们计算的时候先看方向矩阵,首先左上角的值是 90度,那就落在了 80-100 的区间,幅值矩阵对应位置的值是 6,因此 80-100 这个区间的 y 轴值加 6。
    再看方向矩阵的第一行第二列,也是 90,幅值矩阵对应值是 12,于是 80-100 这个区间再加 12,现在总的值是 18 了,相信到这里你已经看懂了,如此类推继续算下来,
    就可以得到这样一个直方图:
     

    计算直方图的函数代码实现是这样的:

    ''' 
    函数名称:calc_hist
    功能:计算直方图
    输入:
    mag    幅值矩阵
    angle  角度矩阵,范围在 0-180
    bin_size    直方图区间大小
    输出:
    hist    直方图
    '''
    def calc_hist(mag, angle, bin_size=9):
        hist = np.zeros((bin_size,), dtype=np.int32)
    
        bin_step = 180 // bin_size
        bins = (angle // bin_step).flatten()
        flat_mag = mag.flatten()
    
        for i,m in zip(bins, flat_mag):
            hist[i] += m
    
        return hist
    

    有了这个函数就可以做计算 cell 的部分,对应这些代码:

    # 将图像切成多个cell
        cell_size = 8
        bin_size = 9
        img_h, img_w = gray.shape[:2]
        cell_h, cell_w = (img_h // cell_size, img_w // cell_size)
    
        cells = np.zeros((cell_h, cell_w, bin_size), dtype=np.int32)
        for i in range(cell_h):
            cell_row = cell_size * i
            for j in range(cell_w):
                cell_col = cell_size * j
                cells[i,j] = calc_hist(mag[cell_row:cell_row+cell_size, cell_col:cell_col+cell_size], 
                    angle[cell_row:cell_row+cell_size, cell_col:cell_col+cell_size], bin_size)
    

    这样子就完成了 cell 部分的计算,接下来看黄格子。

    黄色的格子称为 block,是由多个 cell 组合而成的,典型的组合方式是 2x2 个 cell 组成成一个 block,也就是跟图示的一样。

    我们知道每个 cell 上面都有一个 9 维的表示直方图大小的向量,那么一个 block 上就有 2x2x9 = 36维的向量,黄格子要做的操作就是把每一次选中的这 36 维向量做规范化(normalization)

    ,得到新的 36 维向量。规范化的方法有多种可选:


     
    规范化方法

    我通常使用的是 L2-Norm, 也就是先对整个向量的各个元素都求平方然后求和、开根号 作为规范化因子,

    然后对原向量中每一个元素都除以这个规范化因子,L2 规范化的函数是这样的:

    # 归一化cells
    def l2_norm(cells):
        block = cells.flatten().astype(np.float32)
        norm_factor = np.sqrt(np.sum(block**2) + 1e-6)
        block /= norm_factor
        return block
    

    利用之前得到的 cells 和规范化函数就可以写 黄格子 实现的操作了:

    # 多个cell融合成block
        block_size = 2
        block_h, block_w = (cell_h-block_size+1, cell_w-block_size+1)
        blocks = np.zeros((block_h, block_w, block_size*block_size*bin_size), dtype=np.float32)
        for i in range(block_h):
            for j in range(block_w):
                blocks[i,j] = l2_norm(cells[i:i+block_size, j:j+block_size])
    

    把这么多个 block 的 36维向量拼起来就是 HOG 特征描述子(descriptor)了,在这里来说就是把 blocks 这个 3 维的矩阵摊平,也只要一行代码:

    blocks = blocks.flatten()
    

    我把整个 HOG 的计算过程封成了一个函数,是这样的:

    # 计算HOG特征
    def calc_hog(gray):
        ''' 计算梯度 '''
        dx = cv2.Sobel(gray, cv2.CV_16S, 1, 0)
        dy = cv2.Sobel(gray, cv2.CV_16S, 0, 1)
        sigma = 1e-3
        # 计算角度
        angle = np.int32(np.arctan(dy / (dx + sigma)) * 180 / np.pi) + 90
        dx = cv2.convertScaleAbs(dx)
        dy = cv2.convertScaleAbs(dy)
        # 计算梯度大小
        mag = cv2.addWeighted(dx, 0.5, dy, 0.5, 0)
    
        print('angle
    ', angle[:8,:8])
        print('mag
    ', mag[:8,:8])
        ''' end of 计算梯度 '''
    
        # 将图像切成多个cell
        cell_size = 8
        bin_size = 9
        img_h, img_w = gray.shape[:2]
        cell_h, cell_w = (img_h // cell_size, img_w // cell_size)
    
        cells = np.zeros((cell_h, cell_w, bin_size), dtype=np.int32)
        for i in range(cell_h):
            cell_row = cell_size * i
            for j in range(cell_w):
                cell_col = cell_size * j
                cells[i,j] = calc_hist(mag[cell_row:cell_row+cell_size, cell_col:cell_col+cell_size], 
                    angle[cell_row:cell_row+cell_size, cell_col:cell_col+cell_size], bin_size)
    
        # 多个cell融合成block
        block_size = 2
        block_h, block_w = (cell_h-block_size+1, cell_w-block_size+1)
        blocks = np.zeros((block_h, block_w, block_size*block_size*bin_size), dtype=np.float32)
        for i in range(block_h):
            for j in range(block_w):
                blocks[i,j] = l2_norm(cells[i:i+block_size, j:j+block_size])
    
        return blocks.flatten()
    

    假设输入的图片是 64 x 128 的,cell 就会有 8 x 16 = 128个,block 就有 (8-2+1) x (16 - 2 + 1) = 105 个,每个 block 有 36 维向量,总共就是 105 x 36 = 3780维向量,

    这个向量就是对应这张图片的 HOG 特征。用其他特征得到的东西也是大同小异,都是不同大小表示不同信息的特征。

    特征相当于该物体的 ID,如果同类的物体的特征很相似,我们就说这个特征至少对于该类物体的区分度很好。

    拿现在很火的深度神经网络来说,用它做人脸识别的时候,也是输入图片,输出这么一个长长的向量,如果对于同一个人,这些产生的向量的距离很近,

    而对于不同人的距离则很远,就说这个神经网络精度很高,但本质的流程和这些人工设计的特征没有任何区别。

    介绍完了 HOG特征,私以为徒有这堆向量也没什么卵用,所以想做个示范的应用,但是篇幅有限,知识点不能完全覆盖到,所以接下来讲的东西哪里不懂的另外搜索一下就好。

    既然做行人识别,那就看看 HOG 特征对于行人的区分度怎么样。

    特征区分度

    做这个事情之前首先要介绍一下我使用的公开数据集 INRIA Person,这是一个公开的行人数据集,里面分为正样本和负样本,正样本几乎都是直立的老外行人,负样本是一些风景图片,

    可以给大家看一眼,这个数据集也能从网上直接下载。

     
    正样本.png

     
    负样本.png

    我会把所有图片缩放到高度 128 和宽度 64,因此每张图片的 HOG 特征长度是 3780,如果我把所有这些 3780 维的向量都放在 3780 维空间上去看它们的分布,
    可能正样本会聚集在一堆,负样本聚在另一堆,这样是最好的,但是我们没办法可视化 3780 维的空间,所以我的做法是用 PCA(主成分分析)把它们压到二维,在二维平面上去看。
    核心代码是这样的,需要 sklearn 和 scipy,可以通过 pip 安装:
    from sklearn.decomposition import PCA
    import matplotlib.pyplot as plt
    
    # PCA 降维
    pca = PCA(n_components=2, copy=True)
    data_size = 500
    pos_features = pca.fit_transform(pos_features[:data_size])
    neg_features = pca.fit_transform(neg_features[:data_size])
    # 显示
    plt.plot(pos_features[:,0], pos_features[:,1], 'ro')
    plt.plot(neg_features[:,0], neg_features[:,1], 'bo')
    plt.show()
    

    但是得到的图形是这样的:

     
    HOG降维.png

    蓝色点是行人,红色点是背景。
    这种现象原因可能有两个,一个是降维降太多了,二维信息不足以表达原来的 3000 多维的结构;二是我们看这个图形的角度不对 ,正所谓横看成岭侧成峰。假设这是两坨饼,红色一坨蓝色一坨,
    现在看起来是红色的饼叠在了蓝色的饼上面,所以正确的看法应该是,我们把红色的饼拿起来,然后从侧面去看,就会变成这样:

    这样子不就分成两坨了嘛~
    虽然听起来像是在胡说八道,但是所谓什么 SVM 模型啊,深度学习、神经网络等等等等,干的就是这样一件事,改变我们看数据的角度,直到在我们看来是可以一刀切开的两坨,
    说得屌一点就是线性可分

    SVM

    所以下面就来训练一个 SVM 模型。

    from sklearn import svm
    
    # 合并特征
    features = np.concatenate((pos_features[:data_size], neg_features[:data_size]))
    labels = np.zeros((data_size*2,), dtype=np.int32)
    labels[:data_size] = 1
    
    # SVM分类器
    lin_clf = svm.LinearSVC()
    lin_clf.fit(features, labels)
    

    features 是正样本和负样本的特征合并起来的一个大矩阵,labels 表示的是每个特征对应的是什么类别,这里我设置了 1 对应行人,0 对应背景。

    为什么需要 labels,因为训练模型要用,训练模型跟老师教学生学习很像,我们要先给学生一吨的题,并且告诉他们背后有答案,自己对,

    这些题就是 features,答案就是 labels,于是他们做完对完这些题以后我们就希望他们能够举一反三,看到新的题的时候不方。

    lin_clf 就是 SVM模型,使用 fit 方法训练,稍等几秒就训练完了。下面就用我喜欢的知名舞见=咬人猫=的照片来测试一下吧,就是题图。

    miao2.jpg

    测试代码是:

    miao = cv2.imread('miao2.jpg')
    miao = cv2.resize(miao, (64,128))
    miao = cv2.cvtColor(miao, cv2.COLOR_BGR2GRAY)
    miao_feature = calc_hog(miao)
    pred_result = lin_clf.predict(np.array([miao_feature]))
    

    结果 pred_result 当然是 1 了,如果不是我就不会放上来了。




  • 相关阅读:
    [转].net mvc + vuejs 的项目结构
    Outlook IMAP 修改PST文件存储路径
    VS2017 性能优化方法
    查询存储过程所需参数
    如何保障微服务架构下的数据一致性
    sqlserver批量给用户配置存储过程权限
    vue中刷新当前页面或重新加载的两种方法
    vue history模式下的微信支付,及微信支付授权目录的填写,处理URL未注册
    Vue 四行代码实现无感知上拉加载更多
    2019年前端必用正则(js)
  • 原文地址:https://www.cnblogs.com/Jack-Elvis/p/11285290.html
Copyright © 2011-2022 走看看