简介:基于人脸识别的景区票务系统毕业设计源码,面向需要完成Python课程设计或毕业设计的在校学生,也适合希望学习Django+MySQL开发流程的初级开发者。系统采用前台+后台双模式设计,前台支持用户注册、公告须知、票务查看与在线购票,购票时包含人脸录入、单号生成及支付环节;后台提供管理员信息管理、用户管理、公告发布、订单与支付统计(图表)、验票信息查看及退票登记等功能,覆盖面较完整,具备真实项目的模块化结构。资源包共663个文件,以282个Python源码文件为主,辅以静态页面资源(CSS、JS、图片)、部分编译文件与Django依赖文件,压缩包约111MB。已有60人学习浏览,适合直接导入PyCharm配合Python3.6.8与MySQL5.7运行调试,并借助附带SQL文件快速初始化数据库,方便二次开发与答辩讲解。
1. 人脸识别景区票务系统的构成与边界
游客到达景区闸机口,不再掏手机、翻身份证,而是对着摄像头停顿一秒,闸机自动放行,后台同时记录入场时间与剩余次数。这不是科幻片里的无感通行,而是一套基于 Python 的景区票务系统能实现的基本能力:前端摄像头采集人脸,后台用开源人脸识别算法提取特征,到 MySQL 里比对游客身份,匹配成功后再完成一次门票核销。标题里的“源代码 + LW”暴露了它的真实身份——这是一套面向毕业设计交付的完整工程,核心科目是 Python、OpenCV 与数据库设计,而不是某个商业级 SaaS 产品。
这类项目最有价值的点恰恰在于它的链路完整:注册端、数据库、识别端、业务动作一个不少,比单独撸一个“人脸识别 demo”难在业务闭环,又比工业级人脸平台简单在不需要高并发与高精度。适合两类人:一是需要快速搭起毕设主线的学生,二是想把 OpenCV/人脸识别框架在实际业务流程里跑通的开发者。接下来我按自己会做的方案,把选型、代码、参数和验证一条龙讲清楚。
2. 选型:Python 生态下的人脸识别算法与系统架构
2.1 人脸识别算法方案对比:OpenCV、dlib 还是深度学习模型
做毕业设计级的人脸识别,最容易掉进去的坑是一上来就奔着训练模型去。实际上,站在 2025 年往回看,成熟的开源方案已经多到不需要自己训练分类器,你需要做的是在“精度、开发量、部署难度”之间取一个平衡。
| 方案 | 检测方式 | 特征维度 | CPU 推理耗时 | 开发难度 | 适用场景 |
|---|---|---|---|---|---|
| OpenCV Haar Cascade | Haar 特征 + Adaboost | 无特征向量 | 极快,毫秒级 | 低 | 仅人脸检测,无法做身份比对 |
| OpenCV DNN(ResNet SSD) | 深度学习检测器 | 需另配特征提取 | 较快 | 中 | 检测框质量好,适合做前置检测 |
| dlib HOG + 线性分类器 | HOG 特征 | 128 维(配合 face_recognition) | 快,百毫秒级 | 低 | 正面人脸识别,毕设首选 |
| dlib CNN(MMOD) | 深度学习检测 | 128 维 | 慢,秒级 | 中 | 侧脸、遮挡场景,离线处理 |
| InsightFace / FaceNet | 深度学习 | 512 维 | 需 GPU 或加速 | 高 | 高精度业务系统 |
这里我一般会推荐face_recognition库,它把 dlib 的检测与特征提取封装成了三五行调用,返回的 128 维浮点特征可以直接存进数据库。它的精度在正面、光线均匀的闸机场景里完全够用,而景区检票口恰好就是这种受控场景——游客会主动面向摄像头,戴口罩的概率在毕设演示环境里也可以当成边界情况处理。OpenCV 的 Haar 只能告诉你“这里是不是一张脸”,给不出可用于比对的向量,所以不适合单独作为识别方案。InsightFace 虽然精度更高,但依赖 PyTorch 与 GPU,毕设答辩现场如果只有一台笔记本,就直接被实时性拖垮了。
选择 face_recognition 的另一个理由是可解释性强。答辩时你能清楚说出检测用的是 HOG、特征提取用的是 dlib 的 ResNet 残差网络、比对用的是欧氏距离,这三层每一层都有对应论文可查,也都有对应代码可以现场演示。
2.2 系统模块划分:注册端、管理端与检票端如何协作
整套景区票务系统不是单文件脚本,而是三个功能端配合。注册端负责把游客的人脸照片转成特征写入数据库,可以做成 PyQt5 界面,也可以做一个简单的 Web 上传页;管理端处理门票类型、有效日期与入园次数;检票端是实时程序,启动后打开摄像头,对每一帧画面做人脸检测与比对,命中后调用核销逻辑。
三个端共用同一张 MySQL 数据库,模块间通过数据库解耦,这是毕设架构里最稳妥的做法,既避免了写 Socket 通信的复杂度,又能在文档里清楚画出“采集—存储—比对—核销”四条数据流。
注册端有一个容易被忽略的细节:同一张脸在不同角度、不同光线下提取出来的特征是有差异的,所以注册时最好连续拍摄 3 到 5 张照片,分别提取特征存入多条记录。比对时遍历该用户的所有特征,只要有一条距离低于阈值就判定匹配。这样做的代价是库容量翻几倍,但对千人规模的景区演示来说完全不是问题。
2.3 数据库设计:人脸特征到底存在什么字段里
票务系统的数据库至少要落四张表:游客表、门票表、入园记录表、特征表。特征单独成表的原因是“一个人可以有多条特征”,同时避免游客表字段过长影响常规查询性能。
CREATE TABLE visitor ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE face_feature ( id INT PRIMARY KEY AUTO_INCREMENT, visitor_id INT NOT NULL, feature BLOB NOT NULL, image_path VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_visitor (visitor_id), CONSTRAINT fk_face_visitor FOREIGN KEY (visitor_id) REFERENCES visitor(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE ticket ( id INT PRIMARY KEY AUTO_INCREMENT, visitor_id INT NOT NULL, ticket_type VARCHAR(20) COMMENT '单次/多次/年卡', total_times INT DEFAULT 1, used_times INT DEFAULT 0, valid_date DATE, status TINYINT DEFAULT 1, INDEX idx_ticket_visitor (visitor_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE access_log ( id INT PRIMARY KEY AUTO_INCREMENT, visitor_id INT NOT NULL, capture_path VARCHAR(255), result TINYINT COMMENT '1通过 0拒绝', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;特征字段的类型我用了 BLOB,存的是 numpy 数组序列化后的二进制。face_recognition 的face_encodings返回的是一个 128 维的 numpy 数组,每个元素是 float64,序列化后大约 1KB。把 1KB 的二进制丢进 MySQL 完全没有压力,查询时一次性取出来反序列化即可。
不建议用 TEXT 字段存 JSON 字符串,原因是二进制转字符串会膨胀 30% 以上的存储空间,而且每次比对都要做两次解析。BLOB 在 pymysql 里直接用bytes类型读写,配合numpy.frombuffer还原数组,代码量反而更少。外键和索引的建立是必要的,因为检票端每次要先按visitor_id把特征捞出来,没有索引会全表扫描。
3. 人脸识别票务核心代码:注册、比对与核销
3.1 人脸注册:从照片到特征入库的最小实现
注册端的核心逻辑是:读取图片、检测人脸、提取特征、写入数据库。用 face_recognition 完成前三步只需要几个函数调用,但边界情况要处理清楚——图片里没有人脸时必须报错,有多张人脸时只取面积最大的那张作为注册源。
import face_recognition import numpy as np import pymysql def extract_largest_face_feature(image_path): image = face_recognition.load_image_file(image_path) locations = face_recognition.face_locations(image, model="hog") if len(locations) == 0: raise ValueError("图片中未检测到人脸,请重新拍摄") # 取面积最大的人脸框,避免注册进路人 top, right, bottom, left = max(locations, key=lambda loc: (loc[2] - loc[0]) * (loc[1] - loc[3])) encodings = face_recognition.face_encodings(image, known_face_locations=[(top, right, bottom, left)]) if len(encodings) == 0: raise ValueError("人脸特征提取失败") feature_bytes = encodings[0].tobytes() return feature_bytes def register_visitor(name, phone, image_path): feature_bytes = extract_largest_face_feature(image_path) conn = pymysql.connect(host="localhost", user="root", password="123456", database="scenic_ticket", charset="utf8mb4") try: with conn.cursor() as cursor: cursor.execute("INSERT INTO visitor (name, phone) VALUES (%s, %s)", (name, phone)) visitor_id = cursor.lastrowid cursor.execute("INSERT INTO face_feature (visitor_id, feature) VALUES (%s, %s)", (visitor_id, feature_bytes)) cursor.execute("INSERT INTO ticket (visitor_id, ticket_type) VALUES (%s, 'single')", (visitor_id,)) conn.commit() return visitor_id finally: conn.close()代码里face_encodings的第二个参数必须传入known_face_locations,否则它会重新做一次全图检测,导致拿到的特征与前面计算的最大人脸框对不上。tobytes()把 float64 数组序列化成二进制,frombuffer可以原样还原。
注册时把 visitor、face_feature、ticket 三条记录放在同一个事务里,保证不会出现“有脸没票”的脏数据。这是票务系统与纯人脸识别 demo 的关键区别:识别只是手段,核销才是目的。
3.2 检票端实时识别:摄像头流与特征比对循环
检票端程序拉起摄像头后,每一帧都要依次经过“读取画面—人脸定位—特征提取—距离比对—业务核销”这条链路。直接在代码里写死一个while True循环处理每一帧,在 CPU 上很快就会卡成 PPT,所以工程上要做取舍:降低处理分辨率、隔帧处理、把数据库特征预加载到内存。
import cv2 import face_recognition import numpy as np import pymysql def load_all_features(): conn = pymysql.connect(host="localhost", user="root", password="123456", database="scenic_ticket", charset="utf8mb4") known_encodings = [] known_ids = [] with conn.cursor() as cursor: cursor.execute("SELECT visitor_id, feature FROM face_feature") rows = cursor.fetchall() for visitor_id, feature_bytes in rows: known_encodings.append(np.frombuffer(feature_bytes, dtype=np.float64)) known_ids.append(visitor_id) conn.close() return known_encodings, known_ids known_encodings, known_ids = load_all_features() video_capture = cv2.VideoCapture(0) frame_count = 0 while True: ret, frame = video_capture.read() if not ret: break frame_count += 1 if frame_count % 3 != 0: continue # 缩小到 1/2 分辨率,HOG 检测在高分辨率上会慢很多 small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) rgb_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) locations = face_recognition.face_locations(rgb_frame, model="hog") if not locations: continue encodings = face_recognition.face_encodings(rgb_frame, locations) for encoding in encodings: distances = face_recognition.face_distance(known_encodings, encoding) min_index = np.argmin(distances) if distances[min_index] < 0.50: visitor_id = known_ids[min_index] print(f"识别成功 visitor_id={visitor_id} distance={distances[min_index]:.3f}") # 核销逻辑见下一小节 else: print("未匹配到游客") video_capture.release()这里的三个参数直接影响效果。frame_count % 3是隔两帧处理一次,把有效帧率压到摄像头帧率的三分之一,但 HOG 检测本身耗时才是瓶颈,隔帧只是缓解整体 CPU 占用。fx=0.5把图像尺寸缩小一半,检测耗时会降到原来的四分之一左右,但小脸会被漏检;如果闸机摄像头离人脸 1 米以内,半分辨率是安全边界。face_distance的阈值 0.50 是一个经验起点,后面会讲怎么标定。
load_all_features在程序启动时把整库特征加载到内存,是因为每次比对都查 MySQL 会发生网络往返与特征反序列化,在实时循环里不可接受。千人规模的库,加载到内存也就几 MB 到十几 MB,换取的是单帧比对变成纯内存运算。
3.3 门票核销:识别通过后的事务处理
识别通过只代表“人脸匹配上了”,不等于“可以入园”。核销要走一遍票务逻辑:查这个游客有没有状态正常的门票、剩余次数是否大于零、当前日期是否在有效期内,全部满足才扣减次数并写通行日志。
def consume_ticket(visitor_id, conn): try: with conn.cursor() as cursor: cursor.execute( "SELECT id, total_times, used_times, valid_date, status " "FROM ticket WHERE visitor_id=%s AND status=1 FOR UPDATE", (visitor_id,) ) ticket = cursor.fetchone() if ticket is None: return False, "无有效门票" ticket_id, total_times, used_times, valid_date, status = ticket if used_times >= total_times: return False, "入园次数已用完" if valid_date and valid_date < datetime.now().date(): return False, "门票已过期" cursor.execute( "UPDATE ticket SET used_times=used_times+1 WHERE id=%s", (ticket_id,) ) cursor.execute( "INSERT INTO access_log (visitor_id, result) VALUES (%s, 1)", (visitor_id,) ) conn.commit() return True, "通行成功" except Exception: conn.rollback() return False, "系统错误"SELECT ... FOR UPDATE是这节的关键。景区闸机口可能同时有几个人排队,检票程序如果是多线程或者多个摄像头独立运行,两个请求并发读到同一张票的剩余次数都是 1,就可能出现两个人同时入园但次数只扣一次的超卖。FOR UPDATE把这一行锁住,等到 UPDATE 提交后才释放,从数据库层面杜绝了这个竞争条件。如果只是单机单摄像头,不加锁也能跑,但加上了可以在答辩时作为“并发安全设计”的亮点来讲。
核销结果应该有物理输出。代码里先用print占位,实际部署时常见做法是接一个继电器控制闸机电机,或者弹出一个 PyQt5 窗口提示放行。串口控制闸机的扩展在毕设文档里可以写,但不建议在答辩现场真的去拆闸机,用舵机转一下模拟物理动作就足够了。
4. 参数与排错:让人脸识别在真实闸机场景里可用
4.1 face_distance 阈值不是默认值,要自己标定
face_recognition 官方示例里把阈值写成 0.6,这个值的含义是“128 维特征向量的欧氏距离小于 0.6 就算同一个人”。它是在 LFW 数据集上测出来的相对合理默认值,但景区现场的光线、摄像头畸变、游客妆容都会改变特征分布,直接套 0.6 可能会出现“谁都能进”的误放行。
正确做法是构建一个小的标定集。选 20 个同学当作游客,每人拍 1 张登记照入库,再隔天每人拍 3 张作为测试照,同时找 20 个没入库的路人各拍 1 张。对 80 张测试照分别计算与库里最近特征的距离,按阈值从 0.35 到 0.65 逐个统计两个指标:误接受率(路人被放行比例)和误拒绝率(游客被拦下比例)。
| 阈值 | 误接受率趋势 | 误拒绝率趋势 | 适用场景 |
|---|---|---|---|
| 0.40 | 极低,几乎不漏放 | 较高,常拦下本人 | 高安全要求,比如室内部位门禁 |
| 0.50 | 低,偶发相似脸漏放 | 较低,正脸基本放行 | 景区闸机,推荐测试起点 |
| 0.60 | 可能误放行 | 低,体验友好 | 仅限内部演示,不推荐真实检票 |
标定脚本的逻辑很简单:把所有距离结果存成 CSV,然后用 pandas 按阈值列筛选统计。人脸识别门禁机的工业产品通常把阈值设在 0.45 到 0.50 这个区间,景区场景可以适当放松到 0.50 附近,因为这个场景的损失是“漏放一次票”,远小于门禁场景的“放错一个人进机房”。
4.2 摄像头实时链路慢:定位瓶颈在哪一段
检票程序卡顿最让人头疼的是“慢”得很均匀——看不出是哪一行代码拖了后腿。其实整个循环里可优化的三个耗时点非常明确:视频帧读取与解码、HOG 人脸检测、128 维特征提取。
我只在需要观察瓶颈时打点排查,用time.perf_counter()包住每一段,打印耗时分布。通常结果会是:HOG 检测占 60% 到 70%,特征提取占 20% 到 30%,读取帧几乎不耗时。所以优化优先级应该先压检测耗时的头。改小输入分辨率立竿见影,但如果分辨率先降到太小导致检测不到脸,再调大也会卡,基本可以用二分法找边界。
另一个有效方案是跳帧后接跟踪器。在第一帧检测到人脸后,用 OpenCV 的TrackerKCF或TrackerCSRT跟踪人脸位置,之后连续 5 到 10 帧不再重新跑 HOG 检测,直接在上一次位置附近提取特征。这样检测耗时被摊薄到每 10 帧一次,整体吞吐能提升 3 倍以上。代价是人脸快速转头或走出画面时跟踪框会漂移,需要在跟踪失败时立刻回退到全图检测。
采集端的光线问题也经常被误判成算法问题。景区闸机如果背光,人脸会欠曝,提出来的特征距离会比正常光照大 0.1 到 0.2。处理方式是在代码里加一个简单的亮度统计,如果画面过暗就提示“请靠近闸机口”,比在黑暗中反复比对有意义得多。
4.3 画面里出现多张脸:到底放行哪一个
闸机口的摄像头视野里完全可能同时站着五六个人,人脸检测会把每张脸都框出来,然后每一张脸都和库里比对,最危险的情况是后面排队的人刷脸通过,把前面已经入园的游客的票又核销了一次——这等于一次购票多人入园。
常见做法是在检票端加一条单人规则:只取画面中面积最大的那张人脸参与比对。闸机通道的设计本来就确保一次只能通过一个人,站在镜头前的人脸天然是最大的。实现时用face_locations返回的坐标计算宽高乘积,取最大值,再把这个框喂给face_encodings。
def pick_largest_face(frame): locations = face_recognition.face_locations(frame, model="hog") if not locations: return None, None largest = max(locations, key=lambda loc: (loc[2] - loc[0]) * (loc[1] - loc[3])) top, right, bottom, left = largest face_image = frame[top:bottom, left:right] encodings = face_recognition.face_encodings(face_image) if not encodings: return None, None return encodings[0], largest这个改动同时解决了另一个问题:取最大脸之后,后面路人即使被检测出来,也不会参与比对,误核销的概率大幅下降。经验数据是,当画面中有 4 张以上的脸时,最大脸的像素面积通常占所有人脸总面积的 40% 到 60%,按面积选人比按位置选人稳定得多。
5. 验证与进阶:用可控测试集收敛误识,并考虑规模化
5.1 自动回归测试:让答辩演示不翻车
答辩前最怕现场识别失败。与其赌当天光线和设备状态,不如提前建一个自动化回归脚本,把测试集和结果收集起来,确认本次改代码没有让准确率下降。测试脚本的核心是读取一批标注好的照片,跑一遍检票流程,然后输出分类结果的混淆矩阵。
import os import numpy as np import face_recognition known_encodings = ... # 从数据库加载 test_dir = "test_data" # 目录结构: registered/ok, registered/ng, stranger tp = fp = fn = tn = 0 threshold = 0.50 for label_dir in ["registered", "stranger"]: base = os.path.join(test_dir, label_dir) for filename in os.listdir(base): image = face_recognition.load_image_file(os.path.join(base, filename)) encodings = face_recognition.face_encodings(image) if not encodings: continue distance = min(face_recognition.face_distance(known_encodings, encodings[0])) predict_ok = distance < threshold if label_dir == "registered": if predict_ok: tp += 1 else: fn += 1 else: if predict_ok: fp += 1 else: tn += 1 print(f"TP={tp} FN={fn} FP={fp} TN={tn}") print(f"准确率={(tp + tn) / (tp + fp + fn + tn):.2%}")脚本里故意把阈值抽成常量,方便一次性把所有候选阈值跑完整套数据。跑完后看 FP 与 FN 的交叉点,选一个误放行最少、同时误拦不夸张的阈值写进检票程序配置。这里的测试集规模不需要太大,30 个人、每人 3 张照片、加 15 个路人,就能得到一个有统计意义的阈值区间。
5.2 从毕设到大规模场景:缓解特征比对压力
毕设演示几百人特征,内存比对是瞬时完成的事。但真实景区是几万游客的注册量级,遍历全部特征做距离计算会出现可以感知的延迟。如果要在这个方向上扩展,代码层最直接的优化是把特征全部加载进 Redis,用 key 存 visitor_id、value 存特征二进制,检票程序启动时一次性MGET拉取全量进本地内存。这样 MySQL 只负责写日志和票务流水,不再承担识别请求。
另一个方向是把比对做成分段处理:按访客姓氏拼音首字母或注册日期分段,先用粗粒度条件筛掉八成特征,再对剩余部分做精确距离计算。但这套方案会把系统架构复杂度拉高一个量级,对毕设来说,你的核心任务是把“闭环”做漂亮——从注册到识别再到核销,每一步都有数据落库、有日志可查、有异常处理,这已经超出了大多数同类毕设的完成度。
最后别忘了在系统里留一个手工放行按钮。人脸识别永远存在误拒的可能,闸机旁配一个管理员界面,拍照记录手工放行事件,既符合景区运营的实际情况,又能让答辩评委看到你考虑过系统的边界,而不是盲目信任算法。这一条,是我看过很多票务项目后才真正意识到要提醒你留着的。
本文还有配套的精品资源,点击获取