在过去十年中,图像分类是一个快速发展的领域,卷积神经网络(CNNs)和其他深度学习技术的使用也在快速增长。然然而,在CNNs成为主流之前,另一种技术被广泛使用并继续使用:Viola-Jones。

CNN是个单独的分类器,它查看完整的图像并应用矩阵运算来获得分类,而Viola-Jones采用的是集成方法。这意味着Viola-Jones使用了许多不同的分类器,每个分类器查看图像的不同部分。每个单独的分类器比最终的分类器更弱(更不准确,产生更多的误报等),因为它接收的信息更少。但是,当将每个分类器的结果组合在一起时,它们会产生一个强分类器。




图像分类的输入图像一般多大 图像分类最新算法_数组


由于算法的性质,Viola-Jones方法仅限于二进制分类任务(例如对象检测),并且训练周期非常长。但是,由于每个弱分类器只需要少量的参数,并且有足够的弱分类器,因此它的误报率很低,因此它可以快速地对图像进行分类。

特征和积分图像

Viola-Jones做出的重要贡献之一是在图像识别中使用了一组简单的特征。在大多数任务中,像素值是输入到算法中的特征。但是,Viola和Jones引入了以下新特征。


图像分类的输入图像一般多大 图像分类最新算法_数组_02


A和B是两个矩形特征,C是三个矩形特征,D是四个矩形特征。

从无阴影矩形中像素的和减去阴影矩形中像素的和。很容易看到,即使是小图像,也有很多特征(对于24 x 24的图像,有超过160,000个特征)。由于该算法需要遍历所有特征,因此必须非常有效地计算这些特征。为了做到这一点,Viola和Jones引入了积分图像。积分图像由以下递归关系定义。


图像分类的输入图像一般多大 图像分类最新算法_积分图_03


s(x,y)是点(x,y)处的累积行和,ii(x,y)是同一点处的积分图像值,并且i(x,y)是该点的像素值。这个关系式的意思是,在点(x, y)处的积分图像是当前像素的左上方所有像素之和。这使得计算矩形区域内像素的和很容易,如下所示。


图像分类的输入图像一般多大 图像分类最新算法_图像分类算法_04


区域D中的像素之和是ii(4)+ ii(1)-ii(2)-ii(3),即四个数组引用。让我们通过创建一个辅助函数来开始实现算法,该函数计算给定图像的积分图像(表示为2D numpy数组)。

import numpy as np #Don't forget to import numpydef integral_image(image): ii = np.zeros(image.shape) s = np.zeros(image.shape) for y in range(len(image)): for x in range(len(image[y])): s[y][x] = s[y-1][x] + image[y][x] if y-1 >= 0 else image[y][x] ii[y][x] = ii[y][x-1]+s[y][x] if x-1 >= 0 else s[y][x] return ii


图像分类的输入图像一般多大 图像分类最新算法_图像分类的输入图像一般多大_05


这个函数只是实现了上面定义的递归关系。由于s(x,-1)= 0,当y-1 <0时,s(x,y)= i(x,y),同样对于ii(-1,y)。接下来,让我们定义一个辅助类来存储矩形区域,以便以后计算特征值。

class RectangleRegion: def __init__(self, x, y, width, height): self.x = x self.y = y self.width = width self.height = heightdef compute_feature(self, ii): return ii[self.y+self.height][self.x+self.width] + ii[self.y][self.x] - (ii[self.y+self.height][self.x]+ii[self.y][self.x+self.width])


图像分类的输入图像一般多大 图像分类最新算法_图像分类的输入图像一般多大_06


Viola-Jones

现在我们已经设置了辅助类和函数,我们可以开始创建Viola-Jones算法。让我们从定义ViolaJones类开始。我们的算法将使用的唯一超参数是特征的数量(也就是弱分类器的数量)

class ViolaJones: def __init__(self, T = 10): self.T = T


图像分类的输入图像一般多大 图像分类最新算法_图像分类的输入图像一般多大_07


在训练中,Viola-Jones使用Adaboost的变体。增强的概念是针对每个后续的弱分类器来纠正先前分类器的错误。为此,它为每个训练示例分配权重,训练分类器,选择最佳分类器,并根据分类器的误差更新权重。标记不正确的例子将获得更大的权重,因此它们将被选择的下一个分类器正确分类。


图像分类的输入图像一般多大 图像分类最新算法_权重_08


增强的可视化表示。

这给出了算法的大致轮廓:

  1. 初始化权重
  2. 规范化权重
  3. 选择最佳弱分类器(基于加权误差)
  4. 根据所选分类器错误更新权重
  5. 重复步骤2-4 T次,其中T是所需的弱分类器数

在ViolaJones类中添加一个训练方法,我们可以在其中实现训练。

def train(self, training): training_data = [] for x in range(len(training)): training_data.append((integral_image(training[x][0]), training[x][1]))


图像分类的输入图像一般多大 图像分类最新算法_图像分类的输入图像一般多大_09


现在,train将采用单个参数training_data ,即一个元组数组。元组中的第一个元素是一个表示图像的2D numpy数组,第二个元素将是其分类(1表示正数,0表示负数)。循环将所有图像转换为其积分图像格式。我们将积分图像添加到一个新数组中以保存原始数据集。

初始化权重

在算法开始时,我们没有误差作为权值的基础,所以同一个类的每个训练例子的权值都是相同的(所有正例都被加权,所有的负例都是相同的)。这意味着对于第i个图像


图像分类的输入图像一般多大 图像分类最新算法_图像分类算法_10


其中p是正例的数量,n是负例的数量。假设我们事先知道正例和负例的数量,因此train将它们作为参数

def train(self, training, pos_num, neg_num): weights = np.zeros(len(training)) training_data = [] for x in range(len(training)): training_data.append((integral_image(training[x][0]), training[x][1])) if training[x][1] == 1: weights[x] = 1.0 / (2 * pos_num) else: weights[x] = 1.0 / (2 * neg_num)


图像分类的输入图像一般多大 图像分类最新算法_图像分类的输入图像一般多大_11


构建的功能

训练的主循环需要选择最佳弱分类器,但每个可能的特征都有一个弱分类器。因此,在开始实施主循环训练之前,我们必须构建所有特征。特征如下所示。


图像分类的输入图像一般多大 图像分类最新算法_图像分类算法_12


让我们将build_features实现为ViolaJones类的一部分,以返回所有特征的数组。

def build_features(self, image_shape): height, width = image_shape features = [] for w in range(1, width+1): for h in range(1, height+1): i = 0 while i + w < width: j = 0 while j + h < height: #2 rectangle features immediate = RectangleRegion(i, j, w, h) right = RectangleRegion(i+w, j, w, h) if i + 2 * w < width: #Horizontally Adjacent features.append(([right], [immediate])) bottom = RectangleRegion(i, j+h, w, h) if j + 2 * h < height: #Vertically Adjacent features.append(([immediate], [bottom])) right_2 = RectangleRegion(i+2*w, j, w, h) #3 rectangle features if i + 3 * w < width: #Horizontally Adjacent features.append(([right], [right_2, immediate])) bottom_2 = RectangleRegion(i, j+2*h, w, h) if j + 3 * h < height: #Vertically Adjacent features.append(([bottom], [bottom_2, immediate])) #4 rectangle features bottom_right = RectangleRegion(i+w, j+h, w, h) if i + 2 * w < width and j + 2 * h < height: features.append(([right, bottom], [immediate, bottom_right])) j += 1 i += 1 return features


图像分类的输入图像一般多大 图像分类最新算法_图像分类算法_13


image_shape参数是表单的一个元组(height, width) 。算法的其余部分循环遍历图像中的所有矩形,并检查是否可以使用它创建特征。我们对特征的表示是包含两个数组的元组。第一个数组将是对该特征有积极贡献的RectangleRegions,第二个数组将是对该特征产生负面影响的RectangleRegions。这种表示将允许我们稍后轻松保存我们的分类器。

运用这些特征

当我们在以后的算法中寻找最优的弱分类器时,将需要对每个训练示例的每个特征进行评估。为了节省计算,我们将在开始训练分类器之前进行此操作。这样更有效,因为每次选择新分类器时,都需要对每个分类器进行重新训练。如果我们将这些特征应用于训练循环中的训练示例,我们将重复工作,因为图像的每个特征的值永远不会改变。在训练之前应用这些特征还可以让我们稍后预先选择特征以加速训练。保持一个包含每个训练示例标签的单独数组将简化我们的代码,因此我们也将在此步骤中创建该数组。将apply_features方法添加到ViolaJones类。

def apply_features(self, features, training_data): X = np.zeros((len(features), len(training_data))) y = np.array(map(lambda data: data[1], training_data)) i = 0 for positive_regions, negative_regions in features: feature = lambda ii: sum([pos.compute_feature(ii) for pos in positive_regions]) - sum([neg.compute_feature(ii) for neg in negative_regions]) X[i] = list(map(lambda data: feature(data[0]), training_data)) i += 1 return X, y


图像分类的输入图像一般多大 图像分类最新算法_积分图_14


train方法现在应该是这样的

def train(self, training, pos_num, neg_num): weights = np.zeros(len(training)) training_data = [] for x in range(len(training)): training_data.append((integral_image(training[x][0]), training[x][1])) if training[x][1] == 1: weights[x] = 1.0 / (2 * pos_num) else: weights[x] = 1.0 / (2 * neg_num) features = self.build_features(training_data[0][0].shape) X, y = self.apply_features(features, training_data)


图像分类的输入图像一般多大 图像分类最新算法_图像分类算法_15


弱分类器

我们终于有了开始构建和训练弱分类器的所有部分。Viola-Jones使用一系列弱分类器并将其结果加权以产生最终分类。每个弱分类器不能准确地完成分类任务。每个弱分类器都会查看单个特征(f)。它具有阈值(θ)和极性(p)以确定训练示例的分类。


图像分类的输入图像一般多大 图像分类最新算法_积分图_16


极性可以是-1或1。当p = 1时,弱分类器在f(x)θ时,弱分类器输出正结果。现在,让我们定义一个类来封装弱分类器函数。

class WeakClassifier: def __init__(self, positive_regions, negative_regions, threshold, polarity): self.positive_regions = positive_regions self.negative_regions = negative_regions self.threshold = threshold self.polarity = polaritydef classify(self, x): feature = lambda ii: sum([pos.compute_feature(ii) for pos in self.positive_regions]) - sum([neg.compute_feature(ii) for neg in self.negative_regions]) return 1 if self.polarity * feature(x) < self.polarity * self.threshold else 0


图像分类的输入图像一般多大 图像分类最新算法_积分图_17


每个特征都是正矩形区域的和减去负矩形区域的和。算法的下一步是选择最好的弱分类器。要做到这一点,我们需要为每一个找到最佳阈值和极性。

训练弱分类器

训练弱分类器是算法中计算量最大的一部分,因为每次选择新的弱分类器作为最佳分类器时,由于训练示例的加权不同,所以必须重新训练所有弱分类器。然而,有一种有效的方法可以使用权重找到单个弱分类器的最佳阈值和极性。首先,根据对应的特征值对权重进行排序。现在迭代权重数组,如果选择阈值作为该特征,则计算错误。找出具有最小误差的阈值和极性。阈值的可能值是每个训练示例上的特征值。误差可以通过:


图像分类的输入图像一般多大 图像分类最新算法_积分图_18


T表示权重的总和,S表示到目前为止看到的所有例子的权重之和。上标“+”和“ - ”表示总和所属的类别。从概念上讲,如果当前位置下方的所有示例都标记为负数或者当前位置下方的所有示例都标记为正数(考虑每个示例的加权方式),则会错误分类多少示例,此错误会比较多少示例将被错误分类。


图像分类的输入图像一般多大 图像分类最新算法_积分图_19


在本例中,数字表示特征值,气泡的大小表示其相对权重。显然,当任何值小于9的特征被分类为蓝色时,错误将最小化。对应于阈值9,极性为1。

这样,我们可以在恒定时间(O(1))中评估每个可能阈值的误差,并在线性时间(O(n))中评估所有阈值的误差。阈值设置为错误最小的特征值。极性由阈值的左(小于)和右(大于)的正示例和负示例的数量决定。如果阈值还有更多积极例子,则p = 1。否则,p = -1。这是我刚才在代码中描述的内容(ViolaJones类的一部分)。这个方法将训练所有弱分类器并将它们返回到数组中。

def train_weak(self, X, y, features, weights): total_pos, total_neg = 0, 0 for w, label in zip(weights, y): if label == 1: total_pos += w else: total_neg += w classifiers = [] total_features = X.shape[0] for index, feature in enumerate(X): if len(classifiers) % 1000 == 0 and len(classifiers) != 0: print("Trained %d classifiers out of %d" % (len(classifiers), total_features)) applied_feature = sorted(zip(weights, feature, y), key=lambda x: x[1]) pos_seen, neg_seen = 0, 0 pos_weights, neg_weights = 0, 0 min_error, best_feature, best_threshold, best_polarity = float('inf'), None, None, None for w, f, label in applied_feature: error = min(neg_weights + total_pos - pos_weights, pos_weights + total_neg - neg_weights) if error < min_error: min_error = error best_feature = features[index] best_threshold = f best_polarity = 1 if pos_seen > neg_seen else -1 if label == 1: pos_seen += 1 pos_weights += w else: neg_seen += 1 neg_weights += w clf = WeakClassifier(best_feature[0], best_feature[1], best_threshold, best_polarity) classifiers.append(clf) return classifiers


图像分类的输入图像一般多大 图像分类最新算法_图像分类算法_20


选择最好的弱分类器

一旦我们训练了所有弱分类器,我们现在可以找到最好的分类器。这就像遍历所有分类器并计算每个分类器的平均加权误差一样简单。将select_best方法添加到ViolaJones类

def select_best(self, classifiers, weights, training_data): best_clf, best_error, best_accuracy = None, float('inf'), None for clf in classifiers: error, accuracy = 0, [] for data, w in zip(training_data, weights): correctness = abs(clf.classify(data[0]) - data[1]) accuracy.append(correctness) error += w * correctness error = error / len(training_data) if error < best_error: best_clf, best_error, best_accuracy = clf, error, accuracy return best_clf, best_error, best_accuracy


图像分类的输入图像一般多大 图像分类最新算法_图像分类算法_21


请注意,我们返回到训练数据的原始表示形式(元组数组)。这是因为我们需要将积分图像传递给弱分类器,而不是特征值。另外,我们会跟踪准确性。这将在算法的下一步中更新权重时发挥作用。train_weak和select_best一起构成了我前面概述的高级算法的第三步。更新train以使用这两种方法。

def train(self, training, pos_num, neg_num): ... for t in range(self.T): weak_classifiers = self.train_weak(X, y, features, weights) clf, error, accuracy = self.select_best(weak_classifiers, weights, training_data)


图像分类的输入图像一般多大 图像分类最新算法_图像分类算法_22


这些函数属于循环,因为最佳弱分类器依赖于每个训练示例的权重,在选择每个新的弱分类器之后,每个训练示例的权重都会发生变化。

更新权重

Viola-Jones的大部分工作用于构建特征,训练分类器,以及在每次迭代中选择最佳弱分类器。其余的训练方法相对简单。在选择最佳分类器之前,我们应该对权重进行归一化。在选择最佳弱分类器之后,我们需要根据所选弱分类器的误差来更新权重。分类正确的训练样本权重较小,分类错误的训练样本权重保持不变。


图像分类的输入图像一般多大 图像分类最新算法_数组_23


ε表示最佳分类器的误差,w是第i个示例的权重,β是改变权重的原因。β的指数是1-e,如果训练示例被正确分类,则e为0而如果分类错误,则为1(这就是我们在选择最佳弱分类器时返回精度数组的原因)。

def train(self, training, pos_num, neg_num): ... for t in range(self.T): weights = weights / np.linalg.norm(weights) weak_classifiers = self.train_weak(X, y, features, weights) clf, error, accuracy = self.select_best(weak_classifiers, weights, training_data) beta = error / (1.0 - error) for i in range(len(accuracy)): weights[i] = weights[i] * (beta ** (1 - accuracy[i]))


图像分类的输入图像一般多大 图像分类最新算法_权重_24


强分类器

最后,我们必须从弱分类器中编译出强分类器。强分类器定义为


图像分类的输入图像一般多大 图像分类最新算法_权重_25


系数α是每个弱分类器在最终决策中具有多少权重,并且它取决于误差,因为它是β的倒数的自然对数。将弱分类器决策的加权和与一半的α的加权和进行比较,因为这类似于“至少有一半弱分类器与正分类一致”。

我们需要存储所有分类器及其相应的α,因此向__init__中添加两个新的实例变量。

def __init__(self, T = 10): self.T = T self.alphas = [] self.clfs = []


图像分类的输入图像一般多大 图像分类最新算法_图像分类的输入图像一般多大_26


接下来,更新train方法的循环以计算α并存储它们以及分类器

def train(self, training, pos_num, neg_num): ... for t in range(self.T): weights = weights / np.linalg.norm(weights) weak_classifiers = self.train_weak(X, y, features, weights) clf, error, accuracy = self.select_best(weak_classifiers, weights, training_data) beta = error / (1.0 - error) for i in range(len(accuracy)): weights[i] = weights[i] * (beta ** (1 - accuracy[i])) alpha = math.log(1.0/beta) self.alphas.append(alpha) self.clfs.append(clf)


图像分类的输入图像一般多大 图像分类最新算法_数组_27


最终训练方法应如下所示

def train(self, training, pos_num, neg_num): weights = np.zeros(len(training)) training_data = [] for x in range(len(training)): training_data.append((integral_image(training[x][0]), training[x][1])) if training[x][1] == 1: weights[x] = 1.0 / (2 * pos_num) else: weights[x] = 1.0 / (2 * neg_num) features = self.build_features(training_data[0][0].shape) X, y = self.apply_features(features, training_data) for t in range(self.T): weights = weights / np.linalg.norm(weights) weak_classifiers = self.train_weak(X, y, features, weights) clf, error, accuracy = self.select_best(weak_classifiers, weights, training_data) beta = error / (1.0 - error) for i in range(len(accuracy)): weights[i] = weights[i] * (beta ** (1 - accuracy[i])) alpha = math.log(1.0/beta) self.alphas.append(alpha) self.clfs.append(clf)


图像分类的输入图像一般多大 图像分类最新算法_图像分类算法_28


确保添加import math到文件的顶部。最后,实现了强分类器的分类方法。

def classify(self, image): total = 0 ii = integral_image(image) for alpha, clf in zip(self.alphas, self.clfs): total += alpha * clf.classify(ii) return 1 if total >= 0.5 * sum(self.alphas) else 0


图像分类的输入图像一般多大 图像分类最新算法_图像分类的输入图像一般多大_29


改进

特征的数量增长速度非常快。对于24x24图像,有超过160,000个特征,对于28x28图像,有超过250,000个特征。而且,许多这些特征不会提供太多的分类能力,而且大多数都是无用的。删除尽可能多的特征将非常有用,这样我们就可以训练更少的弱分类器。SciKit-Learn软件包可以帮助我们利用SelectPercentile类缩小我们的特征空间。要利用这点,请将以下内容添加到train方法中并导入所需的类。

from sklearn.feature_selection import SelectPercentile, f_classifdef train(self, training_data, pos_num, neg_num): ... X, y = self.apply_features(features, training_data) indices = SelectPercentile(f_classif, percentile=10).fit(X.T, y).get_support(indices=True) X = X[indices] features = features[indices] ...


图像分类的输入图像一般多大 图像分类最新算法_权重_30


拟合SelectPercentile类将找到最佳的k%的特征(我在这里选择了10%)。get_support方法然后返回这些特征的索引。在拟合SelectPercentile时,请注意X.T是传入的而不仅仅是X.这是因为SelectPercentile期望每一行都是一个训练示例,每列都是一个特征值,但X数组对此进行了切换。

保存和加载

由于ViolaJones需要很长时间训练,因此能够从文件中保存和加载模型非常有用。使用Pickle模块很容易。将以下内容添加到ViolaJones类

def save(self, filename): with open(filename+".pkl