Python+Django+OpenCV疲劳检测系统设计与实现 简介一份面向计算机与软件工程专业毕业设计场景的疲劳检测系统文档适合正在做 Python、Django 与 OpenCV 相关选题或需要参考论文结构、系统功能划分与疲劳判定思路的学生。资源包仅含 1 个 docx 文件约 1.01MB内容为学士学位论文完整稿包含中英文摘要、绪论、开发背景、国内外研究现状及目录等部分可据此了解选题背景与写作框架。文档围绕眼动信号与人脸判断展开借助 OpenCV 检测眼睛闭合程度并结合面部表情、眨眼频次表征疲劳状态系统层面采用 Python 与 MySQL涉及图像识别、图片分析、照片管理等功能模块对数据库设计与平台管理表述也有参考价值。已有 366 人学习可作为毕业设计开题、论文撰写和技术路线梳理的对照材料。1. 需求先立住做疲劳检测毕设的人十个里有八个一上来就想训练 CNN 判闭眼数据集没凑齐、显卡跑不动、答辩现场一演示就卡成幻灯片。真正能交上去、现场还能跑通的版本往往是最朴素的那套Python 起一个 Django 服务浏览器调摄像头拍一张脸OpenCV 在后台切出人脸和人眼区域算出闭眼占比和哈欠次数落一条记录进 MySQL前台用柱状图把结果摆出来。这套基于 Python Django OpenCV 的疲劳检测系统源码与数据库设计要解决的就是检测能跑、页面能演示、数据能复盘这三件事适合正在做毕业设计、课程设计或者要给车队、产线做一套低成本岗前状态筛查的读者参考。2. 需求拆解与模块边界六个页面背后的数据流2.1 每个页面到底承担什么职责这套系统的功能清单看着长摊开其实就六个页面而且它们不是并列关系是围绕一张照片信息表长出来的。登录页负责身份用户名、密码加验证码三重校验为的是让每次检测结果能挂到具体人头上首页是统计看板展示系统总用户数、总图片数、当前年月再配一张用户 24 小时行为图表让人一眼看到这个账号最近什么时候在测图片识别页是入口支持摄像头实时拍照和本地图片上传两条路照片分析页把单次或多次检测的眼睛状态、睁眼情况、打哈欠占比用柱状图和饼图摊开供用户复核照片管理页是全集列表能按条件搜索也能删掉拍糊的、识别失败的脏数据用户管理页处理改密码、改资料和新增用户新增只要姓名、密码、手机号三个字段。理解这层关系很重要因为它决定了后面所有代码的走向。一次检测动作的产物只有一条 PhotoInfo 记录首页的统计是对这张表做 count分析页是对这张表做 group by管理页是对这张表做分页查询。很多同学把功能模块写成六个互不相干的 Controller每个里面各写一套查库逻辑改一个字段要动六处这就是没想清楚数据流的代价。从交付角度看这六个页面的优先级也不是平的。图片识别和照片管理是核心链路必须一次做对首页统计和分析图表属于加分项做得好答辩时很出彩做不完也不影响主流程登录和用户管理是底座但可以直接借用 Django 自带的能力省下来的时间全部砸到识别准确率上。我一般建议学生按这个顺序推进先跑通上传一张图 → 返回一个结果的最小闭环再补列表和删除最后才是统计图表。2.2 技术选型为什么是 Python Django OpenCV MySQL技术栈是题目给定的但答辩时老师一定会问为什么不用别的所以选型理由要提前想明白。Python 在这套系统里干两件事一是驱动 OpenCV 做图像运算二是作为 Django 的开发语言。它的优势在于图像处理生态完整numpy 数组和 OpenCV 的 Mat 之间转换几乎是零成本写一个检测函数几十行就能收工。Web 框架选 Django 而不是 Flask核心理由是毕设周期内能少写代码。Django 自带 ORM、自带用户认证、自带 admin 后台用户管理页里新增用户这个功能用 admin 稍微配置一下就能用省下的时间足够你把检测算法调三轮。ORM 还有个隐性好处数据库表结构由模型类定义改字段的时候跑一条 migrate 命令就同步了不用手写 ALTER TABLE也不会出现论文里写的字段名和代码里对不上的翻车现场。OpenCV 承担检测重点是它不需要 GPU、不需要预训练大模型Haar 级联分类器在普通笔记本上单帧处理能压到几十毫秒。这一点对答辩极其关键——你永远不知道答辩教室的电脑是什么配置能不能联网有没有独立显卡。用 Haar 至少保证最坏情况下也能跑起来而深度学习方案一旦环境出问题现场就是黑匣子解释不清。MySQL 存业务数据图片文件本身落磁盘表里只存文件名和判定结果。这个拆分要写进论文因为很多人会把图片二进制塞进数据库几万张图之后查询直接拖垮这是典型的选型错误。2.3 数据库概念结构与照片信息表概念结构用 E-R 图描述用户实体和照片实体是一对多关系一个用户可以上传多张照片一张照片只属于一个用户照片实体的属性包括照片 ID、图片名称、眼睛状态、是否打哈欠、是否疲劳、创建时间。这套结构简单到几乎没有设计空间但恰恰因为简单字段类型的选择就更容易出错。原始设计里的照片信息表是这样定的picid 为 varchar(32) 主键picname 为 varchar(50)eyes 为 varchar(50)haqian 为 varchar(2)pilaov 为 varchar(2)addtime 为 varchar(20)除 picid 外均不可为空。字段含义字段名类型长度可空备注照片 ID主键picidvarchar32是UUID 去横线32 位图片名称picnamevarchar50否存原始文件名眼睛状态eyesvarchar50否睁眼 / 闭眼 / 未检出是否打哈欠haqianvarchar2否是 / 否是否疲劳pilaovvarchar2否是 / 否创建时间addtimevarchar20否yyyy-MM-dd HH:mm:ss这张表能跑但有两处我一般会动手改。第一处是 haqian 和 pilaov 用 varchar(2) 存是/否中文在不同字符集下占的字节数不一样某些环境里 varchar(2) 只够存一个汉字插入直接报错。稳妥做法是改成 char(1) 存 1/0或者干脆用 tinyint前后端约定好 1 表示是、0 表示否判断逻辑还更快。第二处是 addtime 用 varchar 存时间排序和范围查询都会退化成字符串比较2024-9-1 和 2024-10-1 按字典序排会出错。改成 datetime 类型Django 里用 DateTimeField(auto_now_addTrue)首页那个当前年月统计和 24 小时行为图表的查询会顺手很多。提示论文里写的是 varchar代码里改了类型一定要在论文的数据库设计章节同步更新答辩老师翻论文对代码是常规操作。3. 检测算法设计从人脸框到疲劳判定的三级指标3.1 人脸与人眼定位Haar 级联的参数怎么调整条检测链路的第一层是人脸。OpenCV 自带 Haar 级联分类器加载 xml 模型文件之后调 detectMultiScale 就能拿到人脸矩形框代码量极小import cv2 # 加载 OpenCV 自带的级联模型路径用 cv2.data.haarcascades 拿避免硬编码 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml) eye_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_eye.xml) def detect_face_and_eyes(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 直方图均衡化逆光或室内灯光偏暗时能明显提升检出率 gray cv2.equalizeHist(gray) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, # 每次缩放 10%越小越慢但越不容易漏 minNeighbors5, # 一个候选框至少被复核 5 次才留下抑制误检 minSize(80, 80) # 小于 80x80 的人脸直接丢弃过滤远处背景 ) if len(faces) 0: return None, [] # 只取面积最大的人脸多人同框时以主体为准 x, y, w, h max(faces, keylambda f: f[2] * f[3]) # 人眼几乎不会出现在人脸框的下半部分只在上半区搜索 roi_gray gray[y:y h // 2, x:x w] eyes eye_cascade.detectMultiScale(roi_gray, 1.1, 4, minSize(20, 20)) return (x, y, w, h), eyes这三个参数是踩坑重灾区。scaleFactor 设成 1.1 意味着图像金字塔每次缩小 10%一张 640×480 的图大概扫十几层耗时可控调成 1.05 检出率会上去一点但耗时接近翻倍如果是视频流实时检测掉帧会很明显。minNeighbors 是误检的闸门设成 2 或 3 的时候墙上的插座、衣服上的花纹都可能被认成人脸设到 8 以上又容易漏检侧脸。minSize 这一项最容易被忽略不设的话远处的路人脸也会被框进来导致程序在多个检测结果之间来回跳。人眼定位这里有个必须提前讲清楚的事实Haar 的眼睛分类器是用睁眼样本训练的闭眼时大概率检测不到眼睛。所以检测不到眼睛这件事本身就携带信息含混地把它当作失败来处理是后面判定逻辑出错的主要源头。我的做法是把人脸框上半区裁出来先跑一次眼睛检测检到了说明睁眼再对这个区域做灰度阈值二值化用暗像素占比做二次确认。3.2 闭眼判定与 PERCLOS单帧不可信要看窗口单帧判闭眼的可靠性有限眨眼那 100 到 300 毫秒里任何一次抓拍都可能抓到闭眼按帧判断会满屏疲劳。行业里通用的做法是 PERCLOS即统计一个时间窗口内闭眼帧所占的比例而不是看某一帧。这套系统的检测对象是一张上传的静态照片没法直接算时间窗口所以我一般做两步处理单张图给出瞬时状态多张图按 addtime 排序后回溯 30 秒或 60 秒算占比。先看单帧的眼睛状态判定思路是裁出人眼区域后统计暗像素比例import numpy as np def eye_state(eye_roi): 返回 open / close / unknown输入是人眼小块彩色图 if eye_roi is None or eye_roi.size 0: return unknown gray cv2.cvtColor(eye_roi, cv2.COLOR_BGR2GRAY) gray cv2.GaussianBlur(gray, (5, 5), 0) # 反向二值化瞳孔和睫毛变白眼白变黑 _, thresh cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY_INV) dark_ratio np.count_nonzero(thresh) / thresh.size # 经验阈值闭眼时睫毛和眼睑占满暗像素比例明显升高 if dark_ratio 0.42: return close elif dark_ratio 0.08: return open return unknown0.42 和 0.08 这两个数不是拍脑袋来的是拿几十张自己拍的样本调出来的睁眼时暗像素集中在瞳孔和睫毛比例通常在 0.1 到 0.3 之间闭眼时整条眼缝都是暗的比例会冲到 0.5 以上。不同人种、是否戴眼镜、光照强弱都会让这两个阈值偏移戴黑框眼镜的同学尤其明显眼镜框本身就会被算进暗像素导致睁眼也被判成闭眼。所以这两个数在论文里要写成经验阈值可通过配置文件调整别写成定论。有了单帧状态多帧融合就有了基础。窗口逻辑是取当前时间往前 30 秒内的全部检测记录统计 eyes 字段为闭眼的条数占总条数的比例这个比例就是简化版 PERCLOS。正常清醒状态下这个值一般在 0.15 以下超过 0.3 就该提示超过 0.4 属于明显疲劳。这个指标比是否疲劳这个布尔值有说服力得多答辩时讲清楚它的来源比干巴巴说一句准确率 90%有力。3.3 打哈欠检测与三路指标的融合打分打哈欠靠嘴部区域判断。OpenCV 没有现成的嘴部分类器haarcascade_smile 只能识别微笑且对张嘴无效所以只能自己做从人脸框下半区裁出嘴巴 ROI二值化后找最大轮廓算轮廓的外接矩形高宽比比例明显大于常态就是张嘴。def mouth_state(face_roi): 输入人脸下半区返回 yawn / normal / unknown gray cv2.cvtColor(face_roi, cv2.COLOR_BGR2GRAY) _, thresh cv2.threshold(gray, 50, 255, cv2.THRESH_BINARY_INV) contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return unknown c max(contours, keycv2.contourArea) _, _, w, h cv2.boundingRect(c) if w 0: return unknown ratio h / float(w) # 闭嘴时高宽比接近 0.2~0.4打哈欠张嘴会拉到 0.6 以上 return yawn if ratio 0.6 else normal三路指标闭眼占比、眨眼频次异常、哈欠次数各自独立最后要合成一个是否疲劳的结论写进 pilaov 字段。最省事的做法是加权求和指标计算方式权重触发阈值闭眼占比 PERCLOS30 秒窗口内闭眼条数 / 总条数0.5 0.30眨眼频次异常60 秒内闭眼次数 10 或 300.2越界记 1 分打哈欠次数60 秒内哈欠条数0.3≥ 2 次记 1 分总分超过 0.6 判为疲劳。权重的分配逻辑是PERCLOS 是学术界公认最可靠的单一指标所以给一半权重哈欠是强信号但发生频率低给 0.3眨眼频次波动大、个体差异明显只给 0.2 做辅助修正。这套打分表建议直接写进论文第三章比只写结合多项指标判断要实在得多。需要提醒的是这套算法在单张静态图上只能给出当前帧是不是闭眼/张嘴真正的疲劳判定依赖历史记录的窗口统计。所以第一次使用系统的用户头几条记录一定是不疲劳这是正常的冷启动别在验收时把这条当成 bug 报上去。我的处理方式是在分析页加一句说明告诉用户至少上传 5 张不同时刻的照片才有参考价值。4. Django 后端落地模型、视图与识别链路4.1 数据模型与表结构映射Django 的 ORM 把数据库表定义搬到 Python 类里模型类写完之后 makemigrations 和 migrate 两条命令下去表就建好了。按照第 2 章的字段调整建议模型定义如下# models.py from django.db import models class PhotoInfo(models.Model): # 主键用 UUID避免自增 ID 在前端暴露业务量也方便分布式扩展 picid models.CharField(照片ID, max_length32, primary_keyTrue) picname models.CharField(图片名称, max_length50) eyes models.CharField(眼睛状态, max_length10, defaultunknown) # 是否打哈欠 / 是否疲劳1是 0否比存中文稳 haqian models.SmallIntegerField(是否打哈欠, default0) pilaov models.SmallIntegerField(是否疲劳, default0) # 用 DateTimeField范围查询和排序都正常走索引 addtime models.DateTimeField(创建时间, auto_now_addTrue) # 关联用户方便首页做当前用户行为图表 owner models.ForeignKey(auth.User, on_deletemodels.CASCADE) class Meta: db_table photo_info ordering [-addtime] # 默认按时间倒序列表页直接受益几个设计点值得说明。picid 用 CharField 而不是 AutoField是为了在文件落盘时就确定唯一文件名先有 ID 再存文件避免文件存了但数据库插入失败产生的孤儿文件。owner 外键指向 Django 内置的 auth.User用户管理页的增删改可以直接复用 admin登录态也不用自己维护 session。addtime 用 auto_now_add插入时自动填当前时间前端不用传也不存在时区格式不统一的问题。照片管理页的搜索功能本质上就是在这个模型上叠加查询条件。眼睛状态、是否疲劳、时间范围三个条件组合用 Django 的 Q 对象拼一下就行不需要写原生 SQL。分页用 Paginator每页 10 条和原始设计里的列表页对得上。4.2 上传识别视图一条请求走完五步图片识别的视图是整个系统最核心的一段代码它要把收文件、存磁盘、读图像、跑检测、写数据库五件事串起来中间任何一步出错都要给前端明确的反馈。# views.py import os, uuid import cv2 import numpy as np from django.conf import settings from django.http import JsonResponse from django.shortcuts import render from .models import PhotoInfo from .detector import analyze_frame # 封装第 3 章的检测逻辑 def photo_detect(request): if request.method GET: return render(request, detect.html) file request.FILES.get(image) if not file: return JsonResponse({code: 400, msg: 没有拿到图片}) # 第一步生成 UUID 做文件名避开中文名和重名问题 picid uuid.uuid4().hex ext os.path.splitext(file.name)[1].lower() or .jpg save_path os.path.join(settings.MEDIA_ROOT, photos, picid ext) # 第二步分块写盘避免大图一次性读进内存 with open(save_path, wb) as f: for chunk in file.chunks(): f.write(chunk) # 第三步用 imdecode 读盘cv2.imread 在中文路径下会返回 None img cv2.imdecode(np.fromfile(save_path, dtypenp.uint8), cv2.IMREAD_COLOR) if img is None: return JsonResponse({code: 415, msg: 图片格式不支持}) # 第四步跑检测analyze_frame 内部含人脸过滤和三级指标计算 result analyze_frame(img) # 第五步入库字段和前端返回保持一致 PhotoInfo.objects.create( picidpicid, picnamefile.name, eyesresult[eyes], haqianresult[haqian], pilaovresult[pilaov], ownerrequest.user ) return JsonResponse({code: 200, data: result})第三步那个中文路径的坑必须单独说cv2.imread 底层是 C 实现遇到中文路径直接返回 None报错信息还只有一句image is None很多人对着这段代码查半天。np.fromfile 先把文件读成字节流再让 imdecode 解码就绕开了文件系统编码的问题这一行改动能省掉一整晚的排查时间。返回值统一用 {code, msg, data} 三段式前端只看 code 决定走成功还是失败分支data 里放 eyes、haqian、pilaov 三个字段页面拿到之后直接渲染结果卡片。这种约定在前后端分离的项目里是标配但在毕设里常被忽略很多同学的做法是后端直接 return render 一个拼好的 HTML结果前端想加个 loading 动画都加不进去。摄像头拍照走的是另一条路前端用 getUserMedia 拿到视频流canvas 截帧转 base64POST 到同一个接口后端把 base64 解成 ndarray 之后复用 analyze_frame。前端这段逻辑浏览器兼容性差异挺大Safari 对 getUserMedia 要求 HTTPS 或 localhost答辩现场如果是用局域网 IP 访问摄像头会直接调不起来。稳妥办法是提前在本地起服务演示或者准备几张图片做备用。4.3 照片分析与统计接口图表数据从哪来照片分析页要出柱状图和饼图数据不可能是前端硬编码的得由后端按条件聚合。Django 的 annotate 加 Count 能直接生成 group by 查询不需要手写 SQL# views.py from django.db.models import Count from django.utils import timezone from datetime import timedelta def analysis_data(request): # 默认看最近 7 天前端传 days 参数可以改 days int(request.GET.get(days, 7)) start timezone.now() - timedelta(daysdays) qs PhotoInfo.objects.filter(ownerrequest.user, addtime__gtestart) # 疲劳占比饼图数据 fatigue_dist list(qs.values(pilaov).annotate(cntCount(picid))) # 按天统计检测次数和疲劳次数柱状图数据 daily (qs.extra(select{day: date(addtime)}) .values(day) .annotate(totalCount(picid), fatigueCount(picid, filtermodels.Q(pilaov1))) .order_by(day)) return JsonResponse({ code: 200, data: {fatigue_dist: fatigue_dist, daily: list(daily)} })这里有一个设计上的取舍值得写进论文按天聚合用的是数据库的 date() 函数意味着这条查询绑定了 MySQL换成别的数据库要改写法。如果追求跨库兼容可以在 Python 层把查询结果取出来再用 itertools.groupby 分组代价是多查一批数据到内存。毕设场景下数据量不会大两种都行但要在文档里写清楚选了哪种、为什么。前端的图表建议用 ECharts配置项少、中文文档全饼图和柱状图各二三十行配置就能出效果。数据接口返回的 fatigue_dist 是 [{pilaov: 1, cnt: 8}, {pilaov: 0, cnt: 22}] 这种结构前端做个映射把 1 显示成疲劳、0 显示成正常就行。首页那个用户 24 小时行为图表同理把 addtime 按小时分组统计即可SQL 层用 HOUR(addtime) 提取小时数。另外提醒一点统计接口一定要带 owner 过滤。原始设计里的首页统计是系统总用户数、总图片数那是管理员视角普通用户登录后看到的应该是自己的数据。这两套口径在代码里是同一个视图加个 if 判断但如果忘了加A 用户能看到 B 用户的检测记录答辩时被问到数据隔离就不好回答了。5. 常见问题排查四类最容易翻车的现场5.1 摄像头能打开但人脸框一直不出现现象是页面视频流正常点了拍照也返回成功但结果永远是未检出人脸。原因通常有三个层次最表层是光照逆光或者室内只有一盏顶灯的时候人脸整体偏暗equalizeHist 拉不回来中间层是分辨率很多笔记本前置摄像头默认输出 1280×720缩放到检测尺寸时人脸只剩几十像素低于 minSize 直接被过滤最深层是分类器选错用了 haarcascade_frontalface_alt 这种对正脸要求极高的模型稍微侧一点就检不到。解决顺序是从便宜到贵先在拍照前加一步亮度检测画面平均灰度低于 60 就提示光线不足请面向光源然后把 minSize 从 (80, 80) 降到 (60, 60)同时把输入图缩放到宽 640 再做检测最后换回默认的 haarcascade_frontalface_default它对侧脸的容忍度比 alt 版本好。如果三步做完还是检不到那就是摄像头本身的问题换一台机器验证别在代码里继续折腾。5.2 白天测得挺准晚上全是疲劳现象很典型下午演示一切正常晚上或者拉上窗帘再测十条记录里八条判疲劳。原因是第 3 章那两个暗像素阈值在低照度下彻底失效环境一暗整张脸都是暗的二值化之后人眼区域几乎全白dark_ratio 轻松超过 0.42稳定被判成闭眼。解决要分两层。算法层加一步自适应阈值用 cv2.adaptiveThreshold 替代固定阈值 60让阈值跟着局部亮度走业务层加一个前置判断计算整张人脸区域的平均灰度低于 50 就直接返回图像质量不足请改善光照后重试而不是硬着头皮给一个错误结论。后者更重要宁可让系统说测不了也别让它自信地给出错误答案这在答辩演示时是保命设计。5.3 上传大图后页面转圈最后 500 错误现象是手机拍的 4000×3000 照片一上传请求卡住十几秒然后报服务器内部错误。原因是原图直接送进 detectMultiScale图像金字塔要扫十几层每层都是千万像素级的卷积运算内存和 CPU 同时顶到天花板再加上 Django 默认的上传内存限制请求体超过阈值就被拒。解决分三步上传前在前端用 canvas 压缩到长边 1280这一步能砍掉 90% 的数据量后端在 analyze_frame 入口统一 resize 到宽 640检测完如果需要展示原图再按比例把坐标映射回去Django 配置里把 FILE_UPLOAD_MAX_MEMORY_SIZE 调到 10MBDATA_UPLOAD_MAX_MEMORY_SIZE 同步调整。三步里前端压缩收益最大而且用户几乎感知不到画质损失因为检测只用得到人脸那几百个像素。5.4 统计图和照片列表对不上数现象是照片管理页显示 30 条记录分析页的饼图加起来只有 25 条。原因一般是统计接口用了额外的过滤条件比如只统计最近 7 天而列表页是全量或者统计时用了 distinct 去重而 picid 在某些异常情况下重复了。还有一种更隐蔽的情况删除照片时只删了数据库记录磁盘文件还在或者反过来导致两边口径不一致。解决办法是把统计口径写成一个常量放在配置里列表页和统计页都引用同一个时间范围定义删除操作封装成一个事务先删数据库记录再删文件文件删除失败只记日志不回滚避免因为磁盘权限问题导致数据库记录删不掉。验收的时候拿同一时间段的数据两边对一遍数字对得上才算过。6. 验收与答辩用例怎么造指标怎么讲系统测试这一章最容易被写成流水账点击登录按钮登录成功测试通过这种写法老师看三行就翻页了。真正有说服力的做法是把测试拆成功能测试和可用性测试两条线每条线都有明确的输入、预期和判定标准。功能测试围绕核心链路造用例重点是异常路径而不是正常路径。正常路径谁都能跑通异常路径才体现你想没想过边界。建议至少准备下面这张表里的六条每条都实际跑一遍并截图存档用例编号测试场景输入预期结果判定F-01正常登录正确用户名 密码 验证码跳转首页显示当前用户通过F-02验证码错误正确账号密码 错误验证码停留登录页并提示通过F-03上传清晰正脸照640×480 室内光照片返回眼睛状态记录入库通过F-04上传无人脸图风景照返回未检出人脸不写库通过F-05上传超大图4000×3000 照片自动压缩后正常识别通过F-06跨用户访问A 用户请求 B 的记录 ID返回无权限不泄露数据通过F-04 和 F-06 这两条是加分项。前者证明你考虑了误输入后者证明你想过数据隔离很多毕设系统在这两点上是空的。可用性测试量化页面响应别写运行流畅这种形容词。用浏览器开发者工具的网络面板记录登录、上传识别、列表加载三个动作的耗时各测五次取平均。一般标准是页面跳转 1 秒内上传识别 3 秒内含网络传输列表查询 500 毫秒内。达不到就去看慢在哪是图像处理耗时还是数据库查询没走索引。addtime 字段如果按第 2 章的建议改成了 DateTimeField记得在模型里加 db_indexTrue否则按时间倒序的列表页在数据量上千之后会明显变慢。答辩环节真正拉开差距的是演示策略不是算法多先进。我的建议是准备三组对照素材一组清醒状态的正脸照、一组明显闭眼的照片、一组打哈欠的照片每组三到五张提前跑一遍确认结果符合预期。现场演示时先走图片上传这条稳定路径把三组结果依次展示让老师看到判定确实有区分度摄像头实时检测放在最后作为还支持实时采集的补充即使现场光线不理想也不影响主体印象。被问到准确率时不要张口就说 95%。更稳妥的讲法是拆开说人脸检出率在正常光照下接近百分之百闭眼判定的单帧准确率受光照和眼镜影响在自建的小样本集上大约八成疲劳结论依赖 30 秒窗口的 PERCLOS 统计样本量越大越稳。然后主动说出局限——没做多人脸跟踪、没处理戴墨镜的情况、冷启动阶段结论不可靠。主动暴露边界的回答比一个漂亮但站不住的数字可信得多。还有一个细节值得提前准备老师很可能会问为什么不用深度学习。标准答案是成本与可解释性——Haar 加规则的方式每个判定都能指到具体的阈值和像素比例出问题能调神经网络是个黑匣子答辩现场一旦判错你解释不了它为什么错。这个回答既真实又显得你想过取舍。从那以后我每次带人做这类检测系统都强制在验收前跑一遍那张异常用例表尤其是上传无人脸图和跨用户访问这两条十次里有六次能揪出问题。系统的可信度不来自它多聪明来自它在明显该失败的时候确实失败了。希望这套结构和这些踩过的坑能帮到你。本文还有配套的精品资源点击获取