简介:面向毕业设计场景的基于OpenCV的人脸识别打卡签到考勤管理系统基础框架,借助Python与OpenCV实现核心的人脸检测、识别和考勤记录流程,可基于树莓派等低成本设备部署,适合需要快速搭建人脸识别应用、或希望参考模块拆分方式的学生及开发者。资源包共25个文件,压缩后大小约718KB,包含Python脚本、OpenCV级联分类器XML、文本配置、Excel数据模板、HTML页面及图标图片等,目录结构按功能划分,便于二次开发。系统采用Haar级联检测人脸,使用LBPH算法进行人脸训练与识别,并通过Tkinter完成桌面界面,同时利用Flask与HTML实现网络实时图像推流及控制;此外还集成了验证码、xlsxwriter导出Excel和邮件发送等实用功能。目前已有3781人学习/下载,参考价值获得一定认可。对于准备毕业设计或课程设计的学生而言,它提供了一个可运行的基础框架,配合内置的数据集组织方式、训练脚本和配置文本,能够较顺畅地走通人脸录入、模型训练、考勤记录与导出的完整链路,剩余GUI交互和业务细节可结合自身需求继续完善。
1. 基于OpenCV的人脸识别打卡系统:看着简单,落地全是坑
基于OpenCV的人脸识别打卡/签到/考勤管理系统,这几年在毕设和中小型离线场景里被反复做,可大多数实现都败在同一个地方:方案太贪。一上来就上深度学习,人脸检测、特征提取、分类器一条龙,最后树莓派上跑不动,演示当场翻车。真正“基础完整”的做法,是用OpenCV自带的级联检测器加LBPH识别器,把样本采集、模型训练、实时识别、考勤落库、GUI交互这条链路完整闭环,每个环节都能讲清原理、能现场改参数。这个标题的潜台词其实是:识别链路已经给到你,GUI要按自己的业务需求去补全。适合三类人做:毕设方向是考勤签到、想快速拿出一版能答辩能演示的同学;公司或实验室里想做不依赖云服务、不出内网的刷脸考勤工具的人;以及想在树莓派上把一个完整项目串起来的嵌入式入门者。看下去之前先记住一句话:这套方案的价值不在算法精度,而在“离线、低算力、可交付”。
2. 识别方案选型:为什么最简基本库开发必选LBPH
2.1 三种内置识别器对比:树莓派上只有LBPH能扛
OpenCV的face模块里内置了三种人脸识别器:LBPH、EigenFace和FisherFace。EigenFace本质是PCA主成分分析,FisherFace是LDA线性判别分析,两者都要求训练图像的尺寸完全一致,训练前必须手动统一resize,而且对光照变化的容忍度很低。同一个员工上午在窗边、下午在背光位,识别结果可能完全不同。LBPH走的是局部二值模式:把图像切成网格,对每个像素邻域做二进制纹理编码,再统计直方图。它比较的是局部结构而不是全局像素,所以对光照、表情、小幅偏头的容忍度高得多,这也是它能在低算力设备上存活的核心原因。
训练代价上差距更明显。EigenFace做PCA要对整批训练图做矩阵分解,样本一多,树莓派的CPU就开始吃力;FisherFace还要求每个类别样本数均衡,否则判别方向会偏。LBPH只是统计直方图,几百张样本训练完几乎是一瞬间的事,模型文件也就几十KB,存成YAML放任意目录就能加载。识别阶段LBPH同样轻量,单帧匹配在树莓派上能控制在几百毫秒内,真正拖后腿的是人脸检测那一环,不是识别器本身。
“最简基本库开发”这几个字,其实就是在提示你:目标环境算力有限,依赖要少,选型要稳。OpenCV自带的人脸检测器加LBPH识别器,配合Python标准库里的sqlite3和Tkinter,就能覆盖从采集到交付的全部链路。这里必须提醒一句:cv2.face模块在最新版OpenCV里被拆分到了扩展包,如果你是本机自带的opencv-python,直接import cv2.face会报错。我一般先跑一条命令确认:python -c "import cv2.face",报错就去装opencv-contrib-python,而不是折腾换源或降版本。
| 识别器 | 原理 | 训练要求 | 光照鲁棒性 | 树莓派适配 | 适合场景 |
|---|---|---|---|---|---|
| LBPH | 局部二值纹理直方图 | 尺寸建议统一,不强制 | 较好 | 极佳 | 打卡签到、门禁 |
| EigenFace | PCA主成分分析 | 尺寸必须完全一致 | 差 | 一般 | 受控光照的演示 |
| FisherFace | LDA线性判别分析 | 尺寸一致且类别样本均衡 | 一般 | 一般 | 小数据集实验 |
2.2 LBPH四个参数和confidence阈值:识别率的分水岭都在这里
创建识别器就一行:recognizer = cv2.face.LBPHFaceRecognizer_create()。它的核心参数有四个:radius是邻域半径,默认1,半径越大特征越宏观,对光照越钝感,但细节也会丢,我一般设1到2;neighbors是邻域采样点数,默认8,降到4会明显变快但特征弱;grid_x和grid_y是图像分块数,默认8x8,分块越多直方图维度越高,树莓派上一味加网格只会变慢,8x8是性价比很高的档位,样本少时可以降到6x6让泛化好一点。
接下来是整条方案里最玄学的地方:阈值。predict()返回的是(label, confidence),confidence表示当前人脸和训练集的距离,越小越像。识别成功的判定条件是confidence小于阈值,但OpenCV里阈值默认值非常大,基本等于“来者不拒”。不设置阈值的话,任何人站在摄像头前都会被识别成你训练过的人,这是新手最常见的翻车点。阈值给多少没有通解,跟摄像头、光线、训练样本都有关系。我的做法是先跑一个批量自检脚本,把同一人多次识别的confidence分布和陌生人之间的最小值打印出来,取中间值作为初始阈值,不是拍脑袋填个80就完事。这个脚本我放在最后一章,直接抄。
人脸检测器也有两个选择:haarcascade_frontalface_default.xml和lbpcascade_frontalface.xml。前者是Haar特征,检测更准但慢;后者是LBP特征,快但偶尔漏检。PC上我默认Haar,树莓派上改用LBP级联,三个改动加起来能把检测耗时砍掉大半。两个文件都在OpenCV安装目录的data文件夹里自带,不需要联网下载,把路径复制出来用就好。
3. 最小考勤框架搭建:从样本采集到签到落库
3.1 样本采集、模型训练、实时识别三条脚本
先把目录结构定死,后面所有脚本都按这个路径走。我为每个员工建一个以整数ID命名的文件夹,ID同时是训练标签,也是数据库外键,一路保持一致。
attendance_system/ ├── dataset/ │ └── 1/ # 员工ID=1的样本,每人至少20张 │ ├── 000.jpg │ └── ... ├── stranger/ # 陌生人测试照片,自检脚本用 ├── trainer.yml # 训练输出的模型 ├── collect_faces.py ├── train_model.py ├── recognize.py └── attendance.db样本采集脚本要做三件事:只保存检测到人脸且尺寸达标的帧、转灰度后统一缩放到200x200、隔几帧再采集避免连续帧几乎一模一样。
import cv2 import os emp_id = 1 # 每个员工一个整数ID,建议从1递增 save_dir = f'dataset/{emp_id}' os.makedirs(save_dir, exist_ok=True) cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) count = 0 while count < 30: ret, frame = cap.read() if not ret: continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(100, 100) ) for (x, y, w, h) in faces: # 框往外扩一点,把额头和下巴包全,识别率会高一些 x = max(0, x - 10) y = max(0, y - 10) w = min(gray.shape[1] - x, w + 20) h = min(gray.shape[0] - y, h + 20) face = cv2.resize(gray[y:y + h, x:x + w], (200, 200)) cv2.imwrite(f'{save_dir}/{count:03d}.jpg', face) count += 1 # 跳过几帧再采下一张,避免照片几乎一样 for _ in range(5): cap.read() cv2.imshow('collect', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()detectMultiScale的minSize参数是(100, 100),远处的小脸和噪点不会被当成样本,这是保证训练集质量的第一道关卡。采集阶段就转灰度,训练脚本就不需要重复转换,少一道出错的可能。循环里那个for _ in range(5)是故意丢帧的,摄像头连续帧高度相似,全存进数据集只会让模型过拟合,没有信息增益。
训练脚本遍历dataset目录,把每张灰度图读进来,对应的目录名就是标签:
import cv2 import os import numpy as np faces, labels = [], [] for emp_id in os.listdir('dataset'): emp_dir = f'dataset/{emp_id}' if not os.path.isdir(emp_dir): continue for img_name in os.listdir(emp_dir): img = cv2.imread(f'{emp_dir}/{img_name}', cv2.IMREAD_GRAYSCALE) if img is None: continue # 单张损坏不要中断整个训练,跳过最稳 faces.append(img) labels.append(int(emp_id)) recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.setRadius(1) recognizer.setNeighbors(8) recognizer.setGridX(8) recognizer.setGridY(8) recognizer.train(faces, np.array(labels)) recognizer.save('trainer.yml') print(f'trained {len(faces)} samples')train()不接受纯列表,labels必须转成numpy数组,否则报错。读图时第二个参数传cv2.IMREAD_GRAYSCALE,如果漏了,train()收到的是三通道图,LBPH的输入维度不匹配,报错信息还不太直观。每个人20到30张样本起步,少于10张时识别结果的置信度波动会非常大,阈值很难调。
实时识别时,检测到人脸后,把ROI转灰度、缩放到200x200,再predict。这里最容易被忽略的是:训练和识别的预处理必须完全一致,训练用了灰度+200x200,识别时也必须走同一套,否则confidence整体漂移,阈值怎么调都别扭。
3.2 考勤表设计:签到/签退的判别规则
数据库两张表就够,不要在表结构上过度设计。employee表放员工信息,attendance表记录每天上下班。
CREATE TABLE employee ( emp_id INTEGER PRIMARY KEY, name TEXT NOT NULL ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, emp_id INTEGER NOT NULL, date TEXT NOT NULL, check_in TEXT, check_out TEXT, UNIQUE(emp_id, date) );我故意把日期和时间拆成两列。考勤最自然的查询单元是“某天某人”,date列做主查询条件会让SQL简单很多,跨天问题也被天然的隔离掉了。唯一约束UNIQUE(emp_id, date)是防重复针,数据库层面兜底,比应用层判断更可靠。
签到签退的判定逻辑我固定写成三段式:查当天该员工有没有记录;没有就插入check_in,表示上班;有记录但check_out为空就更新check_out,表示下班;两个时间都齐了就直接拒绝,防止反复刷脸刷出多条记录。
import sqlite3 from datetime import datetime def punch_in_out(emp_id): today = datetime.now().strftime('%Y-%m-%d') now = datetime.now().strftime('%H:%M:%S') conn = sqlite3.connect('attendance.db') cur = conn.cursor() row = cur.execute( "SELECT id, check_in, check_out FROM attendance " "WHERE emp_id=? AND date=?", (emp_id, today) ).fetchone() if row is None: cur.execute( "INSERT INTO attendance(emp_id, date, check_in) VALUES(?,?,?)", (emp_id, today, now) ) conn.commit() result = f'员工{emp_id} 上班打卡 {now}' elif row[2] is None: cur.execute( "UPDATE attendance SET check_out=? WHERE id=?", (now, row[0]) ) conn.commit() result = f'员工{emp_id} 下班签退 {now}' else: result = '今日已完整打卡,无需重复操作' conn.close() return resultcommit放在分支内部而不是函数末尾,是想保证每种路径都明确提交;如果是生产环境还要包一层try/except回滚,这里做演示保持可读性。迟到早退不建议写进SQL,因为上班时间在不同公司是配置项,今天九点明天可能调成九点半,放在Python里做一个可配置参数更灵活。
3.3 识别结果防抖:连续6帧同一人才算打卡成功
实时识别里最常见的毛病是:人一走动人一转头,label就跳变。如果一帧识别到张三就立刻触发打卡,一个路过的人都能替你签到。我给识别结果加了一个滑动窗口,连续N帧输出同一个label才进入打卡逻辑。
from collections import deque window = deque(maxlen=6) def is_stable(label): window.append(label) if len(window) < window.maxlen: return False return len(set(window)) == 1deque(maxlen=6)自动丢弃最旧的帧,set去重后长度为1说明连续6帧都是同一人。在25fps的帧率下,这个判定大约花0.24秒,既不会迟钝到影响体验,也不会被单帧噪声干扰。追求更强的防抖可以把maxlen提到10,但代价是刷脸通过要等近半秒,树莓派上这个延迟体感很明显,自己权衡。
3.4 查询最近考勤记录:给GUI准备数据源
识别落库之后,第一件事就是把记录查出来做验证。这个小函数在后面GUI的表格控件里会直接复用。
def show_recent(limit=30): conn = sqlite3.connect('attendance.db') rows = conn.execute( "SELECT e.name, a.date, a.check_in, a.check_out " "FROM attendance a JOIN employee e ON a.emp_id=e.emp_id " "ORDER BY a.date DESC, a.check_in DESC LIMIT ?", (limit,) ).fetchall() conn.close() for r in rows: print(f'{r[0]} {r[1]} 上班:{r[2]} 下班:{r[3] or "未签退"}')下班时间为空时用or '未签退'兜底,当天还在上班的员工不会显示成空串。LIMIT用?参数绑定而不是拼字符串,demo规模虽小,但写SQL的习惯要从第一行开始养成。
4. 按需求补全GUI:从命令行到可交付界面的最短路径
4.1 界面框架三选一:Tkinter、PyQt5、还是OpenCV窗口
标题里写明“需要按需求补全GUI”,说明项目的交付重点本来就不是界面,而是识别链路。补GUI之前先看运行环境:目标是树莓派的话,PyQt5的依赖在ARM架构下偶尔会遇到编译问题,而Tkinter在官方系统里基本预装,零成本启动。目标是PC演示想要好看,PyQt5当然更强,表格控件、样式、线程模型都成熟,但代价是部署时要多带一堆动态库。我的判断标准是:能在不联网、不装额外包的情况下跑起来的GUI,才符合“最简基本库开发”的定位。
OpenCV自带的imshow窗口不能作为交付界面,它放不了按钮、文本框、表格,键盘事件也只能处理单个按键,演示时一个“按q退出”已经是极限。所以界面框架选Tkinter:标准库自带、树莓派兼容好、出问题好搜方案。想让界面好看一点,就用同为标准库的ttk主题控件,换一套clam主题,观感和原生控件完全是两个档次,成本为零。
4.2 界面三块核心:视频预览、按钮、记录表格
布局我固定成这样:左上角视频预览,右上角操作按钮和状态文本,底部是考勤记录表格。视频预览不能直接把OpenCV的frame塞给Tkinter,两者色彩通道和图像格式都对不上,需要经过PIL转换。这里有个依赖上的取舍:Pillow不在标准库,想要实时预览就必须装它,它比PyQt轻得多,也是这套方案里唯一值得破例引入的第三方库。如果连Pillow都不想装,可以放弃视频预览,只用文本显示识别结果,树莓派上还能省一部分CPU,但答辩时没有画面说服力弱一些。
刷新帧用after定时器而不是while循环:
import tkinter as tk from PIL import Image, ImageTk class App: def __init__(self, root): self.root = root self.cap = cv2.VideoCapture(0) self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240) self.label = tk.Label(root) self.label.pack() self.update_frame() def update_frame(self): ret, frame = self.cap.read() if ret: rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = Image.fromarray(rgb) imgtk = ImageTk.PhotoImage(image=img) self.label.configure(image=imgtk) self.label.image = imgtk # 不保存引用会被垃圾回收,画面空白 self.root.after(15, self.update_frame)那行self.label.image = imgtk是Tkinter的经典坑。PhotoImage对象如果没有被实例属性引用,函数一结束就被回收,视频预览直接黑屏,这个Bug很隐蔽,报错都不会有。摄像头分辨率降到320x240,树莓派上这是预览流畅度和画质的平衡点,识别用的ROI从原始帧裁,预览只是降采样后的画面,两者互不影响。
4.3 识别循环别写进按钮回调:界面假死的根因
新手最典型的错误是:点击“开始打卡”按钮,然后在回调里写while True识别循环,窗口立刻无响应。Tkinter是单线程事件循环,回调阻塞等于整个界面冻结,按钮的按下状态都画不出来。
推荐做法是把识别也放进after调度链里,串行执行,不引入线程:
def handle_recognize(self): ret, frame = self.cap.read() if not ret: self.root.after(200, self.handle_recognize) return label, conf = self.predict(frame) if label is not None and conf < self.threshold: window.append(label) if is_stable(label): self.punch(label) # 调用上一章的punch_in_out,落库并更新状态栏 self.result_var.set(f'最近识别: 员工{label} 置信度{conf:.1f}') self.root.after(200, self.handle_recognize)每200毫秒调度一次识别,人站在摄像头前大约半秒到一秒内能出结果,体感是正常的。按钮回调里只启动这条调度链,停止时用self.running标志让下一次after不再续调,界面从头到尾不会卡死。识别本身不用每秒跑满,跑满除了让CPU风扇起飞,对打卡判定没有帮助。
4.4 手动签到入口:刷脸失败时的兜底
不管阈值调得多细,总有人刷不上:帽子、口罩、背光、新员工还没录入。GUI里必须留一个手工兜底:输入员工ID,点按钮写入当天考勤。这个功能在答辩演示时几乎一定会被问到,提前做了能省很多解释成本。
界面放一个输入框和一个“手动签到”按钮,回调里直接调用punch_in_out(),返回的提示文本显示到状态栏。手动补签在数据库层面和刷脸记录没有区别,统计报表不需要区分来源;如果导师要求区分识别方式和人工补签,在attendance表加一列source,刷脸写'face',手动写'manual',成本只有加一列SQL。
4.5 用ttk.Treeview展示考勤记录
记录页不需要分页,中小规模考勤几千条数据,Treeview一次全渲染也没压力。
from tkinter import ttk columns = ('name', 'date', 'check_in', 'check_out') tree = ttk.Treeview(root, columns=columns, show='headings') for col, text in zip(columns, ('姓名', '日期', '上班', '下班')): tree.heading(col, text=text) def refresh(): tree.delete(*tree.get_children()) for row in show_recent(50): tree.insert('', 'end', values=row)刷新按钮回调里先清空再插入,tree.delete(*tree.get_children())是Treeview的标准清空写法。columns元组顺序决定显示顺序,show_recent返回的元组结构和columns严格对应,拿到手直接喂给insert。列表太长时配一个ttk.Scrollbar,用yscrollcommand联动,这部分代码固定套路,照着文档接上就行。
5. 人脸考勤系统的避坑指南:5个最容易翻车的点
5.1 谁都匹配成同一个人:阈值没设等于没设
现象:换不同的人站在摄像头前,界面显示的永远都是同一个员工的名字,只有置信度数字在变化。原因分析:predict()返回的confidence根本没有和阈值比较,或者创建识别器之后忘了setThreshold()。OpenCV的默认阈值是极大值,所有距离都会小于它,等于来者不拒。解决:按第2章自检流程把阈值定出来。如果同一人置信度和陌生人之间没有清晰分界,说明训练样本量不足或光线差异过大,先去补样本,不要靠压低阈值硬扛,压低的结果是熟人也被拒绝。
5.2 白天识别晚上失灵:训练集的光照太单一
现象:上午在工位测试一切正常,傍晚开灯再试,同一张脸识别失败,有时连人脸框都检测不到。原因:检测阶段,背光下人脸对比度低,Haar特征对边缘响应变弱,框都没出来;识别阶段,训练样本可能就是同一时段采集的,光照分布太窄。解决:样本采集分散到多个时段,至少要覆盖上午、下午、室内灯三种环境。识别前对人脸ROI做直方图均衡化cv2.equalizeHist()再predict,注意训练和识别两端都要做这个操作,只做一端会让距离分布整体偏移,等于重新引入系统误差。
5.3 树莓派预览卡成幻灯片:分辨率与级联选型问题
现象:PC上35帧流畅,同样的代码搬到树莓派上,画面一帧一帧跳,识别的结果滞后两三秒才出来。原因:摄像头分辨率没降、Haar级联在VGA分辨率上逐帧扫描太慢,树莓派的CPU处理不过来。解决:摄像头分辨率降到320x240,检测改用lbpcascade_frontalface.xml替换Haar级联文件,LBP级联在低算力平台上优势非常明显。检测只是定位人脸,识别时从原始帧裁出ROI、放大后再predict,不要在缩小后的帧上识别,否则丢失的纹理细节会让置信度整体升高。这三个改动叠加,树莓派上的预览和识别都能回到可交互的级别。
5.4 考勤时间全乱了:树莓派没电池,断电就回到出厂
现象:考勤记录里时间比实际晚了8小时,甚至直接显示成1970年,日期字段完全错乱。原因:树莓派不像PC有主板电池,断电重启后系统时间会回到默认值,只有联网同步NTP才会恢复。考勤系统读的是本地时钟,时间错了打卡必然错。解决:部署环境有外网时,配置好NTP自动同步;完全离线的话,加一块DS3231 RTC硬件时钟模块,通过I2C接上,并在启动脚本里把硬件时间写入系统。这个问题不在代码里,属于部署必查项,演示现场断电重启最容易暴露,提前处理。
5.5 训练时bad argument或predict永远是-1
现象:训练脚本跑到train()那一行直接报错,或者训练成功但predict永远返回-1,谁也识别不出来。原因:faces和labels两个列表没有对齐。常见写法是用os.listdir的返回顺序来推标签,但目录遍历顺序和文件编号不一致时,标签就错位了。另一个常见原因是读图用了彩色模式没转灰度,train()收到的通道数和LBPH期望的不匹配。解决:目录结构强制命名成dataset/员工ID/xxx.jpg,ID就是整数标签;读图用cv2.imread(path, cv2.IMREAD_GRAYSCALE)确保是单通道。train()之前把每张图对应的label打印出来核对一遍,大部分-1问题当场就能看出来。
6. 进阶:给考勤系统加一个自检脚本,用数据钉死识别阈值
交付前我习惯先跑一遍自检脚本,不是等演示时再赌运气。脚本思路很简单:把每个人的样本分成训练组和验证组,验证组不参与训练,只用来统计置信度分布,再用没录入的陌生人照片做压力测试,看陌生人最低置信度会不会误闯阈值。
import cv2 import os import numpy as np X_train, y_train, X_test, y_test = [], [], [], [] for emp_id in os.listdir('dataset'): files = sorted(os.listdir(f'dataset/{emp_id}')) if len(files) < 4: continue # 样本太少就不参与自检,否则验证组不够分 for f in files[:-2]: # 前面N-2张做训练 img = cv2.imread(f'dataset/{emp_id}/{f}', cv2.IMREAD_GRAYSCALE) X_train.append(img) y_train.append(int(emp_id)) for f in files[-2:]: # 每人最后2张做验证,严格隔离 img = cv2.imread(f'dataset/{emp_id}/{f}', cv2.IMREAD_GRAYSCALE) X_test.append(img) y_test.append(int(emp_id)) recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.setThreshold(1000) # 先放开阈值,只统计距离分布 recognizer.train(X_train, np.array(y_train)) for img, label in zip(X_test, y_test): pred, conf = recognizer.predict(img) print(f'true={label} pred={pred} conf={conf:.1f}') # 陌生人最低置信度,所有文件放到stranger/目录 stranger_conf = [] for f in os.listdir('stranger'): img = cv2.imread(f'stranger/{f}', cv2.IMREAD_GRAYSCALE) if img is not None: stranger_conf.append(recognizer.predict(img)[1]) if stranger_conf: print(f'陌生人最小conf={min(stranger_conf):.1f}')输出里重点看两个数:同一人验证集的最高置信度,和陌生人的最低置信度。阈值应该落在两者之间,留出余量。如果两个区间重叠,说明训练样本不够或预处理不一致,先补数据再上线,不要改阈值硬撑。这套自检脚本成本极低,但能把“调参玄学”变成看得见的数字。
我第一版演示时就吃过亏:阈值用的默认值,现场随便一个路过的同学都被识别成了我自己,场面极其尴尬。后来每次换摄像头、换光照环境,我都会先跑一遍这个脚本再调阈值。这个习惯帮我避掉了后面所有类似问题。希望帮到你。
本文还有配套的精品资源,点击获取