ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+Django+CNN:身份证识别考勤系统毕业设计全解析

Python+Django+CNN:身份证识别考勤系统毕业设计全解析 简介基于PythonDjango深度学习的身份证识别考勤系统设计与实现是一份面向计算机相关专业毕业设计、课程项目开发的完整方案文档重点解决传统线下签到信息不全、考勤效率低等问题。文档围绕深度学习身份证识别与考勤管理展开融合CNN模型、Django框架、MySQL数据库与B/S架构并兼顾前端交互、数据存储及安全验证等设计要点。压缩包内为1个docx文档大小981KB结构包含中英文摘要、目录、系统设计、实现与关键技术说明便于参考整体框架与核心思路。已有156人学习适合正在筹备毕业设计或希望掌握Python深度学习应用开发的读者可作为开题、架构设计及论文撰写的直接参考资料。1. 基于PythonDjango深度学习的身份证识别考勤系统一份毕业设计项目包的落地拆解先说结论这份项目包不是一套纯展示的Demo而是一条完整的毕业设计技术路线——从Django的B/S架构、MySQL三张核心表到用深度学习CNN模型做身份证信息识别再落到打卡、考勤统计、用户管理这些真实业务模块。传统考勤靠手写签到或工牌刷卡信息不全、补卡扯皮是常态开发者真正要解决的是“如何把身份证照片变成一条可靠的考勤记录”。如果你是准备做Python方向毕业设计的学生或者想在公司内部搭一套低成本、免硬件的考勤系统的工程师这份资源值得照着拆一遍。项目源代码、论文文档、数据库脚本都能拿到我下面说的每个模块都能在代码里找到对应实现。2. 先理清架构与数据B/S、Django、MySQL 三张表怎么搭考勤系统这类项目最大的特点就是“业务逻辑不复杂但数据关系绕不开”。一个员工有身份证号、联系方式、部门信息一天可能有多条打卡记录管理员要能查、能改、能删。所以动手写代码之前先把架构选型和数据模型定死后面基本就是往模板里填逻辑。2.1 选型理由为什么是B/S、Django、MySQL的组合项目正文里写得很直白这套系统采用B/S访问结构。B/S和C/S最大的区别在于C/S要装客户端B/S只要有浏览器就能访问。对一个需要人事部门、考勤管理员、普通员工多角色使用的系统来说B/S意味着不需要给任何一台电脑装软件更新逻辑也只发生在服务器端。这就是项目里说的“不用安装任何东西”。这个优势在后期维护时特别明显——你改了后端代码刷新浏览器就是新版本不存在客户端老旧版本不兼容的问题。后端框架选Django而不是Flask我猜作者考虑的是两点。第一Django自带Admin后台、ORM、Session会话管理和模板引擎考勤系统这种CRUD密集型的业务用Django能省掉一大半重复代码。第二Django的ORM对MySQL的支持很成熟迁移命令一套就自动建表不用手写一堆JDBC或者pymysql的样板代码。项目正文里反复提到“日后的升级和需求可以通过多种途径来解决毕竟还是开源的体系”Django社区活跃、文档全、出问题一搜就有答案对毕业设计这种时间紧的开发场景非常友好。数据库选MySQL而不是Oracle或者SQL Server核心原因是成本和体量。项目正文里算过一笔账如果系统要覆盖几十万员工的量级Oracle的授权费用根本不是一个毕业设计能承受的。MySQL免费、开源、并发能力在线配合InnoDB引擎做行级锁和事务考勤记录这种高频写入场景完全扛得住。另外本项目没有特别复杂的事务嵌套MySQL默认的隔离级别足够用了。这部分我见过太多人翻车上来不梳理数据关系直接写前端页面结果做到一半发现用户表和考勤表对不上又回头改数据库。正确顺序永远是先定义数据模型再写业务逻辑最后才碰页面。2.2 数据库设计从E-R图到三张核心表的建表SQL项目正文的第四章给了E-R图的实体描述管理员信息有用户名、密码、编号用户信息有编号、姓名、性别、年龄、电话、邮箱、地址、身份证号打卡信息对应考勤记录。按我的习惯会把它拆成三张表管理员表、员工表也就是被考勤的人、考勤记录表。建表时要注意几个细节。身份证号字段必须加唯一索引否则同一个员工会被重复建档后面打卡匹配会出大问题。密码字段用VARCHAR(64)因为MD5加密后的十六进制字符串正好是32位留64位是给将来换更强算法做余量。考勤记录表要用“员工ID日期”做联合唯一键这样一个人一天只能有一条核心记录重复拍照打卡会被判定为更新下班时间而不是插入新记录。-- 管理员表 CREATE TABLE admin ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录用户名, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密码值, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员账号表; -- 员工信息表 CREATE TABLE employee ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, age INT UNSIGNED DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, email VARCHAR(100) DEFAULT NULL, address VARCHAR(255) DEFAULT NULL, id_card_no CHAR(18) NOT NULL UNIQUE COMMENT 身份证号唯一索引防止重复建档, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工信息表; -- 考勤记录表 CREATE TABLE attendance ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, employee_id INT UNSIGNED NOT NULL, work_date DATE NOT NULL COMMENT 考勤日期, check_in_time DATETIME DEFAULT NULL COMMENT 上班打卡时间, check_out_time DATETIME DEFAULT NULL COMMENT 下班打卡时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1迟到 2早退 3缺卡, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_emp_date (employee_id, work_date), FOREIGN KEY (employee_id) REFERENCES employee(id), CONSTRAINT chk_status CHECK (status IN (0, 1, 2, 3)) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤记录表;上面这段SQL的逻辑重点有三个。第一utf8mb4字符集必须写清楚否则姓名里的生僻字、少数民族名字里的特殊符号存进去就变问号这是身份证识别考勤系统里最容易踩的隐性坑。第二attendance表的联合唯一键uk_emp_date很重要它把“一个人一天只能有一条考勤记录”这个业务规则直接压到数据库层面比在代码里写if exists再判断要可靠得多。第三外键约束在毕业设计里可以加但生产环境我一般会去掉外键只保留普通索引因为外键在高并发写入时会影响性能这是后话。状态字段status我用TINYINT而不是VARCHAR是为了让迟到、早退、缺卡这些判断逻辑放在后端代码里算好再落库而不是存一堆中文文本。文本字段做统计的时候GROUP BY很难看数字枚举才是正道。项目正文里提到的“修改密码”“个人信息”模块本质都是对admin表和employee表的增删改查不用额外建表。2.3 系统模块划分和一个打卡请求的完整数据流项目正文的功能需求分析里列了这些模块首页、后台打卡、考勤管理、修改密码、个人信息、用户管理。画成模块图就是两层一层是面向普通员工的打卡和查看个人信息另一层是面向管理员的用户管理和考勤管理。很多人做这类系统会纠结要不要做角色区分我的建议是“员工和管理员共用一张登录表用字段区分角色”比建两张登录表省事也符合这个项目的体量。一个打卡请求在Django里的完整数据流是这样的浏览器上传带身份证照片的POST请求Django的URL路由根据/api/checkin/把请求分发到视图函数视图函数把图片交给身份证识别模块识别出身份证号后去employee表做精确匹配匹配成功就往attendance表插入或更新记录最后返回一个JSON结果。整个过程不涉及页面跳转所以前端只需要一个上传控件和一个结果提示框这比传统表单提交要清爽得多。模块划分这里要特别提醒一点不要把身份证识别逻辑直接写在视图函数里。正确做法是单独建一个recognition.py模块视图函数只负责“拿图片、调接口、存结果”。这既方便以后换更好的识别模型也方便单元测试——你不可能每次测试都翻出身份证重新拍照。项目源码里如果能看到views.py和recognition.py分开那这个项目的结构就是合格的。数据安全方面项目正文提到密码用MD5加密。这里我要多说一句MD5在今天只能算是“基础保护”不是“安全方案”。毕业设计用MD5没问题论文里也好解释但真要部署到公司内网我建议至少换成Django自带的pbkdf2_sha256算法或者直接用bcrypt。好消息是由于我们建表时密码字段留了64位后面升级算法不需要改表结构只需要在verify函数里加一个版本标识。3. 身份证识别怎么落地CNN 训练骨架、预处理与推理对接项目标题里最扎眼的词是“深度学习”但很多人打开代码后发现找不到训练过程只有一堆权重文件和调用代码。这很正常——身份证识别这种场景训练和推理是两套完全不同的工程。模型训练需要GPU、需要标注数据、需要几天甚至几周的时间而考勤系统里跑推理只需要CPU和几十毫秒。所以我们要做的是两件事搞清楚模型是怎么训练的以及部署时怎么把模型接进Django。3.1 身份证识别的本质目标检测 文字识别两步走身份证照片识别不是“一张图扔进CNN就出结果”这么简单。身份证上有姓名、性别、民族、出生日期、住址、身份证号、头像照片我们需要的是身份证号这一串18位数字和姓名其余都是干扰信息。所以常见方案是拆成两个阶段第一阶段用目标检测定位身份证区域甚至更细一点定位到身份证号码那一行的位置第二阶段对定位出来的区域做文字识别。第一个阶段其实可以手工完成。考勤场景下摄像头拍摄的身份证位置虽然有一定随机性但角度不会太离谱用OpenCV的轮廓检测加透视变换就能把身份证区域矫正成一张正面的矩形图。第二个阶段才轮到CNN上场把号码区域按字符切分成18个小图每个小图过一个数字分类CNN最后拼出完整号码。为什么用CNN而不用传统OCR文本识别用Tesseract也能跑但身份证号码是印刷体数字背景有长城图案、有防伪纹理、还有反光干扰传统OCR的模板匹配在这种低对比度场景下会频繁翻车。CNN的卷积核能自动学习“数字轮廓”和“背景纹理”的差异对光照变化、倾斜、模糊的鲁棒性比手工特征强很多。项目摘要里提到的“利用深度学习模型如卷积神经网络CNN对身份证上的文字和图像信息进行分析和识别”指的就是这个阶段。3.2 预处理流程灰度化、透视矫正、字符切分不管用什么模型预处理决定了识别的上限。我见过太多人把原始照片直接喂给CNN结果识别率只有60%然后就抱怨模型不行。实际上问题几乎都出在预处理上。第一步是透视矫正。身份证是矩形拍照时镜头有角度照片里的身份证就变成了梯形。用OpenCV的findContours找到证件四个角点再用getPerspectiveTransform做四点变换把梯形拉回矩形。这一步不做后面的字符切分全是歪的CNN再强也白搭。第二步是灰度化和二值化。身份证背景有底纹直接切分会把底纹误判成字符。一般用大津法Otsu自动算阈值做二值化把前景字符和背景分离。注意光照不均时全局阈值效果会变差这时候可以先用adaptiveThreshold做局部自适应二值化效果比全局阈值好一个档次。第三步是按行投影切分。二值化之后对图像做水平投影统计每一行有多少个黑色像素点字符行会有明显的峰值空白行值接近零用这个峰谷位置就能把号码行切出来。同理再做垂直投影就能把18位数字逐个切分。每个数字再归一化到统一尺寸比如32×128像素喂给CNN。import cv2 import numpy as np def preprocess_for_recognition(image_path): img cv2.imread(image_path) # 灰度化去掉颜色干扰只保留亮度信息 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊抑制传感器噪点避免二值化后出现杂点 gray cv2.GaussianBlur(gray, (3, 3), 0) # 透视矫正检测矩形轮廓并拉正 edges cv2.Canny(gray, 50, 150) contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 按轮廓面积排序身份证区域通常是画面中最大的四边形 contours sorted(contours, keycv2.contourArea, reverseTrue) for cnt in contours[:5]: approx cv2.approxPolyDP(cnt, 0.02 * cv2.arcLength(cnt, True), True) if len(approx) 4: # 四点变换把透视角度下的四边形矫正为矩形 pts approx.reshape(4, 2).astype(np.float32) width, height 300, 190 dst np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtypenp.float32) matrix cv2.getPerspectiveTransform(pts, dst) warped cv2.warpPerspective(gray, matrix, (width, height)) break else: raise ValueError(未检测到身份证区域可能是照片背景过于复杂) # 大津法二值化 _, binary cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) return binary这段预处理代码的逻辑核心有两个。第一approxPolyDP的第二个参数是“最大距离误差”取轮廓周长的2%是一个经验值太大容易把四边形拟合成三角形太小会保留锯齿状细节。第二warpPerspective的目标尺寸我固定为300×190像素这是身份证的2:1比例统一尺寸之后字符切分的坐标才可预测。THRESH_OTSU会自动计算最佳阈值比写死THRESH_BINARY的127要稳因为它会根据每个样本的灰度分布动态调整。预处理做完下一步是字符切分。这一步没有特别漂亮的算法就是基于投影的峰谷分析。切分失败通常发生在身份证号码区域和姓名区域混在一起时所以二值化之前先锁定号码区域的位置很重要——常见做法是用一个固定比例框身份证号码一般位于证件底部1/3区域。3.3 CNN训练骨架一个可以直接改参数跑起来的模型真正要自己从头训练一个身份证识别模型数据量小很容易过拟合。网上能找到的开源身份证OCR数据集不多常见做法是自拍几百张身份证照片然后用数据增强把样本量扩到几千张。如果只做毕业设计也可以在开源OCR模型基础上做迁移学习——冻结前面的卷积层只训练最后几层全连接层这样几百张样本就够用了。下面给一个标准的CNN训练骨架基于TensorFlow/Keras。注意这里训练的是“单字符分类器”也就是只辨别0到9十个数字不直接端到端识别整串身份证号。这样做的好处是输出类别少、收敛快、每个字符独立预测只要切分正确准确率很容易做到99%以上。from tensorflow.keras import layers, models from tensorflow.keras.preprocessing.image import ImageDataGenerator def build_char_cnn(input_shape(32, 128, 1), num_classes10): # 输入是单通道灰度图32x128 是统一后的字符尺寸 model models.Sequential([ layers.Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activationrelu), layers.Flatten(), layers.Dropout(0.5), # 防止小数据集过拟合 layers.Dense(256, activationrelu), layers.Dense(num_classes, activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) return model # 数据增强随机平移、旋转、缩放提升模型对拍摄角度变化的鲁棒性 datagen ImageDataGenerator( rotation_range5, width_shift_range0.1, height_shift_range0.1, zoom_range0.1, validation_split0.2 ) model build_char_cnn() train_flow datagen.flow_from_directory( data/chars_train, target_size(32, 128), color_modegrayscale, class_modecategorical, subsettraining, batch_size32 ) val_flow datagen.flow_from_directory( data/chars_train, target_size(32, 128), color_modegrayscale, class_modecategorical, subsetvalidation, batch_size32 ) model.fit(train_flow, validation_dataval_flow, epochs30) model.save(char_cnn.h5)这段代码我做了两个关键取舍。第一输入尺寸用32×128而不是常规的224×224因为单个数字字符本身信息量很少不需要大尺寸小尺寸反而能加快推理速度、减少显存占用。第二每层卷积核数量从32、64到128逐层翻倍网络结构足够简单但参数容量对10分类任务完全够用不用堆到ResNet那个级别。rotation_range5只做小幅旋转因为身份证号码本来就是印刷体旋转超过10度切分阶段就已经失败了数据增强不应该去弥补预处理该解决的问题。训练30个epoch在小数据集上够用了如果验证准确率长时间不涨优先检查切分后的字符图像是否干净而不是盲目增加epoch。3.4 推理对接把识别结果变成一条考勤记录模型训练好之后部署到Django里有一个最常见的坑模型文件是HDF5格式加载时需要TensorFlow环境。如果Django跑在纯CPU的服务器上加载模型第一次会特别慢可能要两三秒。解决办法是在视图模块的顶层加载一次模型让模型常驻内存而不是每次请求都重新加载。推理代码的流程是接收上传图片调用预处理函数拿到二值化字符图按水平垂直投影切出每个数字的小图每个小图经过model.predict得到分类结果最后把所有数字拼接成18位身份证号。注意predict接收的输入张量形状是(batch, 32, 128, 1)所以每个字符都要走一次np.expand_dims加维度。识别得到身份证号之后就是纯粹的数据库操作了。去employee表按id_card_no精确匹配查不到就返回“该身份证未登记”查到就写入attendance表。这里有个容易忽略的小坑识别结果偶尔会把8和6混淆或者把1和7看错。解办法是在匹配之前对识别结果做一次数字合法性校验身份证号前6位是地区码第7到14位是出生日期日期部分可以做基本格式检查明显不合法的直接判定为识别失败让员工重新拍一张比录入一条脏数据再人工修复省心得多。4. 考勤核心模块实现登录、打卡、考勤统计的 Django 代码数据库和识别模块准备好之后就到了把业务逻辑串起来的阶段。这一章只讲三个最核心的模块登录、打卡、考勤统计。用户管理、修改密码、个人信息这些本质上是同一个套路会了这三个剩下的就是复制粘贴。4.1 登录与密码安全MD5 加密、Session 会话登录模块的设计很简单就是用Session保存登录状态每次请求时检查Session里有没有管理员或员工ID。项目正文里说密码加密用MD5这里我按项目的原始设计实现但代码里留出了升级空间。import hashlib from django.shortcuts import render, redirect from .models import Admin def md5_encode(raw): # 这里可以加盐md5_encode(raw app_settings.SALT) return hashlib.md5(raw.encode(utf-8)).hexdigest() def login_view(request): if request.method POST: username request.POST.get(username, ).strip() password request.POST.get(password, ) admin Admin.objects.filter( usernameusername, passwordmd5_encode(password) ).first() if admin: # 登录成功把用户ID写进session request.session[admin_id] admin.id request.session[role] admin return redirect(/dashboard/) error 用户名或密码错误 return render(request, login.html, {error: error}) return render(request, login.html) def logout_view(request): # 清除session中的登录标志 request.session.flush() return redirect(/login/)MD5加密的32位十六进制输出决定了数据库字段长度这也是我在第二章建表时把password字段设计成VARCHAR(64)的原因。hashlib.md5的输入必须是字节串所以要先做encode(utf-8)再加密。.strip()处理用户名首尾空格这是一个很小的细节但能避免“用户复制粘贴多了个空格导致登录失败”的售后问题。Session方面Django默认会把Session数据存到数据库的django_session表里不需要额外配置。但要注意两点第一SESSION_COOKIE_AGE默认是两周如果觉得太短可以调长第二登录接口一定要用POST请求而且模板里要加{% csrf_token %}否则Django的CSRF中间件会直接拒绝请求页面会报403。4.2 身份证识别打卡上传照片到写入考勤记录的完整视图打卡是身份证考勤系统的核心动作。前端是一个文件上传控件用户拍照上传后端接收图片、调用识别模块、匹配员工、写入考勤表。这里要处理两个边界情况识别失败怎么办重复打卡怎么办。import os from datetime import datetime, date from django.http import JsonResponse from django.views.decorators.http import require_POST from .recognition import recognize_id_card from .models import Employee, Attendance require_POST def checkin_view(request): photo request.FILES.get(photo) if not photo: return JsonResponse({code: 400, msg: 未收到身份证照片}) # 把上传的图片存到临时目录识别完成后删除 temp_path os.path.join(/tmp, photo.name) with open(temp_path, wb) as f: for chunk in photo.chunks(): f.write(chunk) try: id_card_no, name recognize_id_card(temp_path) except ValueError as e: return JsonResponse({code: 422, msg: f识别失败{e}}) finally: os.remove(temp_path) emp Employee.objects.filter(id_card_noid_card_no).first() if not emp: return JsonResponse({code: 404, msg: 该身份证未在系统中登记}) today date.today() now datetime.now() # get_or_create同一员工同一天只保留一条考勤记录 att, created Attendance.objects.get_or_create( employeeemp, work_datetoday, defaults{check_in_time: now, status: 0} ) if not created: # 第二次打卡视为下班更新check_out_time att.check_out_time now att.status 2 if (now - att.check_in_time).total_seconds() 4 * 3600 else 0 att.save() return JsonResponse({code: 0, msg: f{emp.name} 打卡成功})这段代码的逻辑分四步。第一request.FILES.get(photo)拿上传文件用photo.chunks()分块写盘避免大图片一次性读入内存把服务搞崩。第二recognize_id_card返回身份证号和姓名如果内部OpenCV没找到身份证区域会抛ValueError这里用try/except接住并返回422错误。第三get_or_create是Django ORM里非常实用的一个方法它会先按employee work_date查记录查不到就插入一条查到就返回现有记录和createdFalse正好对应第一次打卡和第二次打卡两种场景。第四考勤状态的判定被我写在了更新分支里如果两次打卡间隔小于4小时判定为早退状态置为2否则置为正常。这里有一个设计上的取舍想说明一下。为什么不在上午9点整自动判断迟到而是在后端写死规则因为考勤系统的迟到判定标准因企业而异有的公司弹性工作制有的按排班表把这些都写死在代码里反而不好维护。项目里把status字段暴露出来管理员可以在后台手动调整这比自动判定来得更灵活。4.3 考勤统计按日期查询、按员工聚合考勤统计的逻辑是考勤管理页面的主体给定一个日期展示所有员工的上下班打卡时间和状态给定一个员工展示他一个月内的考勤汇总。用Django ORM做这块非常顺手但要注意查询效率问题特别是员工数量上来之后。from django.db.models import Count from django.http import JsonResponse from .models import Attendance, Employee def attendance_by_date(request): date_str request.GET.get(date, date.today().isoformat()) rows Attendance.objects.filter(work_datedate_str).select_related(employee) data [] for r in rows: data.append({ name: r.employee.name, id_card_no: r.employee.id_card_no, check_in: r.check_in_time.strftime(%H:%M:%S) if r.check_in_time else -, check_out: r.check_out_time.strftime(%H:%M:%S) if r.check_out_time else -, status: r.get_status_display(), }) return JsonResponse({code: 0, data: data}) def attendance_summary(request, emp_id): month request.GET.get(month, datetime.now().strftime(%Y-%m)) rows Attendance.objects.filter( employee_idemp_id, work_date__startswithmonth ) summary rows.values(status).annotate(totalCount(id)) return JsonResponse({code: 0, summary: list(summary)})select_related是用来解决外键查询的N1问题的。如果不加ORM在循环里访问r.employee.name时每一条记录都会再发一次SQL查询100个人就是101次查询加了这个方法Django会用一张JOIN把员工表的数据一次性带出来。这是Django开发里几乎必考的性能优化点面试官问到这块能答上来会很加分。values(status).annotate(totalCount(id))会按状态分组统计数量返回一个类似[{status: 0, total: 22}, {status: 1, total: 1}]的结构前端拿到这个数据画饼图还是做表格都很方便。startswith对日期字段做前缀匹配传2024-06就能把整个六月的记录捞出来。5. 避坑与排查身份证考勤系统最常见的五个坑做这种“深度学习Web系统”结合的项目代码本身的难度往往不是最大的真正的坑集中在图像预处理、数据库字符集、Session配置和模型部署这些边缘环节。下面这五条是我认为最容易翻车的地方每一条都按“现象→原因→解决”写清楚。5.1 身份证照片歪斜导致识别结果全错现象员工正常拍照打卡但系统要么识别失败要么把身份证号里的数字看错好几个比如把8识别成6把1识别成7。原因摄像头拍出来的身份证不可能是严格正面的多少都会有一定透视角度。如果跳过透视矫正直接把照片喂给字符切分切出来的字符就是歪的CNN再强也认不出来。很多人的代码里只做了灰度化和二值化恰恰漏了最关键的getPerspectiveTransform。解决在识别流程里强制加入透视矫正步骤。OpenCV里先findContours找到最大四边形的四个角点再用warpPerspective拉正。如果确认代码里已经做了矫正但还是识别错误优先检查角点检测是否把外面的背景框当成了证件边界可以用approxPolyDP的精度参数和轮廓面积过滤来排除干扰。5.2 姓名生僻字在页面和数据库里变成问号现象员工姓名里带有“頔、鑫、曦”等生僻字录入系统时显示正常但刷新页面后变成“”或者乱码。原因MySQL数据库和表的默认字符集是latin1或utf8而utf8在MySQL里最多只能存3字节的字符很多生僻字需要4字节也就是utf8mb4才能存下。项目里如果建库时没指定字符集连接也没指定编码写进去的中文就会在存储层被截断。解决建库时指定DEFAULT CHARACTER SET utf8mb4建表时同步指定同时检查MySQL连接串里有没有加charsetutf8mb4。如果数据库已经建好了可以用ALTER DATABASE和ALTER TABLE CONVERT TO CHARACTER SET utf8mb4补救但最干净的方案是在项目初始化脚本里就统一字符集。Django的settings里数据库配置加OPTIONS: {charset: utf8mb4}这一步能省很多事。5.3 登录后跳转页面又回到登录页现象登录成功后进系统首页没问题但再点一个菜单跳到其他页面就又被踢回登录页。原因Django的Session会话状态没有正确保持。最常见的情况是Session的Cookie过期时间太短或者SESSION_SAVE_EVERY_REQUEST没有开启导致长时间停留在某个页面后Session过期。另一种可能是浏览器设置了阻止第三方Cookie但本地开发时很少碰到。解决在settings.py里把SESSION_COOKIE_AGE设为适当值比如60 * 60 * 88小时同时加SESSION_SAVE_EVERY_REQUEST True让用户每次刷新页面都会刷新Session过期时间。验证办法很直接登录后打开开发者工具的Application面板看Cookie里有没有sessionid再看请求头有没有把这个Cookie发回服务器两步就能定位问题出在生成还是传递。5.4 前端页面静态文件和上传图片访问404现象本地调试一切正常但把项目部署到服务器或者开启DEBUGFalse之后CSS、JS、上传的员工头像全部打不开控制台全是404。原因Django默认的静态文件服务和上传文件服务只在开发模式下生效。DEBUGFalse时Django不会自动处理STATIC_URL和MEDIA_URL这是新手最容易踩的部署坑。项目里上传的身份证照片、员工头像走的是MEDIA_ROOT如果不配这个目录和URL映射上传成功也看不到图片。解决开发阶段可以保持DEBUGTrue生产部署时用whitenoise挂静态文件或者用Nginx直接把/static/和/media/指到对应目录。如果只是应急跑通也可以在urls.py里加一段static()函数做临时映射但这段代码在生产环境坚决不能留。5.5 CPU推理速度慢员工排队打卡卡成PPT现象摄像头拍照后点提交要转两到三秒才能出结果高峰期排队的人一多体验非常差。原因深度学习模型推理是计算密集操作尤其第一次请求要加载HDF5模型文件CPU上加载就要一两秒。如果每次请求都重新加载模型或者模型没有做量化速度就很难上去。解决第一个技巧是把模型加载放到模块顶层让模型常驻内存不要在视图函数里加载。第二个技巧是模型优化把训练好的Keras模型转成ONNX格式用onnxruntime跑CPU推理速度能提升一倍以上如果追求极致可以把权重从32位浮点降到16位甚至8位整数。第三个技巧是改造成异步任务照片上传后立刻返回“处理中”识别完再通知前端出结果排队问题就变成了吞吐量问题。这个方案在项目源码里可能没有但做生产升级时是必选项。6. 上生产前的验证与加固自测脚本、异步推理与 HTTPS项目跑通只是第一步离真正能用的考勤系统还差两道关识别准确率够不够、数据链路安不安全。这一章我给三个马上能用的验证和加固手段。先做一个模型自测脚本。不要凭感觉说“识别挺准”要用真实样片跑出数字。找5到10张不同光线、不同角度下拍摄的身份证照片标注好真实身份证号批量调用识别接口统计号码级准确率和字符级准确率。号码级准确率低于90%的时候不要急着调模型先看失败的照片是不是都栽在预处理上。# evaluate.py 识别准确率自测脚本 from recognition import recognize_id_card cases [ (samples/01.jpg, 110101199001011234), (samples/02.jpg, 110105199202022345), (samples/03.jpg, 310101198803033456), ] total 0 exact_hit 0 char_total 0 char_hit 0 for img_path, true_no in cases: pred_no, _ recognize_id_card(img_path) total 1 if pred_no true_no: exact_hit 1 for a, b in zip(pred_no, true_no): char_total 1 if a b: char_hit 1 print(f{img_path}: 预测 {pred_no} 真实 {true_no}) print(f号码级准确率: {exact_hit / total:.2%}) print(f字符级准确率: {char_hit / char_total:.2%})这个脚本的价值在做回归测试。改一次预处理参数、换一个模型跑一遍脚本看数字变化比肉眼观察客观得多。我一般会把准确率低于95%的样本单独存到一个文件夹逐个看是切分歪了还是分类错了这样能快速定位问题出在哪个环节。自测脚本里zip截断的问题要注意如果预测号码长度不是18位zip只会比较较短的序列所以只要发现len(pred_no) ! 18直接记为零分。再做异步推理改造。打卡体验卡顿的根源是同步推理占满了请求线程。快速方案是做一个全局线程池识别任务提交到线程池前端轮询结果接口。这个方案不需要引入Celery和Redis改动量最小——在Django项目里建一个tasks.py用concurrent.futures.ThreadPoolExecutor(max_workers4)包住识别函数视图函数提交任务后立即返回任务ID另写一个查询接口对应任务状态。线程池方案虽然不支持分布式但单机撑一个几百人的工厂足够。最后是传输安全加固。项目正文里花了很大篇幅讲数据安全和网络传输安全实际落地时两件事最重要。第一把密码算法从MD5换成Django内置的pbkdf2_sha256只需要自定义一个make_password函数兼容方案是在数据库的password字段里加前缀标识比如pbkdf2_sha256$开头旧数据用MD5校验新数据用新算法两套逻辑并行不相互影响。第二部署时配上HTTPS证书Nginx配置里加SSL把HTTP请求301跳转到HTTPS。证书用免费的Lets Encrypt就行配置好之后浏览器地址栏出现锁标识考勤数据在网络传输过程中就不会被人裸抓包了。做这类项目一次之后我的习惯就固定成了先定数据模型再写识别模块最后接视图上线前强制跑一遍回归测试脚本号码级准确率不过90%不上线。从那以后我每次接这种“深度学习落地”项目都会先花半天把输入样本的质检规则定死再碰模型——照片不合格宁可提示重拍也不放行进入流程这比事后补数据省心得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表