OpenCV Recipes:目标识别

In this post, we are going to learn about object recognition and how we can use it to build a visual search engine.

目标检测与目标识别

你一定经常听到目标检测和目标识别这两个术语,它们经常被误认为是同一件事,其实两者之间有非常明显的区别。目标检测是指检测给定场景中特定对象的存在。我们不知道目标可能是什么。目标识别是识别给定图像中的对象的过程。例如,目标识别系统可以告诉你给定的图像是包含一件衣服还是一双鞋子。事实上,我们可以训练一个目标识别系统来识别许多不同的物体。问题是目标识别是一个很难解决的问题。几十年来,计算机视觉研究人员一直回避它,它已经成为计算机视觉的圣杯。

完美的目标识别器(object recognizer)会给你所有与该对象相关的信息。如果目标识别器知道对象的位置,它就会更加准确。因此,第一步是检测目标并获得边界框。一旦有了这个,我们就可以运行目标识别器来提取更多信息。

什么是 Dense 特征检测器?

为了从图像中提取有意义的大量信息,我们需要确保我们的特征提取器从给定图像的所有部分提取特征。考虑以下图像:

如果使用特征提取器提取特征,它将如下所示:

cv2.FeaturetureDetector_create("Dense") 检测器已经在 OpenCV 3.2 中删除了,因此我们需要自己实现一个:

我们也可以控制密度。让它变得稀疏:

这样做我们可以确保图像中的每个部分都得到处理。下面是执行此操作的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import sys
import cv2
import numpy as np

class DenseDetector():
def __init__(self, step_size=20, feature_scale=20, img_bound=20):
# Create a dense feature detector
self.initXyStep = step_size
self.initFeatureScale = feature_scale
self.initImgBound = img_bound

def detect(self, img):
keypoints = []
rows, cols = img.shape[:2]
for x in range(self.initImgBound, rows, self.initFeatureScale):
for y in range(self.initImgBound, cols, self.initFeatureScale):
keypoints.append(cv2.KeyPoint(float(x), float(y),
self.initXyStep))
return keypoints

class SIFTDetector():
def __init__(self):
self.detector = cv2.xfeatures2d.SIFT_create()

def detect(self, img):
# Convert to grayscale
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Detect keypoints using SIFT
return self.detector.detect(gray_image, None)

if __name__=='__main__':
input_image = cv2.imread(sys.argv[1])
input_image_dense = np.copy(input_image)
input_image_sift = np.copy(input_image)

keypoints = DenseDetector(20,20,5).detect(input_image)
# Draw keypoints on top of the input image
input_image_dense = cv2.drawKeypoints(input_image_dense, keypoints, None,
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# Display the output image
cv2.imshow('Dense feature detector', input_image_dense)

keypoints = SIFTDetector().detect(input_image)
# Draw SIFT keypoints on the input image
input_image_sift = cv2.drawKeypoints(input_image_sift, keypoints, None,
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# Display the output image
cv2.imshow('SIFT detector', input_image_sift)

# Wait until user presses a key
cv2.waitKey()

这给了我们对提取的信息的密切控制。当我们使用 SIFT 检测器时,图像的某些部分会被忽略。当我们构建一个目标识别器,我们需要评估图像的所有部分。因此,我们使用 dense 检测器,然后从这些关键点提取特征。

什么是视觉词典?

我们将使用词袋(Bag of Words)模型来构建我们的目标识别器。每个图像被表示为视觉单词的直方图。这些视觉单词基本上是使用从训练图像中提取的所有关键点构建的 N 个质心。其管道如下图所示:

从每个训练图像中,我们检测一组关键点,并提取每个关键点的特征。每幅图像都会产生不同数量的关键点。为了训练分类器,每个图像必须使用固定长度的特征向量来表示。该特征向量仅仅是一个直方图,其中每个 bin 对应于一个视觉单词。

当我们从训练图像的所有关键点提取所有特征时,我们执行 K-means 聚类并提取 N 个质心。N 是给定图像的特征向量的长度。每个图像将表示为直方图,其中每个 bin 对应于 N 个质心中的一个。为了简单起见,假设 N 设为 4。现在,在给定的图像中,我们提取 K 个关键点。在这 K 个关键点中,一些最接近第一质心,一些最接近第二质心,依此类推。因此,我们根据离每个关键点最近的质心建立直方图。这个直方图成为我们的特征向量。这个过程称为矢量量化(vector quantization)。

为了理解矢量量化,让我们考虑一个例子。假设我们有一幅图像,并且我们已经从中提取了一定数量的特征点。现在我们的目标是以特征向量的形式来表示这个图像。考虑以下图像:

正如你所看到的,我们有四个质心。请记住,图中所示的点代表特征空间,而不是图像中这些特征点的实际几何位置,图像中许多不同几何位置的点可以在特征空间中彼此靠近。我们的目标是将这张图像表示为直方图,其中每个 bin 对应于一个质心。这样,无论我们从图像中提取多少个特征点,它总是会被转换成固定长度的特征向量。因此,我们将每个特征点舍入到其最近的质心,如下图所示:

如果为此图像构建直方图,它将如下所示:

现在,如果考虑具有不同特征点分布的不同图像,如下所示:

聚类将如下所示:

直方图如下所示:

正如你所看到的,两个图像的直方图差别很大。有许多不同的方法可以做到这一点,其准确性取决于你希望的细粒度。如果你增加质心的数量,你将能够更好地代表图像,从而增加特征向量的唯一性。

什么是监督学习和无监督学习?

监督学习(supervised learning)是指基于标记样本构建函数。无监督学习(unsupervised learning)刚好相反,没有标记数据。假设我们有一堆图像,我们只想把它们分成三组。我们不知道标准是什么。因此,一个无监督的学习算法试图以最好的方式将给定的数据集分成三组。我们讨论这些的原因是,我们将使用监督学习和无监督学习相结合的方法来构建我们的目标识别系统。

什么是支持向量机?

支持向量机(support vector machines,SVM)是机器学习领域中非常流行的监督学习模型。SVMs 非常擅长分析标记数据和检测模式。给定一堆数据点和相关标签,SVMs将以最佳方式构建分离超平面。

等等,什么是超平面?为了理解这一点,让我们考虑下图:

正如你所看到的,这些点被与这些点等距的直线边界分隔开。很容易在二维上可视化。如果是三维,分隔将是平面。当我们为图像构建特征时,特征向量的长度通常在 6 内。所以,当我们去这样一个高维空间时,线的等效物就是超平面。一旦超平面形成,我们使用这个数学模型根据未知数据在映射上的位置对其进行分类。

如果不能用简单的直线将数据分开会怎么样?

我们在 SVMs 中使用了一种叫做核技巧(kernel trick)的东西。考虑以下图像:

正如我们所看到的,我们不能画一条简单的直线来区分红色点和蓝色点。给出一个完美的曲线边界来满足所有的点是非常计算昂贵的。SVMs 非常擅长画直线,它们可以在任意维度上绘制这些直线。因此,从技术上讲,如果你将这些点投影到一个高维空间中,在那里它们可以被一个简单的超平面分开,SVMs 将会给出一个精确的边界。一旦我们有了边界,我们就可以将它投影回原始空间。这个超平面在我们原始的低维空间上的投影看起来是弯曲的,如下图所示:

SVMs的主题非常深入,我们将无法在这里详细讨论。如果你真的感兴趣,网上有大量的资料。

我们到底是如何实现的?

现在,让我们构建一个目标识别器,该识别器可以识别给定图像是包含裙子、鞋子还是包。可以很容易地扩展这个系统来检测任意数量的物品。

在开始之前,需要确保我们有一组训练图像。在线上有许多数据库,其中的图像整理好了。Caltech256 是最受欢迎的对象识别数据库之一。创建一个名为 images 的文件夹,并在其中创建三个子文件夹,即 dress、footwear 以及 bag。在每个子文件夹中,添加 20 幅与该项相对应的图片。你可以从互联网上下载这些图片,但是要确保这些图片有一个干净的背景。

现在我们有 60 张培训图片,可以准备开始了。另外,目标识别系统实际上需要成千上万的训练图像才能在现实世界中表现良好。因为我们正在构建一个目标识别器来检测三种类型的对象,所以我们将只对每个对象取 20 幅训练图像。添加更多的训练图像将提高我们系统的准确性和鲁棒性。

这里的第一步是从所有训练图像中提取特征向量并构建视觉词典。首先,重用我们以前的 DenseDetector 类,加上 SIFT 特征检测器(见前面代码)。

然后用 Quantizer 类计算矢量量化并建立特征矢量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# Vector quantization
class Quantizer(object):
def __init__(self, num_clusters=32):
self.num_dims = 128
self.extractor = SIFTExtractor()
self.num_clusters = num_clusters
self.num_retries = 10

def quantize(self, datapoints):
# Create KMeans object
kmeans = KMeans(self.num_clusters,
n_init=max(self.num_retries, 1),
max_iter=10, tol=1.0)

# Run KMeans on the datapoints
res = kmeans.fit(datapoints)

# Extract the centroids of those clusters
centroids = res.cluster_centers_

return kmeans, centroids

def normalize(self, input_data):
sum_input = np.sum(input_data)
if sum_input > 0:
return input_data / sum_input
else:
return input_data

# Extract feature vector from the image
def get_feature_vector(self, img, kmeans, centroids):
kps = DenseDetector().detect(img)
kps, fvs = self.extractor.compute(img, kps)
labels = kmeans.predict(fvs)
fv = np.zeros(self.num_clusters)

for i, item in enumerate(fvs):
fv[labels[i]] += 1

fv_image = np.reshape(fv, ((1, fv.shape[0])))
return self.normalize(fv_image)

另一个必需的类是 FeaturementExtractor 类,它用于提取每个图像的质心:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class FeatureExtractor(object):
def extract_image_features(self, img):
# Dense feature detector
kps = DenseDetector().detect(img)

# SIFT feature extractor
kps, fvs = SIFTExtractor().compute(img, kps)

return fvs

# Extract the centroids from the feature points
def get_centroids(self, input_map, num_samples_to_fit=10):
kps_all = []

count = 0
cur_label = ''
for item in input_map:
if count >= num_samples_to_fit:
if cur_label != item['label']:
count = 0
else:
continue

count += 1

if count == num_samples_to_fit:
print("Built centroids for", item['label'])

cur_label = item['label']
img = cv2.imread(item['image'])
img = resize_to_size(img, 150)

num_dims = 128
fvs = self.extract_image_features(img)
kps_all.extend(fvs)

kmeans, centroids = Quantizer().quantize(kps_all)
return kmeans, centroids

def get_feature_vector(self, img, kmeans, centroids):
return Quantizer().get_feature_vector(img, kmeans, centroids)

下面的脚本将为我们提供一个特征字典来分类图像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import os
import sys
import argparse
import _pickle as pickle
import json

import cv2
import numpy as np
from sklearn.cluster import KMeans


def build_arg_parser():
parser = argparse.ArgumentParser(
description='Creates features for given images')
parser.add_argument("--samples", dest="cls", nargs="+", action="append",
required=True,
help="Folders containing the training images"
".\nThe first element needs to be the class label.")
parser.add_argument("--codebook-file", dest='codebook_file',
required=True,
help="Base file name to store the codebook")
parser.add_argument("--feature-map-file", dest='feature_map_file',
required=True,
help="Base file name to store the feature map")

return parser

# Loading the images from the input folder
def load_input_map(label, input_folder):
combined_data = []

if not os.path.isdir(input_folder):
raise IOError("The folder " + input_folder + " doesn't exist")

# Parse the input folder and assign the labels
for root, dirs, files in os.walk(input_folder):
for filename in (x for x in files if x.endswith('.jpg')):
combined_data.append({'label': label, 'image':
os.path.join(root, filename)})

return combined_data

def extract_feature_map(input_map, kmeans, centroids):
feature_map = []

for item in input_map:
temp_dict = {}
temp_dict['label'] = item['label']

print("Extracting features for", item['image'])
img = cv2.imread(item['image'])
img = resize_to_size(img, 150)

temp_dict['feature_vector'] = FeatureExtractor().get_feature_vector(
img, kmeans, centroids)

if temp_dict['feature_vector'] is not None:
feature_map.append(temp_dict)

return feature_map

# Resize the shorter dimension to 'new_size'
# while maintaining the aspect ratio
def resize_to_size(input_image, new_size=150):
h, w = input_image.shape[0], input_image.shape[1]
ds_factor = new_size / float(h)

if w < h:
ds_factor = new_size / float(w)

new_size = (int(w * ds_factor), int(h * ds_factor))
return cv2.resize(input_image, new_size)

if __name__=='__main__':
args = build_arg_parser().parse_args()

input_map = []
for cls in args.cls:
assert len(cls) >= 2, "Format for classes is `<label> file`"
label = cls[0]
input_map += load_input_map(label, cls[1])

# Building the codebook
print("===== Building codebook =====")
kmeans, centroids = FeatureExtractor().get_centroids(input_map)
if args.codebook_file:
with open(args.codebook_file, 'wb') as f:
print('kmeans', kmeans)
print('centroids', centroids)
pickle.dump((kmeans, centroids), f)

# Input data and labels
print("===== Building feature map =====")
feature_map = extract_feature_map(input_map, kmeans,
centroids)
if args.feature_map_file:
with open(args.feature_map_file, 'wb') as f:
pickle.dump(feature_map, f)

背后细节?

我们需要做的第一件事是提取质心。这就是我们将如何构建我们的视觉词典。FeaturementExtractor 类中的 get_centroids 方法就是为了这样做而设计的。我们不断收集图像特征,直到我们有足够的特征。因为我们使用的是 dense 检测器,所以 10 幅图像就足够了。我们取 10 张照片的原因是因为已经产生了大量的特征,即使添加了更多的特征,质心也不会有太大变化。

一旦提取了质心,我们就可以进入下一步的特征提取。质心集是我们的视觉词典。函数 extract_feature_map 将从每幅图像中提取一个特征向量,并将其与相应的标签相关联。我们这样做的原因是因为我们需要这个映射来训练我们的分类器。我们需要一组关键点,每个关键点都应该与一个标签相关联。因此,我们从图像开始,提取特征向量,然后将其与相应的标签(如 bag、dress 或 footwear)相关联。

Quantizer 类旨在实现矢量量化并构建特征矢量。对于从图像中提取的每个关键点,get_feature_vector 方法会在字典中找到最近的视觉单词。这样我们最终基于视觉词典建立了一个直方图。现在,每个图像都被表示为一组视觉单词的组合。因此有了这个名字,Bag of Words。

下一步是使用这些特征训练分类器。为此,我们实现了ClassifierTrainer类。现在,基于我们之前的特征字典,我们生成 SVM 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import os
import sys
import argparse

import _pickle as pickle
import numpy as np
from sklearn.multiclass import OneVsOneClassifier
from sklearn.svm import LinearSVC
from sklearn import preprocessing

# To train the classifier
class ClassifierTrainer(object):
def __init__(self, X, label_words):
# Encoding the labels (words to numbers)
self.le = preprocessing.LabelEncoder()

# Initialize One vs One Classifier using a linear kernel
self.clf = OneVsOneClassifier(LinearSVC(random_state=0))

y = self._encodeLabels(label_words)
X = np.asarray(X)
self.clf.fit(X, y)

# Predict the output class for the input datapoint
def _fit(self, X):
X = np.asarray(X)
return self.clf.predict(X)

# Encode the labels (convert words to numbers)
def _encodeLabels(self, labels_words):
self.le.fit(labels_words)
return np.array(self.le.transform(labels_words),
dtype=np.float32)

# Classify the input datapoint
def classify(self, X):
labels_nums = self._fit(X)
labels_words = self.le.inverse_transform([int(x) for x in
labels_nums])
return labels_words

def build_arg_parser():
parser = argparse.ArgumentParser(description='Trains the classifier models')
parser.add_argument("--feature-map-file", dest="feature_map_file", required=True,\
help="Input pickle file containing the feature map")
parser.add_argument("--svm-file", dest="svm_file", required=False,\
help="Output file where the pickled SVM model will be stored")
return parser

if __name__=='__main__':
args = build_arg_parser().parse_args()
feature_map_file = args.feature_map_file
svm_file = args.svm_file

# Load the feature map
with open(feature_map_file, 'rb') as f:
feature_map = pickle.load(f)

# Extract feature vectors and the labels
labels_words = [x['label'] for x in feature_map]

# Here, 0 refers to the first element in the
# feature_map, and 1 refers to the second
# element in the shape vector of that element
# (which gives us the size)
dim_size = feature_map[0]['feature_vector'].shape[1]

X = [np.reshape(x['feature_vector'], (dim_size,)) for x in feature_map]

# Train the SVM
svm = ClassifierTrainer(X, labels_words)
if args.svm_file:
with open(args.svm_file, 'wb') as f:
pickle.dump(svm, f)

我们是如何构建训练的?

我们使用 scikit-learn 软件包建立 SVM 模型和 scipy 为数学优化工具。

我们从标记的数据开始,并将其提供给 OneVsOneClassifier 方法。我们有 classify 方法,对输入图像进行分类,并将标签与其相关联。

让我们试一试,创建一个名为 models 的文件夹,学习模型将存储在其中。在终端上运行以下命令来创建特征并训练分类器:

1
2
3
$ python create_features.py --samples bag images/bag/ --samples dress images/dress/ --samples footwear images/footwear/ --codebook-file models/codebook.pkl --feature-map-file models/feature_map.pkl

$ python training.py --feature-map-file models/feature_map.pkl --svm-file models/svm.pkl

现在分类器已经过训练,我们需要一个模块来分类输入图像并检测里面的物体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import os
import sys
import argparse
import _pickle as pickle

import cv2
import numpy as np

import create_features as cf
from training import ClassifierTrainer

# Classifying an image
class ImageClassifier(object):
def __init__(self, svm_file, codebook_file):
# Load the SVM classifier
with open(svm_file, 'rb') as f:
self.svm = pickle.load(f)

# Load the codebook
with open(codebook_file, 'rb') as f:
self.kmeans, self.centroids = pickle.load(f)

# Method to get the output image tag
def getImageTag(self, img):
# Resize the input image
img = cf.resize_to_size(img)

# Extract the feature vector
feature_vector = cf.FeatureExtractor().get_feature_vector(img, self.kmeans, self.centroids)

# Classify the feature vector and get the output tag
image_tag = self.svm.classify(feature_vector)

return image_tag


def build_arg_parser():
parser = argparse.ArgumentParser(
description='Extracts features from each line and classifies the data')
parser.add_argument("--input-image", dest="input_image", required=True,
help="Input image to be classified")
parser.add_argument("--svm-file", dest="svm_file", required=True,
help="File containing the trained SVM model")
parser.add_argument("--codebook-file", dest="codebook_file", required=True,
help="File containing the codebook")
return parser

if __name__=='__main__':
args = build_arg_parser().parse_args()
svm_file = args.svm_file
codebook_file = args.codebook_file
input_image = cv2.imread(args.input_image)

tag = ImageClassifier(svm_file, codebook_file).getImageTag(input_image)
print("Output class:", tag)

我们从输入图像中提取 feature 向量,并将其用作分类器的输入参数。让我们去看看这是否行得通。从网上下载一张随机的鞋子图片,确保它有一个干净的背景。运行以下命令:

1
$ python classify_data.py --input-image new_image.jpg --svm-file models/svm.pkl --codebook-file models/codebook.pkl

我们可以使用同样的技术来构建视觉搜索引擎。视觉搜索引擎查看输入图像,并显示一堆与输入图像相似的图像。我们可以重用对象识别框架来构建它。从输入图像中提取特征向量,并将其与训练数据集中的所有特征向量进行比较。挑选最匹配的并显示结果。

GreatX wechat
Subscribe to my blog by scanning my public wechat account