赞
踩
[部分三]:https://blog.csdn.net/Thomson617/article/details/103987952
kNN 可以说是最简单的监督学习分类器了。想法也很简单,就是找出测试 数据在特征空间中的最近邻居。我们将使用下面的图片介绍它。
上图中的对象可以分成两组:蓝色方块和红色三角。每一组也可以称为一个类。
我们可以把所有的这些对象看成是一个城镇中房子,而所有的房子分别属于蓝色和红色家族,而这个城镇就是所谓的特征空间。(你可以把一个特征空间看成是所有点的投影所在的空间。例如在一个2D的坐标空间中,每个数据都两个特征x坐标和y坐标,你可以在2D坐标空间中表示这些数据。如果每个数据都有3个特征呢,我们就需要一个3D空间。N个特征就需要N维空间,这个N维空间就是特征空间。在上图中,我们可以认为是具有两个特征色2D空间)。现在城镇中来了一个新人,它的新房子用绿色圆盘表示。我们要根据它房子的位置把它归为蓝色家族或红色家族。我们把这过程称为分类。我们应该怎么做呢?一个方法就是查看它最近的邻居属于那个家族,从图像中我们知道最近的是红色三角家族。所以它被分到红色家族。这种方法被称为简单近邻,因为分类仅仅决定与它最近的邻居。
但是这里还有一个问题。红色三角可能是最近的,但如果它周围还有很多蓝色方块怎么办呢?此时蓝色方块对局部的影响应该大于红色三角。所以仅仅检测最近的一个邻居是不足的。所以我们检测k个最近邻居。谁在这k个邻居中占据多数,那新的成员就属于谁那一类。如果k等于3,也就是在上面图像中检测3个最近的邻居。它有两个红的和一个蓝的邻居,所以它还是属于红色家族。但是如果k等于7呢?它有5个蓝色和2个红色邻居,现在它就会被分到蓝色家族了。k的取值对结果影响非常大。更有趣的是,如果k等于4呢?两个红两个蓝。这是一个死结。所以k的取值最好为奇数。这中根据k个最近邻居进行分类的方法被称为kNN。
在kNN中我们考虑了k个最近邻居,但是我们给了这些邻居相等的权重,这样做公平吗?以k等于4为例,我们说她是一个死结。但是两个红色三角比两个蓝色方块距离新成员更近一些。所以它更应该被分为红色家族。那用数学应该如何表示呢?我们要根据每个房子与新房子的距离对每个房子赋予不同的权重。距离近的具有更高的权重,距离远的权重更低。然后我们根据两个家族的权重和来判断新房子的归属,谁的权重大就属于谁。这被称为修改过的kNN。
那这里面哪些是重要的呢?
• 我们需要整个城镇中每个房子的信息。因为我们要测量新来者到所有现存房子的距离,并在其中找到最近的。如果那里有很多房子,就要占用很大的内存和更多的计算时间。
• 训练和处理几乎不需要时间。现在我们看看OpenCV中的kNN。
我们这里来举一个简单的例子,和上面一样有两个类。下一节我们会有一 个更好的例子。
这里我们将红色家族标记为 Class-0,蓝色家族标记为 Class-1。还要 再创建 25 个训练数据,把它们非别标记为 Class-0 或者 Class-1。Numpy 中随机数产生器可以帮助我们完成这个任务。
然后借助 Matplotlib 将这些点绘制出来。红色家族显示为红色三角蓝色 家族显示为蓝色方块。
- import numpy as np
- import cv2
- import matplotlib.pyplot as plt
-
- # Feature set containing (x,y) values of 25 known/training data
- trainData = np.random.randint(0,100,(25,2)).astype(np.float32)
- # Labels each one either Red or Blue with numbers 0 and 1
- responses = np.random.randint(0,2,(25,1)).astype(np.float32)
- # Take Red families and plot them
- red = trainData[responses.ravel()==0]
- plt.scatter(red[:,0],red[:,1],80,'r','^')
- # Take Blue families and plot them
- blue = trainData[responses.ravel()==1]
- plt.scatter(blue[:,0],blue[:,1],80,'b','s')
- plt.show()
你可能会得到一个与上面类似的图形,但不会完全一样,因为你使用了随机数生成器,每次你运行代码都会得到不同的结果。
下面就是 kNN 算法分类器的初始化,我们要传入一个训练数据集,以及与训练数据对应的分类来训练 kNN 分类器(构建搜索树)。
最后要使用 OpenCV 中的 kNN 分类器,我们给它一个测试数据,让它来 进行分类。在使用 kNN 之前,我们应该对测试数据有所了解。我们的数据应 该是大小为数据数目乘以特征数目的浮点性数组。然后我们就可以通过计算找 到测试数据最近的邻居了。我们可以设置返回的最近邻居的数目。返回值包括:
1. 由kNN算法计算得到的测试数据的类别标志(0或1)。如果你想使用最近邻算法,只需要将 k 设置为 1,k 就是最近邻的数目。
2. k 个最近邻居的类别标志。
3. 每个最近邻居到测试数据的距离。 让我们看看它是如何工作的。测试数据被标记为绿色。
- newcomer = np.random.randint(0,100,(1,2)).astype(np.float32)
- plt.scatter(newcomer[:,0],newcomer[:,1],80,'g','o')
- knn = cv2.KNearest()
- knn.train(trainData,responses)
- ret, results, neighbours ,dist = knn.find_nearest(newcomer, 3)
-
- print ("result: ", results,"\n")
- print ("neighbours: ", neighbours,"\n")
- print ("distance: ", dist )
- plt.show()
结果如下:
这说明我们的测试数据有 3 个邻居,它们都是蓝色,所以它被分为蓝色家 族。结果很明显,如下图所示:
如果有大量数据要进行测试,可以直接传入一个数组。对应的结果同样也是数组。
- # 10 new comers
- newcomers = np.random.randint(0,100,(10,2)).astype(np.float32)
- ret, results,neighbours,dist = knn.find_nearest(newcomer, 3)
- # The results also will contain 10 labels.
46.2 使用 kNN 对手写数字 OCR
创建一个可以对手写数字进行识别的程序,为了达到这个目 的我们需要训练数据和测试数据。OpenCV 安装包中有一副图片(/samples/python2/data/digits.png), 其中有 5000 个手写数字(每个数字重复 500遍)。每个数字是一个 20x20 的小图。所以第一步就是将这个图像分割成 5000个不同的数字。我们在将拆分后的每一个数字的图像重排成一行含有 400 个像 素点的新图像。这个就是我们的特征集,所有像素的灰度值。这是我们能创建 的最简单的特征集。我们使用每个数字的前 250 个样本做训练数据,剩余的250 个做测试数据。让我们先准备一下:
- import numpy as np
- import cv2
-
- img = cv2.imread('digits.png')
- gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
-
- # Now we split the image to 5000 cells, each 20x20 size
- cells = [np.hsplit(row,100) for row in np.vsplit(gray,50)]
-
- # Make it into a Numpy array. It size will be (50,100,20,20)
- x = np.array(cells)
-
- # Now we prepare train_data and test_data.
- train = x[:,:50].reshape(-1,400).astype(np.float32) # Size = (2500,400)
- test = x[:,50:100].reshape(-1,400).astype(np.float32) # Size = (2500,400)
-
- # Create labels for train and test data
- k = np.arange(10)
- train_labels = np.repeat(k,250)[:,np.newaxis]
- test_labels = train_labels.copy()
-
- # Initiate kNN, train the data, then test it with test data for k=1
- knn = cv2.KNearest()
- knn.train(train,train_labels)
- ret,result,neighbours,dist = knn.find_nearest(test,k=5)
-
- # Now we check the accuracy of classification
- # For that, compare the result with test_labels and check which are wrong
- matches = result==test_labels
- correct = np.count_nonzero(matches)
- accuracy = correct*100.0/result.size
- print (accuracy)
现在最基本的 OCR 程序已经准备好了,这个示例中我们得到的准确率为 91%。改善准确度的一个办法是提供更多的训练数据,尤其是判断错误的那 些数字。为了避免每次运行程序都要准备和训练分类器,我们最好把它保留, 这样在下次运行是时,只需要从文件中读取这些数据开始进行分类就可以了。 Numpy 函数 np.savetxt,np.load 等可以帮助我们搞定这些。
- np.savez('knn_data.npz',train=train, train_labels=train_labels)
-
- # Now load the data
- with np.load('knn_data.npz') as data:
- print (data.files)
- train = data['train']
- train_labels = data['train_labels']
在我的系统中, 占用的空间大概为 4.4M。 由于我们现在使用灰度值(unint8)作为特征,在保存之前最好先把这些数据装换成 np.uint8 格式,这 样就只需要占用 1.1M 的空间。在加载数据时再转会到 float32。
接下来我们来做英文字母的 OCR。和上面做法一样,但是数据和特征集有 一些不同。现在 OpenCV 给出的不是图片了,而是一个数据文件(/samples/ cpp/letter-recognition.data)。如果打开它的话,你会发现它有 20000 行, 第一样看上去就像是垃圾。实际上每一行的第一列是我们的一个字母标记。接 下来的 16 个数字是它的不同特征。这些特征来源于UCI Machine Learning Repository。你可以在此页找到更多相关信息。
有 20000 个样本可以使用,我们取前 10000 个作为训练样本,剩下的 10000 个作为测试样本。我们应在先把字母表转换成 asc 码,因为我们不正 直接处理字母。
- import cv2
- import numpy as np
-
- # Load the data, converters convert the letter to a number
- data= np.loadtxt('letter-recognition.data', dtype= 'float32', delimiter = ',', converters= {0: lambda ch: ord(ch)-ord('A')})
-
- # split the data to two, 10000 each for train and test
- train, test = np.vsplit(data,2)
-
- # split trainData and testData to features and responses
- responses, trainData = np.hsplit(train,[1])
- labels, testData = np.hsplit(test,[1])
-
- # Initiate the kNN, classify, measure accuracy.
- knn = cv2.KNearest()
- knn.train(trainData, responses)
- ret, result, neighbours, dist = knn.find_nearest(testData, k=5)
-
- correct = np.count_nonzero(result == labels)
- accuracy = correct*100.0/10000
- print (accuracy)
准确率达到了 93.22%。同样你可以通过增加训练样本的数量来提高准确率。
如下图所示,其中含有两类数据,红的和蓝的。如果是使用 kNN,对于一 个测试数据我们要测量它到每一个样本的距离,从而根据最近邻居分类。测量 所有的距离需要足够的时间,并且需要大量的内存存储训练样本。但是分类下 图所示的数据真的需要占用这么多资源吗?
我们在考虑另外一个想法。我们找到了一条直线,f (x) = ax1 + bx2 + c, 它可以将所有的数据分割到两个区域。当我们拿到一个测试数据 X 时,我们只 需要把它代入 f (x)。如果 |f (X) | > 0,它就属于蓝色组,否则就属于红色组。 我们把这条线称为决定边界(Decision_Boundary)。很简单而且内存使用 效率也很高。这种使用一条直线(或者是高位空间种的超平面)上述数据分成 两组的方法成为线性分割。
从上图中我们看到有很多条直线可以将数据分为蓝红两组,那一条直线是 最好的呢?直觉上讲这条直线应该是与两组数据的距离越远越好。为什么呢? 因为测试数据可能有噪音影响(真实数据 + 噪声)。这些数据不应该影响分类 的准确性。所以这条距离远的直线抗噪声能力也就最强。所以 SVM 要做就是 找到一条直线,并使这条直线到(训练样本)各组数据的最短距离最大。下图中加粗的直线经过中心。
要找到决定边界,就需要使用训练数据。我们需要所有的训练数据吗?不, 只需要那些靠近边界的数据,如上图中一个蓝色的圆盘和两个红色的方块。我 们叫它们支持向量,经过它们的直线叫做支持平面。有了这些数据就足以找到 决定边界了。我们担心所有的数据。这对于数据简化有帮助。
We need not worry about all the data. It helps in data reduction.
到底发生了什么呢?首先我们找到了分别代表两组数据的超平面。例如,蓝 色数据可以用 > 1 表示,而红色数据可以用 < −1 表示,ω 叫 做权重向量(ω = [ω1, ω2, . . . , ω3]),x 为特征向量(x = [x1, x2, . . . , xn])。b0 被成为 bias(截距?)。权重向量决定了决定边界的走向,而 bias 点决定了它(决 定边界)的位置。决定边界被定义为这两个超平面的中间线(平面),表达式为 = 0。从支持向量到决定边界的最短距离为 。
边缘长度为这个距离的两倍,我们需要使这个边缘长度最大。我们要创建一个 新的函数 L (ω, b0) 并使它的值最小:
其中 ti 是每一组的标记,ti ∈ [−1, 1]。
想象一下,如果一组数据不能被一条直线分为两组怎么办?例如,在一维 空间中 X 类包含的数据点有(-3,3),O 类包含的数据点有(-1,1)。很明显 不可能使用线性分割将 X 和 O 分开。但是有一个方法可以帮我们解决这个问 题。使用函数 f (x) = x2 对这组数据进行映射,得到的 X 为 9,O 为 1,这时就可以使用线性分割了。
或者我们也可以把一维数据转换成两维数据。我们可以使用函数 f (x) =
(x, x2) 对数据进行映射。这样 X 就变成了(-3,9)和(3,9)而 O 就变成了
(-1,1)和(1,1)。同样可以线性分割,简单来说就是在低维空间不能线性 分割的数据在高维空间很有可能可以线性分割。
通常我们可以将 d 维数据映射到 D 维数据来检测是否可以线性分割(D>d)。这种想法可以帮助我们通过对低维输入(特征)空间的计算来获得高 维空间的积。我们可以用下面的例子说明。
假设我们有二维空间的两个点:p = (p1, p2) 和 q = (q1, q2)。用 Ø 表示映 射函数,它可以按如下方式将二维的点映射到三维空间中:
我们要定义一个核函数 K (p, q),它可以用来计算两个点的内积,如下所示
这说明三维空间中的内积可以通过计算二维空间中内积的平方来获得。这 可以扩展到更高维的空间。所以根据低维的数据来计算它们的高维特征。在进 行完映射后,我们就得到了一个高维空间数据。
除了上面的这些概念之外,还有一个问题需要解决,那就是分类错误。仅 仅找到具有最大边缘的决定边界是不够的。我们还需要考虑错误分类带来的误 差。有时我们找到的决定边界的边缘可能不是最大的但是错误分类是最少的。 所以我们需要对我们的模型进行修正来找到一个更好的决定边界:最大的边缘, 最小的错误分类。评判标准就被修改为:
下图显示这个概念。对于训练数据的每一个样本又增加了一个参数 ξi。它表示训练样本到它们所属类(实际所属类)的超平面的距离。对于那些分类正确的样本这个参数为 0,因为它们会落在它们的支持平面上。
现在新的最优化问题就变成了:
参数 C 的取值应该如何选择呢?很明显应该取决于你的训练数据。虽然没有一个统一的答案,但是在选取 C 的取值时我们还是应该考虑一下下面的规 则:
• 如果 C 的取值比较大,错误分类会减少,但是边缘也会减小。其实就是 错误分类的代价比较高,惩罚比较大。(在数据噪声很小时我们可以选取较大的 C 值。)
• 如果 C 的取值比较小,边缘会比较大,但错误分类的数量会升高。其实就是错误分类的代价比较低,惩罚很小。整个优化过程就是为了找到一个具有最大边缘的超平面对数据进行分类。(如果数据噪声比较大时,应该考虑)
更多资源:
NPTEL notes on Statistical Pattern Recognition, Chapters 25- 29.
在计算 HOG 前我们使用图片的二阶矩对其进行抗扭斜(deskew)处理。 所以我们首先要定义一个函数 deskew(),它可以对一个图像进行抗扭斜处 理。下面就是 deskew() 函数:
- def deskew(img):
- m = cv2.moments(img)
- if abs(m['mu02']) < 1e-2:
- return img.copy()
- skew = m['mu11'] / m['mu02']
- M = np.float32([[1, skew, -0.5 * SZ * skew], [0, 1, 0]])
- img = cv2.warpAffine(img, M, (SZ, SZ), flags=affine_flags)
- return img
下图显示了对含有数字 0 的图片进行抗扭斜处理后的效果。左侧是原始图 像,右侧是处理后的结果。
接下来我们要计算图像的HOG描述符,创建一个函数hog()。为此我们计算图像X方向和Y方向的Sobel导数。然后计算得到每个像素的梯度的方向和大小。把这个梯度转换成16位的整数。将图像分为4个小的方块,对每一个小方块计算它们的朝向直方图(16个bin),使用梯度的大小做权重。这样每一个小方块都会得到一个含有16个成员的向量。4个小方块的4个向量就组成了这个图像的特征向量(包含64个成员)。这就是我们要训练数据的特征向量。
- def hog(img):
- gx = cv2.Sobel(img, cv2.CV_32F, 1, 0)
- gy = cv2.Sobel(img, cv2.CV_32F, 0, 1)
- mag, ang = cv2.cartToPolar(gx, gy)
- bins = np.int32(bin_n*ang/(2*np.pi)) # quantizing binvalues in (0...16)
- bin_cells = bins[:10,:10], bins[10:,:10], bins[:10,10:], bins[10:,10:]
- mag_cells = mag[:10,:10], mag[10:,:10], mag[:10,10:], mag[10:,10:]
- hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
- hist = np.hstack(hists) # hist is a 64 bit vector
- return hist
最后,和前面一样,我们将大图分割成小图。使用每个数字的前250个作为训练数据,后250个作为测试数据。全部代码如下所示:
- import cv2
- import numpy as np
-
- SZ=20
- bin_n = 16 # Number of bins
-
- svm_params = dict( kernel_type = cv2.SVM_LINEAR,svm_type = cv2.SVM_C_SVC, C=2.67, gamma=5.383 )
-
- affine_flags = cv2.WARP_INVERSE_MAP|cv2.INTER_LINEAR
-
- def deskew(img):
- m = cv2.moments(img)
- if abs(m['mu02']) < 1e-2:
- return img.copy()
- skew = m['mu11']/m['mu02']
- M = np.float32([[1, skew, -0.5*SZ*skew], [0, 1, 0]])
- img = cv2.warpAffine(img,M,(SZ, SZ),flags=affine_flags)
- return img
-
- def hog(img):
- gx = cv2.Sobel(img, cv2.CV_32F, 1, 0)
- gy = cv2.Sobel(img, cv2.CV_32F, 0, 1)
- mag, ang = cv2.cartToPolar(gx, gy)
- bins = np.int32(bin_n*ang/(2*np.pi)) # quantizing binvalues in (0...16)
- bin_cells = bins[:10,:10], bins[10:,:10], bins[:10,10:], bins[10:,10:]
- mag_cells = mag[:10,:10], mag[10:,:10], mag[:10,10:], mag[10:,10:]
- hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
- hist = np.hstack(hists) # hist is a 64 bit vector
- return hist
-
- img = cv2.imread('digits.png',0)
- cells = [np.hsplit(row,100) for row in np.vsplit(img,50)]
-
- # First half is trainData, remaining is testData
- train_cells = [ i[:50] for i in cells ]
- test_cells = [ i[50:] for i in cells]
-
- ###### Now training ########################
- deskewed = [map(deskew,row) for row in train_cells]
- hogdata = [map(hog,row) for row in deskewed]
- trainData = np.float32(hogdata).reshape(-1,64)
- responses = np.float32(np.repeat(np.arange(10),250)[:,np.newaxis])
-
- svm = cv2.SVM()
- svm.train(trainData,responses, params=svm_params) svm.save('svm_data.dat')
-
- ###### Now testing ########################
- deskewed = [map(deskew,row) for row in test_cells]
- hogdata = [map(hog,row) for row in deskewed]
- testData = np.float32(hogdata).reshape(-1,bin_n*4)
- result = svm.predict_all(testData)
-
- ####### Check Accuracy ########################
- mask = result==responses
- correct = np.count_nonzero(mask)
- print (correct*100.0/result.size)
准确率达到了 94%。你可以尝试一下不同的参数值,看看能不能达到更高 的准确率。或者也可以读一下这个领域的文章并用代码实现它。
更多资源:Histograms of Oriented Gradients Video
话说有一个公司要生产一批新的 T 恤。很明显它们要生产不同大小的 T 恤 来满足不同顾客的需求。所以这个公司收集了很多人的身高和体重信息,并把 这些数据绘制在图上,如下所示:
肯定不能把每个大小的 T 恤都生产出来,所以它们把所有的人分为三组: 小,中,大,这三组要覆盖所有的人。我们可以使用 K 值聚类的方法将所有人 分为 3 组,这个算法可以找到一个最好的分法,并能覆盖所有人。如果不能覆盖全部人的话,公司就只能把这些人分为更多的组,可能是 4 个或 5 个甚至更 多。如下图:
它的工作原理
这个算法是一个迭代过程,我们会借助图片逐步介绍它。 考虑下面这组数据(你也可以把它当成 T恤问题),我们需要把它们分成两组。
第一步:随机选取两个重心点,C1 和 C2(有时可以选取数据中的两个点 作为起始重心)。
第二步:计算每个点到这两个重心点的距离,如果距离 C1 比较近就标记 为 0,如果距离 C2 比较近就标记为 1。(如果有更多的重心点,可以标记为 “2”,“3”等)
在我们的例子中我们把属于 0 的标记为红色,属于 1 的标记为蓝色。我们 就会得到下面这幅图。
第三步:重新计算所有蓝色点的重心,和所有红色点的重心,并以这两个 点更新重心点的位置。(图片只是为了演示说明而已,并不代表实际数据)
重复步骤 2,更新所有的点标记。 我们就会得到下面的图:
继续迭代步骤 2 和 3,直到两个重心点的位置稳定下来。(当然也可以通 过设置迭代次数,或者设置重心移动距离的阈值来终止迭代。)。此时这些点到 它们相应重心的距离之和最小。简单来说,C1 到红色点的距离与 C2 到蓝色点 的距离之和最小。
最终结果如下图所示:
这就是对 K 值聚类的一个直观解释。要想知道更多细节和数据解释,你应该读一本关于机器学习的教科书或者参考更多资源中的链接。这只是 K 值聚类 的基础。现在对这个算法有很多改进,比如:如何选取好的起始重心点,怎样加速迭代过程等。
更多资源
1. Machine Learning Course, Video lectures by Prof. Andrew Ng (Some of the images are taken from this)
(1). 理解函数的参数
输入参数:
1. samples: 应该是 np.float32 类型的数据,每个特征应该放在一列。
2. nclusters(K): 聚类的最终数目。
3. criteria: 终止迭代的条件。当条件满足时,算法的迭代终止。它应该是一个含有 3 个成员的元组,它们是(typw,max_iter,epsilon):
• type 终止的类型:有如下三种选择:
- cv2.TERM_CRITERIA_EPS 只有精确度 epsilon 满足是 停止迭代。
- cv2.TERM_CRITERIA_MAX_ITER 当迭代次数超过阈值 时停止迭代。
- cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER
- 上面的任何一个条件满足时停止迭代。
• max_iter 表示最大迭代次数。
• epsilon 精确度阈值。
4. attempts: 使用不同的起始标记来执行算法的次数。算法会返回紧密度最好的标记。紧密度也会作为输出被返回。
5. flags:用来设置如何选择起始重心。通常我们有两个选择: cv2.KMEANS_PP_CENTERS和 cv2.KMEANS_RANDOM_CENTERS。
输出参数:
1. compactness:紧密度,返回每个点到相应重心的距离的平方和。
2. labels:标志数组(与上一节提到的代码相同),每个成员被标记为 0,1等
3. centers:由聚类的中心组成的数组。 现在我们用 3 个例子来演示如何使用 K 值聚类。
假设我们有一组数据,每个数据只有一个特征(1 维)。例如前面的 T 恤 问题,我们只使用人们的身高来决定 T 恤的大小。
我们先来产生一些随机数据,并使用 Matplotlib 将它们绘制出来。
- import numpy as np
- from matplotlib import pyplot as plt
-
- x = np.random.randint(25,100,25)
- y = np.random.randint(175,255,25)
- z = np.hstack((x,y))
- z = z.reshape((50,1))
- z = np.float32(z)
- plt.hist(z,256,[0,256]),plt.show()
'运行
现在我们有一个长度为 50,取值范围为 0 到 255 的向量 z。我已经将向 量 z 进行了重排,将它变成了一个列向量。当每个数据含有多个特征是这会很 有用。然后我们数据类型转换成 np.float32。
我们得到下图:
现在我们使用 KMeans 函数。在这之前我们应该首先设置好终止条件。我 的终止条件是:算法执行 10 次迭代或者精确度 epsilon = 1.0。
- # Define criteria = ( type, max_iter = 10 , epsilon = 1.0 )
- criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
- # Set flags (Just to avoid line break in the code)
- flags = cv2.KMEANS_RANDOM_CENTERS
- # Apply KMeans
- compactness,labels,centers = cv2.kmeans(z,2,None,criteria,10,flags
返回值有紧密度(compactness),标志和中心。在本例中我的到的中心是 60 和 207。标志的数目与测试数据的多少是相同的,每个数据都会被标记 上“0”,“1”等。这取决于它们的中心是什么。现在我们可以根据它们的标志将把数据分两组。
- A = z[labels==0]
- B = z[labels==1]
现在将 A 组数用红色表示,将 B 组数据用蓝色表示,重心用黄色表示。
- # Now plot 'A' in red, 'B' in blue, 'centers' in yellow
- plt.hist(A,256,[0,256],color = 'r')
- plt.hist(B,256,[0,256],color = 'b')
- plt.hist(centers,32,[0,256],color = 'y')
- plt.show()
下面就是结果:
含有多个特征的数据:
在前面的 T 恤例子中我们只考虑了身高,现在我们也把体重考虑进去,也 就是两个特征。在前一节我们的数据是一个单列向量。每一个特征被排列成一列,每一行对应一个测试样本。
在本例中我们的测试数据适应 50x2 的向量,其中包含 50 个人的身高和 体重。第一列对应与身高,第二列对应与体重。第一行包含两个元素,第一个 是第一个人的身高,第二个是第一个人的体重。剩下的行对应与其它人的身高和体重。如下图所示:
现在我们来编写代码:
- import numpy as np
- import cv2
- from matplotlib import pyplot as plt
-
- X = np.random.randint(25,50,(25,2))
- Y = np.random.randint(60,85,(25,2))
- Z = np.vstack((X,Y))
- # convert to np.float32
- Z = np.float32(Z)
- # define criteria and apply kmeans()
- criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
- ret,label,center=cv2.kmeans(Z,2,None,criteria,10,cv2.KMEANS_RANDOM_CENTERS)
- # Now separate the data, Note the flatten()
- A = Z[label.ravel()==0]
- B = Z[label.ravel()==1]
- # Plot the data
- plt.scatter(A[:,0],A[:,1])
- plt.scatter(B[:,0],B[:,1],c = 'r')
- plt.scatter(center[:,0],center[:,1],s = 80,c = 'y', marker = 's')
- plt.xlabel('Height'),plt.ylabel('Weight')
- plt.show()
结果如下:
颜色量化就是减少图片中颜色数目的一个过程。为什么要减少图片中的颜 色呢?减少内存消耗!有些设备的资源有限,只能显示很少的颜色。在这种情 况下就需要进行颜色量化。我们使用 K 值聚类的方法来进行颜色量化。
没有什么新的知识需要介绍了。现在有 3 个特征:R,G,B。所以我们需 要把图片数据变形成 Mx3(M 是图片中像素点的数目)的向量。聚类完成后, 我们用聚类中心值替换与其同组的像素值,这样结果图片就只含有指定数目的颜色了。下面是代码:
- import numpy as np
- import cv2
-
- img = cv2.imread('home.jpg')
- Z = img.reshape((-1,3))
- # convert to np.float32
- Z = np.float32(Z)
- # define criteria, number of clusters(K) and apply kmeans()
- criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
- K = 8
- ret,label,center=cv2.kmeans(Z,K,None,criteria,10,cv2.KMEANS_RANDOM_CENTERS)
- # Now convert back into uint8, and make original image
- center = np.uint8(center)
- res = center[label.flatten()]
- res2 = res.reshape((img.shape))
- cv2.imshow('res2',res2)
- cv2.waitKey(0)
- cv2.destroyAllWindows()
下面是 K=8 的结果:
学习函数 cv2.fastNlMeansDenoising(),cv2.fastNlMeansDenoisingColored()等
在前面的章节中我们已经学习了很多图像平滑技术,比如高斯平滑,中值 平滑等,当噪声比较小时这些技术的效果都是很好的。在这些技术中我们选取 像素周围一个小的邻域然后用高斯平均值或者中值平均值取代中心像素。简单来说,像素级别的噪声去除是限制在局部邻域的。
噪声有一个性质。我们认为噪声是平均值为一的随机变量。考虑一个带噪声的像素点,p = p0 + n,其中 p0 为像素的真实值,n 为这个像素的噪声。我 们可以从不同图片中选取大量的相同像素(N)然后计算平均值。理想情况下 我们会得到 p = p0。因为噪声的平均值为 0。
通过简单的设置我们就可以去除这些噪声。将一个静态摄像头固定在一个 位置连续拍摄几秒钟。这样我们就会得到足够多的图像帧,或者同一场景的大 量图像。写一段代码求解这些帧的平均值(这对你来说应该是小菜一碟)。将最 终结果与第一帧图像对比一下。你会发现噪声减小了。不幸的是这种简单的方 法对于摄像头和运动场景并不总是适用。大多数情况下我们只有一张导游带有 噪音的图像。
想法很简单,我们需要一组相似的图片,通过取平均值的方法可以去除噪 音。考虑图像中一个小的窗口(5x5),有很大可能图像中的其它区域也存在一 个相似的窗口。有时这个相似窗口就在邻域周围。如果我们找到这些相似的窗 口并取它们的平均值会怎样呢?对于特定的窗口这样做挺好的。如下图所示:
上图中的蓝色窗口看起来是相似的。绿色窗口看起来也是相似的。所以我们可以选取包含目标像素的一个小窗口,然后在图像中搜索相似的窗口,最后 求取所有窗口的平均值,并用这个值取代目标像素的值。这种方法就是非局部 平均值去噪。与我们以前学习的平滑技术相比这种算法要消耗更多的时间,但 是结果很好。你可以在更多资源中找到更多的细节和在线演示。
对于彩色图像,要先转换到 CIELAB 颜色空间,然后对 L 和 AB 成分分 别去噪。
OpenCV 提供了这种技术的四个变本:
- • cv2.fastNlMeansDenoising() 适用对象为灰度图。
- • cv2.fastNlMeansDenoisingColored() 适用对象为彩色图。
- • cv2.fastNlMeansDenoisingMulti() 适用于短时间的图像序列(灰度图像)
- • cv2.fastNlMeansDenoisingColoredMulti() 适用于短时间的图像序列(彩色图像)
共同参数有:
- • h : 决定过滤器强度。h 值高可以很好的去除噪声,但也会把图像的细节抹去。(取10 的效果不错)
- • hForColorComponents : 与 h 相同,但使用与彩色图像。(与 h 相 同)
- • templateWindowSize : 奇数。(推荐值为 7)
- • searchWindowSize : 奇数。(推荐值为 21)
请查看跟多资源获取这些参数的细节。 这里我们会演示 2 和 3,其余就留给你了。
cv2.fastNlMeansDenoisingColored()
和上面提到的一样,它可以被用来去除彩色图像的噪声(假设是高斯噪声)。下面是示例。
- import cv2
- from matplotlib import pyplot as plt
-
- img = cv2.imread('die.png')
- dst = cv2.fastNlMeansDenoisingColored(img,None,10,10,7,21)
- plt.subplot(121),plt.imshow(img)
- plt.subplot(122),plt.imshow(dst)
- plt.show()
下面是结果的放大图,我们的输入图像中含有方差为 25 的噪声,下面是结果。
cv2.fastNlMeansDenoisingMulti()
现在我们要对一段视频使用这个方法。第一个参数是一个噪声帧的列表。 第二个参数 imgtoDenoiseIndex 设定那些帧需要去噪,我们可以传入一 个帧的索引。第三个参数 temporaWindowSize 可以设置用于去噪的相邻帧的数目,它应该是一个奇数。在这种情况下 temporaWindowSize 帧的 图像会被用于去噪,中间的帧就是要去噪的帧。例如,我们传入 5 帧图像, imgToDenoiseIndex = 2 和 temporalWindowSize = 3。那么第一帧,第二帧, 第三帧图像将被用于第二帧图像的去噪。让我们来看一个例子:
- import numpy as np
- import cv2
- from matplotlib import pyplot as plt
-
- cap = cv2.VideoCapture('vtest.avi')
- # create a list of first 5 frames
- img = [cap.read()[1] for i in range(5)]
- # convert all to grayscale
- gray = [cv2.cvtColor(i, cv2.COLOR_BGR2GRAY) for i in img]
- # convert all to float64
- gray = [np.float64(i) for i in gray]
- # create a noise of variance 25
- noise = np.random.randn(*gray[1].shape)*10
- # Add this noise to images
- noisy = [i+noise for i in gray]
- # Convert back to uint8
- noisy = [np.uint8(np.clip(i,0,255)) for i in noisy]
- # Denoise 3rd frame considering all the 5 frames
- dst = cv2.fastNlMeansDenoisingMulti(noisy, 2, 5, None, 4, 7, 35)
-
- plt.subplot(131),plt.imshow(gray[2],'gray')
- plt.subplot(132),plt.imshow(noisy[2],'gray')
- plt.subplot(133),plt.imshow(dst,'gray')
- plt.show()
下图是我得到结果的放大版本:
计算消耗了相当可观的时间。第一张图是原始图像,第二个是带噪音个图 像,第三个是去噪音之后的图像。
在我们每个人的家中可能都会几张退化的老照片,有时候上面不小心在上 面弄上了点污渍或者是画了几笔。你有没有想过要修复这些照片呢?我们可以 使用笔刷工具轻易在上面涂抹两下,但这没用,你只是用白色笔画取代了黑色笔画。此时我们就要求助于图像修补技术了。这种技术的基本想法很简单:使 用坏点周围的像素取代坏点,这样它看起来和周围像素就比较像了。如下图所 示:
为了实现这个目的,科学家们已经提出了好几种算法,OpenCV 提供了其中的两种。这两种算法都可以通过使用函数cv2.inpaint()来实施。
第一个算法是根据Alexandru_Telea在2004发表的文章实现的。它是基于快速行进算法的。以图像中一个要修补的区域为例。算法从这个区域的边界开始向区域内部慢慢前进,首先填充区域边界像素。它要选取待修补像素周围的一个小的邻域,使用这个邻域内的归一化加权和更新待修复的像素值。权重的选择是非常重要的。对于靠近带修复点的像素点,靠近正常边界像素点和在轮廓上的像素点给予更高的权重。当一个像素被修复之后,使用快速行进算法(FMM)移动到下一个最近的像素。FMM保证了靠近已知(没有退化的)像素点的坏点先被修复,这与手工启发式操作比较类似。可以通过设置标签参数为cv2.INPAINT_TELEA来使用此算法。
第二个算法是根据Bertalmio,Marcelo,Andrea_L.Bertozzi和Guillermo_Sapiro在2001年发表的文章实现的。这个算法是基于流体动力学并使用了偏微分方程。基本原理是启发式的。它首先沿着正常区域的边界向退化区域的前进(因为边界是连续的,所以退化区域非边界与正常区域的边界应该也是连续的)。它通过匹配待修复区域中的梯度向量来延伸等光强线(isophotes,由灰度值相等的点练成的线)。为了实现这个目的,作者是用流体动力学中的一些方法。完成这一步之后,通过填充颜色来使这个区域内的灰度值变化最小。可以通过设置标签参数为cv2.INPAINT_NS来使用此算法。
我们要创建一个与输入图像大小相等的掩模图像,将待修复区域的像素设置为255(其它地方为0)。所有的操作都很简单。我要修复的图像中有几个黑色笔画。我是使用画笔工具添加的。
- import cv2
-
- img = cv2.imread('messi_2.jpg')
- mask = cv2.imread('mask2.png',0)
- dst = cv2.inpaint(img,mask,3,cv2.INPAINT_TELEA) cv2.imshow('dst',dst)
- cv2.waitKey(0)
- cv2.destroyAllWindows()
结果如下:(第一幅图是退化的输入图像,第二幅是掩模图像;第三幅是使用第一个算法的结果;最后一副是使用第二个算法的结果)
以 Haar 特征分类器为基础的对象检测技术是一种非常有效的对象检测技术(2001 年 Paul_Viola 和 Michael_Jones 提出)。它是基于机器学习的, 通过使用大量的正负样本图像训练得到一个 cascade_function,最后再用它 来做对象检测。
现在我们来学习面部检测。开始时,算法需要大量的正样本图像(面部图 像)和负样本图像(不含面部的图像)来训练分类器。我们需要从其中提取特征。下图中的 Haar 特征会被使用。它们就像我们的卷积核。每一个特征是一个值,这个值等于黑色矩形中的像素值之后减去白色矩形中的像素值之和。
使用所有可能的核来计算足够多的特征(仅仅是一个 24x24 的窗口就有 160000 个特征)。对于每一个特征的计算我们 好需要计算白色和黑色矩形内的像素和。为了解决这个问题,作者引入了积分 图像,这可以大大的简化求和运算,对于任何一个区域的像素和只需要对积分 图像上的四个像素操作即可。非常漂亮,它可以使运算速度飞快!但是在我们计算得到的所有的这些特征中,大多数是不相关的。如下图所 示。上边一行显示了两个好的特征,第一个特征看上去是对眼部周围区域的描述,因为眼睛总是比鼻子黑一些。第二个特征是描述的是眼睛比鼻梁要黑一些。
如果把这两个窗口放到脸颊的话,就一点都不相关。那么我们怎样从超过160000+ 个特征中选出最好的特征呢?使用 Adaboost。
为了达到这个目的,我们将每一个特征应用于所有的训练图像。对于每一 个特征,我们要找到它能够区分出正样本和负样本的最佳阈值。但是很明显, 这会产生错误或者错误分类。我们要选取错误率最低的特征,这说明它们是检 测面部和非面部图像最好的特征(这个过程其实不像我们说的这么简单。在开始时每一张图像都具有相同的权重,每一次分类之后,被错分的图像的权重会 增大。同样的过程会被再做一遍。然后我们又得到新的错误率和新的权重。重复执行这个过程知道到达要求的准确率或者错误率或者要求数目的特征找到)。
最终的分类器是这些弱分类器的加权和。之所以成为弱分类器是应为只是 用这些分类器不足以对图像进行分类,但是与其它的分类器联合起来就是一个 很强的分类器了。文章中说 200 个特征就能够提供 95% 的准确度了。它们最后使用 6000 个特征(从 160000 减到 6000,效果显著呀!)。 现在你有一幅图像,对每一个 24x24 的窗口使用这 6000 个特征来做检查,看它是不是面部。这是不是很低效很耗时呢?的确如此,但作者有更好的解决方法。
在一副图像中大多数区域是非面部区域。所以最好有一个简单的方法来证 明这个窗口不是面部区域,如果不是就直接抛弃,不用对它再做处理。而不是 集中在研究这个区域是不是面部。按照这种方法我们可以在可能是面部的区域 多花点时间。
为了达到这个目的作者提出了级联分类器的概念。不是在一开始就对窗口进行这 6000 个特征测试,将这些特征分成不同组。在不同的分类阶段逐个使用(通常前面很少的几个阶段使用较少的特征检测)。如果一个窗口第一阶段 的检测都过不了就可以直接放弃后面的测试了,如果它通过了就进入第二阶段 的检测。如果一个窗口经过了所有的测试,那么这个窗口就被认为是面部区域。 这个计划是不是很帅!!
作者将 6000 多个特征分为 38 个阶段,前五个阶段的特征数分别为 1, 10,25,25 和 50。(上图中的两个特征其实就是从 Adaboost 获得的最好特征)。平均而言,6000多个特性中有10个是针对每个子窗口进行评估的。
上面是我们对 Viola-Jones 面部检测是如何工作的直观解释。读一下原始文献或者更多资源中非参考文献将会对你有更大帮助。
OpenCV 自带了训练器和检测器。如果你想自己训练一个分类器来检测 汽车,飞机等的话,可以使用 OpenCV 构建。其中的细节在这里:Cascade Classifier Training
现在我们来学习一下如何使用检测器。OpenCV 已经包含了很多已经训练好的分类器,其中包括:面部,眼睛,微笑等。这些 XML 文件保存在/opencv/data/haarcascades/文件夹中。下面我们将使用 OpenCV 创建一个面部和眼部检测器。
首先我们要加载需要的 XML 分类器。然后以灰度格式加载输入图像或者是视频:
- import numpy as np
- import cv2
-
- face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
- eye_cascade = cv2.CascadeClassifier('haarcascade_eye.xml')
-
- img = cv2.imread('sachin.jpg')
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
如果检测到面部,它会返回面部所在的矩形 区域 Rect(x,y,w,h)。一旦我们获得这个位置,我们可以创建一个 ROI 并在 其中进行眼部检测。
- # 检测输入图像中不同大小的对象.
- # 检测到的对象作为矩形列表返回.
- #cv2.CascadeClassifier.detectMultiScale(image, scaleFactor, minNeighbors, flags, minSize, maxSize)
- # scaleFactor – 指定在每个图像尺度上缩小多少图像大小.
- # minNeighbors – 指定每个候选矩形应该保留多少个邻居.
- # minSize – 最小可能的对象大小,小于该值的对象将被忽略.
- # maxSize – 最大可能的对象大小,大于该值的对象将被忽略.
- faces = face_cascade.detectMultiScale(gray, 1.3, 5)
- for (x,y,w,h) in faces:
- img = cv2.rectangle(img,(x,y),(x+w,y+h),(255,0,0),2)
- roi_gray = gray[y:y+h, x:x+w]
- roi_color = img[y:y+h, x:x+w]
- eyes = eye_cascade.detectMultiScale(roi_gray)
- for (ex,ey,ew,eh) in eyes:
- cv2.rectangle(roi_color,(ex,ey),(ex+ew,ey+eh),(0,255,0),2)
-
- cv2.imshow('img',img)
- cv2.waitKey(0)
- cv2.destroyAllWindows()
结果如下:
更多资源:
1.Video Lecture on Face Detection and Tracking
2.An interesting interview regarding Face Detection by Adam Har- vey
使用弱分类器的增强级联包括两个主要阶段:训练和检测阶段。对象检测教程中介绍了使用基于HAAR或LBP模型的检测阶段。本文档概述了训练自己的弱分类器的级联所需的功能。当前指南将分各个阶段进行:收集训练数据,准备训练数据并执行实际模型训练。
为了支持本教程,将使用几个官方的OpenCV应用程序:opencv_createsamples,opencv_annotation,opencv_traincascade和opencv_visualisation。
注意:
除了霍夫变换之外,还有别的方法来提取图像空间信息。最常用到的三种空间特征分别为HOG特征、LBP特征及Haar特征。
1) Haar描述的是图像在局部范围内像素值明暗变换信息;
2) LBP描述的是图像在局部范围内对应的纹理信息;
3) HOG描述的则是图像在局部范围内对应的形状边缘梯度信息。
HOG特征
概念与思想:方向梯度直方图(Histogram of Oriented Gradient, HOG)特征是一种在计算机视觉和图像处理中用来描述图像局部特征的描述子。
在一幅图像中,局部目标的表象和形状(appearance and shape)能够被梯度或边缘的方向密度分布很好地描述。其本质是梯度的统计信息,而梯度主要存在于边缘所在的地方。
特点:与其它特征相比,HOG的优势在于能更好的描述形状,在行人识别方面有很好的效果。
由于HOG是在图像的局部方格单元上操作,所以它对图像几何的和光学的形变都能保持很好的不变性,这两种形变只会出现在更大的空间领域上。其次,在粗的空域抽样、精细的方向抽样以及较强的局部光学归一化等条件下,只要行人大体上能够保持直立的姿势,可以容许行人有一些细微的肢体动作,这些细微的动作可以被忽略而不影响检测效果。
流程:简单来说,首先需要将图像分成小的连通区域,称之为细胞单元。然后采集细胞单元中各像素点的梯度或边缘的方向直方图。最后把这些直方图组合起来就可以构成特征描述器。
STEP 1:读入所需要的检测目标即输入的image。
STEP 2:将图像进行灰度化(将输入的彩色的图像的r,g,b值通过特定公式转换为灰度值)。
STEP 3:采用Gamma校正法对输入图像进行颜色空间的标准化(归一化)。
STEP 4:计算图像每个像素的梯度(包括大小和方向),捕获轮廓信息;统计每个cell的梯度直方图(不同梯度的个数),形成每个cell的descriptor;将每几个cell组成一个block(以3*3为例),一个block内所有cell的特征串联起来得到该block的HOG特征descriptor。
STEP 5:将图像image内所有block的HOG特征descriptor串联起来得到该image(检测目标)的HOG特征descriptor,这就是最终分类的特征向量。
LBP特征
概念与思想:局部二值模式(Local Binary Pattern,LBP)是一种计算机视觉和图像处理中用来描述图像局部特征的描述子。
其基本思想是用其中心像素的灰度值作为阈值,与它的邻域相比较得到的二进制码来表述局部纹理特征。
特点:与其它特征相比,LBP的优势在于能更好地描述纹理,在人脸识别方面有很好的效果,比Haar快很多倍(提取的准确率会低)因此适合用在移动设备上。
流程:
STEP 1:首先将检测窗口划分为16X16的小区域(cell)
STEP 2:对于每个cell中的一个像素,将相邻的8个像素的灰度值与其进行比较,若周围像素值大于中心像素值,则该像素点的位置被标记为1,否则为0。这样,3X3领域内的8个点经过比较可产生8位二进制数,即得到该窗口中心像素点的LBP值
STEP 3:然后计算每个cell的直方图,即每个数字出现的频率,然后对该直方图进行归一化处理
STEP 4:最后将得到的每个cell的统计直方图进行连接成一个特征向量,也就是整幅图的LBP纹理特征向量
Haar特征
概念与思想:Haar特征分为三类:边缘特征、线性特征、中心特征和对角线特征,组合成特征模板。特征模板内有白色和黑色两种矩形,并定义该模板的特征值为白色矩形像素和减去黑色矩形像素和。
特点:与其它特征相比,Haar的优势在于能更好地描述灰度变化情况,用于检测正面的人脸(正脸由于鼻子等凸起的存在,使得脸上的光影变化十分明显)。
为了训练弱分类器的增强级联,我们需要一组正样本(包含您要检测的实际对象)和一组负图像(包含您不想检测的所有内容)。负样本集必须手动准备,而正样本集是使用opencv_createsamples应用程序创建的。
负样本
负样本取自任意图像,其中不包含要检测的对象。这些负图像(从中生成样本)应在特殊的负图像文件中列出,该文件每行包含一个图像路径(可以是绝对路径,也可以是相对路径)。注意,负样本和样本图像也称为背景样本或背景图像,在本文档中可以互换使用。
所描述的图像可能具有不同的尺寸。但是,每个图像都应等于或大于所需的训练窗口大小(与模型尺寸相对应,大多数情况下是对象的平均大小),因为这些图像用于将给定的负像子采样为几个图像具有此训练窗口大小的样本。
否定描述文件的示例:
目录结构:
- / img
-
- img1.jpg
- img2.jpg
- bg.txt
文件bg.txt:
- img / img1.jpg
- img / img2.jpg
您的一组否定窗口样本将用于告诉机器学习步骤,在这种情况下,当尝试查找您感兴趣的对象时,可以增强不需要查找的内容。
正样本
正样本由opencv_createsamples应用程序创建。增强过程使用它们来定义在尝试找到感兴趣的对象时模型应实际寻找的内容。该应用程序支持两种生成正样本数据集的方式。
虽然第一种方法对固定对象(例如非常刚性的徽标)效果不错,但对于刚性较差的对象,它往往很快就会失效。在这种情况下,我们建议使用第二种方法。网络上的许多教程甚至都指出,使用opencv_createsamples应用程序,与1000个人工生成的正片相比,可以生成100个真实的对象图像更好的模型。但是,如果您决定采用第一种方法,请记住以下几点:
第一种方法采用带有公司徽标的单个对象图像,并通过随机旋转对象,更改图像强度以及将图像放置在任意背景上,从给定的对象图像中创建大量正样本。随机性的数量和范围可以由opencv_createsamples应用程序的命令行参数控制。
命令行参数:
以这种方式运行opencv_createsamples时,将使用以下过程创建一个示例对象实例:给定的源图像在三个轴上随机旋转。选择的角度受-maxxangle、-maxyangle和-maxzangle的限制。然后是像素具有[bg_color-bg_color_threshold; bg_color+bg_color_threshold]范围的强度解释为透明。白噪声增加了前景的强度。如果指定了-inv键,则反转前景像素强度。如果指定了-randinv键,则算法随机选择是否对该样本应用反转。最后,将获得的图像从背景描述文件放置到任意背景上,将大小调整到-w和-h指定的期望大小,并存储到-vec命令行选项指定的向量文件中。
也可以从以前标记的图像的集合中获取正样本,这是构建鲁棒对象模型时的理想方式。该集合由类似于背景描述文件的文本文件描述。该文件的每一行都对应一个图像。该行的第一个元素是文件名,后跟对象注释的数量,后跟描述包围矩形(x,y,宽度,高度)的对象坐标的数字。
描述文件的示例:
目录结构:
- / img
-
- img1.jpg
- img2.jpg
- info.dat
文件info.dat:
- img / img1.jpg 1140100 45 45
- img / img2.jpg 210020050 50 50 30 25 25
图像img1.jpg包含具有以下边界矩形坐标的单个对象实例:(140,100,45,45)。图像img2.jpg包含两个对象实例。
为了从此类集合中创建阳性样本,-info应指定参数而不是-img:
请注意,在这种情况下,像这样-bg, -bgcolor, -bgthreshold, -inv, -randinv, -maxxangle, -maxyangle, -maxzangle的参数将被简单地忽略并且不再使用。在这种情况下,样本创建的方案如下。通过从原始图像中切出提供的边界框,从给定图像中获取对象实例。然后将它们调整为目标样本大小(由-w和定义-h),并存储在由-vec参数定义的输出vec文件中。无失真应用,所以只能影响参数是-w,-h,-show和-num。
-info也可以使用opencv_annotation工具完成手动创建文件的过程。这是一个开放源代码工具,用于在任何给定图像中直观地选择对象实例的关注区域。以下小节将详细讨论如何使用此应用程序。
额外备注
使用OpenCV的集成注释工具
从OpenCV 3.x开始,社区一直在提供和维护用于生成-info文件的开源注释工具。如果构建了OpenCV应用程序,则可以通过命令opencv_annotation访问该工具。
使用该工具非常简单。该工具接受几个必需参数和一些可选参数:
请注意,可选参数只能一起使用。可以使用的命令示例如下所示:
opencv_annotation --annotations=/path/to/annotations/file.txt --images=/path/to/image/folder/
此命令将启动一个窗口,其中包含第一张图像和您的鼠标光标,这些窗口将用于注释。有关如何使用注释工具的视频,请参见此处。基本上,有几个按键可以触发一个动作。鼠标左键用于选择对象的第一个角,然后一直进行绘图直到您感觉很好为止,并在记录第二次鼠标左键单击时停止。每次选择后,您有以下选择:
最后,您将获得一个可用的注释文件,该文件可以传递给-infoopencv_createsamples 的参数。
下一步是基于预先准备的正数和负数数据集对弱分类器的增强级联进行实际训练。
opencv_traincascade应用程序的命令行参数按用途分组:
opencv_traincascade应用程序完成工作后,经过训练的级联将保存cascade.xml在该-data文件夹中的文件中。此文件夹中的其它文件是为中断训练而创建的,因此您可以在训练完成后将其删除。
训练已完成,您可以测试级联分类器!
有时,可视化受过训练的级联,查看其选择的功能以及其阶段的复杂性可能会很有用。为此,OpenCV提供了一个opencv_visualisation应用程序。该应用程序具有以下命令:
下面是一个示例命令
opencv_visualisation --image=/data/object.png --model=/data/model.xml --data=/data/result/
当前可视化工具的一些限制
HAAR / LBP人脸模型的示例在Angelina Jolie的给定窗口上运行,该窗口具有与级联分类器文件相同的预处理-> 24x24像素图像,灰度转换和直方图均衡化:
每个阶段都会制作一个视频,以显示每个功能:
每个阶段都作为图像存储,以供将来对功能进行验证:
这项工作是由StevenPuttemans 为OpenCV 3蓝图创建的,但是Packt Publishing同意将其集成到OpenCV中。
在视频后续帧中定位一个物体,称为追踪。虽然定义简单,但是目标追踪是一个相对广义的定义,比如以下问题也属于目标追踪问题:
稠密光流:此类算法用来评估一个视频帧中的每个像素的运动向量
稀疏光流:此类算法,像Kanade-Lucas-Tomashi(KLT)特征追踪,追踪一张图片中几个特征点的位置
Kalman Filtering:一个非常出名的信号处理算法基于先前的运动信息用来预测运动目标的位置。早期用于导弹的导航
MeanShift和Camshift:这些算法是用来定位密度函数的最大值,也用于追踪
单一目标追踪:此类追踪器中,第一帧中的用矩形标识目标的位置。然后在接下来的帧中用追踪算法。日常生活中,此类追踪器用于与目标检测混合使用。
多目标追踪查找算法:如果我们有一个非常快的目标检测器,在每一帧中检测多个目标,然后运行一个追踪查找算法,来识别当前帧中某个矩形对应下一帧中的某个矩形。
运动物体的识别方法很多,主要就是要提取相关物体的特征,主要分为:
(1)各种色彩空间直方图,利用色彩空间的直方图分布作为目标跟踪的特征的一个显著性特点是可以减少物体远近距离对跟踪的影响,因为其颜色分布大致相同。
(2)轮廓特征,提取目标的轮廓特征不但可以加快算法的速度还可以在目标有小部分影响的情况下同样有效果。
(3)纹理特征,如果被跟踪目标是有纹理的,根据其纹理特征来跟踪,效果会有所改善。
运动物体的跟踪涉及到的算法也比较多,其主要分类为:
(1)质心跟踪算法(Centroid):这种跟踪方式用于跟踪有界目标如飞机,目标完全包含在摄像机的视场范围内,对于这种跟踪方式可选用一些预处理算法:如白热(正对比度)增强、黑热(负对比度)增强,和基于直方图的统计(双极性)增强。
(2)多目标跟踪算法(MTT):多目标跟踪用于有界目标如飞机、地面汽车等。它们完全在跟踪窗口内。在复杂环境里的小目标跟踪MMT能给出一个较好的性能。
(3)相关跟踪算法(Correlation):相关可用来跟踪多种类型的目标,当跟踪目标无边界且动态不是很强时这种方式非常有效。典型应用于:目标在近距离的范围,且目标扩展到摄像机视场范围外,如一艘船。
(4)边缘跟踪算法(Edge):当跟踪目标有一个或多个确定的边缘而同时却又具有不确定的边缘,这时边缘跟踪是最有效的算法。典型地火箭发射,它有确定好的前边缘,但尾边缘由于喷气而不定。
(5)相位相关跟踪算法(Phase Correlation):相位相关算法是非常通用的算法,既可以用来跟踪无界目标也可以用来跟踪有界目标。在复杂环境下(如地面的汽车)能给出一个好的效果。
(6)场景锁定算法(SceneLock):该算法专门用于复杂场景的跟踪。适合于空对地和地对地场景。这个算法跟踪场景中的多个目标,然后依据每个点的运动,从而估计整个场景全局运动,场景中的目标和定位是自动选择的。当存在跟踪点移动到摄像机视场外时,新的跟踪点能自动被标识。瞄准点初始化到场景中的某个点,跟踪启动,同时定位瞄准线。在这种模式下,能连续跟踪和报告场景里的目标的位置。
(7)组合(Combined)跟踪算法:顾名思义这种跟踪方式是两种具有互补特性的跟踪算法的组合:相关类算法 +质心类算法。它适合于目标尺寸、表面、特征改变很大的场景(如小船在波涛汹涌的大海里行驶)。
我们首先考虑几个问题:
跟踪比检测更快:通常跟踪算法比检测算法更快。原因很简单,当你跟踪前一帧中的某个物体时,你已经知道了此物体的外观信息。同时你也知道前一帧的位置,以及运行的速度和方向。因而,在下一帧你可以用所有的信息来预测下一帧中物体的位置,以及在一个很小范围内搜索即可得到目标的位置。好的追踪算法会利用所有已知信息来追踪点,但是检测算法每次都要重头开始。所以,通常,如果我们在第n帧开始检测,那么我们需要在第n-1(或者n-2)帧开始跟踪。那么为什么不简单地第一帧开始检测,并从后续所有帧开始跟踪。因为跟踪会利用其已知信息,但是也可能会丢失目标,因为目标可能被障碍物遮挡,甚至于目标移动速度太快,算法跟不上。通常,跟踪算法会累计误差,而且bbox 会慢慢偏离目标。为了修复这些问题,需要不断运行检测算法。检测算法用大量样本训练之后,更清楚目标类别的大体特征。另一方面,跟踪算法更清楚它所跟踪的类别中某一个特定实例。
检测失败时跟踪可以帮忙:如果你在视频中检测人脸,然后人脸被某个物体遮挡了,人脸检测算法大概率会失败。另一方面,好的跟踪算法可以处理一定程度的遮挡。
跟踪会保存实体:目标检测算法的输出是包含物体的一个矩形的数组,但是没有此物体的个体信息。
OpenCV3.0有4种跟踪器:
BOOSTING, MIL, TLD, MEDIANFLOW;
OpenCV3.1有5种跟踪器:
BOOSTING, MIL, KCF, TLD, MEDIANFLOW;
OpenCV3.2有6种跟踪器:
BOOSTING, MIL, KCF, TLD, MEDIANFLOW, GOTURN.
OpenCV4.1有9种跟踪器:
BOOSTING, MIL, KCF, TLD, MEDIANFLOW, GOTURN, MULTI, MOSSE, CSRT.
如果需要更高的准确率,并且可以容忍延迟的话,使用CSRT
如果需要更快的FPS,并且可以容许稍低一点的准确率的话,使用KCF
如果纯粹的需要速度的话,用MOSSE。
(1).Boosting 追踪器
此跟踪器基于在线版本的AdaBoost,这个是以Haar特征级联的人脸检测器内部使用。此分类器需要在运行时以正负样本来训练。其初始框由用户指定,作为追踪的正样本,而在框范围之外许多其它patch都作为背景。在新的一帧图像中,分类器在前一帧框的周围的每个像素上分类,并给出得分。目标的新位置即得分最高的这样一来有新的正样本来重新训练分类器。依次类推。(最低支持OpenCV 3.0.0)
优点:几乎没有,几十年前的技术。
缺点:速度较慢,并且表现不好,追踪性能一般,它无法感知追踪什么时候会失败。
(2).MIL追踪
算法与Boosting很像,唯一的区别是:它会考虑当前标定框周围小部分框同时作为正样本,你可能认为这个想法比较烂,因为大部分的这些正样本其实目标并不在中心。这就是MIL(Multiple Instance Learning)的独特之处,在MIL中你不需要指定正负样本,而是正负样包(bags)。在正样本包中的并不全是正样本,而是仅需要一个样本是正样本即可。当前示例中,正样本包里面的样本包含的是处于中心位置的框,以及中心位置周围的像素所形成的框。即便当前位置的跟踪目标不准确,从以当前位置为中心在周围像素抽取的样本框所构成的正样本包中,仍然有很大概率命中一个恰好处于中心位置的框。
优点:性能很好,不会像Boosting那样会偏移,即便出现部分遮挡依然表现不错。 在Opencv3.0中效果最好的追踪器,如果在更高版本里,选择KCF。
缺点:没法应对全遮挡,追踪失败也较难的得到反馈。
(3).KCF 追踪
KCF即Kernelized Correlation Filters,思路借鉴了前面两个。注意到MIL所使用的多个正样本之间存在交大的重叠区域。这些重叠数据可以引出一些较好的数学特性,这些特性同时可以用来构造更快更准确的分类器。
优点:速度和精度都比MIL效果好,并且追踪失败反馈比Boosting和MIL好。Opencv 3.1以上版本最好的分类器。
缺点:完全遮挡之后没法恢复。
(4).TLD追踪
TLD即Tracking, learning and detection,如其名此算法由三部分组成追踪,学习,检测。追踪器逐帧追踪目标,检测器定位所有到当前为止观察到的外观,如果有必要则纠正追踪器。学习会评估检测器的错误并更新,以避免进一步出错。此追踪器可能会产生跳跃,比如你正在追踪一个人,但是场景中存在多个人,此追踪器可能会突然跳到另外一个行人进行追踪。优势是:此追踪器可以应对大尺度变换,运行以及遮挡问题。如果说你的视频序列中,某个物体隐藏在另外一个物体之后,此追踪器可能是个好选择。
优势:多帧中被遮挡依然可以被检测到,目标尺度变换也可以被处理。
缺点:容易误跟踪导致基本不可用。
(5).MedianFlow 追踪
此追踪器在视频的前向时间和后向时间同时追踪目标,然后评估两个方向的轨迹的误差。最小化前后向误差,使得其可以有效地检测追踪失败的清情形,并在视频中选择相对可靠的轨迹。
实测时发现,此追踪器在运动可预测以及运动速度较小时性能最好。而不像其它追踪器那样,即便追踪失败继续追踪,此追踪器很快就知道追踪失败
优点:对追踪失败反馈敏锐,当场景中运动可预测以及没有遮挡时较好。
缺点:急速运动场景下会失败。
(6).GoTurn 追踪
这个是唯一使用CNN方法的追踪器,此算法对视点变化,光照变换以及形变等问题的鲁棒性较好。但是无法处理遮挡问题。
注意:它使用caffe模型来追踪,需要下载caffe模型以及proto txt文件。
(7).MOSSE 追踪
MOSSE即Minimum Output Sum of Squared Error,使用一个自适应协相关来追踪,产生稳定的协相关过滤器,并使用单帧来初始化。MOSSE鲁棒性较好,可以应对光线变换,尺度变换,姿势变动以及非网格化的变形。它也能基于峰值与旁瓣比例来检测遮挡,这使得追踪可以在目标消失时暂停并在目标出现时重启。MOSSE可以在高FPS(高达450以上)的场景下运行。易于实现,并且与其它复杂追踪器一样精确,并且可以更快。但是,在性能尺度上看,大幅度落后于基于深度学习的追踪器。
(8).CSRT 追踪
在Discriminative Correlation Filter with Channel and Spatial Reliability (DCF-CSR)中,我们使用空间依赖图来调整过滤器支持
(9).MULTI追踪
MULTI Tracker:是组合追踪器
- # -*- coding:utf-8 -*-
- from collections import deque
- import cv2
- import numpy as np
-
- (major_ver, minor_ver, subminor_ver) = (cv2.__version__).split('.')
- if __name__ == '__main__':
- head_cascade = cv2.CascadeClassifier('../faceDR/Cascades/cascade_head_s8.xml')
- tracker_types = ['BOOSTING', 'MIL', 'KCF', 'CRST', 'TLD', 'MEDIANFLOW', 'GOTURN']
- tracker_type = tracker_types[1]
- if int(minor_ver) < 3:
- tracker = cv2.TrackerKCF_create()
- else:
- if tracker_type == 'BOOSTING':
- tracker = cv2.TrackerBoosting_create()
- if tracker_type == 'MIL':
- tracker = cv2.TrackerMIL_create()
- if tracker_type == 'KCF':
- tracker = cv2.TrackerKCF_create()
- if tracker_type == 'CRST':
- tracker = cv2.TrackerCRST_create()
- if tracker_type == 'TLD':
- tracker = cv2.TrackerTLD_create()
- if tracker_type == 'MEDIANFLOW':
- tracker = cv2.TrackerMedianFlow_create()
- if tracker_type == 'GOTURN':
- tracker = cv2.TrackerGOTURN_create()
-
- video = cv2.VideoCapture(0)
- if not video.isOpened():
- print("Could not open video")
-
- head_id = 0
- box_list = []
- head_id_list = []
- variable = locals()
- # pts = deque(maxlen=124)
- for i in range(10):
- variable['pts' + str(i + 1)] = deque(maxlen=124)
- # 初始化10个检测框(最多追踪10个)
- for i in range(10):
- variable['bbox' + str(i + 1)] = None
-
- while True:
- ok, frame = video.read() # 读取视频流
- gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 转灰度图
- # 人头检测部分
- heads = head_cascade.detectMultiScale(
- gray,
- scaleFactor=1.2,
- minNeighbors=3,
- minSize=(20, 20)
- )
-
- if len(heads) > 0 and len(heads) > len(head_id_list):
- for (x, y, w, h) in heads:
- #cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 0, 255), 3)
- if len(head_id_list)==0:
- head_id+=1
- variable['bbox' + str(head_id+1)] = (x, y, w, h)
- variable['tracker' + str(head_id+1)] = cv2.TrackerKCF_create()
- # 初始化跟踪器
- variable['ok' + str(head_id+1)] = variable['tracker' + str(head_id+1)].init(
- frame,
- variable['bbox' + str(head_id+1)])
- head_id_list.append(head_id+1)
- box_list.append((head_id+1, (x, y, w, h)))
- else:
- for head_id in range(10):
- if head_id + 1 not in head_id_list:
- variable['tracker' + str(head_id+1)] = cv2.TrackerKCF_create()
- # 初始化跟踪器
- variable['ok' + str(head_id+1)] = variable[
- 'tracker' + str(head_id+1)].init(
- frame,
- variable['bbox' + str(head_id+1)])
- head_id_list.append(head_id+1)
- box_list.append((head_id+1, (x, y, w, h)))
- break
-
- for i in head_id_list:
- variable['ok' + str(i)], variable['bbox' + str(i)] = variable[ 'tracker' + str(i)].update(frame)
- # Draw bounding box
- if variable['ok' + str(i)]:
- # Tracking success
- x = int(variable['bbox' + str(i)][0])
- y = int(variable['bbox' + str(i)][1])
- w = int(variable['bbox' + str(i)][2])
- h = int(variable['bbox' + str(i)][3])
- p1 = (x, y)
- p2 = (x + w, y + h)
- cv2.rectangle(frame, p1, p2, (255, 0, 0), 2, 1)
- cv2.putText(frame, str(i), (x + 5, y + 5), cv2.FONT_HERSHEY_COMPLEX_SMALL, 0.8,
- (200, 255, 0))
-
- center = (x + w // 2, y + h // 2)
- variable['pts' + str(i)].appendleft(center)
- for j in range(1, len(variable['pts' + str(i)])):
- if variable['pts' + str(i)][j - 1] is None or variable['pts' + str(i)][j] is None:
- continue
- thickness = int(np.sqrt(64 / float(j + 1)) * 2.5)
- cv2.line(frame, variable['pts' + str(i)][j - 1],
- variable['pts' + str(i)][j], (255, 255, 0), thickness)
- else:
- # Tracking failure
- cv2.putText(frame, "Tracking failure detected", (100, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2)
- # for k in range(head_id):
- # variable['pts' + str(k + 1)].clear
- head_id_list.remove(i)
- head_id=0
-
- cv2.imshow("Tracking", frame)
- k = cv2.waitKey(1) & 0XFF
- if k == 27: break
-
- video.release()
- cv2.destroyAllWindows()
特别说明:每个跟踪器都有三个组件TrackerSampler,TrackerFeatureSet和TrackerModel。前两个是从Tracker基类实例化的,而最后一个组件是抽象的,因此您必须实现TrackerModel。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。