批量识别身份证并导出excel

[复制链接]
140 |11
发表于 2025-11-1 08:44:21 | 显示全部楼层 |阅读模式
版本更新第五版, 版本号1.0.5
1.0.4更新内容: 模型改为随程序分发, 初次启动时无需再下载模型, 解决黑框终端无法关闭的问题
1.0.5更新内容: 优化字段匹配方法, 识别精确度有所提高
版本更新第三版, 代码已开源(见附件)
更新内容: 优化软件启动速度, 增加识别字段, 增加图片重命名(可选可配置), 增加excel内图片选项(路径/图片)
由于代码量较大, 无法在此处展开, 有对源码或进一步优化感兴趣的伙伴可以下载源码阅读修改
下面的代码我只放核心逻辑部分
由于一些行业需要手动录入大量的身份证信息, 因此编写本软件用于减少工作
软件说明: 本软件使用python3.12.10编写, 因此无法在win10以下的电脑上运行,gui改为pySide6,识别功能依赖包:paddlepaddle==2.6.2,paddleocr,paddlenlp,调用cpu进行识别,不依赖显卡。如果你的电脑显卡比较好,可以将依赖改为GPU版,再进行打包,使用gpu性能会更强。
代码提供Windows版本,由于ocr模型在本地运行,因此软件对电脑性能要求较高,我Linux云服务器则是2核2G,带不动。Windows电脑一般不会有那么低的配置,则不用担心。可断网进行识别。
代码如下:

[Python]  
  1. # 在gui.py文件中添加以下代码
  2. import re
  3. from PySide6.QtCore import QThread, Signal
  4. import traceback
  5. from openpyxl.workbook import Workbook
  6. from openpyxl.drawing.image import Image as XLImage
  7. from openpyxl.styles import Alignment
  8. import cv2
  9. import numpy as np
  10. from PIL import Image
  11. '''
  12. ocr实际开始工作的线程
  13. 需要将前边加载好的模型传递过来
  14. '''
  15. class OCRWorker(QThread):
  16. [size=5]定义信号,用于通知主线程处理进度和结果[/size]
  17. progress_updated = Signal(int, int)  # 当前进度,总数量
  18. finished_signal = Signal()  # 处理完成信号
  19. error_occurred = Signal(str)  # 错误信息信号
  20. def __init__(self, file_paths, export_options, ocr):
  21.     super().__init__()
  22.     self.file_paths = file_paths
  23.     self.export_options = export_options
  24.     self.ocr = ocr
  25.     self._should_terminate = False  # 添加终止标志
  26. def run(self):
  27.     try:
  28.         # 处理所有文件
  29.         self.process_files(self.file_paths)
  30.     except Exception as e:
  31.         error_msg = f"处理过程中发生错误: {str(e)}\n{traceback.format_exc()}"
  32.         self.error_occurred.emit(error_msg)
  33. def process_files(self, file_paths):
  34.     wb = Workbook()
  35.     ws = wb.active
  36.     ws.append(["图片", "姓名", "性别", "民族", "出生日期", "住址", "身份证号", "有效期限"])
  37.     row_idx = 2
  38.     total_files = len(self.file_paths)
  39.     processed_count = 0
  40.     for i, path in enumerate(file_paths):
  41.         # 检查是否收到终止请求
  42.         if self._should_terminate:
  43.             print("收到终止请求,正在保存已处理的数据...")
  44.             break
  45.         # 发送进度更新信号
  46.         self.progress_updated.emit(i + 1, total_files)
  47.         info = self.extract_info_from_image(path)
  48.         # 检查线程是否被中断
  49.         if self.isInterruptionRequested():
  50.             break
  51.         if info:
  52.             ws.cell(row=row_idx, column=2, value=info["姓名"])
  53.             ws.cell(row=row_idx, column=3, value=info["性别"])
  54.             ws.cell(row=row_idx, column=4, value=info["民族"])
  55.             ws.cell(row=row_idx, column=5, value=info["出生日期"])
  56.             ws.cell(row=row_idx, column=6, value=info["住址"])
  57.             ws.cell(row=row_idx, column=7, value=info["身份证号"])
  58.             ws.cell(row=row_idx, column=8, value=info["有效期限"])
  59.             # 根据导出选项决定如何处理图片
  60.             export_option = self.export_options.get("export_option", "image_path")
  61.             # 处理重命名(如果配置了重命名选项)
  62.             if self.should_rename_file(info):
  63.                 new_path = self.rename_file(path, info)
  64.                 # 更新图片路径为重命名后的路径
  65.                 if export_option == "image_path":
  66.                     ws.cell(row=row_idx, column=1, value=new_path)
  67.                 # 更新返回数据中的图片路径
  68.                 info["图片路径"] = new_path
  69.             if export_option == "image_file":
  70.                 # 直接嵌入图片文件
  71.                 try:
  72.                     img = XLImage(info["图片路径"])
  73.                     img.width = 500
  74.                     img.height = 300
  75.                     ws.row_dimensions[zxsq-anti-bbcode-row_idx].height = img.height
  76.                     ws.add_image(img, f"A{row_idx}")
  77.                     ws.column_dimensions['A'].width = img.width * 0.14
  78.                 except Exception as e:
  79.                     print(f"无法插入图片 {path}: {e}")
  80.             else :
  81.                 # 仅保存图片路径
  82.                 ws.cell(row=row_idx, column=1, value=info["图片路径"])
  83.             for col in range(1, 9):
  84.                 cell = ws.cell(row=row_idx, column=col)
  85.                 cell.alignment = Alignment(horizontal='center', vertical='center')
  86.             row_idx += 1
  87.             processed_count += 1
  88.     for col in range(1, 9):
  89.         header_cell = ws.cell(row=1, column=col)
  90.         header_cell.alignment = Alignment(horizontal='center', vertical='center')
  91.     output_path = "身份证识别结果.xlsx"
  92.     wb.save(output_path)
  93.     if self._should_terminate:
  94.         print(f"处理已终止,已完成 {processed_count}/{total_files} 个文件,结果已保存到 {output_path}")
  95.     else:
  96.         print(f"处理完成,共处理 {processed_count} 个文件,结果已保存到 {output_path}")
  97.     # 发送完成信号
  98.     self.finished_signal.emit()
  99. def extract_info_from_image(self, image_path):
  100.     """从图片中提取信息(优化版文本处理)"""
  101.     try:
  102.         # 检查文件是否存在和可读
  103.         # import os
  104.         # if not os.path.exists(image_path):
  105.         #     raise FileNotFoundError(f"图片文件不存在: {image_path}")
  106.         #
  107.         # if not os.access(image_path, os.R_OK):
  108.         #     raise PermissionError(f"没有权限读取图片文件: {image_path}")
  109.         # # 检查是否需要预处理身份证图片
  110.         # if self.export_options.get("preprocess_id_card", True):
  111.         #     processed_image_path = self.preprocess_id_card_image(image_path)
  112.         # else:
  113.         #     processed_image_path = image_path
  114.         #
  115.         # result = self.ocr.ocr(processed_image_path, cls=True)
  116.         result = self.ocr.ocr(image_path, cls=True)
  117.         # 1. 先整体拼接所有文本
  118.         all_text = ""
  119.         for res in result:
  120.             for line in res:
  121.                 text = line[zxsq-anti-bbcode-1][zxsq-anti-bbcode-0]
  122.                 if text:
  123.                     all_text += text
  124.         # 2. 去除"中华人民共和国居民身份证"标题
  125.         all_text = re.sub(r'中华人民共和国居民身份证', '', all_text)
  126.         # 3. 去除所有空格和特殊空白字符
  127.         all_text = re.sub(r'\s+', '', all_text)
  128.         # 4. 在关键字段前添加换行符
  129.         keywords = ['姓名', '性别', '民族', '出生', '住址', '公民身份号码', '签发机关', '有效期限']
  130.         for keyword in keywords:
  131.             all_text = re.sub(f'({keyword})', r'\n\1', all_text)
  132.         print(f"处理后的文本: {all_text}")
  133.         # 初始化提取结果
  134.         name = gender = nation = birth = address = id_number = expire = ""
  135.         # 提取各字段信息
  136.         # 提取身份证号
  137.         # 直接匹配17位数字+1位校验码(数字或X)
  138.         id_match = re.search(r'[\d]{17}[\dXx]', all_text)
  139.         if id_match:
  140.             id_number = id_match.group().strip()
  141.             # 移除身份证号码干扰
  142.             all_text = all_text.replace(id_match.group(), '')
  143.         # 提取姓名
  144.         name_match = re.search(r'姓名(.+?)(?=\n|$)', all_text)
  145.         if name_match:
  146.             name = name_match.group(1).strip()
  147.         # 提取性别
  148.         gender_match = re.search(r'性别(男|女)', all_text)
  149.         if gender_match:
  150.             gender = gender_match.group(1).strip()
  151.         # 提取民族
  152.         nation_match = re.search(r'民族(.+?)(?=\n|$)', all_text)
  153.         if nation_match:
  154.             nation = nation_match.group(1).strip()
  155.         # 提取出生日期
  156.         birth_match = re.search(r'出生(.+?)(?=\n|$)', all_text)
  157.         if birth_match:
  158.             birth = birth_match.group(1).strip()
  159.         # 提取住址
  160.         address_match = re.search(r'住址(.+?)(?=\n|$)', all_text)
  161.         if address_match:
  162.             address = address_match.group(1).strip()
  163.         # 提取有效期限
  164.         expire_match = re.search(r'有效期限(.+?)(?=\n|$)', all_text)
  165.         if expire_match:
  166.             expire = expire_match.group(1).strip()
  167.         data = {
  168.             "姓名": name,
  169.             "性别": gender,
  170.             "民族": nation,
  171.             "出生日期": birth,
  172.             "住址": address,
  173.             "身份证号": id_number,
  174.             "有效期限": expire,
  175.             "图片路径": image_path
  176.         }
  177.         print(f"data == {data}")
  178.         return data
  179.     except Exception as e:
  180.         print(f"处理 {image_path} 失败: {e}")
  181.         return None
  182. def should_rename_file(self, info):
  183.     """检查是否需要重命名文件"""
  184.     rename_options = self.export_options.get("rename_options", [])
  185.     return len(rename_options) > 0
  186. def rename_file(self, original_path, info):
  187.     """根据配置重命名文件"""
  188.     if not self.should_rename_file(info):
  189.         return original_path
  190.     rename_options = self.export_options.get("rename_options", [])
  191.     separator = self.export_options.get("separator", "_")
  192.     # 构建新的文件名部分
  193.     name_parts = []
  194.     for option in rename_options:
  195.         if option == "name" and info.get("姓名"):
  196.             name_parts.append(info["姓名"])
  197.         elif option == "id" and info.get("身份证号"):
  198.             name_parts.append(info["身份证号"])
  199.         elif option == "nation" and info.get("民族"):
  200.             name_parts.append(info["民族"])
  201.         elif option == "sex" and info.get("性别"):
  202.             name_parts.append(info["性别"])
  203.         elif option == "address" and info.get("住址"):
  204.             name_parts.append(info["住址"])
  205.     if not name_parts:
  206.         return original_path
  207.     # 构造新文件名
  208.     new_name = separator.join(name_parts)
  209.     # 保持原始文件扩展名
  210.     import os
  211.     dir_name = os.path.dirname(original_path)
  212.     file_ext = os.path.splitext(original_path)[zxsq-anti-bbcode-1]
  213.     new_path = os.path.join(dir_name, new_name + file_ext)
  214.     # 重命名文件
  215.     try:
  216.         os.rename(original_path, new_path)
  217.         return new_path
  218.     except Exception as e:
  219.         print(f"重命名文件失败 {original_path} -> {new_path}: {e}")
  220.         return original_path
  221. # 图片灰度处理, 处理成扫描件, 下面还没写好 不要用
  222. # def preprocess_id_card_image(self, image_path):
  223. #     """对身份证图片进行校正、裁剪并转换为黑白扫描件"""
  224. #     try:
  225. #         # 读取图片
  226. #         img = cv2.imread(image_path)
  227. #         if img is None:
  228. #             return image_path
  229. #
  230. #         # 1. 转换为灰度图
  231. #         gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
  232. #
  233. #         # 2. 使用中值滤波代替高斯模糊
  234. #         denoised = cv2.medianBlur(gray, 3)
  235. #
  236. #         # 3. 使用自适应阈值
  237. #         binary = cv2.adaptiveThreshold(
  238. #             denoised, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
  239. #             cv2.THRESH_BINARY, 15, 3
  240. #         )
  241. #
  242. #         # 4. 可选:轻微平滑处理
  243. #         smoothed = cv2.medianBlur(binary, 1)
  244. #
  245. #         # 5. 保存处理后的图片
  246. #         import os
  247. #         dir_name = os.path.dirname(image_path)
  248. #         file_name = os.path.splitext(os.path.basename(image_path))[zxsq-anti-bbcode-0]
  249. #         file_ext = os.path.splitext(image_path)[zxsq-anti-bbcode-1]
  250. #         processed_path = os.path.join(dir_name, f"{file_name}_processed{file_ext}")
  251. #
  252. #         cv2.imwrite(processed_path, smoothed)
  253. #
  254. #         return processed_path
  255. #     except Exception as e:
  256. #         print(f"身份证图片预处理失败 {image_path}: {e}")
  257. #         return image_path
  258. #
  259. # def order_points(self, pts):
  260. #     """对四个点进行排序:左上、右上、右下、左下"""
  261. #     rect = np.zeros((4, 2), dtype="float32")
  262. #
  263. #     # 计算坐标和
  264. #     s = pts.sum(axis=1)
  265. #     rect[zxsq-anti-bbcode-0] = pts[np.argmin(s)]  # 左上角点(坐标和最小)
  266. #     rect[zxsq-anti-bbcode-2] = pts[np.argmax(s)]  # 右下角点(坐标和最大)
  267. #
  268. #     # 计算坐标差
  269. #     diff = np.diff(pts, axis=1)
  270. #     rect[zxsq-anti-bbcode-1] = pts[np.argmin(diff)]  # 右上角点(坐标差最小)
  271. #     rect[zxsq-anti-bbcode-3] = pts[np.argmax(diff)]  # 左下角点(坐标差最大)
  272. #
  273. #     return rect
  274. #
  275. # def four_point_transform(self, image, pts):
  276. #     """四点透视变换"""
  277. #     # 获取排序后的坐标
  278. #     rect = self.order_points(pts)
  279. #     (tl, tr, br, bl) = rect
  280. #
  281. #     # 计算新图像的宽度和高度
  282. #     width_a = np.sqrt(((br[zxsq-anti-bbcode-0] - bl[zxsq-anti-bbcode-0]) ** 2) + ((br[zxsq-anti-bbcode-1] - bl[zxsq-anti-bbcode-1]) ** 2))
  283. #     width_b = np.sqrt(((tr[zxsq-anti-bbcode-0] - tl[zxsq-anti-bbcode-0]) ** 2) + ((tr[zxsq-anti-bbcode-1] - tl[zxsq-anti-bbcode-1]) ** 2))
  284. #     max_width = max(int(width_a), int(width_b))
  285. #
  286. #     height_a = np.sqrt(((tr[zxsq-anti-bbcode-0] - br[zxsq-anti-bbcode-0]) ** 2) + ((tr[zxsq-anti-bbcode-1] - br[zxsq-anti-bbcode-1]) ** 2))
  287. #     height_b = np.sqrt(((tl[zxsq-anti-bbcode-0] - bl[zxsq-anti-bbcode-0]) ** 2) + ((tl[zxsq-anti-bbcode-1] - bl[zxsq-anti-bbcode-1]) ** 2))
  288. #     max_height = max(int(height_a), int(height_b))
  289. #
  290. #     # 目标点
  291. #     dst = np.array([
  292. #         [0, 0],
  293. #         [max_width - 1, 0],
  294. #         [max_width - 1, max_height - 1],
  295. #         [0, max_height - 1]], dtype="float32")
  296. #
  297. #     # 计算透视变换矩阵并应用
  298. #     M = cv2.getPerspectiveTransform(rect, dst)
  299. #     warped = cv2.warpPerspective(image, M, (max_width, max_height))
  300. #
  301. #     return warped
  302. # 中断处理, 此处不要直接中断线程, 可能导致excel未能处理完毕线程就退出了
  303. # 我们应该保证excel
  304. def request_termination(self):
  305.     """请求终止处理过程"""
  306.     self._should_terminate = True
复制代码
[code][/code]





本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

发表于 2025-11-1 08:45:17 | 显示全部楼层
更新第四版:
更新内容: 1. 该版本模型随程序一起分发, 即使是初次运行也无需再联网下载模型了, 本地化更加完全  2.隐藏控制台黑窗口, 更美观一些
3.但是该版本程序所在文件夹不能包含中文路径了, 否则将无法识别模型
通过网盘分享的文件:id_card_ocr.zip
链接: https://pan.baidu.com/s/1amlQldVsmuggp4HLcYeq2g?pwd=c94k 提取码: c94k
--来自百度网盘超级会员v9的分享
回复

使用道具 举报

发表于 2025-11-1 08:46:04 | 显示全部楼层
身份证有效期也需要识别!
回复

使用道具 举报

发表于 2025-11-1 08:46:56 | 显示全部楼层
感谢楼主分享
回复

使用道具 举报

发表于 2025-11-1 08:47:49 | 显示全部楼层
谢谢分享,小旅馆登记需要
回复

使用道具 举报

发表于 2025-11-1 08:48:12 | 显示全部楼层
感谢分享,试一下
回复

使用道具 举报

发表于 2025-11-1 08:48:50 | 显示全部楼层
谢谢分享,很实用
回复

使用道具 举报

发表于 2025-11-1 08:49:47 | 显示全部楼层
谢谢分享,正反都能识别吗?
回复

使用道具 举报

发表于 2025-11-1 08:49:55 | 显示全部楼层
感谢分享,坐等离线版
回复

使用道具 举报

发表于 2025-11-1 08:50:55 | 显示全部楼层
这个只有第一次下载模型需要联网, 后面再用就是离线版的了
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表