1. 项目概述当单板计算机遇上人脸识别最近在捣鼓一个嵌入式项目手头正好有一块虚谷号琢磨着给它加点“视觉智能”。人脸识别听起来高大上但在资源受限的嵌入式设备上跑起来其实有不少门道。虚谷号作为一款集成了高性能处理核心和丰富接口的单板计算机它不像树莓派那样家喻户晓但在教育、创客和轻量级AIoT场景里其实是个挺有意思的选择。它内置的NPU神经网络处理单元对于跑一些轻量级模型来说是个不小的优势。这个“人脸识别操作文档”项目本质上就是一次软硬件结合的实践如何在虚谷号上从零搭建一个能够实时捕获视频、检测并识别人脸的系统。它解决的不仅仅是“能不能跑起来”的问题更是“如何跑得稳、跑得好”的问题。这里面涉及到环境配置、模型选择与优化、前后端数据流设计、以及性能调优等一系列环节。无论你是嵌入式开发者想给产品增加人脸门禁功能还是学生、爱好者想学习边缘AI部署这个流程都有直接的参考价值。整个过程我会把踩过的坑、试出来的最优配置以及那些官方文档里不会写的细节都掰开揉碎了讲清楚。2. 核心思路与方案选型在虚谷号上做人脸识别核心思路很清晰摄像头捕获图像 - 人脸检测模型定位人脸 - 人脸特征提取模型获取特征向量 - 与数据库中的特征向量进行比对 - 输出识别结果。但每一步的实现都有多种技术路径选型直接决定了最终的体验和性能。2.1 硬件与基础软件栈选择虚谷号通常预装了基于Linux的系统如Ubuntu或定制发行版。我们的工作就在这个基础上展开。摄像头选型优先选择支持UVC协议的USB摄像头即插即用兼容性最好。像罗技C270/C920这类经典型号驱动完善在Linux下通过V4L2框架可以轻松调用。如果项目要求小型化也可以考虑虚谷号本身的CSI接口摄像头模组但需要确认内核驱动是否完备。编程语言Python是不二之选。其在计算机视觉和机器学习领域的生态无比强大OpenCV、NumPy等库提供了坚实的基础。虽然C在性能上可能有细微优势但Python的开发效率和丰富的库支持能让我们快速搭建原型并迭代。核心视觉库OpenCV。它提供了完整的图像捕获、预处理、显示和基础绘图功能。我们主要用它的VideoCapture接口读摄像头用cvtColor做色彩空间转换用imshow显示画面开发调试用。2.2 人脸检测与识别模型选型这是项目的技术核心。我们有两种主流路径传统特征方法如Haar CascadeOpenCV内置了基于Haar特征的级联分类器。优点是速度快资源占用极低在虚谷号上纯CPU运行也能达到很高的帧率。缺点是精度相对较低对光照、角度变化敏感且只能完成“检测”框出人脸无法进行“识别”这是谁。深度学习模型这是实现高精度人脸识别的关键。我们需要两个模型人脸检测模型如MTCNN、SSD、YOLO-Face等负责在图像中找到所有人脸的位置。人脸特征提取模型如FaceNet、ArcFace、MobileFaceNet等负责将裁剪出的人脸图像转换为一个固定长度的特征向量embedding。识别过程就是计算两个特征向量之间的相似度常用余弦相似度或欧氏距离。我们的选择为了平衡精度和速度特别是在利用虚谷号NPU的考虑下我推荐以下方案人脸检测采用轻量级的Ultra-Light-Fast-Generic-Face-Detector-1MB简称ULFD。这个模型非常小巧精度足够在CPU上也能实时运行非常适合嵌入式设备。特征提取采用MobileFaceNet。它专为移动和嵌入式设备设计在精度与速度之间取得了绝佳平衡。更重要的是我们可以将其转换为适合NPU加速的格式如.kmodel从而大幅提升推理速度。注意如果追求极致的检测速度在简单场景下可以先用Haar Cascade进行初筛再用深度学习模型精定位。但为了流程清晰本项目将统一使用深度学习方案。2.3 开发框架与推理引擎模型需要特定的框架来加载和运行。训练框架模型通常来源于PyTorch或TensorFlow。MobileFaceNet在PyTorch和TensorFlow下都有开源实现。部署推理引擎这是关键。我们需要一个能在虚谷号上高效运行模型的工具。ONNX Runtime通用性强支持多种硬件后端CPU GPU。我们可以将训练好的模型导出为ONNX格式然后用ONNX Runtime推理。这是一个稳妥的跨平台方案。厂商专用工具链为了发挥虚谷号NPU的最大效能必须使用其官方提供的模型转换工具和推理库。例如将PyTorch/TensorFlow模型 - ONNX - 专用工具转换 -.kmodel格式 - 调用专属NPU推理库。这一步是性能飞跃的关键但也是坑最多的地方。最终方案流程图USB摄像头 - OpenCV捕获帧 - 图像预处理(缩放归一化) - ULFD人脸检测模型(CPU/NPU) - 获取人脸框 - 对齐裁剪 - MobileFaceNet特征提取模型(优先NPU) - 生成128维特征向量 - 与本地特征库比对(余弦相似度) - 识别结果 - OpenCV标注显示3. 环境搭建与核心依赖部署一套干净、可复现的环境是成功的基石。下面是在虚谷号的全新系统上搭建环境的步骤。3.1 系统基础更新与摄像头测试首先通过SSH或直接连接显示器键盘操作虚谷号。# 1. 更新软件源和系统 sudo apt update sudo apt upgrade -y # 2. 安装必要的系统工具 sudo apt install -y git cmake build-essential pkg-config libatlas-base-dev # 3. 测试摄像头是否被识别 ls /dev/video* # 通常会出现 /dev/video0 或 /dev/video1 # 安装一个小工具来测试摄像头画面 sudo apt install -y v4l-utils # 使用v4l2-ctl查看摄像头信息 v4l2-ctl --list-devices # 使用ffplay快速预览如果安装了ffmpeg ffplay -f v4l2 -i /dev/video0如果能看到视频流说明摄像头驱动正常。3.2 Python环境与OpenCV安装虚谷号的系统可能预装了Python3我们使用pip3进行包管理。强烈建议使用虚拟环境。# 1. 安装pip和虚拟环境工具 sudo apt install -y python3-pip python3-venv # 2. 创建项目目录并进入 mkdir face_recognition_vugu cd face_recognition_vugu # 3. 创建Python虚拟环境 python3 -m venv venv # 4. 激活虚拟环境 source venv/bin/activate # 你的命令行提示符前会出现 (venv) # 5. 安装OpenCV。 # 在嵌入式平台直接安装OpenCV-Python头文件版可能会因为编译依赖出错。 # 最稳妥的方法是安装系统仓库预编译的版本虽然版本可能略旧但最稳定。 sudo apt install -y python3-opencv # 验证安装 python3 -c import cv2; print(cv2.__version__)3.3 深度学习框架与模型部署这里我们以ONNX Runtime为例展示一个不依赖NPU的通用方案。NPU加速方案需要根据虚谷号官方文档安装特定的SDK和工具链流程类似但更具体。# 在虚拟环境中安装其他依赖 pip3 install numpy onnxruntime opencv-python-headless pillow scipy # 下载我们选定的模型 # 创建模型目录 mkdir models cd models # 假设我们已经拥有转换好的ONNX模型文件 # 1. face_detector_ulfd.onnx (人脸检测) # 2. mobilefacenet.onnx (特征提取) # 你可以从开源项目如 deepinsight/insightface中获取预训练模型并自行转换或寻找可靠的转换后模型。 # 这里以手动放置模型文件为例。模型转换简要说明关键步骤获取原始模型PyTorch的.pth或TensorFlow的.pb。使用对应框架的导出功能将模型转换为ONNX格式。例如在PyTorch中import torch dummy_input torch.randn(1, 3, 112, 112) # MobileFaceNet的输入尺寸 torch.onnx.export(model, dummy_input, mobilefacenet.onnx, opset_version11)如果需要使用NPU则使用虚谷号提供的nncase等工具将ONNX模型转换为.kmodel格式。这个步骤通常涉及量化将FP32精度转换为INT8以提升速度是影响精度的关键需要仔细调整量化参数。3.4 构建人脸特征数据库识别的前提是要有一个已知人脸的数据库。我们需要一个脚本来录入人脸。# enroll.py 人脸注册脚本 import cv2 import numpy as np import onnxruntime as ort import os from PIL import Image # 初始化模型 detection_session ort.InferenceSession(models/face_detector_ulfd.onnx) recognition_session ort.InferenceSession(models/mobilefacenet.onnx) def get_face_embedding(face_img): 对人脸图像进行预处理并提取特征向量 # 1. 将OpenCV的BGR图像转换为RGB face_rgb cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) # 2. 调整为模型输入尺寸例如112x112 face_resized cv2.resize(face_rgb, (112, 112)) # 3. 归一化 (像素值从0-255映射到-1到1根据模型训练方式调整) face_normalized (face_resized.astype(np.float32) - 127.5) / 128.0 # 4. 转换维度为 (1, C, H, W) 即 (1, 3, 112, 112) input_blob np.transpose(face_normalized, (2, 0, 1)) input_blob np.expand_dims(input_blob, axis0) # 5. 推理 embedding recognition_session.run(None, {input: input_blob})[0] # 6. 归一化特征向量L2归一化方便后续余弦相似度计算 embedding embedding / np.linalg.norm(embedding) return embedding.flatten() def main(): name input(请输入注册人的姓名: ) cap cv2.VideoCapture(0) print(请正对摄像头调整好位置。按 s 键捕获人脸并注册按 q 键退出。) while True: ret, frame cap.read() if not ret: break # 此处简化假设直接使用整个画面中心区域作为人脸。实际应运行检测模型。 h, w frame.shape[:2] # 取画面中心部分作为示例 margin 100 face_roi frame[h//2-margin:h//2margin, w//2-margin:w//2margin] cv2.rectangle(frame, (w//2-margin, h//2-margin), (w//2margin, h//2margin), (0, 255, 0), 2) cv2.imshow(Enroll Face, frame) key cv2.waitKey(1) 0xFF if key ord(s): # 提取特征 embedding get_face_embedding(face_roi) # 保存到文件 data_dir face_data os.makedirs(data_dir, exist_okTrue) np.save(os.path.join(data_dir, f{name}.npy), embedding) print(f人脸数据已保存为 {name}.npy) break elif key ord(q): break cap.release() cv2.destroyAllWindows() if __name__ __main__: main()这个脚本是一个简化示例。完整版需要集成人脸检测模型自动框选并裁剪出最清晰的一张人脸进行注册。4. 核心代码实现与流程解析接下来我们实现主识别程序。它将串联起所有模块。4.1 主程序架构# main.py import cv2 import numpy as np import onnxruntime as ort import os import time class FaceRecognizer: def __init__(self, det_model_path, rec_model_path, data_dirface_data, threshold0.6): 初始化识别器 Args: threshold: 相似度阈值高于此值则认为识别成功 # 初始化ONNX Runtime会话 self.det_session ort.InferenceSession(det_model_path) self.rec_session ort.InferenceSession(rec_model_path) # 加载人脸数据库 self.face_database {} self.load_database(data_dir) self.threshold threshold # 获取模型输入信息 self.rec_input_name self.rec_session.get_inputs()[0].name det_input_shape self.det_session.get_inputs()[0].shape self.det_input_size (det_input_shape[3], det_input_shape[2]) # (W, H) 通常是320x240 def load_database(self, data_dir): 加载所有.npy特征文件到内存 if not os.path.exists(data_dir): os.makedirs(data_dir) print(f数据库目录 {data_dir} 已创建当前为空。) return for file in os.listdir(data_dir): if file.endswith(.npy): name os.path.splitext(file)[0] embedding np.load(os.path.join(data_dir, file)) self.face_database[name] embedding print(f加载用户: {name}) print(f数据库加载完成共 {len(self.face_database)} 人。) def preprocess_for_detection(self, img): 预处理图像以适应检测模型输入 # 调整尺寸 img_resized cv2.resize(img, self.det_input_size) # 归一化 (根据模型要求ULFD通常需要) img_normalized img_resized.astype(np.float32) img_normalized (img_normalized - 127.5) / 128.0 # 转换维度为 NCHW img_chw np.transpose(img_normalized, (2, 0, 1)) img_batch np.expand_dims(img_chw, axis0) return img_batch def detect_faces(self, img): 使用ULFD模型检测人脸返回边界框列表 input_blob self.preprocess_for_detection(img) # 运行推理 outputs self.det_session.run(None, {input: input_blob}) # 这里需要根据ULFD模型的输出结构进行解析获取框、置信度等。 # 假设outputs[0]是形状为[1, 4420, 4]的框outputs[1]是置信度 boxes outputs[0][0] # (4420, 4) [x1, y1, x2, y2] 相对坐标 scores outputs[1][0] # (4420,) # 后处理根据置信度过滤并将相对坐标转换为原图绝对坐标 det_boxes [] conf_threshold 0.7 h, w img.shape[:2] for i, score in enumerate(scores): if score conf_threshold: box boxes[i] # 将相对坐标[0,1]转换为绝对坐标 x1 int(box[0] * w) y1 int(box[1] * h) x2 int(box[2] * w) y2 int(box[3] * h) det_boxes.append((x1, y1, x2, y2, score)) return det_boxes def get_embedding(self, face_img): 对人脸区域图像提取特征向量 # 与enroll.py中的预处理保持一致 face_rgb cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) face_resized cv2.resize(face_rgb, (112, 112)) face_normalized (face_resized.astype(np.float32) - 127.5) / 128.0 input_blob np.transpose(face_normalized, (2, 0, 1)) input_blob np.expand_dims(input_blob, axis0) embedding self.rec_session.run(None, {self.rec_input_name: input_blob})[0] embedding embedding / np.linalg.norm(embedding) return embedding.flatten() def recognize(self, embedding): 将待识别特征与数据库比对返回姓名和相似度 max_sim -1 identity Unknown for name, db_emb in self.face_database.items(): # 计算余弦相似度 sim np.dot(embedding, db_emb) / (np.linalg.norm(embedding) * np.linalg.norm(db_emb)) if sim max_sim: max_sim sim identity name if sim self.threshold else Unknown return identity, max_sim def run(self, camera_id0): 主循环捕获视频检测识别显示 cap cv2.VideoCapture(camera_id) if not cap.isOpened(): print(无法打开摄像头) return print(开始实时识别按 q 键退出。) fps_time time.time() frame_count 0 while True: ret, frame cap.read() if not ret: break frame_count 1 # 1. 人脸检测 det_boxes self.detect_faces(frame) # 2. 对每个检测到的人脸进行识别 for (x1, y1, x2, y2, score) in det_boxes: # 裁剪人脸区域 face_roi frame[y1:y2, x1:x2] if face_roi.size 0: continue # 3. 提取特征 try: embedding self.get_embedding(face_roi) except Exception as e: print(f特征提取失败: {e}) continue # 4. 识别 name, sim self.recognize(embedding) # 5. 绘制结果 color (0, 255, 0) if name ! Unknown else (0, 0, 255) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) label f{name}: {sim:.2f} cv2.putText(frame, label, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) # 计算并显示FPS if frame_count % 30 0: fps 30 / (time.time() - fps_time) fps_time time.time() cv2.putText(frame, fFPS: {fps:.1f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) cv2.imshow(Face Recognition on Vugu, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() if __name__ __main__: recognizer FaceRecognizer( det_model_pathmodels/face_detector_ulfd.onnx, rec_model_pathmodels/mobilefacenet.onnx, threshold0.65 # 相似度阈值可根据实际情况调整 ) recognizer.run()4.2 关键代码段解析模型推理使用onnxruntime.InferenceSession加载模型。run方法执行推理需要传入正确的输入节点名称和预处理后的数据。人脸检测后处理detect_faces函数返回的框是模型输出的原始格式需要根据模型设计进行解析。ULFD模型通常输出大量候选框和置信度我们需要应用置信度过滤和非极大值抑制NMS来去除重叠框。上述代码简化了NMS步骤实际应用中必须加上否则同一个脸上可能会有多个框。特征比对recognize函数采用最简单的线性扫描比对。当数据库很大时如超过1000人这会成为性能瓶颈。生产环境应考虑使用向量数据库如FAISS进行加速。性能显示计算FPS有助于我们直观了解系统实时性是性能调优的重要参考。5. 性能优化与NPU加速实战在CPU上运行两个深度学习模型即使是轻量级模型虚谷号的负担也会很重FPS可能只有个位数。要达到流畅的实时识别15 FPS以上必须利用NPU。5.1 模型转换与量化以K210 NPU为例虚谷号若采用嘉楠Kendryte K210芯片其工具链通常为nncase。转换流程概要如下安装nncase根据官方文档在x86开发机上安装nncase。准备ONNX模型确保你的mobilefacenet.onnx和ulfd.onnx是静态形状即输入维度固定。编写转换脚本# 假设的转换命令具体参数需参考nncase文档 ncc compile mobilefacenet.onnx mobilefacenet.kmodel \ --target k210 \ --input-format onnx \ --output-format kmodel \ --input-shape [1,3,112,112] \ --dataset ./calibration_images \ # 量化所需的校准数据集 --quant-type uint8校准数据集准备几百张涵盖不同光照、人脸的图片放在calibration_images目录下用于量化时校准激活值分布这对保持量化后模型精度至关重要。5.2 使用NPU推理库转换得到.kmodel文件后需要将其拷贝到虚谷号上并使用K210的专用推理库如libnncase.so或Python bindingkpu来加载和运行。# 假设虚谷号系统提供了kpu库 from kpu import KPU # 初始化KPU并加载模型 kpu_rec KPU() kpu_rec.load_kmodel(/path/to/mobilefacenet.kmodel) # 准备数据数据需要预处理并转换为KPU接受的格式例如从OpenCV图像到特定内存布局 # ... # 运行推理 kpu_rec.run(input_data) output kpu_rec.get_outputs()关键点NPU通常对输入数据的布局NCHW/NHWC、数值范围0-255或归一化后有严格要求必须与模型转换时的设定完全一致。这部分的代码需要仔细对照工具链文档。5.3 系统级优化技巧即使使用了NPU整个流水线的瓶颈也可能出现在其他地方。图像采集与预处理降低分辨率将摄像头采集分辨率从1080p降至720p或480p能极大减轻后续所有环节的压力。OpenCV优化使用cv2.CAP_PROP_FPS设置合适的帧率避免无意义的超高频采集。使用cv2.UMat尝试使用OpenCL加速如果虚谷号支持。流水线并行识别流程是串行的读图 - 检测 - (对每个人脸) 裁剪 - 对齐 - 特征提取 - 比对。可以考虑使用多线程或异步编程让检测和上一帧的特征提取/比对同时进行提升整体吞吐量。数据库比对优化如前所述使用FAISS库。将特征数据库构建成FAISS索引如IndexFlatIP用于内积相似度比对速度可提升数十倍。import faiss index faiss.IndexFlatIP(128) # 128维特征 # 将所有已知特征向量添加到index database_embeddings np.array(list(self.face_database.values())).astype(float32) index.add(database_embeddings) # 查询 D, I index.search(query_embedding.reshape(1, -1), k1) # k1找最相似的6. 常见问题排查与调试心得在实际部署中你几乎一定会遇到下面这些问题。6.1 摄像头无法打开或画面异常问题cv2.VideoCapture(0)返回False或画面卡顿、花屏。排查检查设备号运行ls /dev/video*尝试cv2.VideoCapture(/dev/video0)。检查权限当前用户是否有摄像头设备访问权限可尝试sudo chmod 666 /dev/video0临时解决或将用户加入video组sudo usermod -aG video $USER。检查格式有些摄像头需要指定格式。尝试cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M,J,P,G))。降低参数先以最低分辨率、帧率打开cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640); cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480); cap.set(cv2.CAP_PROP_FPS, 15)。6.2 模型推理速度慢问题FPS低于5无法实时。排查与解决确认硬件加速首先确认ONNX Runtime是否使用了正确的Execution Provider。在虚谷号上可能只有CPU可用。print(ort.get_available_providers())。降低输入尺寸将检测模型的输入尺寸从320x240降至160x120速度会成倍提升但小脸检测能力会下降。量化模型如果使用NPU务必进行INT8量化。如果只用CPU也可以尝试使用ONNX Runtime的量化工具生成INT8模型通常能提升速度且精度损失可控。帧采样不必每帧都进行识别。可以每3帧或5帧处理一次中间帧直接沿用上一帧的结果这是提升视觉流畅感的常用技巧。6.3 识别准确率低问题明明注册过的人却经常识别为Unknown或者张冠李戴。排查与解决检查阈值threshold是平衡误识和漏识的关键。通过计算注册人脸与若干干扰人脸的相似度分布来选择一个合适的阈值如0.5-0.7。可以写一个测试脚本统计相似度得分。改善注册质量注册时确保人脸光照均匀、正对摄像头、表情自然。最好能采集多张不同角度的照片并取其特征向量的平均值作为最终存档这能显著提升模型鲁棒性。人脸对齐在特征提取前增加一个人脸关键点检测和对齐的步骤例如根据双眼坐标进行仿射变换使输入模型的人脸都是“摆正”的能极大提升MobileFaceNet等模型的识别精度。数据预处理一致性确保注册和识别时人脸图像的预处理流程裁剪、缩放、归一化公式完全一致。一个像素值范围的差异都可能导致特征向量天差地别。6.4 内存或进程崩溃问题运行一段时间后程序占用内存越来越大最终崩溃。排查检查循环引用在长时间运行的循环中确保大的临时变量如图像数组被及时释放或覆盖。ONNX Runtime内存ONNX Runtime会话在推理时可能会缓存一些内存。对于长期运行的服务定期监控内存使用。虚谷号内存限制虚谷号内存可能只有几百MB。确保同时运行的程序不多。可以使用htop命令监控内存和CPU使用情况。6.5 NPU推理结果异常问题使用NPU推理后得到的特征向量全是0或NaN或者相似度计算完全不对。排查数据预处理这是最常见的原因。百分之百确认输入给NPU模型的数据格式、布局、数值范围与模型转换时设定的完全一致。对比CPUONNX Runtime和NPU推理的输入数据确保每一个字节都相同。量化误差INT8量化会带来精度损失。如果校准数据集不具有代表性例如全是白种人男性照片那么对差异较大的输入如深色皮肤、女性量化误差可能被放大。尝试使用更多样化的校准集重新量化。模型输出解析NPU模型的输出数据布局可能与ONNX模型不同。仔细阅读推理库的API文档确认输出张量的形状和含义。调试这类问题一个非常有效的方法是“分步对比法”准备一张固定的测试图片分别用原始的PyTorch/TensorFlow模型、转换后的ONNX模型在CPU上运行、以及最终的NPU模型进行推理逐层对比中间关键节点的输出例如预处理后的第一个像素值、网络第一层的输出、最终的特征向量。一旦发现哪一步开始出现显著差异问题就锁定在哪一步。这个过程很枯燥但却是解决部署难题的唯一捷径。