机器学习与计算机视觉入门项目——视频投篮检测(二)

机器学习与计算机视觉入门项目——视频投篮检测(二)

一、手工特征与CNN特征

在上一次的博客中,介绍了计算机视觉和机器学习的关系、篮球进球检测的基本问题和数据集的制作。这次的我们主要介绍如何从原始图像中提取有用的图像特征,以便应用于之后的分类器。

如下图所示,我们现在要做的是Feature Extraction。在前DeepLearning时代,图像特征都是由人设计的,对于不同的图像如人脸、行人,都有各自所适合的特征,也就是所谓的Hand-crafted Feature。比如常用Haar特征来描述人脸,常用HOG特征来描述行人。
这里写图片描述
因此,我们可以拿先前中医看病的例子来类比前深度学习时代的工程师是怎样解决计算机视觉问题的。假设有这样一位精通医术的老中医在药店坐堂,前来看病的人病情各不一样。老中医需要根据每位病人的具体情况开出药方,然后抓药,以特定的方式熬制汤药并且服用才能治疗病情。比如,有位病人患了“脸盲症”,老中医开出药方:准备“人脸数据集”一副,取“光照归一化”一两,再称“Haar特征”三钱,送入“Adaboost级联分类器”慢火熬制,连续服用若干代,脸盲症可愈。

从这里我们可以看到,解决CV问题是非常依赖人工经验的,他需要工程师对手头上的“工具”,也就是“特征”和“分类器”的使用条件以及性能都有充分的了解,必要时甚至需要针对这类问题设计新的图像特征。因此拿中医诊病来类比前深度学习时代的CV工程问题一点也不为过。

在卷积神经网络开始大显神威之后,feature extraction这一步基本都交给CNN来做了。利用多个卷积层的级联,依靠数据驱动(data-driven)的学习方式,CNN可以很好地学习到数据集的特定的数据分布,多数情况下,比人类手工设计的特征好使多了。
这里写图片描述
在篮球进球检测这个简单的任务的开始阶段,我们采用HOG特征足以应对了。

二、HOG特征简介

HOG百度百科上总结了HOG特征的基本思想:

把样本图像分割为若干个像素的单元,把梯度方向平均划分为多个区间,在每个单元里面对所有像素的梯度方向在各个方向区间进行直方图统计,得到一个多维的特征向量,每相邻的单元构成一个区间,把一个区间内的特征向量联起来得到多维的特征向量,用区间对样本图像进行扫描,扫描步长为一个单元。最后将所有块的特征串联起来,就得到了人体的特征。

原版论文在此。我在实习时,王老师要求把文章的每一处地方都推导一遍,然后自己写代码实现HOG算法。因此我主要是参考了上面这篇CVPR2005的最早的文章。另外,知乎上有一篇文章讲的也比较清楚,在这里贴出来图像学习-HOG特征

网上讲HOG特征的文章不在少数,只要认真看过两三篇,基本可以了解HOG特征是怎么提取的了。所以在这里就不再赘述了。如果希望进一步挖掘HOG特征的潜力,可以看这里

三、HOG特征的Python实现

学习一个算法最好的办法就是亲自实现一遍。只有亲手经历一遍这样的过程,才能体会到算法实现过程中出现了什么样的问题,才能对算法的性能和算法的擅长领域做到心中有数。下面是我实现的HOG算法的Python代码,当然也参考了前辈的CSDN博文。

class Hog_descriptor():
    '''
    提取灰度图片的HOG特征,画HOG特征图
    '''
    def __init__(self, img, cell_size=8, bin_size=9):
        # 输入的图片数据应是np.array格式的uint8数据,0-255
        self.img = np.sqrt(img / float(np.max(img)))# 伽马校正
        self.cell_size = cell_size # 计算梯度直方图的最小单元的尺寸
        self.bin_size = bin_size # 梯度直方图中的条数
        self.angle_unit = 360 / self.bin_size # 角度增加的步长
        height, width = self.img.shape

        # 将图像尺寸resize成cell_size的整数倍,方便之后运算
        self.width = int(np.ceil(width/cell_size)*cell_size)
        self.height = int(np.ceil(height/cell_size)*cell_size)
        img = cv2.resize(self.img,(width,height),interpolation=cv2.INTER_CUBIC)
        self.img = img

    def global_gradient(self): 
        # 在原图上求梯度,函数返回与原图尺寸相同
        # 1,0在x方向求梯度,0,1在y方向求梯度,sobel滤波器尺寸=3
        gradient_values_x = cv2.Sobel(self.img, cv2.CV_64F, 1, 0, ksize=3)
        gradient_values_y = cv2.Sobel(self.img, cv2.CV_64F, 0, 1, ksize=3)
        # 梯度幅值=sqrt(gx^2+gy^2)
        gradient_magnitude = np.sqrt(np.square(gradient_values_x)+np.square(gradient_values_y))
        # 梯度相角=arctan(gy/gx)
        gradient_angle = cv2.phase(gradient_values_x, gradient_values_y, angleInDegrees=True)
        return gradient_magnitude, gradient_angle

    def extract(self):
        # 1.提特征向量 2.画特征图
        # 对原图每一点求梯度幅值和相角
        gradient_magnitude, gradient_angle = self.global_gradient()
        gradient_magnitude = abs(gradient_magnitude)
        # 记录每一个cell的hog向量(1*bin_size维)的矩阵
        cell_histogram_vector = np.zeros((int(self.height / self.cell_size), 
                                          int(self.width / self.cell_size), self.bin_size))
        # 对每个cell,计算hog向量
        for i in range(cell_histogram_vector.shape[0]):
            for j in range(cell_histogram_vector.shape[1]):
                # 从整张图的梯度信息中抠出该cell的梯度信息
                cell_magnitude = gradient_magnitude[i * self.cell_size:(i + 1) * self.cell_size,
                                 j * self.cell_size:(j + 1) * self.cell_size]
                cell_angle = gradient_angle[i * self.cell_size:(i + 1) * self.cell_size,
                             j * self.cell_size:(j + 1) * self.cell_size]
                # 将每一个cell的直方图向量写入总的存储矩阵
                cell_histogram_vector[i][j] = self.cell_gradient(cell_magnitude, cell_angle)
        # 可视化所有cell的hog向量
        hog_image = self.render_gradient(np.zeros([self.height*5, self.width*5]), cell_histogram_vector)
        # 将block以stride=cell_size为步长滑动,将提取的36维hog向量拼接起来构成描述全图的特征向量
        hog_vector = []
        for i in range(cell_histogram_vector.shape[0] - 1):
            for j in range(cell_histogram_vector.shape[1] - 1):
                # 在一个block内,拼接4个hog向量,并对其归一化,消除光照不均的影响
                block_vector = []
                block_vector.extend(cell_histogram_vector[i][j])
                block_vector.extend(cell_histogram_vector[i][j + 1])
                block_vector.extend(cell_histogram_vector[i + 1][j])
                block_vector.extend(cell_histogram_vector[i + 1][j + 1])
                mag = lambda vector: math.sqrt(sum(i ** 2 for i in vector))
                magnitude = mag(block_vector)# lambda定义匿名函数,求L2范,vector是自变量,冒号后是函数体
                if magnitude != 0:# 用L2范归一化block直方图向量
                    normalize = lambda block_vector, magnitude: [element / magnitude for element in block_vector]
                    block_vector = normalize(block_vector, magnitude)
                hog_vector.extend(block_vector)
        return hog_vector, hog_image



    def cell_gradient(self, cell_magnitude, cell_angle):# 计算一个cell的直方图向量
        orientation_centers = [0] * self.bin_size# bin_size维的列表,默认是9维
        for i in range(cell_magnitude.shape[0]):
            for j in range(cell_magnitude.shape[1]):
                gradient_strength = cell_magnitude[i][j]# 遍历cell中的每一个梯度幅值
                gradient_angle = cell_angle[i][j]# 取出每个幅值对应的角度
                min_angle, max_angle, mod = self.get_closest_bins(gradient_angle)
                orientation_centers[min_angle] += (gradient_strength * (1 - (mod / self.angle_unit)))
                orientation_centers[max_angle] += (gradient_strength * (mod / self.angle_unit))
        return orientation_centers

    def get_closest_bins(self, gradient_angle):
        '''
        若有角度330°,bin=9,angle_unit=40°,则330°应落在320°到0°之间,其幅值应按比例加到320和0对应的bin上
        min_angle = 8,max_angle = (8+1)%9 = 0
        mod=10°,离320°更近,分配到320对应的bin应占总幅值的3/4
        离0°远,幅值分配1/4
        '''
        idx = int(gradient_angle / self.angle_unit)% self.bin_size
        mod = gradient_angle % self.angle_unit
        return idx, (idx + 1) % self.bin_size, mod

    def render_gradient(self, image, cell_histogram):# 画图
        # 为了使HOG图像更为清晰,特征图的尺寸扩大为原来的五倍。
        cell_width = int(self.cell_size) / 2 * 5
        max_magnitude = np.array(cell_histogram).max()
        for x_height in range(cell_histogram.shape[0]): # x_height代表img高度,y_width代表img宽度
            for y_width in range(cell_histogram.shape[1]):
                cell_hist = cell_histogram[x_height][y_width] # 取一个cell代表的histogram
                cell_hist /= max_magnitude # 幅值归一化
                angle = 0
                # 找cell的中心
                x_center = 5*(x_height)*self.cell_size+int(self.cell_size*5)/2
                y_center = 5*(y_width)*self.cell_size+int(self.cell_size*5)/2
                angle_gap = self.angle_unit
                for magnitude in cell_hist:
                    angle_radian = math.radians(angle)
                    x1 = int(x_center + cell_width*5 * math.cos(angle_radian))
                    y1 = int(y_center - cell_width*5 * math.sin(angle_radian))
                    x2 = int(x_center - cell_width*5 * math.cos(angle_radian))
                    y2 = int(y_center + cell_width*5 * math.sin(angle_radian))                    
                    cv2.line(image, (y1, x1), (y2, x2), int(255 * math.sqrt(magnitude)))
                    angle += angle_gap
        return image

    def hog_imshow_save(self,hog_img_name = 'hog_img.png'):
        # 保存特征图
        vector, image = self.extract()
        plt.imshow(image, cmap=plt.cm.gray)
        plt.show()
        cv2.imwrite(hog_img_name,image)

这是调用Hog_descriptor类提取图像特征并展示特征图的example。

# example of hog class    
img = cv2.imread('test.png', cv2.IMREAD_GRAYSCALE) # 读入灰度图
hog = Hog_descriptor(img, cell_size=8, bin_size=9)
hog.hog_imshow_save('D:/hog_img.png')

效果如下。
这里写图片描述
这里写图片描述
第二张图是第一张图的尺寸放大五倍后的效果,可以看到,条状亮线是每一个cell特征可视化的结果,很好地反映了原图的边缘信息。

四、一个小问题

HOG特征描述的是图像的边缘信息,因为边缘处梯度的幅值才比较大。那么针对我们的篮球进球检测问题来说,如果由于摄像头的视线原因,篮球恰好出现在篮网前面,就像下图这样:
这里写图片描述
它和正常进球的样本区别就在于几根比较稀疏的篮网线在篮球的前面或者后面,此时,我们的HOG特征还能准确地描述两者在纹理、边缘、轮廓上的不同吗?考虑到一张图片的尺寸大约在60*50,是比较小的,没有办法把篮网的轮廓精细地描绘出来。下面的hog特征图就是上面这个迷惑性很强的负样本的可视化图。
这里写图片描述

我认为在HOG特征这个层次上,实际上是无法分清这两者的区别的,所幸的是,实时的进球检测效果看起来还不错,至少模型没有对这样的假阳性样本出现误判。在随后的使用的MLP和CNN做检测时,对这类样本的鲁棒性要更好一些。

五、制作整个数据集的HOG特征集

第一部分我们提取了视频中的篮筐位置并保存为小图片。为了之后存取的方便,将其储存成pkl文件形式。

img_dir = 'D:/dataset/'
filelist = os.listdir(img_dir)
# 先读一张图片,获得尺寸信息
img = cv2.imread(img_dir + filelist[0])
height,width = np.shape(img)
# 开辟空矩阵
img_to_save = np.zeros((np.shape(filelist)[0],height,width),np.uint8)
for i in range(0,len(filelist)):
    tmp = cv2.imread(img_dir+filelist[i])
    img_to_save[i] = tmp
# 存成pkl文件
img_output = open('img.pkl','wb')
pickle.dump(img_to_save,img_output)
img_output.close()

现在对img.pkl的每一张图片都提取hog特征

# 读数据集
img_input = open('img.pkl','rb')
img_matrix = pickle.load(img_input)
img_input.close()

hog_vector = []
for i in range(0,np.shape(img_matrix)[0]):
    hog = Hog_descriptor(img_matrix[i], cell_size=8, bin_size=9)
    temp,_ = hog.extract()
    hog_vector.append(temp)
    if i % 500 == 0:
    print(i)
hog_output = open('hog.pkl','wb')
pickle.dump(hog_vector,hog_output)
hog_output.close()

猜你喜欢

转载自blog.csdn.net/hhy_csdn/article/details/82097883