使用 SVM 进行手写数据识别

目标

在本章

  • 我们将再次学习手写数据 OCR,但是,使用 SVM 而不是 kNN。

手写数字的 OCR

在 kNN 中,我们直接使用像素强度作为特征向量。这次我们将使用方向梯度直方图(HOG)作为特征向量。

在找 HOG 之前,我们使用其二阶矩来校正图像。所以我们首先定义一个函数deskew(),它取一个数字图像并对其进行校正。下面是 deskew()函数:

  1. def deskew(img):
  2. m = cv.moments(img)
  3. if abs(m['mu02']) < 1e-2:
  4. return img.copy()
  5. skew = m['mu11']/m['mu02']
  6. M = np.float32([[1, skew, -0.5*SZ*skew], [0, 1, 0]])
  7. img = cv.warpAffine(img,M,(SZ, SZ),flags=affine_flags)
  8. return img

下图展示了应用于零图像的上述矫正函数。左图是原始图像,右图是矫正后的图像。

使用 SVM 进行手写数据识别 - 图1
图像
接下来,我们必须找到每个单元的 HOG 描述符。为此,我们在 X 和 Y 方向找到每个单元的 Sobel 导数。然后,在每个像素处找到它们的大小和梯度方向。该梯度被量化为 0~16 间的整数。将此图像分为四个子方块。对于每个子方块,计算使用大小加权的方向的直方图(16 bins)。因此,每个子方块都会给你一个有 16 个值的向量。四个这样的向量(四个子方块)一起给出了有着 64 个值的特征向量。这是我们用来训练数据的特征向量。

  1. def hog(img):
  2. gx = cv.Sobel(img, cv.CV_32F, 1, 0)
  3. gy = cv.Sobel(img, cv.CV_32F, 0, 1)
  4. mag, ang = cv.cartToPolar(gx, gy)
  5. bins = np.int32(bin_n*ang/(2*np.pi)) # quantizing binvalues in (0...16)
  6. bin_cells = bins[:10,:10], bins[10:,:10], bins[:10,10:], bins[10:,10:]
  7. mag_cells = mag[:10,:10], mag[10:,:10], mag[:10,10:], mag[10:,10:]
  8. hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
  9. hist = np.hstack(hists) # hist is a 64 bit vector
  10. return hist

最后,与先前一样,我们首先将大数据集拆分为独立的单元。对每个数字,保留 250 个单元用于训练数据,剩余的 250 个数据被留下来用于测试。完整代码如下,你也可以从这里下载:

  1. #!/usr/bin/env python
  2. import cv2 as cv
  3. import numpy as np
  4. SZ=20
  5. bin_n = 16 # Number of bins
  6. affine_flags = cv.WARP_INVERSE_MAP|cv.INTER_LINEAR
  7. def deskew(img):
  8. m = cv.moments(img)
  9. if abs(m['mu02']) < 1e-2:
  10. return img.copy()
  11. skew = m['mu11']/m['mu02']
  12. M = np.float32([[1, skew, -0.5*SZ*skew], [0, 1, 0]])
  13. img = cv.warpAffine(img,M,(SZ, SZ),flags=affine_flags)
  14. return img
  15. def hog(img):
  16. gx = cv.Sobel(img, cv.CV_32F, 1, 0)
  17. gy = cv.Sobel(img, cv.CV_32F, 0, 1)
  18. mag, ang = cv.cartToPolar(gx, gy)
  19. bins = np.int32(bin_n*ang/(2*np.pi)) # quantizing binvalues in (0...16)
  20. bin_cells = bins[:10,:10], bins[10:,:10], bins[:10,10:], bins[10:,10:]
  21. mag_cells = mag[:10,:10], mag[10:,:10], mag[:10,10:], mag[10:,10:]
  22. hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
  23. hist = np.hstack(hists) # hist is a 64 bit vector
  24. return hist
  25. img = cv.imread('digits.png',0)
  26. if img is None:
  27. raise Exception("we need the digits.png image from samples/data here !")
  28. cells = [np.hsplit(row,100) for row in np.vsplit(img,50)]
  29. # First half is trainData, remaining is testData
  30. train_cells = [ i[:50] for i in cells ]
  31. test_cells = [ i[50:] for i in cells]
  32. deskewed = [list(map(deskew,row)) for row in train_cells]
  33. hogdata = [list(map(hog,row)) for row in deskewed]
  34. trainData = np.float32(hogdata).reshape(-1,64)
  35. responses = np.repeat(np.arange(10),250)[:,np.newaxis]
  36. svm = cv.ml.SVM_create()
  37. svm.setKernel(cv.ml.SVM_LINEAR)
  38. svm.setType(cv.ml.SVM_C_SVC)
  39. svm.setC(2.67)
  40. svm.setGamma(5.383)
  41. svm.train(trainData, cv.ml.ROW_SAMPLE, responses)
  42. svm.save('svm_data.dat')
  43. deskewed = [list(map(deskew,row)) for row in test_cells]
  44. hogdata = [list(map(hog,row)) for row in deskewed]
  45. testData = np.float32(hogdata).reshape(-1,bin_n*4)
  46. result = svm.predict(testData)[1]
  47. mask = result==responses
  48. correct = np.count_nonzero(mask)
  49. print(correct*100.0/result.size)

这种特殊技术给了我近 94%的准确率。你可以尝试为 SVM 的各种参数设置不同的值,以检查是否可以获得更高的精度。或者你也可以阅读该领域的技术论文并尝试实现它们。

额外资源

  1. 方向梯度直方图视频

练习

  1. OpenCV 示例里有个 digits.py,它对上述方法稍微做了改进,并获得了更好的效果。它还包含参考资料。阅读并理解它。