找回密码
 立即注册
搜索
查看: 65|回复: 11

批量识别身份证并导出excel

[复制链接]
灌水成绩
1
2
35
主题
帖子
积分

等级头衔 ID : 687
用户组 : 新手上路

积分成就 测量币 : 35
违规 : 0
在线时间 : 0 小时
注册时间 : 2026-4-6
最后登录 : 2026-4-23

勋章

活跃会员最佳新人

联系方式

发表于 2025-5-22 10:10:00 | 显示全部楼层 |阅读模式
版本更新第五版, 版本号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] 纯文本查看 复制代码# 在gui.py文件中添加以下代码import re
from PySide6.QtCore import QThread, Signalimport tracebackfrom openpyxl.workbook import Workbookfrom openpyxl.drawing.image import Image as XLImagefrom openpyxl.styles import Alignment
import cv2import numpy as npfrom PIL import Image
'''ocr实际开始工作的线程需要将前边加载好的模型传递过来'''class OCRWorker(QThread):
定义信号,用于通知主线程处理进度和结果

progress_updated = Signal(int, int)  # 当前进度,总数量finished_signal = Signal()  # 处理完成信号error_occurred = Signal(str)  # 错误信息信号def __init__(self, file_paths, export_options, ocr):    super().__init__()    self.file_paths = file_paths    self.export_options = export_options    self.ocr = ocr    self._should_terminate = False  # 添加终止标志def run(self):    try:        # 处理所有文件        self.process_files(self.file_paths)    except Exception as e:        error_msg = f"处理过程中发生错误: {str(e)}\n{traceback.format_exc()}"        self.error_occurred.emit(error_msg)def process_files(self, file_paths):    wb = Workbook()    ws = wb.active    ws.append(["图片", "姓名", "性别", "民族", "出生日期", "住址", "身份证号", "有效期限"])    row_idx = 2    total_files = len(self.file_paths)    processed_count = 0    for i, path in enumerate(file_paths):        # 检查是否收到终止请求        if self._should_terminate:            print("收到终止请求,正在保存已处理的数据...")            break        # 发送进度更新信号        self.progress_updated.emit(i + 1, total_files)        info = self.extract_info_from_image(path)        # 检查线程是否被中断        if self.isInterruptionRequested():            break        if info:            ws.cell(row=row_idx, column=2, value=info["姓名"])            ws.cell(row=row_idx, column=3, value=info["性别"])            ws.cell(row=row_idx, column=4, value=info["民族"])            ws.cell(row=row_idx, column=5, value=info["出生日期"])            ws.cell(row=row_idx, column=6, value=info["住址"])            ws.cell(row=row_idx, column=7, value=info["身份证号"])            ws.cell(row=row_idx, column=8, value=info["有效期限"])            # 根据导出选项决定如何处理图片            export_option = self.export_options.get("export_option", "image_path")            # 处理重命名(如果配置了重命名选项)            if self.should_rename_file(info):                new_path = self.rename_file(path, info)                # 更新图片路径为重命名后的路径                if export_option == "image_path":                    ws.cell(row=row_idx, column=1, value=new_path)                # 更新返回数据中的图片路径                info["图片路径"] = new_path            if export_option == "image_file":                # 直接嵌入图片文件                try:                    img = XLImage(info["图片路径"])                    img.width = 500                    img.height = 300                    ws.row_dimensions[zxsq-anti-bbcode-row_idx].height = img.height                    ws.add_image(img, f"A{row_idx}")                    ws.column_dimensions['A'].width = img.width * 0.14                except Exception as e:                    print(f"无法插入图片 {path}: {e}")            else :                # 仅保存图片路径                ws.cell(row=row_idx, column=1, value=info["图片路径"])            for col in range(1, 9):                cell = ws.cell(row=row_idx, column=col)                cell.alignment = Alignment(horizontal='center', vertical='center')            row_idx += 1            processed_count += 1    for col in range(1, 9):        header_cell = ws.cell(row=1, column=col)        header_cell.alignment = Alignment(horizontal='center', vertical='center')    output_path = "身份证识别结果.xlsx"    wb.save(output_path)    if self._should_terminate:        print(f"处理已终止,已完成 {processed_count}/{total_files} 个文件,结果已保存到 {output_path}")    else:        print(f"处理完成,共处理 {processed_count} 个文件,结果已保存到 {output_path}")    # 发送完成信号    self.finished_signal.emit()def extract_info_from_image(self, image_path):    """从图片中提取信息(优化版文本处理)"""    try:        # 检查文件是否存在和可读        # import os        # if not os.path.exists(image_path):        #     raise FileNotFoundError(f"图片文件不存在: {image_path}")        #        # if not os.access(image_path, os.R_OK):        #     raise PermissionError(f"没有权限读取图片文件: {image_path}")        # # 检查是否需要预处理身份证图片        # if self.export_options.get("preprocess_id_card", True):        #     processed_image_path = self.preprocess_id_card_image(image_path)        # else:        #     processed_image_path = image_path        #        # result = self.ocr.ocr(processed_image_path, cls=True)        result = self.ocr.ocr(image_path, cls=True)        # 1. 先整体拼接所有文本        all_text = ""        for res in result:            for line in res:                text = line[zxsq-anti-bbcode-1][zxsq-anti-bbcode-0]                if text:                    all_text += text        # 2. 去除"中华人民共和国居民身份证"标题        all_text = re.sub(r'中华人民共和国居民身份证', '', all_text)        # 3. 去除所有空格和特殊空白字符        all_text = re.sub(r'\s+', '', all_text)        # 4. 在关键字段前添加换行符        keywords = ['姓名', '性别', '民族', '出生', '住址', '公民身份号码', '签发机关', '有效期限']        for keyword in keywords:            all_text = re.sub(f'({keyword})', r'\n\1', all_text)        print(f"处理后的文本: {all_text}")        # 初始化提取结果        name = gender = nation = birth = address = id_number = expire = ""        # 提取各字段信息        # 提取身份证号        # 直接匹配17位数字+1位校验码(数字或X)        id_match = re.search(r'[\d]{17}[\dXx]', all_text)        if id_match:            id_number = id_match.group().strip()            # 移除身份证号码干扰            all_text = all_text.replace(id_match.group(), '')        # 提取姓名        name_match = re.search(r'姓名(.+?)(?=\n|$)', all_text)        if name_match:            name = name_match.group(1).strip()        # 提取性别        gender_match = re.search(r'性别(男|女)', all_text)        if gender_match:            gender = gender_match.group(1).strip()        # 提取民族        nation_match = re.search(r'民族(.+?)(?=\n|$)', all_text)        if nation_match:            nation = nation_match.group(1).strip()        # 提取出生日期        birth_match = re.search(r'出生(.+?)(?=\n|$)', all_text)        if birth_match:            birth = birth_match.group(1).strip()        # 提取住址        address_match = re.search(r'住址(.+?)(?=\n|$)', all_text)        if address_match:            address = address_match.group(1).strip()        # 提取有效期限        expire_match = re.search(r'有效期限(.+?)(?=\n|$)', all_text)        if expire_match:            expire = expire_match.group(1).strip()        data = {            "姓名": name,            "性别": gender,            "民族": nation,            "出生日期": birth,            "住址": address,            "身份证号": id_number,            "有效期限": expire,            "图片路径": image_path        }        print(f"data == {data}")        return data    except Exception as e:        print(f"处理 {image_path} 失败: {e}")        return Nonedef should_rename_file(self, info):    """检查是否需要重命名文件"""    rename_options = self.export_options.get("rename_options", [])    return len(rename_options) > 0def rename_file(self, original_path, info):    """根据配置重命名文件"""    if not self.should_rename_file(info):        return original_path    rename_options = self.export_options.get("rename_options", [])    separator = self.export_options.get("separator", "_")    # 构建新的文件名部分    name_parts = []    for option in rename_options:        if option == "name" and info.get("姓名"):            name_parts.append(info["姓名"])        elif option == "id" and info.get("身份证号"):            name_parts.append(info["身份证号"])        elif option == "nation" and info.get("民族"):            name_parts.append(info["民族"])        elif option == "sex" and info.get("性别"):            name_parts.append(info["性别"])        elif option == "address" and info.get("住址"):            name_parts.append(info["住址"])    if not name_parts:        return original_path    # 构造新文件名    new_name = separator.join(name_parts)    # 保持原始文件扩展名    import os    dir_name = os.path.dirname(original_path)    file_ext = os.path.splitext(original_path)[zxsq-anti-bbcode-1]    new_path = os.path.join(dir_name, new_name + file_ext)    # 重命名文件    try:        os.rename(original_path, new_path)        return new_path    except Exception as e:        print(f"重命名文件失败 {original_path} -> {new_path}: {e}")        return original_path# 图片灰度处理, 处理成扫描件, 下面还没写好 不要用# def preprocess_id_card_image(self, image_path):#     """对身份证图片进行校正、裁剪并转换为黑白扫描件"""#     try:#         # 读取图片#         img = cv2.imread(image_path)#         if img is None:#             return image_path##         # 1. 转换为灰度图#         gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)##         # 2. 使用中值滤波代替高斯模糊#         denoised = cv2.medianBlur(gray, 3)##         # 3. 使用自适应阈值#         binary = cv2.adaptiveThreshold(#             denoised, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,#             cv2.THRESH_BINARY, 15, 3#         )##         # 4. 可选:轻微平滑处理#         smoothed = cv2.medianBlur(binary, 1)##         # 5. 保存处理后的图片#         import os#         dir_name = os.path.dirname(image_path)#         file_name = os.path.splitext(os.path.basename(image_path))[zxsq-anti-bbcode-0]#         file_ext = os.path.splitext(image_path)[zxsq-anti-bbcode-1]#         processed_path = os.path.join(dir_name, f"{file_name}_processed{file_ext}")##         cv2.imwrite(processed_path, smoothed)##         return processed_path#     except Exception as e:#         print(f"身份证图片预处理失败 {image_path}: {e}")#         return image_path## def order_points(self, pts):#     """对四个点进行排序:左上、右上、右下、左下"""#     rect = np.zeros((4, 2), dtype="float32")##     # 计算坐标和#     s = pts.sum(axis=1)#     rect[zxsq-anti-bbcode-0] = pts[np.argmin(s)]  # 左上角点(坐标和最小)#     rect[zxsq-anti-bbcode-2] = pts[np.argmax(s)]  # 右下角点(坐标和最大)##     # 计算坐标差#     diff = np.diff(pts, axis=1)#     rect[zxsq-anti-bbcode-1] = pts[np.argmin(diff)]  # 右上角点(坐标差最小)#     rect[zxsq-anti-bbcode-3] = pts[np.argmax(diff)]  # 左下角点(坐标差最大)##     return rect## def four_point_transform(self, image, pts):#     """四点透视变换"""#     # 获取排序后的坐标#     rect = self.order_points(pts)#     (tl, tr, br, bl) = rect##     # 计算新图像的宽度和高度#     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))#     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))#     max_width = max(int(width_a), int(width_b))##     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))#     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))#     max_height = max(int(height_a), int(height_b))##     # 目标点#     dst = np.array([#         [0, 0],#         [max_width - 1, 0],#         [max_width - 1, max_height - 1],#         [0, max_height - 1]], dtype="float32")##     # 计算透视变换矩阵并应用#     M = cv2.getPerspectiveTransform(rect, dst)#     warped = cv2.warpPerspective(image, M, (max_width, max_height))##     return warped# 中断处理, 此处不要直接中断线程, 可能导致excel未能处理完毕线程就退出了# 我们应该保证exceldef request_termination(self):    """请求终止处理过程"""    self._should_terminate = True
下载链接.txt (27.9 KB, 下载次数: 0)

重要声明:以上内容仅代表作者戴明利观点,不代表本站测量协会立场。如有涉及侵权请尽快告知,我们将会在第一时间处理。作者原创内容未经允许不得转载!

站长联系邮箱:1339305021@qq.com

站长联系微信:dddnnbbb

灌水成绩
0
25
38
主题
帖子
积分

等级头衔 ID : 860
用户组 : 新手上路

积分成就 测量币 : 38
违规 : 0
在线时间 : 0 小时
注册时间 : 2026-4-6
最后登录 : 2026-4-23

勋章

活跃会员最佳新人

联系方式

发表于 2025-11-19 16:56:00 | 显示全部楼层
更新第六版, 支持在excel 中以超链接的形式查看图片 (可选), 版本号1.0.6
通过网盘分享的文件:id_card_ocr1.0.6.zip
链接: https://pan.baidu.com/s/1sosnohwgV-i7cv7x6y_G6g 提取码: dc49
--来自百度网盘超级会员v9的分享
回复

使用道具 举报

灌水成绩
1
20
22
主题
帖子
积分

等级头衔 ID : 893
用户组 : 新手上路

积分成就 测量币 : 22
违规 : 0
在线时间 : 0 小时
注册时间 : 2026-4-6
最后登录 : 2026-4-23

勋章

活跃会员最佳新人

联系方式

发表于 2025-5-23 14:12:00 | 显示全部楼层
身份证有效期也需要识别!
回复

使用道具 举报

灌水成绩
2
23
57
主题
帖子
积分

等级头衔 ID : 824
用户组 : 注册会员

积分成就 测量币 : 57
违规 : 0
在线时间 : 0 小时
注册时间 : 2026-4-6
最后登录 : 2026-4-23

勋章

活跃会员最佳新人

联系方式

发表于 2025-5-26 10:11:00 | 显示全部楼层
感谢楼主分享
回复

使用道具 举报

灌水成绩
1
18
33
主题
帖子
积分

等级头衔 ID : 815
用户组 : 新手上路

积分成就 测量币 : 33
违规 : 0
在线时间 : 0 小时
注册时间 : 2026-4-6
最后登录 : 2026-4-23

勋章

活跃会员最佳新人

联系方式

发表于 2025-5-23 17:52:00 | 显示全部楼层
谢谢分享,小旅馆登记需要
回复

使用道具 举报

灌水成绩
0
26
41
主题
帖子
积分

等级头衔 ID : 843
用户组 : 新手上路

积分成就 测量币 : 41
违规 : 0
在线时间 : 0 小时
注册时间 : 2026-4-6
最后登录 : 2026-4-23

勋章

活跃会员最佳新人

联系方式

发表于 2025-5-23 18:08:00 | 显示全部楼层
感谢分享,试一下
回复

使用道具 举报

灌水成绩
2
24
54
主题
帖子
积分

等级头衔 ID : 806
用户组 : 注册会员

积分成就 测量币 : 54
违规 : 0
在线时间 : 0 小时
注册时间 : 2026-4-6
最后登录 : 2026-4-23

勋章

活跃会员最佳新人

联系方式

发表于 2025-5-23 21:10:00 | 显示全部楼层
谢谢分享,很实用
回复

使用道具 举报

灌水成绩
0
36
38
主题
帖子
积分

等级头衔 ID : 835
用户组 : 新手上路

积分成就 测量币 : 38
违规 : 0
在线时间 : 0 小时
注册时间 : 2026-4-6
最后登录 : 2026-4-23

勋章

活跃会员最佳新人

联系方式

发表于 2025-5-24 01:46:00 | 显示全部楼层
谢谢分享,正反都能识别吗?
回复

使用道具 举报

灌水成绩
3
18
30
主题
帖子
积分

等级头衔 ID : 879
用户组 : 新手上路

积分成就 测量币 : 30
违规 : 0
在线时间 : 0 小时
注册时间 : 2026-4-6
最后登录 : 2026-4-23

勋章

活跃会员最佳新人

联系方式

发表于 2025-5-24 08:27:00 | 显示全部楼层
感谢分享,坐等离线版
回复

使用道具 举报

灌水成绩
2
24
35
主题
帖子
积分

等级头衔 ID : 803
用户组 : 新手上路

积分成就 测量币 : 35
违规 : 0
在线时间 : 0 小时
注册时间 : 2026-4-6
最后登录 : 2026-4-23

勋章

活跃会员最佳新人

联系方式

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

使用道具 举报

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

本版积分规则

Archiver|手机版|小黑屋|测量协会 ( 桂ICP备2026007449号-1 )

GMT+8, 2026-4-25 03:16 , Processed in 0.076145 second(s), 28 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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