初次接触OCR技术,OCR技术在工业检测上有极大的用处,如工件上面得数字标号识别、印刷纸票识别、车牌识别、身份证号码识别等。但中文字体识别较难,如今百度OCR、谷歌tesseract等提供识别接口,可以取得较好的识别效果。
通过贾志刚老师的印刷字体识别课程和一些OpenCV函数的学习,用身份证号码识别检测一下所学知识。
主要步骤

  1. 使用OpenCV进行图像仿射变换或者透视变换,将图像摆正;
  2. 通过二值化和形态学处理,粗定位文本信息区域;
  3. 分割ROI区域,通过轮廓筛选,确定身份证号码区域;
  4. 字符分割和字符排序;
  5. 字符识别模型训练;
  6. 字符识别。

具体实现:
1、透视变换
先检测身份证外轮廓,获取身份证四个角点,然后利用OpenCV中getPerspectiveTransform()函数获取变换矩阵,warpPerspective()函数透视变换。特别需要注意角点坐标的顺序必须按顺序一一对应,按顺序找到对应坐标0123分别是 左上,右上,右下,左下。

#轮廓检测
cnts,hierarchy = cv.findContours(edged, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# cnts = sorted(cnts, key = cv.contourArea, reverse = True)[:5]
for c in cnts:
    peri = cv.arcLength(c,True)
    approx = cv.approxPolyDP(c,0.02*peri,True) #轮廓多边形逼近,找到角点

    if len(approx) == 4:
        screenCnt = approx
        #print(screenCnt)
        break

cv.drawContours(gray, [screenCnt], -1, (0, 255, 0), 2)
pts = screenCnt.reshape(4, 2)
#print(a)

rect = np.zeros((4, 2), dtype = "float32")

# 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
# 计算左上,右下
s = pts.sum(axis = 1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]

# 计算右上和左下
diff = np.diff(pts, axis = 1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]

(tl, tr, br, bl) = rect

	# 计算输入的w和h值
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))

heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxHeight = max(int(heightA), int(heightB))

	# 变换后对应坐标位置
dst = np.array([
	[0, 0],
	[maxWidth - 1, 0],
	[maxWidth - 1, maxHeight - 1],
	[0, maxHeight - 1]], dtype = "float32")


# 计算变换矩阵
M = cv.getPerspectiveTransform(rect, dst)
warped = cv.warpPerspective(img, M, (maxWidth, maxHeight))

java opencv身份证是被 opencv身份证图片识别_OpenCV


java opencv身份证是被 opencv身份证图片识别_OCR_02


2、文本粗定位

通过Canny边缘检测,提取文本边缘,但此时边缘之间是断开的,需要通过形态学膨胀操作,将边缘之间连接起来。

canny = cv.Canny(gray,60,255)

# 形态学操作
kernel = cv.getStructuringElement(cv.MORPH_RECT,(11,5))
dilation = cv.dilate(canny,kernel,iterations = 1)

java opencv身份证是被 opencv身份证图片识别_OpenCV_03


3、身份证号码ROI分割

观察到身份证号码区域的长度是最长的,因此可以通过轮廓检测,用boungdingRect()函数计算各轮廓的长宽值,设定合适阈值,找到ROI区域的轮廓进行分割。

#轮廓检测
cnts,hiri = cv.findContours(dilation, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
for c in cnts:
    x, y, w, h = cv.boundingRect(c)
    if w < 250:
        continue
    cv.rectangle(gray, (x,y), (x+w,y+h), (255,0,0), 2)
    dst = src[y-2:y + h, x:x + w]

4、字符分割与排序
因为数字字符是连接的,字符之间有间隙,可以直接使用轮廓提取,因此可以以此为据,分割字符。轮廓的检测不能保证是有序的,通过轮廓的X坐标进行排序,字符排序保证以正确顺序依次进行识别。

# 字符分割
contours, hireachy = cv.findContours(canny1, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
#获取字符
rois = []
for c in range(len(contours)):
    box = cv.boundingRect(contours[c])
    if box[3] < 10:
        continue
    rois.append(box)

# 字符排序
num = len(rois)
for i in range(num):
    for j in range(i+1, num, 1):
        x1, y1, w1, h1 = rois[i]
        x2, y2, w2, h2 = rois[j]
        if x2 < x1:
            temp = rois[j]
            rois[j] = rois[i]
            rois[i] = temp

java opencv身份证是被 opencv身份证图片识别_OCR_04


5、识别模型训练

样本的准确采集与最后的结果准确度息息相关,将上述分割的字符进行样本拓展,因为数字像素特征比较明显,因此将像素特征作为输入,采用OpenCV封装的SVM网络进行训练与识别。

# 获取数据
train_data, train_labels = load_data()

# 网络构建
svm = cv.ml.SVM_create()
svm.setKernel(cv.ml.SVM_LINEAR)
svm.setType(cv.ml.SVM_C_SVC)
svm.setC(2.67)
svm.setGamma(5.383)
svm.train(train_data, cv.ml.ROW_SAMPLE, train_labels)
svm.save("svm_data.yml")

svm = cv.ml.SVM_load("svm_data.yml")
result = svm.predict(train_data)[1]
print(result)

6、身份证号码识别

#数字识别
svm = cv.ml.SVM_load("svm_data.yml")
result = svm.predict(digit_data)[1]
text = ""
for i in range(len(result)):
    text += str(np.int32(result[i][0]))
print(text)

java opencv身份证是被 opencv身份证图片识别_轮廓检测_05