当前位置:   article > 正文

Python OpenCV 裁剪身份证正反面_opencv身份证图片裁剪

opencv身份证图片裁剪

银行业务经常采集的身份证复印件如上图所示,有时候进行某项深度学习业务时,比如文字识别之类,可能需要把身份证的正反面裁剪下来作为训练样本,裁剪demo代码如下所示:

1、灰度转换  锐化:

      对图像进行灰度转换,转换成灰度图像;

      对图像进行了两次锐化操作,增强图像的高频分量增强图像细节边缘和轮廓,增强灰度反差,便于后期对目标的识别和处理。

  1. def gray_and_fliter(img, image_name='1.jpg', save_path='./'): # 转为灰度图并滤波,后面两个参数调试用
  2. """
  3. 将图片灰度化,并锐化滤波
  4. :param img: 输入RGB图片
  5. :param image_name: 输入图片名称,测试时使用
  6. :param save_path: 滤波结果保存路径,测试时使用
  7. :return: 灰度化、滤波后图片
  8. """
  9. img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转换为灰度图片
  10. img_blurred = cv2.filter2D(img_gray, -1,kernel=np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32)) #对图像进行滤波,是锐化操作
  11. img_blurred = cv2.filter2D(img_blurred, -1, kernel=np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32))
  12. return img_blurred

     图像锐化与图像平滑是相反的操作,锐化是通过增强高频分量来减少图像中的模糊,增强图像细节边缘和轮廓,增强灰度反差,便于后期对目标的识别和处理。锐化处理在增强图像边缘的同时也增加了图像的噪声。方法通常有微分法高通滤波法

微分法:

  1)梯度法

  2)罗伯特梯度算子法

  3)拉普拉斯算子法

高通滤波法:

2、图像边缘提取  形态学关开操作 形态学腐蚀膨胀操作 

经过一系列操作后,得到二值化图像

  1. def gradient_and_binary(img_blurred, image_name='1.jpg', save_path='./'): # 将灰度图二值化,后面两个参数调试用
  2. """
  3. 求取梯度,二值化
  4. :param img_blurred: 滤波后的图片
  5. :param image_name: 图片名,测试用
  6. :param save_path: 保存路径,测试用
  7. :return: 二值化后的图片
  8. """
  9. gradX = cv2.Sobel(img_blurred, ddepth=cv2.CV_32F, dx=1, dy=0) # sobel算子,计算梯度, 也可以用canny算子替代
  10. gradY = cv2.Sobel(img_blurred, ddepth=cv2.CV_32F, dx=0, dy=1)
  11. img_gradient = cv2.subtract(gradX, gradY) #使用减法作图像融合?
  12. #img_gradient = cv2.addWeighted(gradX,2, gradY,2,0)
  13. img_gradient = cv2.convertScaleAbs(img_gradient) #用convertScaleAbs()函数将其转回原来的uint8形式
  14. # 这里改进成自适应阈值,貌似没用
  15. img_binary = cv2.adaptiveThreshold(img_gradient, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 3, -3)
  16. cv2.imshow("img_binary", img_binary)
  17. # 这里调整了kernel大小(减小),腐蚀膨胀次数后(增大),出错的概率大幅减小
  18. kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
  19. img_closed = cv2.morphologyEx(img_binary, cv2.MORPH_CLOSE, kernel) #形态学关操作
  20. mg_closed = cv2.morphologyEx(img_closed, cv2.MORPH_OPEN, kernel) #形态学开操作
  21. img_closed = cv2.erode(mg_closed, None, iterations=9) #腐蚀
  22. img_closed = cv2.dilate(img_closed, None, iterations=9) # 膨胀
  23. return img_closed

3、在二值化图像找到身份证的 正面区域 和 反面区域(涉及到仿射变换矩阵

1)找出轮廓

2)刷选出2个最大的轮廓,找到轮廓对应的最小外接矩形,使用boxPoints获得矩形的4个角点坐标

3)确定4个角点坐标的位置关系,左上,左下,右上,右下(src)

4)  把4个角点坐标的位置关系移动到零坐标开始位置(dst)

5) 确定 src 到 dst的投影变换矩阵   

    cv2.getPerspectiveTransform(src, dst)

  1. def find_bbox(img, img_closed): # 寻找身份证正反面区域
  2. """
  3. 根据二值化结果判定并裁剪出身份证正反面区域
  4. :param img: 原始RGB图片
  5. :param img_closed: 二值化后的图片
  6. :return: 身份证正反面区域
  7. """
  8. (contours, _) = cv2.findContours(img_closed.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) # 求出框的个数
  9. # 这里opencv如果版本不对(4.0或以上)会报错,只需把(contours, _)改成 (_, contours, _)
  10. contours = sorted(contours, key=cv2.contourArea, reverse=True) # 按照面积大小排序
  11. countours_res = []
  12. for i in range(0, len(contours)):
  13. area = cv2.contourArea(contours[i]) # 计算面积
  14. if (area <= 0.4 * img.shape[0] * img.shape[1]) and (area >= 0.05 * img.shape[0] * img.shape[1]):
  15. # 人为设定,身份证正反面框的大小不会超过整张图片大小的0.4,不会小于0.05(这个参数随便设置的)
  16. rect = cv2.minAreaRect(contours[i]) # 最小外接矩,返回值有中心点坐标,矩形宽高,倾斜角度三个参数
  17. box = cv2.boxPoints(rect) #将rect使用boxPoints进行提取矩形的4个角点
  18. left_down, right_down, left_up, right_up = point_judge([int(rect[0][0]), int(rect[0][1])], box)
  19. src = np.float32([left_down, right_down, left_up, right_up]) # 这里注意必须对应
  20. dst = np.float32([[0, 0], [int(max(rect[1][0], rect[1][1])), 0], [0, int(min(rect[1][0], rect[1][1]))],
  21. [int(max(rect[1][0], rect[1][1])),
  22. int(min(rect[1][0], rect[1][1]))]]) # rect中的宽高不清楚是个怎么机制,但是对于身份证,肯定是宽大于高,因此加个判定
  23. m = cv2.getPerspectiveTransform(src, dst) # 得到投影变换矩阵
  24. result = cv2.warpPerspective(img, m, (int(max(rect[1][0], rect[1][1])), int(min(rect[1][0], rect[1][1]))),
  25. flags=cv2.INTER_CUBIC) # 投影变换
  26. countours_res.append(result)
  27. return countours_res # 返回身份证区域

4、对身份证的正反区域进行分割

   1)如果出现分割异常,如下图所示,则需要把中间粘连的部分分割

  1. def find_cut_line(img_closed_original): # 对于正反面粘连情况的处理,求取最小点作为中线
  2. """
  3. 根据规则,强行将粘连的区域切分
  4. :param img_closed_original: 二值化图片
  5. :return: 处理后的二值化图片
  6. """
  7. img_closed = img_closed_original.copy()
  8. img_closed = img_closed // 250
  9. #print(img_closed.shape)
  10. width_sum = img_closed.sum(axis=1) # 沿宽度方向求和,统计宽度方向白点个数
  11. start_region_flag = 0
  12. start_region_index = 0 # 身份证起始点高度值
  13. end_region_index = 0 # 身份证结束点高度值
  14. for i in range(img_closed_original.shape[0]): # 1000是原始图片高度值,当然, 这里也可以用 img_closed_original.shape[0]替代
  15. if start_region_flag == 0 and width_sum[i] > 330:
  16. start_region_flag = 1
  17. start_region_index = i # 判定第一个白点个数大于330的是身份证区域的起始点
  18. if width_sum[i] > 330:
  19. end_region_index = i # 只要白点个数大于330,便认为是身份证区域,更新结束点
  20. # 身份证区域中白点最少的高度值,认为这是正反面的交点
  21. # argsort函数中,只取width_sum中判定区域开始和结束的部分,因此结果要加上开始点的高度值
  22. min_line_position = start_region_index + np.argsort(width_sum[start_region_index:end_region_index])[0]
  23. img_closed_original[min_line_position][:] = 0
  24. for i in range(1, 11): # 参数可变,分割10个点
  25. temp_line_position = start_region_index + np.argsort(width_sum[start_region_index:end_region_index])[i]
  26. if abs(temp_line_position - min_line_position) < 30: # 限定范围,在最小点距离【-30, 30】的区域内
  27. img_closed_original[temp_line_position][:] = 0 # 强制变为0
  28. return img_closed_original

 

完整代码如下了:

 

  1. # -*- coding: utf-8 -*-
  2. # @Time :
  3. # @Author :
  4. # @Reference : None
  5. # @File : cut_twist_join.py
  6. # @IDE : PyCharm Community Edition
  7. """
  8. 将身份证正反面从原始图片中切分出来。
  9. 需要的参数有:
  10. 1.图片所在路径。
  11. 输出结果为:
  12. 切分后的身份证正反面图片。
  13. """
  14. import os
  15. import cv2
  16. import numpy as np
  17. def point_judge(center, bbox):
  18. """
  19. 用于将矩形框的边界按顺序排列
  20. :param center: 矩形中心的坐标[x, y]
  21. :param bbox: 矩形顶点坐标[[x1, y1], [x2, y2], [x3, y3], [x4, y4]]
  22. :return: 矩形顶点坐标,依次是 左下, 右下, 左上, 右上
  23. """
  24. left = []
  25. right = []
  26. for i in range(4):
  27. if bbox[i][0] > center[0]: # 只要是x坐标比中心点坐标大,一定是右边
  28. right.append(bbox[i])
  29. else:
  30. left.append(bbox[i])
  31. if right[0][1] > right[1][1]: # 如果y点坐标大,则是右上
  32. right_down = right[1]
  33. right_up = right[0]
  34. else:
  35. right_down = right[0]
  36. right_up = right[1]
  37. if left[0][1] > left[1][1]: # 如果y点坐标大,则是左上
  38. left_down = left[1]
  39. left_up = left[0]
  40. else:
  41. left_down = left[0]
  42. left_up = left[1]
  43. return left_down, right_down, left_up, right_up
  44. def gray_and_fliter(img, image_name='1.jpg', save_path='./'): # 转为灰度图并滤波,后面两个参数调试用
  45. """
  46. 将图片灰度化,并滤波
  47. :param img: 输入RGB图片
  48. :param image_name: 输入图片名称,测试时使用
  49. :param save_path: 滤波结果保存路径,测试时使用
  50. :return: 灰度化、滤波后图片
  51. """
  52. img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转换为灰度图片
  53. img_blurred = cv2.filter2D(img_gray, -1,kernel=np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32)) #对图像进行滤波,是锐化操作
  54. img_blurred = cv2.filter2D(img_blurred, -1, kernel=np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32))
  55. return img_blurred
  56. def gradient_and_binary(img_blurred, image_name='1.jpg', save_path='./'): # 将灰度图二值化,后面两个参数调试用
  57. """
  58. 求取梯度,二值化
  59. :param img_blurred: 滤波后的图片
  60. :param image_name: 图片名,测试用
  61. :param save_path: 保存路径,测试用
  62. :return: 二值化后的图片
  63. """
  64. gradX = cv2.Sobel(img_blurred, ddepth=cv2.CV_32F, dx=1, dy=0) # sobel算子,计算梯度, 也可以用canny算子替代
  65. gradY = cv2.Sobel(img_blurred, ddepth=cv2.CV_32F, dx=0, dy=1)
  66. img_gradient = cv2.subtract(gradX, gradY) #使用减法作图像融合?
  67. #img_gradient = cv2.addWeighted(gradX,2, gradY,2,0)
  68. img_gradient = cv2.convertScaleAbs(img_gradient) #用convertScaleAbs()函数将其转回原来的uint8形式
  69. # 这里改进成自适应阈值,貌似没用
  70. img_binary = cv2.adaptiveThreshold(img_gradient, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 3, -3)
  71. cv2.imshow("img_binary", img_binary)
  72. # 这里调整了kernel大小(减小),腐蚀膨胀次数后(增大),出错的概率大幅减小
  73. kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
  74. img_closed = cv2.morphologyEx(img_binary, cv2.MORPH_CLOSE, kernel) #形态学关操作
  75. mg_closed = cv2.morphologyEx(img_closed, cv2.MORPH_OPEN, kernel) #形态学开操作
  76. img_closed = cv2.erode(mg_closed, None, iterations=9) #腐蚀
  77. img_closed = cv2.dilate(img_closed, None, iterations=9) # 膨胀
  78. return img_closed
  79. def find_bbox(img, img_closed): # 寻找身份证正反面区域
  80. """
  81. 根据二值化结果判定并裁剪出身份证正反面区域
  82. :param img: 原始RGB图片
  83. :param img_closed: 二值化后的图片
  84. :return: 身份证正反面区域
  85. """
  86. (contours, _) = cv2.findContours(img_closed.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) # 求出框的个数
  87. # 这里opencv如果版本不对(4.0或以上)会报错,只需把(contours, _)改成 (_, contours, _)
  88. contours = sorted(contours, key=cv2.contourArea, reverse=True) # 按照面积大小排序
  89. countours_res = []
  90. for i in range(0, len(contours)):
  91. area = cv2.contourArea(contours[i]) # 计算面积
  92. if (area <= 0.4 * img.shape[0] * img.shape[1]) and (area >= 0.05 * img.shape[0] * img.shape[1]):
  93. # 人为设定,身份证正反面框的大小不会超过整张图片大小的0.4,不会小于0.05(这个参数随便设置的)
  94. rect = cv2.minAreaRect(contours[i]) # 最小外接矩,返回值有中心点坐标,矩形宽高,倾斜角度三个参数
  95. box = cv2.boxPoints(rect) #将rect使用boxPoints进行提取矩形的4个角点
  96. left_down, right_down, left_up, right_up = point_judge([int(rect[0][0]), int(rect[0][1])], box)
  97. src = np.float32([left_down, right_down, left_up, right_up]) # 这里注意必须对应
  98. dst = np.float32([[0, 0], [int(max(rect[1][0], rect[1][1])), 0], [0, int(min(rect[1][0], rect[1][1]))],
  99. [int(max(rect[1][0], rect[1][1])),
  100. int(min(rect[1][0], rect[1][1]))]]) # rect中的宽高不清楚是个怎么机制,但是对于身份证,肯定是宽大于高,因此加个判定
  101. m = cv2.getPerspectiveTransform(src, dst) # 得到投影变换矩阵
  102. result = cv2.warpPerspective(img, m, (int(max(rect[1][0], rect[1][1])), int(min(rect[1][0], rect[1][1]))),
  103. flags=cv2.INTER_CUBIC) # 投影变换
  104. countours_res.append(result)
  105. return countours_res # 返回身份证区域
  106. def find_cut_line(img_closed_original): # 对于正反面粘连情况的处理,求取最小点作为中线
  107. """
  108. 根据规则,强行将粘连的区域切分
  109. :param img_closed_original: 二值化图片
  110. :return: 处理后的二值化图片
  111. """
  112. img_closed = img_closed_original.copy()
  113. img_closed = img_closed // 250
  114. #print(img_closed.shape)
  115. width_sum = img_closed.sum(axis=1) # 沿宽度方向求和,统计宽度方向白点个数
  116. start_region_flag = 0
  117. start_region_index = 0 # 身份证起始点高度值
  118. end_region_index = 0 # 身份证结束点高度值
  119. for i in range(img_closed_original.shape[0]): # 1000是原始图片高度值,当然, 这里也可以用 img_closed_original.shape[0]替代
  120. if start_region_flag == 0 and width_sum[i] > 330:
  121. start_region_flag = 1
  122. start_region_index = i # 判定第一个白点个数大于330的是身份证区域的起始点
  123. if width_sum[i] > 330:
  124. end_region_index = i # 只要白点个数大于330,便认为是身份证区域,更新结束点
  125. # 身份证区域中白点最少的高度值,认为这是正反面的交点
  126. # argsort函数中,只取width_sum中判定区域开始和结束的部分,因此结果要加上开始点的高度值
  127. min_line_position = start_region_index + np.argsort(width_sum[start_region_index:end_region_index])[0]
  128. img_closed_original[min_line_position][:] = 0
  129. for i in range(1, 11): # 参数可变,分割10个点
  130. temp_line_position = start_region_index + np.argsort(width_sum[start_region_index:end_region_index])[i]
  131. if abs(temp_line_position - min_line_position) < 30: # 限定范围,在最小点距离【-30, 30】的区域内
  132. img_closed_original[temp_line_position][:] = 0 # 强制变为0
  133. return img_closed_original
  134. def cut_part_img(img, cut_percent):
  135. """
  136. # 从宽度和高度两个方向,裁剪身份证边缘
  137. :param img: 身份证区域
  138. :param cut_percent: 裁剪的比例
  139. :return: 裁剪后的身份证区域
  140. """
  141. height, width, _ = img.shape
  142. height_num = int(height * cut_percent) # 需要裁剪的高度值
  143. h_start = 0 + height_num // 2 # 左右等比例切分
  144. h_end = height - height_num // 2 - 1
  145. width_num = int(width * cut_percent) # 需要裁剪的宽度值
  146. w_start = 0 + width_num // 2
  147. w_end = width - width_num // 2 - 1
  148. return img[h_start:h_end, w_start:w_end] # 返回裁剪后的图片
  149. def preprocess_cut_one_img(img_path, img_name, save_path='./save_imgs/', problem_path='./problem_save/'): # 处理一张图片
  150. """
  151. 裁剪出一张图片中的身份证正反面区域
  152. :param img_path: 图片所在路径
  153. :param img_name: 图片名称
  154. :param save_path: 结果保存路径 测试用
  155. :param problem_path: 出错图片中间结果保存 测试用
  156. :return: 身份证正反面图片
  157. """
  158. img_path_name = os.path.join(img_path, img_name)
  159. if not os.path.exists(img_path_name): # 判断图片是否存在
  160. print('img {name} is not exits'.format(name=img_path_name))
  161. return 1, [] # 图片不存在,直接返回,报错加一
  162. img = cv2.imread(img_path_name) # 读取图片
  163. img_blurred = gray_and_fliter(img, img_name) # 灰度化并滤波
  164. img_t = cv2.filter2D(img, -1, kernel=np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32))
  165. cv2.imshow("img_t",img_t)
  166. # 对图像进行锐化
  167. img_binary = gradient_and_binary(img_blurred) # 梯度加上二值化
  168. cv2.imshow("img_binary", img_binary)
  169. res_bbox = find_bbox(img_t, img_binary) # 切分正反面
  170. if len(res_bbox) != 2: # 异常处理
  171. print('Error happened when cut img {name}, try exception cut program '.format(name=img_path_name))
  172. # cv2.imwrite(os.path.join(problem_path, img_name.split('.')[0] + '_blurred.jpg'), img_blurred)
  173. # cv2.imwrite(os.path.join(problem_path, img_name.split('.')[0] + '_binary.jpg'), img_binary)
  174. # cv2.imwrite(os.path.join(problem_path, img_name), img) # 调试用,保存中间处理结果
  175. img_binary = find_cut_line(img_binary) # 强制分割正反面
  176. cv2.imshow("img_binary1", img_binary)
  177. res_bbox = find_bbox(img_t, img_binary)
  178. if len(res_bbox) != 2: # 纠正失败
  179. print('Failed to cut img {name}, exception program end'.format(name=img_path_name))
  180. return 1, None
  181. else: # 纠正成功
  182. print('Correctly cut img {name}, exception program end'.format(name=img_path_name))
  183. return 0, res_bbox
  184. else: # 裁剪过程正常
  185. # cv2.imwrite(os.path.join(save_path, img_name.split('.')[0] + '_0.jpg'), cut_part_img(res_bbox[0], 0.0))
  186. # cv2.imwrite(os.path.join(save_path, img_name.split('.')[0] + '_1.jpg'), cut_part_img(res_bbox[1], 0.0))
  187. # cv2.imwrite(os.path.join(save_path, img_name.split('.')[0] + '_original.jpg'), img)
  188. return 0, res_bbox
  189. def process_img(img_path, save_path, problem_path):
  190. """
  191. 切分一个目录下的所有图片
  192. :param img_path: 图片所在路径
  193. :param save_path: 结果保存路径
  194. :param problem_path: 问题图片保存路径
  195. :return: None
  196. """
  197. if not os.path.exists(img_path): # 判断图片路径是否存在
  198. print('img path {name} is not exits, program break.'.format(name=img_path))
  199. return
  200. if not os.path.exists(save_path): # 保存路径不存在,则创建路径
  201. os.makedirs(save_path)
  202. if not os.path.exists(problem_path): # 保存路径不存在,则创建路径
  203. os.makedirs(problem_path)
  204. img_names = os.listdir(img_path)
  205. error_count = 0
  206. error_names = []
  207. for img_name in img_names:
  208. error_temp, res_bbox = preprocess_cut_one_img(img_path, img_name, save_path, problem_path)
  209. error_count += error_temp
  210. if error_temp == 0:
  211. cv2.imwrite(os.path.join(save_path, img_name.split('.')[0] + '_0.jpg'), cut_part_img(res_bbox[0], 0.0))
  212. cv2.imwrite(os.path.join(save_path, img_name.split('.')[0] + '_1.jpg'), cut_part_img(res_bbox[1], 0.0))
  213. else:
  214. error_names.append(img_name)
  215. print('total error number is: ', error_count)
  216. print('error images mame :')
  217. for error_img_name in error_names:
  218. print(error_img_name)
  219. return
  220. if __name__ == '__main__':
  221. origin_img_path = './origin_imgs/'
  222. cutted_save_path = './result_imgs/'
  223. cut_problem_path = './problem_imgs/'
  224. process_img(img_path=origin_img_path, save_path=cutted_save_path, problem_path=cut_problem_path)
  225. cv2.waitKey(0)

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/611165
推荐阅读
相关标签
  

闽ICP备14008679号