简介:这份课程设计源码实现了一套基于Python、OpenCV、Django与人脸识别库的人脸识别系统,面向计算机相关专业学生完成人脸识别类课题或期末大作业,可用于快速复现核心功能与扩展二次开发。压缩包共130个文件,约22.62MB,主体为19个Python源码文件与41个编译后的pyc文件,包含项目主逻辑、视图函数与配置脚本;35张PNG图片与30张JPEG图片用于人脸样本、测试图像及界面展示;另含SQLite数据库文件和TensorFlow模型参数文件(pb、data、index),支持模型的加载与持久化存储。该项目曾获导师指导并通过,得到97分高分,代码结构完整,可直接运行。目前已有883人学习下载,适合需要借鉴完整方案或学习Django+OpenCV集成实践的读者。资源内预置了训练权重与示例图片,能减少环境配置和数据集准备成本,帮助使用者聚焦于识别流程与界面联调。
1. 人脸识别系统课程设计:为什么这套技术栈值得你抄作业
一套能跑通的人脸识别系统,放在课程设计里是加分项,放在真实项目里就是人脸门禁、考勤机、课堂签到这类应用的雏形。标题里的 Python + OpenCV + Django + 人脸识别库,恰好覆盖了从图像处理、人脸特征提取到 Web 服务落地的完整链路。很多人栽在第一步:装了 OpenCV 却 import 报错,或者把 Django 当普通脚本写,最后前端调不通接口。这套源码的价值不是给你一堆文件,而是把「摄像头取帧 → 人脸检测 → 特征比对 → Web 展示」这条主线串起来,新手能照着改,熟手能直接抽走里面的识别模块。如果你正打算做类似课题,或者需要快速交付一个带界面的人脸识别 demo,跟着这篇文章的拆解走,能少踩一半的坑。
2. 人脸识别系统的核心链路:检测、对齐、特征提取与比对
2.1 检测与对齐:OpenCV 的 Haar Cascade 与 Dlib 的 68 点模型
人脸识别第一步不是「认人」,而是「找到脸在哪」。常见做法是用 OpenCV 自带的 Haar Cascade 分类器做快速检测,或者用 Dlib 的 HOG + 线性分类器。Haar Cascade 的优势是模型文件小、CPU 上跑得快,对正脸和轻度侧脸效果稳定,但遇到大角度侧脸、遮挡、暗光就容易漏检。Dlib 的检测器更稳,但依赖 dlib 库的安装,Windows 上经常需要编译,很多人在这里翻车。
检测到人脸后,下一步是对齐。Dlib 的 68 点人脸 landmarks 模型可以定位眼睛、鼻子、嘴巴等关键点,然后通过仿射变换把人脸旋转到标准位置。这一步很多人会跳掉,直接拿原图去提取特征,结果同一张脸在不同角度下的特征距离比不同人的还大。对齐后的脸再送入特征提取模型,识别率会有明显提升。
import cv2 import dlib detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") img = cv2.imread("face.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = detector(gray, 1) # 1 表示上采样一次,提高小脸检测率 for face in faces: landmarks = predictor(gray, face) # 通过 landmarks 计算旋转矩阵,做仿射变换对齐这里detector(gray, 1)的第二个参数是上采样次数,值越大越容易检出小脸,但速度会变慢。对齐操作可以用 OpenCV 的cv2.estimateAffinePartial2D配合左右眼坐标计算旋转矩阵。如果你只是做课程设计,不做大角度姿态变化的人脸,这一步可以简化成「只检测不旋转」,但至少要把人脸区域裁出来。
2.2 特征提取与比对:从 face_recognition 库到欧氏距离阈值
标题里说的「人脸识别库」在 Python 生态里最常用的就是 face_recognition,它封装了 Dlib 的深度学习模型,用起来几乎零门槛。face_recognition.face_encodings会返回一个 128 维的特征向量,比对就是算两个向量之间的欧氏距离。阈值一般定在 0.45 到 0.55 之间,低于阈值认为是同一个人,高于则不是。
这个阈值是个玄学。定得太小,容易把同一个人不同光线下的照片误判为陌生人;定得太大,又容易把长得像的两个人混为一谈。我一般会先收集同一个人的多张照片,算出特征向量两两之间的距离分布,再取一个中间值。比如某个人自己的照片距离在 0.3 到 0.45 之间,和其他人的距离在 0.7 以上,那 0.55 就是安全线。
import face_recognition known_img = face_recognition.load_image_file("known.jpg") known_encoding = face_recognition.face_encodings(known_img)[0] unknown_img = face_recognition.load_image_file("unknown.jpg") unknown_encoding = face_recognition.face_encodings(unknown_img)[0] distance = face_recognition.face_distance([known_encoding], unknown_encoding)[0] print(f"distance: {distance:.3f}") if distance < 0.5: print("匹配成功") else: print("未知人员")注意face_encodings返回的是数组,如果一张图里有多张脸,要通过face_locations把每张脸的位置传进去,否则它会默认取最大的人脸。很多新手在测试时发现识别不准,就是因为照片里有多个人脸,特征向量取错了对象。另外,face_recognition 底层是 Dlib 的 ResNet 模型,第一次加载会花一点时间,但识别速度在 CPU 上也能达到每张几十毫秒,课程设计完全够用。
2.3 注册库的设计:用 SQLite 存特征还是存图片路径
一个可用的系统一定有「注册」和「识别」两个流程。注册时把新人的照片和姓名入库,识别时拿摄像头帧的特征去库里面比对。这里的存储设计直接决定后续的扩展性。
最简单的做法是在 SQLite 里存图片的 Base64 字符串或者文件路径,每次识别时重新计算库中所有图片的特征。如果库里有几十个人,每次识别都要算几十次特征提取,CPU 基本跑不动。更合理的方案是注册时就把 128 维特征向量作为 BLOB 存进数据库,识别时只算一次摄像头帧的特征,然后遍历库里的向量做距离计算,速度快一个量级。
import sqlite3 import numpy as np def save_face(name, encoding): conn = sqlite3.connect("faces.db") blob = encoding.tobytes() # 把 numpy 数组转成 bytes conn.execute("INSERT INTO face (name, encoding) VALUES (?, ?)", (name, blob)) conn.commit() conn.close() def load_all_faces(): conn = sqlite3.connect("faces.db") rows = conn.execute("SELECT name, encoding FROM face").fetchall() faces = [(name, np.frombuffer(enc, dtype=np.float64)) for name, enc in rows] conn.close() return faces特征向量用np.float64的字节序列存储,不同的 face_recognition 版本可能返回不同 dtype,读取时一定要保持一致,否则比对结果全是乱的。这个细节看起来小,实际排查时非常头疼。
3. 用 Django 把识别能力包成 Web 服务:从模型加载到 API 设计
3.1 为什么选 Django:Session、ORM 和静态文件管理都是现成的
人脸识别本身和 Django 没什么关系,但一旦要做「用户上传照片注册」、「管理员查看识别记录」这类功能,Django 的 Session 机制和 ORM 能省下大量代码。课程设计通常要求有登录、注册、历史记录,这些 Django 内置的 auth 模块直接能改,比 Flask 裸写要稳妥。
另外一个关键点是摄像头接入。网页端调摄像头一般用 WebRTC 或者简单的navigator.mediaDevices.getUserMedia,然后把视频帧 POST 到 Django 后端识别。后端不能每次启动都重新加载一次 face_recognition 模型,那样响应会慢到没法用。常见做法是写一个单例类,在 Django 启动时把模型和已知人脸的特征库加载到内存。
# recognition/service.py import face_recognition import numpy as np from django.conf import settings class FaceService: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance.known_faces = [] cls._instance.known_names = [] return cls._instance def load_from_db(self): from .models import FaceProfile profiles = FaceProfile.objects.all() self.known_faces = [np.frombuffer(p.encoding, dtype=np.float64) for p in profiles] self.known_names = [p.name for p in profiles]这里用了单例模式,Django 的 view 里每次调用FaceService()拿到的都是同一个实例,特征库只要加载一次。注意load_from_db要在 Django 启动时或者首次请求前调用,否则会出现「库里有人但识别不到」的诡异问题。
3.2 上传图片注册的 API:处理 multipart 数据和头像裁剪
注册接口是 Web 端最基础的功能。前端用表单提交一张图片和一个姓名,后端接收后先做一次人脸检测,确认图片里确实有脸,再提取特征入库。这里有个很多人忽略的问题:用户上传的照片可能很大,直接送进 face_recognition 会慢。应该在检测前先压缩到宽不超过 800 像素。
# recognition/views.py from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from .service import FaceService from .models import FaceProfile import face_recognition import cv2 import numpy as np @csrf_exempt def register(request): if request.method != "POST": return JsonResponse({"error": "method not allowed"}, status=405) name = request.POST.get("name") image_file = request.FILES.get("image") if not name or not image_file: return JsonResponse({"error": "missing name or image"}, status=400) # 读取并压缩图像 img = cv2.imdecode(np.frombuffer(image_file.read(), np.uint8), cv2.IMREAD_COLOR) h, w = img.shape[:2] if w > 800: ratio = 800 / w img = cv2.resize(img, (800, int(h * ratio))) rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) locations = face_recognition.face_locations(rgb_img) if not locations: return JsonResponse({"error": "no face detected"}, status=400) encoding = face_recognition.face_encodings(rgb_img, locations)[0] FaceProfile.objects.create(name=name, encoding=encoding.tobytes()) FaceService().load_from_db() # 更新内存缓存 return JsonResponse({"success": True})csrf_exempt在课程设计里可以用,但真实项目里一定要处理 CSRF token。face_locations用的是 HOG 模型,比 Haar 稳,但速度稍慢,压缩到 800 宽后一般能接受。如果图片里有多张脸,face_encodings会按 locations 顺序返回多个向量,注册时默认取第一张,如果你想做多人注册,得改成前端传 face_id 指定第几张。
3.3 实时识别接口:把视频帧送到后端还是前端跑推理
实时识别有两种做法,一种是前端把每一帧图片 POST 到后端,另一种是前端用 TensorFlow.js 之类的方案在浏览器里跑。既然标题锁定了 Python + OpenCV + Django,后端识别是主线。前端截帧的间隔不能太密,否则后端口舌会被刷爆。我一般会控制在每秒 2 帧,也就是setInterval里做 500ms 间隔的采集。
后端接收帧后,先做缩小和灰度处理,再送去检测和编码。这里有个性能矛盾:检测用 OpenCV 的 Haar 快,但特征编码必须用 face_recognition。其实可以用 Haar 先快速检测出人脸区域,再把人脸区域裁出来做编码,比全图直接送 face_recognition 快不少。
# recognition/views.py @csrf_exempt def recognize_frame(request): image_file = request.FILES.get("frame") if not image_file: return JsonResponse({"error": "no frame"}, status=400) img = cv2.imdecode(np.frombuffer(image_file.read(), np.uint8), cv2.IMREAD_COLOR) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 级联分类器检测 cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml") faces = cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80)) if len(faces) == 0: return JsonResponse({"result": "unknown", "faces": []}) best_name = "unknown" best_distance = 1.0 for (x, y, w, h) in faces: face_crop = img[y:y+h, x:x+w] rgb_crop = cv2.cvtColor(face_crop, cv2.COLOR_BGR2RGB) encodings = face_recognition.face_encodings(rgb_crop) if not encodings: continue distances = face_recognition.face_distance(FaceService().known_faces, encodings[0]) min_idx = np.argmin(distances) if distances[min_idx] < 0.5 and distances[min_idx] < best_distance: best_distance = distances[min_idx] best_name = FaceService().known_names[min_idx] return JsonResponse({"result": best_name, "distance": round(best_distance, 3)})detectMultiScale的minNeighbors=5能过滤掉很多误检,但如果你发现漏检率高,可以降到 3。minSize=(80, 80)是经验值,摄像头离人太远时把脸裁得很小,特征提取质量会下降,宁可检测不到也别提一个糊的特征。每次识别的耗时大头在face_encodings上,如果人脸框比较大,耗时可能到 100ms 级别,所以前端截帧频率一定不能太高。
4. 从源码到可运行项目:环境配置、数据库迁移与启动排错
4.1 Python 虚拟环境与依赖版本:为什么不要直接用 pip install 全家桶
拿到源码后最忌讳的是直接pip install -r requirements.txt然后跑python manage.py runserver。因为 face_recognition 依赖 dlib,而 dlib 在 Windows 上需要 CMake 和 Visual Studio 编译工具,很多人卡在这一步半小时。如果你用的是 Windows,最省事的方式是用 pip 安装预编译的 dlib wheel:
pip install dlib==19.24.0 pip install face_recognition pip install django opencv-python numpy如果pip install dlib报错,说明你的 Python 版本太高或者没有装 Visual C++ Build Tools。建议直接用 Python 3.9 或 3.10,这两个版本对应的 dlib wheel 相对好找。OpenCV 的包名要注意,开发用opencv-python,如果你还需要用到cv2.VideoCapture读取视频文件,没问题;但如果涉及视频编解码的扩展功能,可以再装opencv-contrib-python,不过课程设计一般用不到。
python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # Linux/macOS pip install django==4.2 opencv-python face_recognition numpyDjango 4.2 和 Python 3.10 组合比较成熟。如果源码里用的是 Django 3.x,跑在 4.2 上大概率会出现django.core.exceptions.ImproperlyConfigured错误,主要原因是urls.py里的url()函数被移除了,要改成re_path()。这个排错思路很重要——先看报错是来自哪一层,再去对应改。
4.2 数据库迁移与初始数据:Django ORM 和 SQLite 的坑
源码里如果给了models.py,那么第一次运行时需要做makemigrations和migrate。很多人直接运行python manage.py migrate发现没有建出人脸表,因为 Django 只会迁移当前项目里注册过的 app。先确认settings.py的INSTALLED_APPS里有你的 app 名字。
python manage.py makemigrations recognition python manage.py migrate如果migrate报错说表已存在,可以用python manage.py migrate --fake recognition跳过已有表。SQLite 文件默认在项目根目录,删掉重新迁移也是常见的后悔药。注意 face_recognition 的特征向量是一个 128 维 float64 数组,存进 SQLite 时用 BLOB 字段,Django 的BinaryField正好对应。读取时转 numpy 数组时一定要指定dtype=np.float64,不然距离计算的结果全是错的。
4.3 启动后常见报错:ModuleNotFoundError 和 OpenCV 路径问题
我在搜索引擎的热搜词里看到太多人碰到ModuleNotFoundError: No module named 'cv2',但已经装过 opencv。这种情况八成是因为虚拟环境和系统环境混用了。比如你在系统的 pip 下装了 opencv,又在 venv 里跑 Python,import 自然失败。用pip list看看你当前环境的包列表,别只看pip show输出。
另一个高频坑是 OpenCV 的cv2.error: OpenCV(4.4.0)在调用cvtColor或imdecode时报错。比如传 RGB 转 BGR 时遇到空的 numpy 数组,或者图片路径含中文导致imread返回 None。判断方法是在读图后先判断img is None,如果是 None,多半是路径或文件流问题。Django 里接收UploadedFile时不能直接传给cv2.imread,必须用np.frombuffer先转字节流,这个和本地读文件完全是两回事。
不过相比之下,Django 执行查询的时候也有经典坑:FaceProfile.objects.filter(name='张三').delete()在执行后如果同时有外键关联,可能报ProtectedError。简单的做法是改成逐条obj.delete()。这里的索引设计也很关键,识别时用 Python 遍历所有特征向量的距离是 O(n) 的,n 到几千时还行,上万后就要考虑向量索引了。课程设计一般几十个人,不用焦虑性能。
5. 课程设计答辩前必查的五个雷区:从 OpenCV 摄像头打到人脸阈值调优
5.1 摄像头打开失败的三种伪装
很多源码里会写cv2.VideoCapture(0)取摄像头,但在 Django 的 view 里,VideoCapture往往打开失败,返回的read()是(False, None)。原因有三种:一是笔记本的摄像头被其他程序占用,比如 Zoom 或微信;二是索引不对,多摄像头电脑上0不一定是你想要的那个;三是在 Web 容器里没有访问摄像头的权限。
应对思路很简单:前端用getUserMedia拿摄像头帧,后端完全不知道摄像头存在,这样避开了所有权限和索引问题。如果你确实要在后端直接开摄像头做演示,可以写一个camera_test.py独立脚本验证,不要在 Django 里调试。
import cv2 cap = cv2.VideoCapture(0) if not cap.isOpened(): print("camera open failed") exit(1) ret, frame = cap.read() if ret: cv2.imwrite("test.jpg", frame) cap.release()5.2 人脸特征库为空导致识别结果永远显示「unknown」
这是课程设计 demo 里最丢人的场景——注册了十几个人,但识别时说谁都不认识。原因绝大多数是FaceService单例里的known_faces是空的,因为你启动 Django 时还没加载数据库。我一般会在apps.py的ready()方法里调用一次FaceService().load_from_db(),这样只要 Django 一启动,特征库就在内存里。
# recognition/apps.py from django.apps import AppConfig class RecognitionConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "recognition" def ready(self): from .service import FaceService FaceService().load_from_db()注意ready()会在migrate时也执行,如果此时数据库表还没建好,load_from_db可能报错。稳妥一点是在ready()里包一层try-except,等系统跑起来后再手动触发加载。很多老手会鄙视这种写法,但课程设计阶段能用、稳定就行。
5.3 阈值 0.5 不是万能的:光线和分辨率的影响
人脸特征提取对光照非常敏感。同一个学生在教室白炽灯下注册,走廊昏暗灯光下识别,距离可能从 0.3 跳到 0.7,直接变成「陌生人」。我踩过的坑是阈值定死 0.5,结果下午四点阳光照在脸上识别失败,换成 0.6 又好用了。但 0.6 又会带来误判风险。
我的建议是不要用固定阈值,而是保存这个人最近几次识别成功的平均距离,动态调整这个人自己的阈值。课程设计如果嫌麻烦,至少准备两张注册照片:一张正常光、一张暗光。代码里可以这样处理:
# 注册时支持同一人多张照片 encodings = [] for path in ["zhangsan_1.jpg", "zhangsan_2.jpg"]: img = face_recognition.load_image_file(path) enc = face_recognition.face_encodings(img) if enc: encodings.append(enc[0]) # 存成 dist 列表,比对的用时取最小距离5.4 前端视频帧 POST 被 Django CSRF 拦截
Django 默认所有 POST 请求都要带 CSRF token,如果前端只用fetch不带 token,会收到 403。课程设计里最简单的解法是在后端 view 上加@csrf_exempt,但如果你不想用装饰器,可以改前端代码去读 cookie 里的csrftoken。注意如果你用@csrf_exempt,需要在请求的Content-Type上用application/json或multipart/form-data,别让 Django 的request.FILES拿不到文件。
5.5 模型加载慢的「第一次请求卡死」错觉
face_recognition 底层加载 Dlib 的模型文件大概需要 2~3 秒,第一次请求时你会觉得像是死锁了。实际上后端正在加载模型,第二次请求就快了。我一般在ready()里强制调用一次face_recognition.face_encodings(np.zeros((100, 100, 3), dtype=np.uint8))来热身,这样第一个用户进来时就无感。这个技巧说不上多高级,但演示时很管用。
6. 把课程设计升级成真正的「系统」:加一张 Redis 缓存表和前端体验优化
如果你只想拿个学分,前面五章够了。但如果答辩老师问你「并发来了几百个人怎么办」「识别记录存在哪」,你得有一个可持续的答案。我通常会做两件事:一是把特征向量缓存从 Django 进程内存挪到 Redis,二是增加识别流水表记录每一次识别的距离和结果。
Redis 缓存的好处是 Django 多进程部署时每个 worker 共享同一份特征库,否则用 gunicorn 跑 4 个 worker,每个都要加载一次模型和特征,内存翻四倍。缓存 key 用face:known_faces,value 存 pickle 序列化后的(names, encodings)列表,读取时反序列化。这个改动很小,但也体现你理解 Web 应用的状态管理。
import redis import pickle r = redis.Redis(host="localhost", port=6379, db=0) def load_known_faces_from_redis(): raw = r.get("face:known_faces") if raw: names, encodings = pickle.loads(raw) return names, encodings # 从数据库加载并写回缓存 profiles = FaceProfile.objects.all() names = [p.name for p in profiles] encodings = [np.frombuffer(p.encoding, dtype=np.float64) for p in profiles] r.set("face:known_faces", pickle.dumps((names, encodings)), ex=3600) return names, encodings前端方面,建议把截帧做防抖,检测到人脸后再执行识别请求,人脸没了就停止。用canvas把video帧缩小成 320 宽再转 blob,POST 出去的字节数从几 MB 降到几十 KB,后端响应时间直接减半。识别结果用「绿色框 + 姓名」叠加在视频上,比单纯显示文字更像真实产品。
另一个值得加的功能是识别记录。每次识别都往recognition_log表里写时间、照片路径、识别结果和距离分数。下面这张表是你答辩时最好的素材,能证明系统有数据沉淀能力,而不是一次性 demo。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 主键自增 |
| name | varchar | 识别出的姓名,unknown 也算 |
| confidence | float | 特征距离,越小越可信 |
| image_path | varchar | 保存当前帧的截图路径 |
| created_at | datetime | 识别时间 |
这个表的查询要加上created_at索引,否则数据量一大,Django 的order_by("-created_at")会全表扫描。做课程设计时展示「查某个人今天的识别记录」,用 Django 的 ORM 一条filter(name=xxx)就能完成,但你是否知道如何优化它,是拉开分数的地方。
最后说一个我自己的习惯:每次拿到这类人脸识别源码包,第一件事不是跑起来,而是抽出它的识别核心单独写个脚本,用自己手机里的照片测试曲线。真正的技术消化不是能在 Django 页面上看到一个名字,而是你能说出「为什么这个距离 0.48 算认识,0.52 算不认识」。当你亲手调过阈值、看过日志里的 fake 记录,答辩时老师问什么你都不慌。希望这篇实战笔记帮到你,让你少花时间在环境依赖上,把精力放在真正体现能力的模块设计上。
本文还有配套的精品资源,点击获取