ARTICLE DETAIL

资讯详情

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

基于Django的人脸识别景区票务系统开发实战

基于Django的人脸识别景区票务系统开发实战 简介面向计算机相关专业毕业设计及Django实战学习者这份资源实现了基于人脸识别的景区票务系统涵盖在线购票、人脸验票、票务统计与后台管理等完整业务流程能帮助学生快速落地一个融合AI与Web开发的综合项目。压缩包大小117.81MB主要包含Django前后端源码、MySQL数据库脚本、详细说明文档、学习指南LW以及答辩演示PPT文件类型覆盖py、html、sql、docx与pptx等便于从源码研读到文档参考。目前已有69人学习下载资源整体结构清晰适合作为毕业设计选题参考或项目实践素材。说明文档对系统架构、功能模块和操作流程做了系统梳理学习指南则还原了从设计到编码的完整过程配合PPT可直观展示项目亮点能帮助读者快速理解并二次开发。1. 景区票务系统把“人脸识别”放在哪一环才算没选错景区票务和普通电商票务最大的差别在于“入场核销”这个动作。线上买票、线下排队换票、闸机扫码这条链路里最容易出问题的不是支付而是换票排队和二次入园的身份核验。人脸识别票务系统要拆解的不是“用摄像头认人”这个单一功能而是把购票、绑定人脸、检票比对、订单存储串成一条完整的数据流。一个典型的人脸票务系统由四层组成Django 负责业务逻辑和接口MySQL 存订单和人脸特征数据HTML 模板加少量 JavaScript 处理页面交互人脸识别的核心算法则是个独立的服务模块通过 API 被 Django 调用。标题里“基于 Python 的 Django-html”强调的是完整前后端而“人脸识别”则是业务上的差异化功能它并不是系统的主线而是嵌入到“注册、检票、记录”这条流程里的一个能力组件。所以看这套源码或要自己复刻一套之前先想清楚一个问题人脸识别不是给景区省摄像头的是为了把“人、票、场次、订单”绑定在一起让闸机前的比对结果能作为唯一放行依据。下面从最落地的一条主线讲起来人脸怎么在 Django 里被识别进业务逻辑票务数据怎么建模页面怎么跟摄像头交互最后这套东西怎么才不会在一台普通 Windows 或 Linux 机器上跑崩。2. 在 Django 里接入人脸识别先定好“比对服务”而不是直接跑算法2.1 为什么不能把模型调用硬塞进视图函数常见毕业设计里把人脸识别写在views.py里请求来了就加载模型、做特征提取、再比对返回。这套写法在小并发下能跑通但有两个硬伤第一模型加载和预处理非常耗时每次请求都重新加载会导致明显的卡顿第二Django 的同步视图会被阻塞闸机入口同时来了几个请求进程就假死了。更稳妥的做法是把人脸识别能力封装成一个独立的服务模块进程启动时加载一次模型后续通过函数调用复用。人脸识别在票务场景里只需要两个能力注册时录入人脸照片提取特征向量检票时拍一张照片提取特征向量并与该订单绑定的人脸做相似度比对。特征向量的提取可以用常见的人脸识别算法比如基于深度学习的人脸特征提取模型输出一个 128 维或 512 维的浮点数组。MySQL 里不需要存图片的二进制大文件存特征向量字符串和缩略图路径就够了。一个可复用的比对模块大致划分为加载模型、提取特征、计算相似度三个步骤。提取特征的函数设计成接收图片路径或bytes返回归一化后的特征向量比对函数接收两个向量调用余弦相似度或欧氏距离得出得分然后由业务层决定阈值。# recognition.py import numpy as np import cv2 class FaceService: 封装特征提取与人脸比对进程内复用模型 _instance None def __new__(cls): if cls._instance is None: cls._instance super().__new__(cls) cls._instance.model _load_feature_model() cls._instance.threshold 0.65 # 相似度阈值可调 return cls._instance def extract_feature(self, image) - np.ndarray: 输入 BGR 图像返回 L2 归一化特征向量 face _align_face(image) # 人脸检测 对齐 if face is None: raise ValueError(no face detected) feat self.model.predict(face) # 形状为 (1, 512) 之类 feat feat / np.linalg.norm(feat) return feat.reshape(-1) def compare(self, feat1, feat2) - float: return float(np.dot(feat1, feat2)) # 归一化后余弦相似度 _face_service FaceService()模型加载用单例模式保证进程里只加载一次。_align_face负责从原始画面里定位人脸并按模型要求裁剪缩放常见实现是 OpenCV 的 Haar 级联或更稳的 MTCNN。相似度阈值决定了放行严格程度景区闸机这类场景 0.6 到 0.7 比较合适低于 0.5 会导致误识率升高而高于 0.75 又容易因为发型、口罩、光线造成误拒。2.2 Django 视图与识别服务之间加一层“特征串”转换Django 的视图函数拿到上传图片后调用FaceService.extract_feature得到的是numpy.ndarray不能直接存 MySQL。需要把浮点数组序列化成定长字符串。常见做法是用base64编码float32的tobytes()结果字段类型设为TEXT或BLOB更省空间的做法是直接存为逗号分隔的浮点数字符串但解析效率低一些。这里推荐二进制转 base64 的方式长度可控且方便还原。import base64 import numpy as np def serialize_feature(feat: np.ndarray) - str: data feat.astype(np.float32).tobytes() return base64.b64encode(data).decode(ascii) def deserialize_feature(s: str) - np.ndarray: raw base64.b64decode(s.encode(ascii)) return np.frombuffer(raw, dtypenp.float32)serialize_feature用在注册接口deserialize_feature用在检票比对时读取库里的底库特征。之所以强调这层转换是因为很多人在 MySQL 里直接保存numpy数组的对象字符串不仅乱码而且换 Python 版本后读取困难。特征序列化后检票接口的逻辑就变成先取订单绑定的特征串解析为向量再与刚拍到的照片提取的向量做点积超过阈值即放行。2.3 人脸注册与检票接口的 Django 视图实现注册接口接收图片文件和订单号核心操作是检测人脸是否存在、提取特征、更新订单字段。检票接口接收图片和订单号做比对。这两个接口都属于写操作密集场景不需要实时返回大量数据JSON 响应足够。# views.py import json from django.views.decorators.http import require_POST from django.http import JsonResponse from recognition import FaceService, serialize_feature, deserialize_feature from .models import Order face_service FaceService() require_POST def register_face(request, order_no): img_file request.FILES.get(face_image) if not img_file: return JsonResponse({code: 1, msg: face_image required}) try: image decode_image(img_file.read()) feat face_service.extract_feature(image) except ValueError: return JsonResponse({code: 2, msg: no face detected}) order Order.objects.select_for_update().get(order_noorder_no) order.face_feature serialize_feature(feat) order.face_status 1 order.save(update_fields[face_feature, face_status]) return JsonResponse({code: 0, msg: ok}) require_POST def check_face(request, order_no): img_file request.FILES.get(face_image) try: feat face_service.extract_feature(decode_image(img_file.read())) order Order.objects.get(order_noorder_no) except Order.DoesNotExist: return JsonResponse({code: 3, msg: order not found}) except ValueError: return JsonResponse({code: 2, msg: no face detected}) ref deserialize_feature(order.face_feature) score face_service.compare(feat, ref) if score face_service.threshold: return JsonResponse({code: 0, msg: pass, score: score}) return JsonResponse({code: 4, msg: reject, score: score})select_for_update在注册场景里是为了避免同一订单并发重复写入检票接口没加锁因为入场本身有一个状态机来控制防止并发检票需要配合 MySQL 行锁或 Redis 锁做二次校验稍后在第 3 章处理。这里的decode_image需要兼容上传的图片格式cv2.imdecode从文件字节读取时要用np.frombuffer转一次否则中文路径或内存文件会读不出来。关键参数threshold最好不要写死在代码里存入 Django 的settings或数据库配置表因为不同闸机的补光条件差异很大。我一般会在配置里加一个FACE_THRESHOLD环境变量部署时调试人员可以直接调整而不需要重新发布代码。3. 票务系统 MySQL 建模订单、场次与人脸底库怎么避坑3.1 从 ER 设计看票务状态流转票务系统的核心不是人脸表而是订单表和检票记录表。景区票务场景里常见的票型有成人票、儿童票、联票和场次票场次票必须绑定时间联票允许一天内多次入园这就导致“买到票”和“能入园”不是一回事。数据库模型至少要覆盖四个实体场次表、订单表、检票记录表、游客人脸信息表。订单表是主表关联用票人信息和场次。人脸信息单独拆表而不是直接挂在订单上是因为联票的一个人可能对应多次入园记录而且注册人脸失败时不需要删除整张订单。两个模型之间用一对一或一对多关联一般设计为一张订单对应一个人脸底库记录但允许多个订单共享一张人脸比如同一游客买了两日票。CREATE TABLE spectacle_session ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, stock INT NOT NULL DEFAULT 0, UNIQUE KEY uk_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE visitor_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, session_id INT UNSIGNED NOT NULL, visitor_name VARCHAR(64) NOT NULL, phone VARCHAR(20), ticket_type TINYINT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已入场 3已退款, face_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未注册 1已注册, face_feature TEXT NULL COMMENT base64编码的特征向量, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_session_status (session_id, status), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE check_in_log ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, session_id INT UNSIGNED NOT NULL, device_no VARCHAR(32) NOT NULL, score DECIMAL(5,4) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_order_time (order_no, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表里的face_feature和独立的visitor_order表耦合在这里是因为这套系统的核心是在线购票后绑定人脸同一个游客通常只有一条人脸特征记录。如果景区是做“散客现场购票立即入园”的模式则建议把face_feature拆到单独的visitor_face表原因是现场购票可能存在多人共用一部手机提交事务粒度不同。3.2 防止重复检票的行锁与状态机闸机场景最大的并发风险是同一订单同一秒被两个入口同时请求。Django 层做了任何判断都存在时间窗口正确做法是用数据库行锁在检票接口里先锁住订单行再判断状态。from django.db import transaction transaction.atomic def check_in(order_no, device_no, score): order Order.objects.select_for_update().get(order_noorder_no) if order.status 2: return False, already checked if order.face_status ! 1: return False, face not registered order.status 2 order.save(update_fields[status, updated_at]) CheckInLog.objects.create( order_noorder_no, session_idorder.session_id, device_nodevice_no, scorescore, ) return True, ok把状态判断和日志写入放进同一个事务select_for_update保证两个并发请求到达数据库时后一个会被阻塞等前一个提交后再读取到最新状态status2从而拒绝重复检票。注意status 2这个条件涵盖了2已入场和3已退款两种不可检票状态防止退款后被误放行。3.3 场次库存在高并发下的扣减方式景区票务的场次票有库存限制常见误用是“先查库存再 UPDATE 扣减”。在高并发下会超卖。正确的做法是把库存扣减放到更新语句的条件里让数据库自己保证原子性。Django ORM 的写法是使用F表达式。from django.db.models import F affected Session.objects.filter( idsession_id, stock__gt0 ).update(stockF(stock) - 1) if affected 1: # 扣减成功继续创建订单 pass else: raise ValueError(sold out)update返回受影响行数只有当过滤条件满足stock 0且更新成功时才是 1这个操作为原子操作不需要显式上锁。需要特别注意的是不要把这个更新放在select_for_update的事务里和订单创建写成一个长事务否则闸机入口的并发扣减会被不相关的读操作阻塞。更稳妥的方案是把库存操作独立成一个短事务订单创建另起一个事务中间通过消息或回调衔接。关于 MySQL 安装配置本地开发时常见痛点有两个一是字符集不是 utf8mb4导致人脸特征特征字符串里的和/没影响但订单备注里的 emoji 存不进去二是 MySQL 8 默认的caching_sha2_password认证插件在用 PyMySQL 时会报错。Django 连接 MySQL 时推荐使用mysqlclient若在 Windows 上安装困难则可以临时用pymysql并在__init__.py里执行pymysql.install_as_MySQLdb()注意这只适合本地开发生产环境应解决编译依赖并换回mysqlclient。4. 前端与 Django 模板协作摄像头采集、上传和识别结果回显4.1 为什么选用 Django 模板加原生 JavaScript 而不是前后端分离标题里明确写了 Django-html意味着这个项目用的是服务端渲染的 Django 模板而不是 Vue/React 前后端分离架构。维护这类项目时有一条经验不要把页面逻辑全部堆到模板里而是把 Django 模板当作”页面骨架“交互部分用原生 JavaScript 或轻量 jQuery 实现Django 只负责输出 JSON 和初始数据。人脸识别页面有一个特殊要求需要访问摄像头。浏览器通过navigator.mediaDevices.getUserMedia获取视频流然后把视频帧绘制到 canvas 上截图为 base64 的 JPEG再通过 Fetch 或 Ajax 上传到 Django 接口。由于摄像头视频流一直是打开的上传环节不能简单用表单提交否则每次识别都要重新申请摄像头权限。正确思路是初始化时申请一次视频流识别时停止视频轨或保持视频常开、不断截帧。// face_register.js const video document.getElementById(video-face); const canvas document.getElementById(canvas-snapshot); const btnCapture document.getElementById(btn-capture); async function initCamera() { const stream await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: false, }); video.srcObject stream; await video.play(); } btnCapture.addEventListener(click, function () { const ctx canvas.getContext(2d); ctx.drawImage(video, 0, 0, 640, 480); const dataUrl canvas.toDataURL(image/jpeg, 0.9); // dataUrl 形如 data:image/jpeg;base64,... uploadFace(dataUrl.split(,)[1]); }); async function uploadFace(base64Data) { const blob base64ToBlob(base64Data, image/jpeg); const formData new FormData(); formData.append(face_image, blob, capture.jpg); const resp await fetch(/api/order/20240101001/face/, { method: POST, body: formData, }); const result await resp.json(); if (result.code 0) { document.getElementById(result).innerText 注册成功; } else { document.getElementById(result).innerText result.msg; } }这里的base64ToBlob是把 base64 解码成二进制对象否则FormData里 append 的字符串会被 Django 当成文本字段而不是文件。Django 侧的request.FILES需要收到一个真正的文件对象浏览器端常见的坑是append时缺少文件名参数导致Content-Type解析异常因此必须加上第三个参数capture.jpg。getUserMedia在 HTTP 非 localhost 环境下会被浏览器拦截本地调试必须用http://127.0.0.1:8000访问部署到公网则必须上 HTTPS否则摄像头无法调起。4.2 服务端渲染下的 CSRF 处理Django 模板渲染的页面会输出{% csrf_token %}但 Fetch 请求时不能直接读取 Cookie 里的csrftoken并放到 Header 里就算完。常见做法是从模板里取隐藏字段的值或从 Cookie 读取并设置X-CSRFToken请求头。需要特别注意的是如果 GET 请求页面后长时间不动Django 会轮换 CSRF Token此时 Cookie 里的值可能和页面里的隐藏字段不一致所以每次提交前以隐藏字段为准。const csrfToken document.querySelector([namecsrfmiddlewaretoken]).value; fetch(/api/order/20240101001/face/, { method: POST, headers: { X-CSRFToken: csrfToken }, body: formData, });如果前端页面通过window.location或a标签跳转到登录页后再次提交务必重新从新页面获取 Token 并更新。这个细节在“注册人脸后立即检票”的测试流程里出现的频率极高症状是第一次提交成功第二次提交报 403。4.3 识别结果的后台管理与页面回显管理员端通常会用到 Django admin 来查看订单状态和检票日志。默认的 admin 界面在列表页字段较多时体验不理想常见调整是设置list_display、list_filter和search_fields。虽然标题里没提 admin 的优先级但源码包中说明文档和 PPT 通常会演示这部分所以值得在开发时做好。# admin.py from django.contrib import admin from .models import Order, Session, CheckInLog admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display (order_no, visitor_name, session, status, face_status) list_filter (status, face_status, session) search_fields (order_no, visitor_name, phone) list_per_page 20admin 美化只是锦上添花真正要注意的坑是face_feature字段不要加进list_displaybase64 字符串会让页面渲染卡顿且列表页把一整串字符渲染出来没有业务意义。检票记录表同理列表只展示订单号、设备和相似度得分不在列表页展示原始图片或特征串。4.4 人脸识别算法的选型和精度边界在景区票务这类可控光照场景下人脸识别算法选型的核心指标不是公开测试集上的准确率而是模型文件大小、CPU 推理耗时、对口罩和眼镜的鲁棒性。常见选择有基于传统方法的 LBPH 和基于深度学习的 FaceNet/ArcFace 及国产的开源模型。LBPH 在姿态变化稍大时误拒率飙升只适合闸机这类固定机位、高度统一的场景深度学习模型在嵌入式设备上需要提前转成开放神经网络交换格式并量化否则 CPU 推理会产生明显的延迟。现在可以直接使用一些开源商用模型比如基于 RetinaFace 检测 ArcFace 特征提取的组合。这类模型的部署方式比较统一检测模型负责从整张图片里框出人脸特征模型负责把框出的人脸编码为向量。两个模型可以串行执行但要注意摄像头采集的 640x480 图片里人脸区域可能只占很小一部分直接把整图喂给特征模型会导致特征提取质量大幅下降。正确的做法是先由检测模型裁出人脸区域再缩放到特征模型要求的输入尺寸比如 112x112 或 160x160。def _align_face(image): dets detector.detect(image) if len(dets) 0: return None x, y, w, h dets[0] margin int(0.2 * w) x1 max(0, x - margin) y1 max(0, y - margin) x2 min(image.shape[1], x w margin) y2 min(image.shape[0], y h margin) face image[y1:y2, x1:x2] return cv2.resize(face, (112, 112))特征提取前的人脸对齐非常关键。很多人把票务识别不准确归因于“模型不行”其实是把带额头的整张自拍照误当成了人脸特写。裁人脸时多留 20% 边距有助于保留下巴轮廓和部分背景特征提取效果比严格裁到脸廓更稳定。5. 完整项目的最小运行步骤与本地部署排错5.1 从拿到源码到页面跑通的最小命令序列拿到一个 Django 项目压缩包第一步不是急着配置数据库而是先理清三个东西项目的requirements.txt、根目录下的manage.py所在位置以及settings.py里的数据库配置。常见的源码包尤其是毕设类会附带一个.sql文件或 MySQL 导出文件这时手动创建数据库并导入比运行migrate更可靠因为迁移文件在多人协作时很容易缺漏。# 1. 创建虚拟环境并激活 python -m venv venv # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 2. 安装依赖 pip install -r requirements.txt # 3. 创建数据库并导入 mysql -u root -p -e CREATE DATABASE scenic_ticket DEFAULT CHARACTER SET utf8mb4; mysql -u root -p scenic_ticket scenic_ticket.sql # 4. 修改 settings.py 的 DATABASES 配置 # ENGINE: django.db.backends.mysql # NAME: scenic_ticket # 5. 迁移与启动 python manage.py makemigrations python manage.py migrate python manage.py runserver 0.0.0.0:8000先导入 SQL 再跑migrate的顺序需要注意如果 SQL 文件里已经建好了全部表迁移可能检测不到变化如果 SQL 和迁移文件来自同一版本则可以只导数据不建表用migrate --run-syncdb的方式补全。Django 项目常见报错之一是Table django_session doesnt exist这通常是因为跳过了migrate直接访问了后台管理页面单独运行python manage.py migrate即可恢复。5.2 Python、Django、MySQL 版本兼容的常用匹配版本兼容是毕设源码落地时最容易卡住的部分。Python 版本过高会导致部分依赖没有预编译包过低则 Django 不支持。当前常见匹配是 Python 3.8~3.11 搭配 Django 3.2~4.2高于 Django 4.2 的版本已部分移除对 Python 3.7 的支持。如果requirements.txt里的依赖列表很老比如还是 Django 2.x建议先按原版本跑通不要一上来就升级 Django否则urlpatterns里的re_path和django.conf.urls.url会报错。MySQL 8.0 与旧版 Django 的兼容问题表现在django.db.backends.mysql默认使用的caching_sha2_password认证插件的握手协议不一致。解决方式有两种一是创建一个使用mysql_native_password的数据库账号二是升级到 Django 3.2 以上并安装最新mysqlclient。生产服务器上宝塔面板自带的 MySQL 版本需要同步检查特别是pymysql在 Python 3.10 以上版本存在一些字节编码转换差异无法正常读写 emoji 字符。5.3 人脸识别在部署环境里的性能优化与阈值标定最后落到上线前的两个步骤性能优化与阈值标定。人脸识别服务在开发环境跑通不等于在现场能用在 CPU 机器上每张图片的推理耗时超过 1 秒就会导致闸机口排队。常见优化方式是加一个缓存层把同一订单同一分钟内的识别结果缓存到进程内存或 Redis避免二次识别。import time class RateLimiter: def __init__(self): self._cache {} def allow(self, key, ttl3): now time.time() last self._cache.get(key, 0) if now - last ttl: return False self._cache[key] now return True limiter RateLimiter()这个进程级限流器只适合单机部署多进程或多节点时需要换 RedisSETEX原子操作。阈值标定的方法是准备一个包含 20 人左右、每人 5 张不同光线和角度照片的测试集计算相同人的相似度分布和不同人的相似度分布取两个分布之间的交界点作为初始阈值。注意不要只测相似度得分的绝对数值要同时对比当前闸机机位的人脸检测成功率检测不到人脸的情况下阈值设得再准也无法放行。 提示在项目根目录创建 static/face_test/ 目录存放校准样本用下面的脚本输出分布直方图辅助选阈值比直接在页面里肉眼判断靠谱得多。 def evaluate_threshold(order_no_list): import numpy as np scores [] for order_no in order_no_list: # 模拟一张新拍照片调用提取与比对 s face_service.compare(feat_new, feat_ref) scores.append(s) return np.mean(scores), np.min(scores), np.max(scores)拍摄照片与注册照片的清晰度差异是误差主要来源。现场拍的照片往往存在运动模糊和反光特征提取结果会和注册时有偏移。在闸机部署时优先选择固定焦距的摄像头并让设备后台做自动曝光补偿后再传图而不是把模糊帧直接送进识别模块。最终把threshold和ttl这两个参数做成可在管理后台动态调整的配置项上线后根据投诉记录微调比一次性拍脑袋定值更实际。本文还有配套的精品资源点击获取
返回列表