简介:借助OpenCV与TensorFlow 2.0构建的CNN人脸识别实战项目,利用卷积神经网络识别人脸,进而自动锁定与解锁Windows 10屏幕,解决手动输入密码的繁琐问题。程序启动后可实时采集摄像头画面,识别到授权人脸时自动解锁,未检测到人脸或匹配失败时锁定屏幕,逻辑清晰易懂。内容面向计算机视觉、深度学习方向的课程设计与毕业设计学生,也适合希望快速落地人脸识别应用的开发者。压缩包共7个文件,主体为4个Python脚本,分别负责摄像头人脸采集、CNN模型训练、实时识别与屏幕控制;另有1个VBS脚本隐藏控制台窗口、1个bat批处理一键启动、1份README说明环境依赖与运行命令。整个包仅13KB,结构紧凑却覆盖完整流程。目前已有302人学习下载。通过该资源,可以掌握OpenCV图像预处理与TensorFlow 2.0模型推理的衔接方式,理解CNN在人脸识别中的应用逻辑,并获得一套可直接运行的屏幕解锁/锁定脚本,适合作为毕业设计演示、课程作业拓展或功能二次开发的基础。
1. 人脸识别锁定与解锁 Win10 屏幕:这个 CNN 实战项目能解决什么
大概每个用笔记本办公的人都被同一个问题烦过:离开工位两分钟,回来发现同事动了你的电脑。标题里的方案就是为这个场景准备的——OpenCV 负责抓摄像头画面并把人脸框出来,TensorFlow 2.0 训练一个小型 CNN 判断屏幕前是不是本人,是本人就保持解锁状态,检测到陌生人或者空场景就锁掉 Win10 屏幕。它和 Windows Hello 最大的区别是模型和数据都在你手里,光线、角度变了随时补样本重训,不用把命运交给系统里那个黑匣子。适合两类人:想跑通「采集数据—训练 CNN—部署到系统锁屏」完整闭环的深度学习初学者,以及想给电脑加一道低成本防窥屏屏障的普通用户。下面从方案选型、数据采集、模型训练讲到系统调用,最后把最容易翻车的几个点单独拉出来说。
2. 先定方案再动手:OpenCV 检测、CNN 识别与自采数据的选型理由
2.1 为什么检测用 OpenCV、识别用 TensorFlow 2.0:两段式人脸识别的经典套路
第一次做这个项目的人都会问:既然已经用了 CNN,能不能一个模型直接把「人在哪、是谁」都输出?理论上可以,代价很大。检测任务要回归出边界框,需要大量带框标注的训练数据,还要处理不同尺度、多个人脸的叠加情况,模型体积和训练时间直接翻倍。而人脸识别算法里最经典的套路恰恰是两段式:检测器只负责快速定位人脸框,识别模型只负责对裁剪后的脸做比对。商场门口那些人脸识别门禁机,内部多半也是这个结构,只是部署在嵌入式设备上,模型更小、量化更狠。做桌面端锁屏,完全不需要把架构搞得那么复杂。
OpenCV 的 Haar 级联分类器做正面人脸检测,在 640x480 的画面里单帧只要十几毫秒,完全不占用 GPU。TensorFlow 2.0 的职责因此收敛成「对裁剪后的人脸小图做二分类判断」,输入固定为 96x96 灰度图,模型只有三层卷积。这种分工是整套方案能跑在普通笔记本上的关键。opencv 安装教程在网上一搜一大把,核心命令就一句pip install opencv-python,注意别同时装opencv-contrib-python,两个包会互相覆盖文件。TensorFlow 2.0 相比 1.x 最大的变化是 Keras 成为官方高层 API,模型定义、训练、保存都简洁很多。另外标题里的「tenserflow2.0」是个常见拼写错误,安装时包名拼对:tensorflow。
2.2 自采数据集:自己拍正负样本,比下载公开数据集更省事
公开人脸数据集确实多,但拿来锁屏有个致命问题:训练集里的脸是用别人的摄像头在别人的环境里拍的,和你笔记本摄像头的色温、距离、视角都对不上。模型迁移过来之后,白天可能勉强能用,晚上开台灯就翻车。我一般花 20 分钟自己拍一套,效果比任何公开数据集都好。
正样本就是你自己。找一个光线稳定的位置,脸在画面里占三分之一以上,缓慢转头、变换表情,让 OpenCV 检测到人脸就自动切图保存。下面是采集脚本的核心:
import cv2 import os save_dir = "dataset/me" os.makedirs(save_dir, exist_ok=True) cap = cv2.VideoCapture(0) face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) count = 0 while count < 300: ret, frame = cap.read() if not ret: continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.3, 5) for (x, y, w, h) in faces: face = gray[y:y + h, x:x + w] face = cv2.resize(face, (96, 96)) cv2.imwrite(f"{save_dir}/{count:04d}.jpg", face) count += 1 cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imshow("capture", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()这段代码的逻辑:逐帧读摄像头,转灰度后用 Haar 检测人脸,检测到就裁剪、缩放到 96x96 存盘,同时在画面里画框确认。两个关键参数要解释一下:detectMultiScale的scaleFactor=1.3表示每次把图像缩小 1.3 倍再检测,值越小检测越慢但越细;minNeighbors=5表示一个候选框周围至少要有 5 个相邻框才确认是人脸,值越大误检越少。采集时控制节奏,每秒存 2-3 张,300 张大约两分钟。脸不要怼镜头太近,留一点额头和下巴的边缘,模型能学到完整的脸部结构。
负样本比正样本更重要。新建stranger目录拍同事、室友,或者翻动手机相册里的照片让摄像头拍到;再建empty目录拍没有人的桌面、窗户、走廊。数量上负样本要多于正样本,我通常拍 500 张 stranger 加 200 张 empty,因为「空场景误判成本人」比「陌生人误判成本人」更容易发生。拍摄时故意让几张负样本带模糊和运动残影,增强模型对动态画面的鲁棒性。
2.3 目录结构与标签:用 image_dataset_from_directory 加载数据
训练时用 TensorFlow 2.0 自带的image_dataset_from_directory加载最省事,但它要求目录结构按类别分好,目录名就是标签。我建议这样组织:
dataset/ ├── me/ 本人,约300张 ├── stranger/ 其他人脸,约500张 └── empty/ 无人场景,约200张标签设计成二分类而不是多分类,这是容易被忽略的细节。如果做成「me / stranger / empty」三分类,模型要学三个类别之间的边界,样本不均衡时很容易把 empty 和 stranger 混在一起。而二分类直接输出「安全 / 不安全」的概率,语义更贴合锁屏需求:只要不是本人,输出就低于阈值,触发锁屏动作。加载代码会在下一章出现,这里先记住一个前提:dataset目录里除了这三个子目录,不要放 README、缩略图缓存或者额外的空文件夹,否则会被当成新类别,训练时类别数莫名其妙多一个。
提示:
image_dataset_from_directory默认按目录名的字母顺序给类别编号,empty是 0、me是 1、stranger是 2。如果只保留两个目录,标签顺序也会按字母排,写代码前先用class_names打印确认一下,避免「0 表示本人还是陌生人」搞反。
3. 用 TensorFlow 2.0 训练轻量 CNN:模型定义、训练回调与阈值调整
3.1 CNN 卷积神经网络结构:三层卷积加 Dropout,CPU 也能跑
CNN 卷积神经网络的核心思路是用卷积核逐步提取局部特征,从边缘、纹理抽象到眼睛、鼻子这类语义特征,最后由全连接层做分类。锁屏任务不需要在 ImageNet 上刷分,模型越小,摄像头推理越流畅。下面是我反复用的一套结构,9 层不到,CPU 上单帧推理 60 毫秒左右:
import tensorflow as tf from tensorflow.keras import layers, models model = models.Sequential([ layers.Input(shape=(96, 96, 1)), layers.Conv2D(32, (3, 3), activation="relu"), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation="relu"), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activation="relu"), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dropout(0.5), layers.Dense(128, activation="relu"), layers.Dense(1, activation="sigmoid"), ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), loss="binary_crossentropy", metrics=["accuracy"], ) model.summary()参数选择上几个值得说的点。输入是 96x96 灰度图,单通道,比彩色三通道数据量直接减到三分之一,锁屏场景里肤色和纹理足够,不需要颜色信息。卷积核数量 32-64-128 逐层翻倍,因为越到深层特征越抽象,需要更多通道承载。三个 MaxPooling 把特征图从 96 一路缩到 12,全连接层输入维度被压到几千,训练和推理都更快。Dropout 0.5 是防过拟合的主力——数据集只有一千张左右,全连接层非常容易把训练样本背下来。学习率设 1e-4 而不是默认的 1e-3,数据量小的时候学习率太大会导致 loss 震荡,训练曲线像锯齿一样。
| 层 | 输出尺寸 | 作用 |
|---|---|---|
| Input | 96x96x1 | 灰度人脸输入 |
| Conv2D + MaxPool | 48x48x32 | 提取边缘与纹理 |
| Conv2D + MaxPool | 24x24x64 | 提取五官局部特征 |
| Conv2D + MaxPool | 12x12x128 | 提取全局语义特征 |
| Dense + Dropout | 128 | 特征组合,随机丢弃防过拟合 |
| Dense | 1 | sigmoid 输出安全概率 |
3.2 训练脚本与回调:保存最佳权重、提前停止防过拟合
数据加载和训练我用一段脚本搞定,关键在于回调函数:
train_ds = tf.keras.utils.image_dataset_from_directory( "dataset", validation_split=0.2, subset="training", seed=42, image_size=(96, 96), color_mode="grayscale", batch_size=32, label_mode="binary", ) val_ds = tf.keras.utils.image_dataset_from_directory( "dataset", validation_split=0.2, subset="validation", seed=42, image_size=(96, 96), color_mode="grayscale", batch_size=32, label_mode="binary", ) callbacks = [ tf.keras.callbacks.ModelCheckpoint( "face_lock.h5", monitor="val_accuracy", save_best_only=True, ), tf.keras.callbacks.EarlyStopping( monitor="val_loss", patience=10, restore_best_weights=True, ), ] history = model.fit( train_ds, validation_data=val_ds, epochs=50, callbacks=callbacks, )validation_split=0.2表示从每个类别的文件里随机抽 20% 做验证集,两次调用必须用同一个seed,否则训练集和验证集会互相穿透。batch_size=32在 CPU 上每轮迭代约一秒,50 轮基本一分钟内跑完。ModelCheckpoint 只在验证集准确率提升时覆盖face_lock.h5,这是你的后悔药——训练后期就算过拟合,磁盘上始终留着历史上最好的一版权重。EarlyStopping 的patience=10表示连续 10 轮验证 loss 没改善就提前收工,restore_best_weights=True让模型回滚到最佳权重而不是停在最后一轮。训练结束后用model.save("face_lock.h5")再存一份,部署时直接load_model加载。
3.3 训练完先看分类报告:验证集准确率会骗人
一千张级别的小数据集,验证集准确率的波动很大。正样本里如果连续几十张都在同一个位置拍的,模型可能学到的是背景而不是脸。我训练完第一件事不是部署,而是跑一次分类报告,看「本人被拒」和「陌生人通过」各占多少:
import numpy as np from sklearn.metrics import classification_report y_true = np.concatenate([y for _, y in val_ds], axis=0) y_pred = (model.predict(val_ds, verbose=0) > 0.5).astype(int) print(classification_report(y_true, y_pred, target_names=["lock", "unlock"]))重点关注unlock这一行的 recall,也就是「本人在场时被正确放行」的比例。这个值低于 0.9 就别急着部署,先回去补样本,多换几个角度和光线环境重拍,效果通常比加一层卷积明显得多。分类报告里如果lock的 precision 很低,说明大量陌生场景被当成本人放行,这时候要把负样本拍得更杂一点,或者把部署时的置信度阈值从 0.5 往上调。阈值设置取决于你更怕误锁还是更怕漏锁:自习室场景我推荐 0.7,误锁一次比陌生人偷看一眼更打扰;公开场所推荐 0.5,宁错过勿放行。
4. 把模型接到 Win10 屏幕上:软锁、硬锁与状态机设计
4.1 软锁与硬锁:Windows 解锁权限的边界在哪
这是整个项目里最容易被理想化的一步。Windows 10 的公开 API 只允许你调用LockWorkStation()锁掉工作站,没有任何公开接口能代替用户输入密码或 PIN 解锁——这是操作系统划死的安全边界。也就是说,标题里的「解锁」如果指真正的系统解锁,那最终必须由 Win10 自己的登录界面完成;程序能做的,是在检测到本人在场时不触发锁屏,以及在被锁之前用软锁兜底。
所以我一般把方案拆成两层。硬锁:识别到陌生人靠近或空场景,直接调用LockWorkStation(),物理锁死屏幕,对方必须知道密码才能进来。软锁:平时用全屏黑色遮罩窗口盖住桌面,程序自己控制显隐,本人回来就秒开,不需要输密码。软锁适合自习室防偷看,但它本质上是防君子不防小人,按下 Win 键切走就能绕过;硬锁适合离开工位防别人操作。两者共用同一个识别主循环,只是触发动作不同。
4.2 识别主循环与状态机:抽帧、连续帧投票与锁屏触发
识别循环最忌讳每帧都做推理。摄像头 30 帧每秒,CNN 推理一次 50-80 毫秒,CPU 会被持续占满,风扇狂转。正确做法是固定 1.5 秒抽一帧,而且不要单帧就下结论——检测偶发误判太常见了。先封装一个单帧判断函数:
def check_face(frame): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.3, 5) if len(faces) == 0: return False, 0.0 # 取画面里最大的脸,通常是离摄像头最近的人 x, y, w, h = max(faces, key=lambda f: f[2] * f[3]) face = cv2.resize(gray[y:y + h, x:x + w], (96, 96)) face = face.astype("float32") / 255.0 prob = model.predict(face.reshape(1, 96, 96, 1), verbose=0)[0][0] return prob > 0.5, probmodel.predict拿到的是 0 到 1 之间的概率,越大代表越像本人。astype("float32") / 255.0是归一化,训练时模型看到的是 0-1 输入,推理时不做这一步输出全会偏高。主循环用状态机管理,避免同一个动作反复触发:
state = "IDLE" # IDLE / SOFT_LOCK / HARD_LOCK safe_count = 0 unsafe_count = 0 last_check = 0.0 while True: now = time.time() if now - last_check < 1.5: time.sleep(0.2) continue last_check = now ret, frame = cap.read() if not ret: continue present, prob = check_face(frame) if present: safe_count += 1 unsafe_count = 0 else: unsafe_count += 1 safe_count = 0 # 连续5帧判定不安全,才触发硬锁屏 if state == "IDLE" and unsafe_count >= 5: ctypes.windll.user32.LockWorkStation() state = "HARD_LOCK" # 软锁模式下,连续3帧判定是本人,就撤掉遮罩 elif state == "SOFT_LOCK" and safe_count >= 3: close_soft_lock() state = "IDLE"ctypes.windll.user32.LockWorkStation()是 Windows 锁屏的标准调用,不需要管理员权限。连续 5 帧不通过才锁,是防止人弯腰捡东西、转头说话的瞬间误锁。状态机的好处是:锁过之后unsafe_count清零,不会在锁屏界面背后疯狂调用锁屏 API。
注意:
LockWorkStation()触发后,当前会话会被系统挂起,摄像头对象可能失联。恢复逻辑放在下一章避坑部分详细讲。
4.3 开机自启与后台运行:用 VBS 隐藏窗口并落盘日志
程序真正当后台服务跑,要解决两件事:开机自启和不弹黑窗口。控制台窗口用pythonw.exe启动就不会出现,但pythonw下print无处可去,所以日志必须写文件。我用一个 VBS 脚本做启动入口:
CreateObject("Wscript.Shell").Run "pythonw C:\face_lock\main.py", 0, FalseRun的第二个参数 0 表示隐藏窗口,第三个参数False表示不等待程序退出。把它放进 Win10 的启动文件夹:Win + R输入shell:startup,把 VBS 文件丢进去即可,不需要注册表权限。主程序里每个关键动作写一行带时间戳的日志,锁屏触发、人脸检测失败、摄像头重建都记下来。没有日志,后面排查误锁就只能靠玄学。
5. 实战避坑:从摄像头占用到误锁的 5 个高频翻车点
5.1 摄像头打不开或画面全黑
现象:cv2.VideoCapture(0)返回值是False,或者程序不报错但read()一直返回空。
原因:最常见是摄像头被其他软件占用,比如腾讯会议、微信视频,还有的笔记本带物理开关或快捷键(F8、F10 之类)控制摄像头。OpenCV 打开失败时通常不抛异常,只是在read()里静默失败。
解决:先关掉所有视频软件,检查快捷键开关。代码里给摄像头打开加一个重试循环,失败时每隔 3 秒重试而不是直接退出。多摄像头机器上设备号不一定是 0,用cv2.VideoCapture(-1)或者把参数换成 1、2 逐个试,打印cap.isOpened()确认。
5.2 训练准确率 99%,一到摄像头谁都认成本人
现象:验证集分类报告很漂亮,部署后换个人站在摄像头前,程序照样放行。
原因:小数据集过拟合的典型症状。我踩过最狠的一次,正样本三百张全是在同一个位置拍的,模型把背后那面白墙当成了判别特征,换到晚上开灯环境后,只要背景相似,任何人都被放行。验证集和训练集都来自同一个背景,所以验证准确率完全体现不出问题。
解决:采集时至少换三个环境、三个时段;训练时给数据加翻转和亮度抖动,用tf.keras.layers.RandomFlip("horizontal")和RandomBrightness(0.2)作为模型第一层。最重要的还是检查负样本的多样性,stranger 里多放几张不同肤色、不同姿态、戴帽子口罩拍不到的图片,模型才不会把「肤色深=陌生人」这种粗暴规则学进去。
5.3 锁屏后程序失联,解锁回来摄像头对象失效
现象:调用LockWorkStation()锁屏,回来输密码解锁后,程序像死了一样,画面不检测、日志不更新。
原因:Windows 锁屏后当前桌面会话被挂起,摄像头设备被系统释放,原来的VideoCapture对象变成无效句柄,read()一直阻塞或者返回空。程序没有感知到这个变化,于是表现为「死后复活失败」。
解决:锁屏触发后主动cap.release()释放资源,主循环里检测到连续读取失败就重建摄像头。重建函数加上重试上限,防止系统恢复前死循环:
def reopen_camera(max_retries=10): for _ in range(max_retries): cap = cv2.VideoCapture(0) if cap.isOpened(): return cap time.sleep(3) return None每次重建前把旧的cap释放掉,再重新打开,比盲目等待同一个句柄恢复靠谱得多。
5.4 TensorFlow 装完 import 直接报 DLL 错误
现象:pip install tensorflow成功,但import tensorflow报DLL load failed或者ImportError。
原因:绝大多数是 Python 版本和 TensorFlow 版本不匹配。TensorFlow 2.x 对 Python 3.7-3.9 兼容最好,用 Python 3.10 以上装旧版本会经常碰壁。还有人把tensorflow和tensorflow-gpu混着装,CUDA、cuDNN 版本对不上,GPU 版反而变成负担。
解决:新建干净的虚拟环境,conda create -n face_lock python=3.8,然后pip install opencv-python tensorflow-cpu==2.10。桌面锁屏这个任务用不到 GPU,CPU 版省掉一整条 CUDA 配置链路。顺便提醒:opencv-python和opencv-contrib-python千万别共存,两个包会互相覆盖cv2目录里的文件,出现cv2.error里找不到来源的符号错误。
5.5 人坐着不动却被误锁
现象:人好好坐在屏幕前,盯着代码看,几分钟后屏幕突然锁了。
原因:识别主循环里最初用单帧判断,而 Haar 检测对正面偏转超过 20 度、低头看手机、手托腮这类姿态会漏检。连续 5 次漏检就触发锁屏,「坐着不动低头看代码」这种场景自然频繁误锁。
解决:把触发条件从「连续 N 次不通过」改成「连续 N 次不通过且时间窗口超过 M 秒」,并给置信度留一个中间带。模型输出在 0.4-0.6 之间时既不判安全也不判危险,只记录不动作,能吸收摄像头抖动带来的波动。另外在检测不到人脸时降低scaleFactor重试一次,很多 USB 摄像头在弱光下会间歇性漏检。误锁比漏锁伤体验得多,参数宁可保守。
6. 验证与进阶:置信度日志、模型热更新与误锁调参技巧
部署后别急着当生产力工具,先让程序跑一周「只记录不锁屏」的调试模式。我当时的做法是:每当模型预测出「陌生人」或「空场景」,就把当前帧截图存到日志目录,同时把时间戳和置信度写进 CSV。一周后翻这些截图,你会发现大部分误锁集中在几个固定时段——下午西晒、晚上开台灯、背对窗户。针对这些样本重新补拍重训,比盲目调阈值有用得多。
代码层面有两个值得加的东西。一是置信度中间带,模型输出在 0.4-0.6 之间时只记录不触发锁屏,能显著减少摄像头抖动带来的误判;二是模型热更新,主循环里每隔十分钟重新load_model("face_lock.h5")一次,白天采集的新样本晚上重训完,第二天自动生效,不用重启进程。
调参顺序我建议固定:先调连续帧数,从 3 试到 8,观察误锁率曲线在哪里出现拐点;再调置信度阈值,从 0.5 往 0.7 推;最后才考虑加数据增强。我自己跑这个项目最大的教训就是:不要把系统锁屏当成验证标准,先加截图和日志观察一周,有了数据再让程序真正去锁屏,否则调参全靠玄学。希望帮到你。
本文还有配套的精品资源,点击获取