资讯动态

OpenCV人脸识别实战:摄像头采集、LBPH训练与实时比对

发布时间:2026/10/29 11:19:53 来源:尧图企业网站定制
简介面向想入门机器视觉与人脸识别的Python学习者这份实战压缩包以OpenCV为核心围绕黄晓明、Angelababy、刘亦菲等明星人脸样本演示从图像预处理、Haar级联检测到特征提取与匹配的完整流程。包内共120个文件以110张jpg和5张jpeg人脸图片为主要素材另含2个xml级联分类器、1个py源码、1个txt说明和1个png图示压缩包约51.99MB便于直接运行与对照调试。已有460人学习适合具备基础Python语法、希望把分类器调用、detectMultiScale参数调整和LBPH识别串起来的读者。读者可据此理解灰度化、直方图均衡化等预处理细节查看检测与识别脚本的实现思路并借助现成样本反复试验阈值与窗口参数形成可复用的OpenCV人脸识别小项目经验。1. 从一台笔记本和一颗摄像头说起这套 OpenCV 人脸识别实战包能跑出什么去年帮朋友救一个门禁演示的场条件是「只有一台笔记本、一个 USB 摄像头今天下午要能刷脸开门」。装深度学习框架来不及显卡也没有最后顶上去的就是这类 OpenCV 人脸识别实战包解压、装两个 pip 包、改一行摄像头编号画面里就出现了绿色人脸框和名字从零到能演示大概二十分钟。它做的是机器视觉里最经典的一条链路—— Haar 级联或 DNN 检测人脸、裁出 ROI、LBPH 之类的识别器比对身份全程 CPU 可跑Python 一百来行就能闭环。适合三类人想入门人工智能视觉方向但被环境劝退的学生、要给课程设计或毕设找一个能改能交的基线的人、以及需要快速做个人脸检测原型的开发者。它不适合追求工业级精度的高铁站闸机场景但拿来把「检测—对齐—特征—比对」这条链路摸透性价比很高。2. 环境与目录把压缩包拆开先看它靠什么跑起来拿到一个 zip最忌讳的是双击03_recognize.py直接 F5。人脸识别这条链路上环境错一步后面全是玄学报错摄像头能开但识别器创建不出来、级联文件路径在别人机器上有效在你这里 404、numpy 版本和 opencv 编译的 ABI 对不上。所以顺序应该是先定环境、再认目录、最后用三条最小验证命令把每个环节单独点亮再串起来跑全流程。2.1 依赖与虚拟环境opencv-python 和 opencv-contrib-python 别装错这是整套代码里最容易翻车的一步。cv2.face.LBPHFaceRecognizer_create()不在主包里它在 contrib 扩展模块里。很多人按习惯pip install opencv-python代码跑到创建识别器那一行直接AttributeError: module cv2 has no attribute face然后开始怀疑人生以为是 Python 版本问题。# 1. 建干净环境别用系统 Python 硬装 python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate # 2. 先把可能存在的两个包都卸掉它们会往同一个 cv2 目录里写文件共存会互相覆盖 pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless # 3. 只装 contrib 版本它包含主包全部内容 face、tracking 等扩展 pip install opencv-contrib-python4.9.0.80 # 4. numpy 锁 1.x2.x 与部分 opencv 轮子 ABI 不兼容会报 multiarray failed to import pip install numpy1.26.4参数上唯一值得钉死的是版本。opencv 4.x 里cv2.face的 API 从 4.5 之后基本稳定但 4.10 起部分轮子对 numpy 2.x 的兼容是分平台发布的锁 4.9.0.80 加 numpy 1.26.4 是最省事的组合。装完必须做一次自检别信 pip 的 Successfully installedimport cv2 print(opencv:, cv2.__version__) print(has face module:, hasattr(cv2, face)) # 必须是 True print(cascade dir:, cv2.data.haarcascades) # 级联 xml 的默认目录cv2.data.haarcascades这个属性经常被忽略它返回的是 opencv 安装包里data/的绝对路径串上haarcascade_frontalface_default.xml就是可用的模型路径。下一节会讲到包里自带的 xml 和这个路径是两条路选错一条就换机器失效。2.2 目录结构与脚本职责五个文件构成一条流水线压缩包解开之后一般长这样不同人打包习惯不同但职责划分大同小异。先看清谁依赖谁比急着跑代码有用文件 / 目录职责执行顺序依赖产物01_collect.py开摄像头采集人脸按姓名.ID.序号.jpg落盘1无dataset/采集出的人脸小图灰度、统一尺寸—由 01 生成02_train.py遍历 dataset训练 LBPH导出模型2dataset/trainer/trainer.yml训练出的模型文件几十 KB 到几 MB—由 02 生成03_recognize.py实时识别主循环画框写名字3trainer.yml04_image_test.py对单张图或视频文件跑同一条链路可选trainer.ymlhaarcascade_frontalface_default.xmlHaar 正脸检测模型被 01/03 依赖无这里有个隐藏的耦合点dataset/里的文件名就是标签来源。训练脚本靠姓名.ID.序号.jpg里的第二段解析出整数 ID 喂给train()因为 LBPH 的标签必须是 int字符串标签会直接抛类型错误。所以采集脚本的命名规则一旦改动训练和识别两边都得跟着改这也是后面二次开发时最容易踩的地方。配置文件方面这类包一般没有config.yaml路径全是脚本里的字符串常量。我一般第一件事就是把它们抽到文件头import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) CASCADE_PATH os.path.join(BASE_DIR, haarcascade_frontalface_default.xml) DATASET_DIR os.path.join(BASE_DIR, dataset) TRAINER_PATH os.path.join(BASE_DIR, trainer, trainer.yml) # 目录不存在就先建imwrite 不会自动创建父目录写失败也不报错 os.makedirs(DATASET_DIR, exist_okTrue) os.makedirs(os.path.dirname(TRAINER_PATH), exist_okTrue)用__file__而不是os.getcwd()是血泪经验。cv2.imwrite在父目录不存在时不会抛异常只返回False采集脚本照样跑完 40 张训练时才发现 dataset 是空的报一个train() 收到的样本数为 0排查起来绕一大圈。2.3 三条最小验证命令先确认摄像头和级联文件都活着在跑业务脚本之前把三个外部依赖单独点一遍级联文件能加载、摄像头能打开、单帧能读到。这三步任意一步失败后面的识别逻辑写得再对也没用。import cv2 from your_config import CASCADE_PATH # 换成实际路径 # 1) 级联文件是否可加载empty() 为 True 说明文件损坏或路径不对 detector cv2.CascadeClassifier(CASCADE_PATH) print(cascade loaded:, not detector.empty()) # 2) 摄像头能否打开。Windows 上强烈建议带 CAP_DSHOW # 默认的 MSMF 后端在部分机器上首次打开要 3-5 秒甚至直接失败 cap cv2.VideoCapture(0, cv2.CAP_DSHOW) print(camera opened:, cap.isOpened()) # 3) 能否读到帧读不到时 frame 是 None ok, frame cap.read() print(read ok:, ok, shape:, None if frame is None else frame.shape) cap.release()cap.isOpened()返回 True 但read()返回 False 是很典型的坑设备被别的进程占用了比如浏览器里还开着视频会议或者上一个脚本崩了没 release。这时 Windows 下换cv2.CAP_MSMF试试Linux 下先ls -l /dev/video*确认设备节点和当前用户是否在video组里。三个print全部符合预期再往下走能省掉后面 80% 的「为什么没反应」。3. 人脸检测链路拆解Haar 级联和 DNN 两条路怎么选检测和识别是两件事初学者最容易混。检测回答「画面里有没有脸、在哪」识别回答「这张脸是谁」。包里的 01 和 03 两个脚本共用同一段检测代码所以先把检测这一段吃透后面识别模块基本是照抄。3.1 Haar 级联detectMultiScale 的四个参数决定召回率Haar 级联是 Viola-Jones 那套老方案用积分图快速算 Haar 特征再用 AdaBoost 串起来的强分类器做滑窗判断。它的优点是模型只有几百 KB、纯 CPU 单帧几毫秒到几十毫秒缺点是侧脸、遮挡、逆光基本抓瞎。这份包默认走的就是它。gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 直方图均衡化抵抗逆光和背光对 Haar 和 LBPH 都有正向收益 gray cv2.equalizeHist(gray) faces detector.detectMultiScale( gray, scaleFactor1.15, # 图像金字塔每层的缩放比越小越慢、检出越全 minNeighbors5, # 一个候选框要被多少个邻居框确认才保留越大越保守 minSize(80, 80), # 小于该尺寸的框直接丢弃能滤掉大量背景噪点 flagscv2.CASCADE_SCALE_IMAGE, )四个参数各自的取舍得说清楚。scaleFactor从 1.05 到 1.3 是常规区间1.05 理论上不会漏掉任何尺度但一帧要跑二三十层金字塔1080p 下能掉到 3 fps1.15 是精度和速度比较均衡的点。minNeighbors是误检的主要闸门调到 3 会把墙上的插座、窗帘褶皱都当成脸调到 8 又会漏掉戴帽子的人5 到 6 在室内光线正常时比较稳。minSize必须按你的实际场景定如果摄像头在 60 cm 外拍人脸 ROI 大概就是 150 到 250 像素那(80, 80)合理如果门禁场景人站在两米外人脸只有 60 像素宽这个值就得压到(40, 40)否则一个人都检不出来。这就是为什么同一份代码在别人电脑上好好的搬到你的工位上就集体失效。3.2 DNN SSD 检测器置信度阈值和坐标还原包里的代码通常也带一份 DNN 分支或者至少注释在文件里。它是 OpenCV DNN 模块加载的 res10 SSD 人脸检测器输入固定 300×300输出一批带置信度的框。相比 Haar侧脸和遮挡场景的召回明显好一截代价是单帧要多花十几毫秒。import numpy as np # deploy.prototxt 描述网络结构caffemodel 是权重两个都要在 net cv2.dnn.readNetFromCaffe( models/deploy.prototxt, models/res10_300x300_ssd_iter_140000.caffemodel, ) h, w frame.shape[:2] # 均值 (104, 177, 123) 是 BGR 顺序的训练均值swapRB 保持 False blob cv2.dnn.blobFromImage( cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0), swapRBFalse, cropFalse, ) net.setInput(blob) det net.forward() # 形状 (1, 1, N, 7) for i in range(det.shape[2]): conf float(det[0, 0, i, 2]) if conf 0.60: # 置信度阈值场景干净可以提到 0.7 continue # 输出的 4 个坐标是 0~1 归一化的要乘回原图尺寸 box det[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 box.astype(int) x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2)这里有个特别容易搞混的地方DNN 分支的conf是真正的置信度越大越可信而后面 LBPH 的confidence其实是特征距离越小才越像。两个变量名长得几乎一样、语义完全相反我在同一份代码里同时看到过两次中间用错了判断方向结果整屏全是「陌生人」。另外res10 这个模型对戴口罩、大角度侧脸的能力有限但它对光照的鲁棒性明显好于 Haar。如果这份包的演示环境是室内日光灯Haar 就够如果要拍移动中的人、或者要做个「戴着口罩也能出框」的演示把检测器换成 DNN 是性价比最高的一次改造。3.3 从检测框到 ROI裁剪、灰度、直方图均衡与尺寸统一检测出来的框不能直接丢给识别器中间必须做统一预处理。这一段是全链路里最容易被写成「随手切一下」的地方但它对最终准确率的影响比换识别器还大。def to_roi(gray, x, y, w, h, size(200, 200), pad0.15): 把检测框扩一点边、裁出来、缩放到固定尺寸、再做一次均衡化 px, py int(w * pad), int(h * pad) x1 max(0, x - px) y1 max(0, y - py) x2 min(gray.shape[1], x w px) y2 min(gray.shape[0], y h py) roi gray[y1:y2, x1:x2] if roi.size 0: # 贴边时可能切出空数组 return None roi cv2.resize(roi, size, interpolationcv2.INTER_AREA) return cv2.equalizeHist(roi)pad0.15是我固定加的一个外扩比例。Haar 和 SSD 给出的框普遍偏紧容易切掉一点下巴和额头而 LBPH 的纹理统计对轮廓边缘很敏感框紧一点点、换个人的姿势直方图就跑偏。外扩 15% 能把耳朵根和发际线带进来实测比不外扩稳定。INTER_AREA在缩小场景下比默认的INTER_LINEAR更平滑不会引入采样噪点这一点在 200×200 这种小尺寸上尤其明显。至于为什么要统一成 200×200LBPH 的train()要求所有输入图像同尺寸混着不同分辨率会直接抛异常。有些包里没做 resize靠采集时就存成统一大小逻辑等价但推理路径上如果忘了 resize一样会崩。最关键的一条训练时用的预处理和推理时用的预处理必须是同一个函数。我见过太多改造工程采集脚本里做了equalizeHist识别脚本里直接cvtColor之后就 predict模型不是不工作是准确率从 90% 掉到 50%怎么调阈值都救不回来。所以我一般把to_roi单独抽成preprocess.py两边from preprocess import to_roi物理上杜绝走岔。4. 识别模块实战LBPH 的训练、保存与实时比对检测框给到之后识别要解决的是「这张 200×200 的灰度小图属于谁」。包里默认用 LBPH全称 Local Binary Patterns Histograms思路是把图像每个像素和周围邻域比大小、编成二进制码再分块统计直方图最后比对直方图距离。它的好处是不需要大量样本、单张也能训、对光照变化比 Eigenfaces 稳很适合这种桌面级 Demo。4.1 样本采集命名规则、数量和光照采集是整条链路的地基。地基歪了后面调阈值、换识别器全是在补漏。name, sid zhangsan, 1 # sid 必须是 int用作 LBPH 标签 count, need 0, 40 while count need: ok, frame cap.read() if not ok: break gray cv2.equalizeHist(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) for (x, y, w, h) in detector.detectMultiScale(gray, 1.15, 5, minSize(80, 80)): roi to_roi(gray, x, y, w, h) if roi is None: continue count 1 # 命名姓名.ID.序号.jpg训练脚本靠这三段解析 cv2.imwrite(fdataset/{name}.{sid}.{count:03d}.jpg, roi) cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.imshow(collect, frame) if cv2.waitKey(1) 0xFF ord(q): break数量上 30 到 50 张是常规区间但不是越多越好。真正决定效果的是多样性正脸、微侧、抬头低头、戴不戴眼镜、开灯关灯各来几张比在同一个姿势连拍 40 张强太多。连拍 40 张等于 1 张因为 LBPH 拿到的直方图几乎完全一样。还有两个细节值得单独说。一是count:03d补零保证文件名字典序和采集顺序一致方便人工抽查。二是采集阶段就应该把 ROI 存成灰度小图而不是存整帧原图再在训练时裁——存整帧会让 dataset 目录几秒钟涨到几百 MB而且训练脚本还得再跑一遍检测白白多一层不确定性。4.2 训练与持久化trainer.yml 里到底存了什么训练这一段代码很短但参数和文件格式值得讲清楚。import os import numpy as np import cv2 from PIL import Image def load_dataset(root): faces, labels, names [], [], {} for fn in sorted(os.listdir(root)): if not fn.lower().endswith((.jpg, .png)): continue parts fn.split(.) if len(parts) 3: continue name, sid parts[0], int(parts[1]) img Image.open(os.path.join(root, fn)).convert(L) # 强制灰度 faces.append(np.array(img, dtypeuint8)) labels.append(sid) names[sid] name return faces, np.array(labels), names faces, labels, names load_dataset(dataset) print(f样本 {len(faces)} 张{len(names)} 个人) # radius/neighbors 控制 LBP 算子grid_x/grid_y 决定分块数量 recognizer cv2.face.LBPHFaceRecognizer_create( radius1, neighbors8, grid_x8, grid_y8, ) recognizer.train(faces, labels) recognizer.write(trainer/trainer.yml) print(id - name 映射:, names) # 这个映射必须自己另存radius1, neighbors8就是标准 3×3 邻域的 LBP也是 OpenCV 默认值改大能捕捉更粗的纹理但对小图没好处。grid_x8, grid_y8是把 200×200 的图切成 8×8 共 64 个格子每个格子单独统计 LBP 直方图再拼起来。为什么要分块如果不分块全图共用一个直方图眼睛的纹理和嘴巴的纹理搅在一起两张不同的人脸很可能统计特征撞车。分块本质上是保留了空间结构信息这是 LBPH 相比最朴素 LBP 直方图的关键改进。trainer.yml里存的是每个标签对应的直方图集合注意它不存标签到姓名的映射。names这个 dict 是训练脚本的局部变量跑完就没了。所以包里如果只写recognizer.write()而没有把映射序列化出来识别脚本就必须自己硬编码一份 id 到名字的表或者再遍历一次 dataset 目录重建。这也是下面避坑章节里的一条。4.3 实时识别主循环从 predict 的返回值到屏幕上的名字主循环是整份代码里最像「作品」的部分逻辑不复杂但坑很密集。recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.read(trainer/trainer.yml) THRESHOLD 62.0 # LBPH 的 confidence 是距离越小越像 while True: ok, frame cap.read() if not ok: break frame cv2.flip(frame, 1) # 镜像符合用户照镜子的直觉 gray cv2.equalizeHist(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) faces detector.detectMultiScale(gray, 1.15, 5, minSize(80, 80)) for (x, y, w, h) in faces: roi to_roi(gray, x, y, w, h) if roi is None: continue sid, dist recognizer.predict(roi) # dist 越小越像 if dist THRESHOLD: label, color f{names.get(sid, ?)} {dist:.0f}, (0, 200, 0) else: label, color funknown {dist:.0f}, (0, 0, 220) cv2.rectangle(frame, (x, y), (x w, y h), color, 2) cv2.putText(frame, label, (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.imshow(recognize, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()predict返回的第二个值是距离而非概率取值范围跟图像尺寸、分块数都有关没有普适的「0.9 就是很像」。THRESHOLD 62只是个起点正确做法是拿一批已知身份的图跑一遍把「同一个人」的距离分布和「陌生人」的距离分布都打出来取两个分布之间的谷底。包里如果写死了 50 或者 80 而你又不知道依据先按上面方法重标一次比盲目试快得多。cv2.flip(frame, 1)那行别小看。不镜像的话人往左移画面里往右移演示时体感别扭但镜像之后cv2.putText写在框上方是正常的不会跟着翻。要翻的是 ROI 里人脸的方向吗不需要LBPH 对水平翻转不算特别敏感但如果你后面换成了深度学习特征就得注意别把训练和推理的翻转策略搞不一致。4.4 三种识别器的取舍LBPH、Eigenfaces、Fisherfaces包里通常三种识别器都留了接口只是默认调 LBPH。它们的差别值得一次性搞清楚因为二次开发时换识别器往往是最省成本的一次尝试识别器原理样本需求光照鲁棒性典型场景EigenfacesPCA 降维到特征脸子空间每人 10 张以上差光照一变就崩课程实验、算法对比FisherfacesLDA类间散度/类内散度每人样本数 人数中等小规模、光照一致的库LBPH局部二值模式直方图每人 1-5 张即可较好桌面 Demo、门禁原型一句话概括Eigenfaces 和 Fisherfaces 吃的是全局灰度分布本质在比整张脸的像素相关性LBPH 吃的是局部纹理的统计对绝对亮度不敏感。这也解释了为什么 LBPH 单张样本也能训而 PCA 系列需要每个类别有足够样本才能估出可靠的协方差。换识别器的代码改动只有三行# 三选一API 完全一致 recognizer cv2.face.LBPHFaceRecognizer_create(radius1, neighbors8, grid_x8, grid_y8) # recognizer cv2.face.EigenFaceRecognizer_create(num_components80) # recognizer cv2.face.FisherFaceRecognizer_create(num_components0) # 0 自动取 C-1但要注意阈值语义。Eigenfaces 和 Fisherfaces 的predict返回的第二个值同样是距离部分版本是负的对数似然量纲和 LBPH 完全不同直接沿用 62 这个阈值会全判成陌生人。换识别器之后第一件事一定是重新画一遍距离分布、重新定阈值这一步没法省。5. 避坑与排查跑不起来、认不出、卡成幻灯片这一节按「现象 → 原因 → 解决」记录五条我实际撞过的问题基本覆盖了这类包最常见的翻车现场。现象一AttributeError: module cv2 has no attribute face。原因装的是opencv-python而不是opencv-contrib-pythoncv2.face模块不在主包里。更隐蔽的情况是两个包都装过主包的文件覆盖了 contrib 的cv2目录import cv2能成功但扩展模块缺失。 解决pip uninstall -y opencv-python opencv-contrib-python opencv-python-headless全清再只装 contrib 版本用hasattr(cv2, face)验证。现象二画面上的人名全是「???」或者方框。原因cv2.putText用的是 Hershey 矢量字体只支持 ASCII中文和大部分非拉丁字符直接渲染成问号不是编码问题改# -*- coding: utf-8 -*-也没用。 解决用 PIL 画中文再转回 BGR 数组封装成一个小函数代价是每帧多一两次内存拷贝。import numpy as np from PIL import Image, ImageDraw, ImageFont # 字体路径要按系统改这里只写占位别照抄 FONT ImageFont.truetype(simhei.ttf, 20) def put_text_cn(bgr, text, pos, color(0, 255, 0)): OpenCV 不支持中文用 PIL 绘制后转回 numpy img Image.fromarray(bgr[:, :, ::-1]) # BGR - RGB ImageDraw.Draw(img).text(pos, text, fontFONT, fillcolor[::-1]) return np.array(img)[:, :, ::-1] # RGB - BGR现象三cap.isOpened()是 True但第一帧要等五六秒甚至读完是黑的。原因Windows 默认的 MSMF 后端在部分摄像头驱动上初始化很慢也有可能是分辨率协商失败摄像头默认给的是 640×480代码里却按 1280×720 去读。 解决cv2.VideoCapture(0, cv2.CAP_DSHOW)强制走 DirectShow通常能立刻打开再显式cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)和HEIGHT把分辨率钉住别依赖默认值。现象四predict对所有人返回同一个 label或者全部判成 unknown。原因通常是三个一是names映射没持久化识别脚本里硬编码的 id 和训练时的 id 错位表现就是「名字永远是第一个」二是采集时做了equalizeHist而推理时漏了特征分布整体偏移距离全部超过阈值三是样本极度不均衡某个人占了 80% 的样本模型倾向于全判成他。 解决把id - name映射和模型一起json.dump到trainer/下把预处理收敛到一个公共函数采集时给每个人设同样的need上限并在训练前打印每个标签的样本数做二次确认。现象五帧率从 30 fps 掉到 5 fps画面卡成幻灯片。原因每一帧都在全分辨率上跑一遍detectMultiScale1080p 下 Haar 单帧就可能吃掉 80 到 150 ms再加上imshow的渲染整条链路串行阻塞。 解决三个方向可以叠加。检测降频每 3 帧检一次、中间帧沿用上一次的框检测前把灰度图缩到一半cv2.resize(gray, None, fx0.5, fy0.5)框的坐标再乘回去把imshow之外的所有处理丢到独立线程主线程只负责取帧和显示。实测前两条加起来能把 5 fps 拉回 20 fps 以上。6. 二次开发把它改成你自己的门禁 Demo这份包的价值不在「能跑」而在它把链路的每一段都暴露成了可替换的函数。真正要往产品方向走我一般从三个位置下手。第一处是检测器替换。把detectMultiScale换成上一章的 DNN 分支接口差异只有「输入灰度还是 BGR、输出是框列表还是带置信度的数组」两点包一层detect_faces(frame) - list[(x, y, w, h, conf)]就能让上层代码一行不改。第二处是加去重和落库主循环里同一个sid连续命中超过 N 帧才认为是一次有效识别然后写一条带时间戳的记录进 sqlite这是把「演示」变成「考勤」的最小改动。第三处是隔帧检测加跟踪用cv2.calcOpticalFlowPyrLK或者简单的 IoU 匹配在中间帧维持框位置CPU 占用能降一大半。# 去重 落库的最小骨架接在主循环的 predict 之后 import sqlite3, time conn sqlite3.connect(records.db) conn.execute(CREATE TABLE IF NOT EXISTS logs( name TEXT, ts INTEGER, dist REAL)) hit {} # sid - 连续命中次数 CONFIRM 5 # 连续 5 帧才算一次有效识别 COOLDOWN 10 # 同一个人 10 秒内不重复写 def confirm(sid, dist): hit[sid] hit.get(sid, 0) 1 if hit[sid] CONFIRM: return now int(time.time()) last conn.execute( SELECT MAX(ts) FROM logs WHERE name?, (names.get(sid),) ).fetchone()[0] or 0 if now - last COOLDOWN: return conn.execute(INSERT INTO logs VALUES(?,?,?), (names.get(sid), now, float(dist))) conn.commit()CONFIRM和COOLDOWN这两个数需要按场景调门禁场景人是站着不动的CONFIRM5在 25 fps 下才 0.2 秒可以再大一点考勤场景有人快步走过太大就抓不到。改完之后怎么验证没退化这是很多人忽略的一步。我一般准备一段录好的视频当固定输入把cv2.VideoCapture(0)换成cv2.VideoCapture(test.mp4)主循环其余部分不动跑完统计三个数检出帧率、每个人的正确识别率、陌生人被误认的比例。三个数记在本子上每改一次代码跑一遍对比。只看「感觉比刚才准了」是最不可靠的验证方式光照一变化就能把感觉打回原形。还有个小技巧把阈值扫描做成一条曲线。对每个阈值从 40 扫到 100统计误接受率陌生人被认成某人和误拒绝率本人被判成陌生人两条曲线的交点就是这套数据下的最优阈值比拍脑袋定 62 有依据得多。换摄像头、换场地、换人种之后重跑一次这条曲线是我现在的固定动作。从那以后我每次接这类人脸识别的小工程都会先把 dataset 里每个人的样本数、光照分布和预处理函数打印一遍再动手改识别逻辑——十次里有八次问题就出在这三样上识别器本身反而是最不该先动的地方。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑