Python OpenCV
聚焦Python和OpenCV的图像处理,3D场景重建,对象检测和跟踪
准备
可以免费获得GTSRB数据集。
构思App
为了获得这样的多类分类器(可以区分数据集中的40多个不同符号),我们需要执行以下步骤:
- 预处理数据集:我们需要一种加载数据集,提取感兴趣区域并将数据拆分为适当的训练和测试集的方法。
- 提取功能:原始像素值可能不是数据的最有用信息表示。 我们需要一种从数据中提取有意义的特征的方法,例如基于不同颜色空间和HOG的特征。
- 训练分类器:我们将使用一对多策略在训练数据上训练多分类器。
- 给分类器评分:我们将通过计算不同的性能指标(例如准确性,准确性和召回率)来评估经过训练的集成分类器的质量。
接下来,我们将详细讨论上述步骤。
最终的应用将解析数据集,训练集成分类器,评估其分类性能,并可视化结果。 这将需要以下组件:
- main:启动应用程序需要主函数例程。
- datasets.gtsrb::这是用于解析GTSRB数据集的脚本。 该脚本包含以下功能:
- load_data:此功能用于加载GTSRB数据集,提取所选功能并将数据分为训练集和测试集。
- _featurize, hog_featurize:这些函数将传递到load_data,以从数据集中提取所选特征。 示例函数如下:
- gray_featurize:此功能可基于灰度像素值创建要素。
- surf_featurize:此功能可基于快速强化功能(SURF)创建功能。
监督学习概念简介
机器学习的一个重要子领域是监督学习。 在监督学习中,我们尝试从一组标记的数据中学习-也就是说,每个数据样本都具有所需的目标值或真实的输出值。 这些目标值可以对应于函数的连续输出(例如y = sin(x)中的y),也可以对应于更抽象和离散的类别(例如猫或狗)。
监督学习算法使用已经标记的训练数据,对其进行分析,并生成从特征到标记的映射推断函数,可用于映射新示例。 理想情况下,推断的算法将很好地泛化并为新数据提供正确的目标值。
我们将监督学习任务分为两类:
- 如果我们要处理连续的输出(例如,下雨的概率),则该过程称为回归。
- 如果我们处理离散输出(例如,动物的种类),则此过程称为分类。
在此,我们将重点放在标记GTSRB数据集的图像的分类问题上,并将使用一种称为SVM的算法来推断图像及其标签之间的映射函数。
训练过程
例如,我们可能想了解猫和狗的模样。 为了使它成为有监督的学习任务,首先,我们必须将其作为具有绝对答案或实值答案的问题。
这里有一些示例问题:
- 给定图片中显示了哪种动物?
- 图片中有只猫吗?
- 图片中有狗吗?
之后,我们必须收集示例图片及其相应的正确答案-训练数据。
然后,我们必须选择一种学习算法(学习者),并开始以某种方式调整其参数(学习算法),以便学习者在从训练数据中呈现出一个基准时就可以分辨出正确的答案。
我们重复此过程,直到我们对学习者在训练数据上的表现或得分(可能是准确性,准确性或其他成本函数)感到满意为止。 如果我们不满意,我们将更改学习者的参数,以随着时间的推移提高分数。
以下屏幕快照概述了此过程:
在上一个屏幕截图中,培训数据由一组功能表示。 对于现实生活中的分类任务,这些功能很少是图像的原始像素值,因为它们往往无法很好地表示数据。 通常,查找最能描述数据的特征的过程是整个学习任务(也称为特征选择或特征工程)的重要组成部分。
这就是为什么在考虑建立分类器之前深入研究正在使用的训练集的统计数据和外观总是一个好主意的原因。
您可能已经知道,这里有整个学习者,成本函数和学习算法的动物园。 这些构成了学习过程的核心。 学习者(例如,线性分类器或SVM)定义如何将输入特征转换为得分函数(例如,均方误差),而学习算法(例如,梯度下降)定义学习者的参数的方式 随着时间的推移而改变。
分类任务中的训练过程也可以认为是找到合适的决策边界,这是一条将训练集最好地分为两个子集的线,每个子集一个。 例如,考虑训练样本仅具有两个特征(x和y值)和相应的类别标签(正(+)或负(-))。
在训练过程的开始,分类器尝试画一条线以将所有正标签与所有负标签分开。 随着训练的进行,分类器看到越来越多的数据样本。 这些用于更新决策边界,如以下屏幕截图所示:
与该简单图示相比,SVM试图在高维空间中找到最佳决策边界,因此决策边界可能比直线更复杂。
测试过程
为了使训练有素的分类器具有任何实际价值,我们需要知道将其应用到从未见过的数据样本(也称为泛化)时的性能。 继续前面显示的示例,我们想知道分类器在向其提供以前看不见的猫或狗的图片时预测的类别。
一般而言,我们想知道哪个类? 在下面的屏幕快照中,根据我们在训练阶段中学到的决策边界,该符号对应于:
从前面的屏幕截图中,您可以看到为什么这是一个棘手的问题。 如果问号(?)的位置位于左侧,则可以确定相应的类标签为+。
但是,在这种情况下,有几种方法可以绘制决策边界,以使所有符号都位于其左侧,所有–符号都位于其右侧,如以下屏幕截图所示:
标签?取决于训练过程中得出的确切决策边界。 如果? 前面的屏幕快照中的符号实际上是一个-号,那么只有一个决策边界(最左边)会得到正确的答案。 一个常见的问题是,训练可能会导致决策边界在训练集上无法很好地起作用(也称为过拟合),在应用于看不见的数据时也会犯很多错误。
在此情况下,学习者很可能在决策边界上烙印了特定于训练集的细节,而不是揭示关于数据的一般属性,这对于看不见的数据也可能是正确的。
减少过度拟合影响的常用技术称为正则化。
长话短说:问题总是回到找到最佳的边界,该边界不仅可以最佳地划分训练集,而且可以最佳地划分测试集。 这就是为什么分类器最重要的指标是其泛化性能(即,如何分类训练阶段未看到的数据)。
为了将我们的分类器应用于交通标志识别,我们需要一个合适的数据集。 一个不错的选择可能是GTSRB数据集。
了解GTSRB数据集
GTSRB数据集包含超过50,000个属于43个类别的交通标志图像。
在2011年国际神经网络联合会议(IJCNN)上,专业人士在分类挑战中使用了此数据集。GTSRB数据集庞大,组织化,开源且带有注释,非常适合我们的目的。
尽管实际的交通标志不一定是正方形或在每个图像的中心,但是数据集带有一个注释文件,该文件指定了每个标志的边界框。
在进行任何形式的机器学习之前,一个好主意通常是对数据集,其质量和挑战有所了解。 一些好主意包括手动浏览数据并了解数据的某些特征,读取数据描述(如果页面上有数据描述)以了解哪种模型可能效果最好,等等。
在这里,我们展示了来自data / gtsrb.py的一个片段,该片段加载并绘制出训练数据集的随机15个样本,并进行了100次,因此您可以对数据进行分页:
if __name__ == '__main__':
train_data, train_labels = load_training_data(labels=None)
np.random.seed(75)
for _ in range(100):
indices = np.arange(len(train_data))
np.random.shuffle(indices)
for r in range(3):
for c in range(5):
i = 5 * r + c
ax = plt.subplot(3, 5, 1 + i)
sample = train_data[indices[i]]
ax.imshow(cv2.resize(sample, (32, 32)), cmap=cm.Greys_r)
ax.axis('off')
plt.tight_layout()
plt.show()
np.random.seed(np.random.randint(len(indices)))
另一个好的策略是从43个类别的每一个中绘制15个样本,并查看给定类别的图像如何变化。 以下屏幕截图显示了此数据集的一些示例:
即使从这个小的数据样本中,也很明显这对于任何种类的分类器都是一个挑战性的数据集。 标志的外观会根据视角(方向),观看距离(模糊)和照明条件(阴影和亮点)而发生巨大变化。
对于其中一些标志(例如第三行的第二个标志),即使对于人类(至少对我来说),也很难立即告诉正确的类标签。
解析数据库
GTSRB数据集有21个文件可以下载。 我们选择使用原始数据以使其更具教育性,并下载官方培训数据-用于培训的图像和注释(GTSRB_Final_Training_Images.zip),以及在IJCNN 2011竞赛中使用的官方培训数据集-图像和注释(GTSRBTraining_fixed .zip)进行评分。
以下屏幕快照显示了数据集中的文件:
我们选择分别下载训练和测试数据,而不是从其中一个数据集构建我们自己的训练/测试数据,因为在探索数据之后,通常有30个不同距离的相同符号的图像看起来非常相似。 即使我们的模型可能不能很好地推广,将这30张图像放在不同的数据集中也可以解决问题,并能取得很好的结果。
以下代码是从哥本哈根大学数据档案下载数据的函数:
前面的代码使用文件名(您可以从前面的屏幕快照中查看文件及其名称),并检查文件是否已存在(并检查md5sum是否匹配(如果提供))。 由于不必一次又一次下载文件,因此节省了大量带宽和时间。 然后,它下载文件并将其存储在与包含代码的文件相同的目录中。
注释格式可以在此查看。
下载文件后,我们编写一个函数,该函数使用数据随附的注释格式解压缩并提取数据,如下所示:
- 首先,我们打开下载的.zip文件(可以是训练数据或测试数据),然后迭代所有文件,仅打开.csv文件,其中包含相应类中每个图像的目标信息。 如下代码所示:
python def _load_data(filepath, labels): data, targets = [], [] with ZipFile(filepath) as data_zip: for path in data_zip.namelist(): if not path.endswith('.csv'): continue # Only iterate over annotations files ...
- 然后,我们检查图像的标签是否在我们感兴趣的标签数组中。然后,我们创建一个csv.reader,将其用于遍历.csv文件内容,如下所示:
python .... # Only iterate over annotations files *dir_path, csv_filename = path.split('/') label_str = dir_path[-1] if labels is not None and int(label_str) not in labels: continue with data_zip.open(path, 'r') as csvfile: reader = csv.DictReader(TextIOWrapper(csvfile),delimiter=';') for img_info in reader: ...
- 文件的每一行都包含一个数据样本的注释。 因此,我们提取图像路径,读取数据,并将其转换为NumPy数组。 通常,这些样本中的对象没有被完美切出,而是嵌入其周围。 我们使用档案中提供的边界框信息对每个标签使用.csv文件来剪切图像。 在以下代码中,我们将符号添加到数据中并将标签添加到目标中:
python img_path = '/'.join([*dir_path,img_info['Filename']]) raw_data = data_zip.read(img_path) img = cv2.imdecode(np.frombuffer(raw_data,np.uint8), 1) x1, y1 = np.int(img_info['Roi.X1']),np.int(img_info['Roi.Y1']) x2, y2 = np.int(img_info['Roi.X2']),np.int(img_info['Roi.Y2']) data.append(img[y1: y2, x1: x2]) targets.append(np.int(img_info['ClassId']))
通常,希望执行某种形式的特征提取,因为原始图像数据很少是数据的最佳描述。 我们将把这项工作推迟到另一个函数,稍后详述。
如之前所述,必须将用于训练分类器的样本与用于测试分类器的样本分开。 为此,以下代码段向我们展示了我们有两个不同的功能,它们可以下载训练和测试数据并将其加载到内存中:
def load_training_data(labels):
filepath = _download('GTSRB-Training_fixed.zip',md5sum='513f3c79a4c5141765e10e952eaa2478')
return _load_data(filepath, labels)
def load_test_data(labels):
filepath = _download('GTSRB_Online-Test-Images-Sorted.zip',md5sum='b7bba7dad2a4dc4bc54d6ba2716d163b')
return _load_data(filepath, labels)
现在我们知道了如何将图像转换为NumPy矩阵,我们可以继续进行更有趣的部分,即可以将数据馈送到SVM并对其进行训练以进行预测。