OCR 学习笔记
三、传统机器学习方法绪论
3.1 特征提取方法
3.1.1 基于结构形态的特征提取
- 通常情况下,形状特征有两类表示方法,一类是 轮廓特征,另一类是 区域特征。图像的轮廓特征主要针对物体的外边界,而图像的区域特征则关系到整个形状区域。
- 基于结构形态的特征提取方法主要是将字符结构的图像形态转化为特征向量,主要包括
边界特征法
、傅里叶特征算子法
、形状不变矩法
、几何参数法
3.1.1.1 边界特征法
- 边界特征法 通过对边界特征的描述来获取图像的形状参数。其中 Hough 变换检测平行直线方法 和 边界方向直方图方法 是经典方法。
霍夫变换
- 是利用图像全局特性而将边缘像素连接起来组成区域封闭边界的一种方法,其基本思想是点—线的对偶性
- 相关原理及实现可以参考我的另一篇 博客:『OCR深度实践』OCR学习笔记(2):图像预处理
边界方向直方图法
- 首先微分图像求得图像边缘(即利用常用的图像边缘检测算子求得图像的边缘),然后做出关于边缘大小和方向的直方图,通常的方法是构造图像灰度梯度方向矩阵。
- 在讲解边界方向直方图法之前我们需要先了解 图像边缘检测
- 常用的边缘检测算子有 Laplacian, Sobel, Prewitt, Canny等,它们通过差分的方式来近似图像像素偏导数的值 (需要明白一幅图像是由很多个离散的像素点组成的) 【具体算子的实现原理有时间我再单独写一遍关于算子的文章,本文暂不详细说明】
- Laplacian: n 维欧几里得空间中的一个二阶微分算子;根据图像处理的原理可知,二阶导数往往可以用于边缘检测。
- Sobel: 一阶微分算子,利用单个像素邻近区域的剃度值来计算该像素的剃度值,然后根据一定的规则进行取舍,使用 3x3 的模版
- Prewitt: 一阶微分算子,使用 3x3 的模板
- Canny: 包括4个步骤
- 用高斯滤波器对图像进行平滑处理
- 用一阶偏导的有限差分来计算剃度的幅值和方向
- 对梯度的幅值进行非极大值抑制
- 用双阈值算法检测和连接图像的边缘
# 算子进行图像边缘检测的具体代码示例:
import cv2
import numpy as np
image = cv2.imread("lena.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Canny边缘检测
canny = cv2.Canny(image, 30, 150)
cv2.imwrite("Canny.jpg", canny)
# Laplacian边缘检测
lap = cv2.Laplacian(image, cv2.CV_64F)
cv2.imwrite("Laplacian.jpg", lap)
# Sobel边缘检测
sobelX = cv2.Sobel(image, cv2.CV_64F, 1, 0) # x方向的梯度
sobelX = np.uint8(np.absolute(sobelX)) # x方向梯度的绝对值
sobelY = cv2.Sobel(image, cv2.CV_64F, 0, 1) # y方向的梯度
sobelY = np.uint8(np.absolute(sobely)) # y方向梯度的绝对值
sobelCombined = cv2.bitwise_or(sobelX, sobelY)
cv2.imwrite("sobel_x.jpg", sobelX)
cv2.imwrite("sobel_y.jpg", sobelY)
cv2.imwrite("sobel_xy.jpg", sobelCombined)
# Prewitt算子
import matplotlib.pyplot as plt
from scipy import signal
# x 方向的 Prewitt 算子
suanzi_x = np.array([[-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1]])
# y 方向的 Prewitt 算子
suanzi_x = np.array([[-1, -1, -1],
[0, 0, 0],
[1, 1, 1]])
image_x = signal.convolve2d(image, suanzi_x) # 得到 x 方向矩阵
image_y = signal.convolve2d(image, suanzi_y) # 得到 y 方向矩阵
image_xy = np.sqrt(image_x**2 + image_y**2) # 得到梯度矩阵
image_xy = (255.0 / image_xy.max()) * image_xy # 得到矩阵统一到 0~255
cv2.imwrite("Prewitt_x.jpg", image_x)
cv2.imwrite("Prewitt_y.jpg", image_y)
cv2.imwrite("Prewitt_xy.jpg", image_xy)
3.1.1.2 傅里叶特征算子
- 傅里叶特征算子,又称傅里叶形状描述子,主要作用是通过对目标边界的轮廓进行离散傅里叶变换得到目标边界形状的定量表达。
离散傅里叶变换
- 离散傅里叶变换是图像处理中常用的一种变换手段。通过离散傅里叶变换,我们可以将图像的信号从时域转换到频域。
傅里叶形状描述子
- 当确定了图像中的目标区域的起始点以及方向之后,我们就可以利用一系列的坐标对来描述边界的信息了。假设边界上有 n n n 个边界点,起始点为 ( x 0 , y 0 ) (x_0, y_0) (x0,y0),按照顺时针方向可以表示为一个坐标序列: s n = [ ( x 0 , y 0 ) , ( x 1 , y 1 ) , . . . , ( x n , y n ) ] , n = 0 , 1 , 2 , 3... s_n=[(x_0,y_0), (x_1,y_1), ..., (x_n,y_n)], n=0,1,2,3... sn=[(x0,y0),(x1,y1),...,(xn,yn)],n=0,1,2,3...
- 一般来说,如果我们将目标边界看成是从某一个点出发,则沿着该边界顺时针旋转一周的周边长可以用一个复函数来表示。换句话说就是,边界上点的坐标可以用如下复数来表示: s n = x n + j ∗ y n , n = 1 , 2 , 3... s_n=x_n+j^*y_n, n=1, 2,3... sn=xn+j∗yn,n=1,2,3...
- 通过这种方式,可以成功地将坐标序列的二维表示转换为一维表示。对于复数 s n s_n sn,可以用一个一维离散傅里叶变换系数 F ( u ) F(u) F(u) 来表示:
- 这里的 F ( u ) F(u) F(u) 是图像边界的傅里叶描述子。同理,如果对 F ( u ) F(u) F(u) 进行傅里叶反变换,则可以得到最开始的坐标序列的表达式(仅选取前 N N N 个傅里叶变换系数近似):
- 低阶系数表示的是边界的大致形状,高阶系数表示的是边界的细节特征。傅里叶描述子在描述边界时,对旋转、平移、尺度变化等均不敏感。
# coding=utf-8
import cv2
import numpy as np
# 直接读为灰度图像
img = cv2.imread('fuliye.png', 0)
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
# 先取绝对值,表示取模。再取对数,将数据范围变小
magnitude_spectrum = 20 * np.log(np.abs(fshift))
cv2.imwrite("original.jpg", img)
cv2.imwrite("center.jpg", magnitude_spectrum)
3.1.1.3 形状不变矩法
- 形状不变矩法的主要思想是将对变换不敏感的、基于区域的几何矩特征作为形状特征。之所以称之为“不变矩”,是因为矩特征在旋转、平移、尺度缩放的环境下都不会发生改变。
- 形状的表达和匹配采用更为简单的区域特征描述方法,如采用有关形状定量测量(如矩、面积、周长等)的形状参数法。
- 需要说明的是:形状参数的提取,必须以图像处理及图像分割为前提,参数的准确性必然受到分割效果的影响,对分割效果很差的图像,形状参数甚至无法提取。
import cv2
from datetime import datetime
import numpy as np
np.set_printoptions(suppress=True)
def my_humoments(img_gray):
moments = cv2.moments(img_gray)
humoments = cv2.HuMoments(moments)
humoments = np.log(np.abs(humoments)) # 取对数
print(humoments)
if __name__ == '__main__':
t1 = datetime.now()
fp = 'lena.jpg'
img = cv2.imread(fp)
h,w,_ = img.shape
img = cv2.resize(img, (h/2, w/2), cv2.INTER_LINEAR)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imwrite("scale.jpg",img_gray) # 缩放
(h, w) = img.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, 5, 1.0) # 旋转
img = cv2.warpAffine(img, M, (w, h))
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imwrite("rotate.jpg", img_gray)
img = cv2.flip(img, 0, dst=None) # 垂直镜像
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imwrite("flip.jpg",img_gray)
my_humoments(img_gray)
3.1.1.4 几何参数法
几何参数法主要包括像素与邻域、位置、方向、距离、区域周长和区域面积。
像素与邻域
- 如下所示,为 8 邻域示意图:
位置
- 目标在图片中的位置有两种表达方式,一种叫质心即目标质量的中心,另一种叫形心即目标形状的中心。
方向
- 像圆形这样的图形,很难定义它的方向。一般地,我们在定义方向的时候,为了保证唯一性,事先假定物体的形状是长方形,它的长边即物体的方向。
距离
在图像处理领域,常用的距离公式包括欧几里得距离、4邻域距离以及8邻域距离。
欧几里得距离(欧氏距离)
4邻域距离: D 4 ( A , B ) = ∣ x 0 − x 1 ∣ + ∣ y 0 − y 1 ∣ D_{4}(A, B)=|x_0 - x_1|+|y_0-y_1| D4(A,B)=∣x0−x1∣+∣y0−y1∣
8邻域距离: D 8 ( A , B ) = m a x ( ∣ x 0 − x 1 ∣ + ∣ y 0 − y 1 ∣ ) D_{8}(A, B)=max(|x_0 - x_1|+|y_0-y_1|) D8(A,B)=max(∣x0−x1∣+∣y0−y1∣)
区域周长
- 图像中某个区域的周长的计算方式有三种,具体如下:
- 区域的周长可以用区域边界所占的面积表示,可以认为是边界的像素点数量。
- 如果将像素看成是一个个单独的点,那么区域的周长就可以认为是区域的边界8链码的长度。
- 如果将像素看成是图像中一个个单位面积的小方格,那么可认为区域和背景都是由方格组成的。区域的周长就可以定义为区域和背景的交界线的长度。
区域面积
- 对于二值图来说,区域的面积可以简单地定义为目标物所占像素点的数量,即区域的边界内包含的像素点的个数。
3.1.2 基于几何分布的特征提取
基于几何分布的特征提取方法大致可以分为两类,一类是二维直方图投影法,另一类区域网格统计法。
3.1.2.1 二维直方图投影法
几何分布特征提取方法的代表之一就是二维直方图投影法,也就是获取水平以及竖直方向上各行、列黑色像素累计分布的结果,如下图所示。
3.1.2.2 区域网格统计法
区域网格统计法是另一种常见的基于几何分布的特征提取方法。其主要思想是先利用一个 m ∗ n m*n m∗n 的网格将原图进行分割,然后按从上至下、从左至右的顺序依次统计每一个网格中 “1” 的个数,从而得到最终的特征向量。
3.2 分类方法模型
字符特征提取完成后,接下来的主要任务就是识别字符。传统的机器学习将这一任务转换为一个分类任务。
3.2.1 支持向量机
![](https://i-blog.csdnimg.cn/blog_migrate/6551de23a9ccb6a2a1c6f363743ed9e3.jpeg)
- 简介: 支持向量机(support vector machines, SVM)是一种二分类模型,它的基本模型是定义在特征空间上的间隔最大的线性分类器,间隔最大使它有别于感知机;SVM还包括核技巧,这使它成为实质上的非线性分类器。SVM的的学习策略就是间隔最大化,可形式化为一个求解凸二次规划的问题,也等价于正则化的合页损失函数的最小化问题。SVM的的学习算法就是求解凸二次规划的最优化算法。
- 核心思想: 构建一个超平面,从而将不同类别的数据有效地分开。即求解能够正确划分训练数据集并且几何间隔最大的分离超平面
具体实现的原理以及细节我有时间会单独写一篇博客进行说明讲解。
3.2.2 K 近邻算法
- 核心思想: 如果一个样本在特征空间中的 K 个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。KNN 方法在类别决策时,只与极少量的相邻样本有关。
3.2.3 多层感知器
![](https://i-blog.csdnimg.cn/blog_migrate/59a5d9ca9163b671106350cb8afa4ed8.png)
- 多层感知器(MLP,Multilayer Perceptron)是一种前馈人工神经网络模型,其将输入的多个数据集映射到单一的输出的数据集上。
这没什么好说的,玩深度学习、神经网络的应该都明白,我就不在这里献丑了😂
3.3 身份证号码识别简单实战
3.3.1 KNN 实现
# -*- coding: UTF-8 -*-
import cv2
import os
import numpy as np
from sklearn import neighbors
from PIL import Image
def verticle_projection(thresh1):
""" 垂直投影 """
(h, w) = thresh1.shape
a = [0 for z in range(0, w)]
# 记录每一列的波峰
for j in range(0, w): # 遍历一列
for i in range(0, h): # 遍历一行
if thresh1[i, j] == 0: # 如果该点为黑点
a[j] += 1 # 该列的计数器加一计数
thresh1[i, j] = 255 # 记录完后将其变为白色
for j in range(0, w): # 遍历每一列
for i in range((h - a[j]), h): # 从该列应该变黑的最顶部的点开始向最底部涂黑
thresh1[i, j] = 0 # 涂黑
# 存储所有分割出来的图片
roi_list = list()
start_index = 0
end_index = 0
in_block = False
for i in range(0, w):
if in_block == False and a[i] != 0:
in_block = True
start_index = i
elif a[i] == 0 and in_block:
end_index = i
in_block = False
roiImg = thresh1[0:h, start_index:end_index + 1]
roi_list.append(roiImg)
return roi_list
def get_features(array):
""" 将二值化后的数组转化成网格特征统计图 """
h, w = array.shape
data = []
for x in range(0, w / 4):
offset_y = x * 4
temp = []
for y in range(0, h / 4):
offset_x = y * 4
# 统计每个区域的1的值
sum_temp = array[0 + offset_y:4 + offset_y,
0 + offset_x:4 + offset_x]
temp.append(sum(sum(sum_temp)))
data.append(temp)
return np.asarray(data)
def train_main():
# 读取训练样本
train_path = "../dataset/train/"
train_files = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
train_X = []
train_y = []
for train_file in train_files:
pictures = os.listdir(train_path + train_file)
for picture in pictures:
img = cv2.imread(train_path + train_file + "/" + picture)
img = cv2.resize(img, (32, 32))
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh1 = cv2.threshold(gray_image, 130, 255, cv2.THRESH_BINARY)
feature = get_features(thresh1)
feature = feature.reshape(feature.shape[0] * feature.shape[1])
train_X.append(feature)
train_y.append(train_file)
train_X = np.array(train_X)
train_y = np.array(train_y)
knn_clf = neighbors.KNeighborsClassifier()
knn_clf.fit(train_X, train_y)
return knn_clf
def test_main(knn_clf):
img = cv2.imread("../dataset/test/idcard1.jpg")
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh1 = cv2.threshold(gray_image, 130, 255, cv2.THRESH_BINARY)
roi_list = verticle_projection(thresh1) # 垂直投影
test_X = []
# 输入分类器
for single in roi_list:
single = cv2.resize(single, (32, 32), interpolation=cv2.INTER_CUBIC)
feature = get_features(single)
feature = feature.reshape(feature.shape[0] * feature.shape[1])
test_X.append(feature)
test_X = np.array(test_X)
result = knn_clf.predict(test_X)
print(result)
if __name__ == '__main__':
knn_clf = train_main()
test_main(knn_clf)
3.3.2 MLP 实现
# -*- coding: UTF-8 -*-
import cv2
import os
import numpy as np
from sklearn.neural_network import MLPClassifier
from PIL import Image
def verticle_projection(thresh1):
""" 垂直投影 """
(h, w) = thresh1.shape
a = [0 for z in range(0, w)]
# 记录每一列的波峰
for j in range(0, w): # 遍历一列
for i in range(0, h): # 遍历一行
if thresh1[i, j] == 0: # 如果该点为黑点
a[j] += 1 # 该列的计数器加一计数
thresh1[i, j] = 255 # 记录完后将其变为白色
for j in range(0, w): # 遍历每一列
for i in range((h - a[j]), h): # 从该列应该变黑的最顶部的点开始向最底部涂黑
thresh1[i, j] = 0 # 涂黑
# 存储所有分割出来的图片
roi_list = list()
start_index = 0
end_index = 0
in_block = False
for i in range(0, w):
if in_block == False and a[i] != 0:
in_block = True
start_index = i
elif a[i] == 0 and in_block:
end_index = i
in_block = False
roiImg = thresh1[0:h, start_index:end_index + 1]
roi_list.append(roiImg)
return roi_list
def get_features(array):
""" 将二值化后的数组转化成网格特征统计图 """
h, w = array.shape
data = []
for x in range(0, w / 4):
offset_y = x * 4
temp = []
for y in range(0, h / 4):
offset_x = y * 4
# 统计每个区域的1的值
sum_temp = array[0 + offset_y:4 + offset_y,
0 + offset_x:4 + offset_x]
temp.append(sum(sum(sum_temp)))
data.append(temp)
return np.asarray(data)
def train_main():
# 读取训练样本
train_path = "../dataset/train/"
train_files = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
train_X = []
train_y = []
for train_file in train_files:
pictures = os.listdir(train_path + train_file)
for picture in pictures:
img = cv2.imread(train_path + train_file + "/" + picture)
img = cv2.resize(img, (32, 32))
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh1 = cv2.threshold(gray_image, 130, 255, cv2.THRESH_BINARY)
feature = get_features(thresh1)
feature = feature.reshape(feature.shape[0] * feature.shape[1])
train_X.append(feature)
train_y.append(train_file)
train_X = np.array(train_X)
train_y = np.array(train_y)
mlp_clf = MLPClassifier(solver='lbfgs', alpha=1e-5, hidden_layer_sizes=(32,32), random_state=1)
mlp_clf.fit(train_X, train_y)
return mlp_clf
def test_main(mlp_clf):
img = cv2.imread("../dataset/test/idcard1.jpg")
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh1 = cv2.threshold(gray_image, 130, 255, cv2.THRESH_BINARY)
roi_list = verticle_projection(thresh1) # 垂直投影
test_X = []
# 输入分类器
for single in roi_list:
single = cv2.resize(single, (32, 32), interpolation=cv2.INTER_CUBIC)
feature = get_features(single)
feature = feature.reshape(feature.shape[0] * feature.shape[1])
test_X.append(feature)
test_X = np.array(test_X)
result = mlp_clf.predict(test_X)
print(result)
if __name__ == '__main__':
mlp_clf = train_main()
test_main(mlp_clf)
3.3.3 SVM 实现
# -*- coding: UTF-8 -*-
import cv2
import os
import numpy as np
from sklearn import svm
from PIL import Image
def verticle_projection(thresh1):
""" 垂直投影 """
(h, w) = thresh1.shape
a = [0 for z in range(0, w)]
# 记录每一列的波峰
for j in range(0, w): # 遍历一列
for i in range(0, h): # 遍历一行
if thresh1[i, j] == 0: # 如果该点为黑点
a[j] += 1 # 该列的计数器加一计数
thresh1[i, j] = 255 # 记录完后将其变为白色
for j in range(0, w): # 遍历每一列
for i in range((h - a[j]), h): # 从该列应该变黑的最顶部的点开始向最底部涂黑
thresh1[i, j] = 0 # 涂黑
# 存储所有分割出来的图片
roi_list = list()
start_index = 0
end_index = 0
in_block = False
for i in range(0, w):
if in_block == False and a[i] != 0:
in_block = True
start_index = i
elif a[i] == 0 and in_block:
end_index = i
in_block = False
roiImg = thresh1[0:h, start_index:end_index + 1]
roi_list.append(roiImg)
return roi_list
def get_features(array):
""" 将二值化后的数组转化成网格特征统计图 """
h, w = array.shape
data = []
for x in range(0, w / 4):
offset_y = x * 4
temp = []
for y in range(0, h / 4):
offset_x = y * 4
# 统计每个区域的1的值
sum_temp = array[0 + offset_y:4 + offset_y,
0 + offset_x:4 + offset_x]
temp.append(sum(sum(sum_temp)))
data.append(temp)
return np.asarray(data)
def train_main():
# 读取训练样本
train_path = "../dataset/train/"
train_files = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
train_X = []
train_y = []
for train_file in train_files:
pictures = os.listdir(train_path + train_file)
for picture in pictures:
img = cv2.imread(train_path + train_file + "/" + picture)
img = cv2.resize(img, (32, 32))
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh1 = cv2.threshold(gray_image, 130, 255, cv2.THRESH_BINARY)
feature = get_features(thresh1)
feature = feature.reshape(feature.shape[0] * feature.shape[1])
train_X.append(feature)
train_y.append(train_file)
train_X = np.array(train_X)
train_y = np.array(train_y)
linearsvc_clf = svm.LinearSVC()
linearsvc_clf.fit(train_X, train_y)
return linearsvc_clf
def test_main(linearsvc_clf):
img = cv2.imread("../dataset/test/idcard1.jpg")
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh1 = cv2.threshold(gray_image, 130, 255, cv2.THRESH_BINARY)
roi_list = verticle_projection(thresh1) # 垂直投影
test_X = []
# 输入分类器
for single in roi_list:
single = cv2.resize(single, (32, 32), interpolation=cv2.INTER_CUBIC)
feature = get_features(single)
feature = feature.reshape(feature.shape[0] * feature.shape[1])
test_X.append(feature)
test_X = np.array(test_X)
result = linearsvc_clf.predict(test_X)
print(result)
if __name__ == '__main__':
linearsvc_clf = train_main()
test_main(linearsvc_clf)
四、代码链接
后期我会整理一些有关于 OCR 方面的实战代码上传到我的 github 中,欢迎大家来 marking。也期待和大家一起讨论,不断进行更新 fighting 😋😋😋