3. 人脸检测与识别

任务目标

在Jetson Nano上使用OpenCV进行人脸检测,并通过FaceNet模型提取人脸特征向量,与预注册的人脸特征库比对,实现从摄像头实时画面中识别人脸身份并标注姓名。

环境与依赖

  • Jetson Nano(JetPack 4.6+,已安装CUDA)
  • Python 3.6+,OpenCV(pip install opencv-python)
  • TensorFlow 2.x(JetPack自带或pip install tensorflow-gpu)
  • FaceNet模型文件(facenet_model.h5,可从公开来源下载,输入尺寸160×160,输出128维向量)
  • Haar级联分类器(OpenCV自带haarcascade_frontalface_default.xml)

实现思路

程序分为两个阶段:注册阶段——对每个待识别的人拍摄一张照片,提取128维特征向量并保存到本地文件;识别阶段——打开摄像头,对每一帧检测人脸,提取特征并与注册库中所有向量计算欧氏距离,取最小距离且低于阈值者判定为对应身份,否则标记为“Unknown”。

关键完整代码

import cv2
import numpy as np
import os
import tensorflow as tf
from scipy.spatial.distance import euclidean

# ========== 配置 ==========
MODEL_PATH = "facenet_model.h5"
CASCADE_PATH = "haarcascade_frontalface_default.xml"
REGISTER_DIR = "registered_faces"   # 存放注册照片
FEATURES_FILE = "face_features.npy" # 保存特征向量
NAMES_FILE = "face_names.npy"       # 保存对应姓名
THRESHOLD = 0.9                     # 欧氏距离阈值,小于此值判定为同一人

# ========== 加载模型 ==========
model = tf.keras.models.load_model(MODEL_PATH)
face_cascade = cv2.CascadeClassifier(CASCADE_PATH)

def preprocess_face(face_img):
    """将检测到的人脸区域缩放为160x160并归一化到[-1,1]"""
    resized = cv2.resize(face_img, (160, 160))
    normalized = (resized.astype(np.float32) - 127.5) / 128.0
    return np.expand_dims(normalized, axis=0)

def get_embedding(face_img):
    """提取128维人脸特征向量"""
    preprocessed = preprocess_face(face_img)
    embedding = model.predict(preprocessed, verbose=0)[0]
    return embedding / np.linalg.norm(embedding)  # L2归一化

# ========== 注册新身份 ==========
def register_person(name, image_path):
    """从一张照片中提取特征并保存"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    faces = face_cascade.detectMultiScale(gray, 1.1, 5)
    if len(faces) == 0:
        print(f"错误:{image_path} 中未检测到人脸")
        return False
    
    x, y, w, h = max(faces, key=lambda f: f[2]*f[3])  # 取最大人脸
    face = img[y:y+h, x:x+w]
    embedding = get_embedding(face)
    
    # 加载已有特征库
    if os.path.exists(FEATURES_FILE):
        features = np.load(FEATURES_FILE)
        names = np.load(NAMES_FILE, allow_pickle=True)
    else:
        features = np.empty((0, 128))
        names = np.array([])
    
    # 追加新特征
    features = np.vstack([features, embedding.reshape(1, -1)])
    names = np.append(names, name)
    np.save(FEATURES_FILE, features)
    np.save(NAMES_FILE, names)
    print(f"已注册 {name},特征库现有 {len(names)} 人")
    return True

# ========== 识别摄像头画面 ==========
def recognize_face(face_img):
    """返回识别结果 (姓名, 距离)"""
    if not os.path.exists(FEATURES_FILE):
        return ("Unknown", None)
    
    embedding = get_embedding(face_img)
    features = np.load(FEATURES_FILE)
    names = np.load(NAMES_FILE, allow_pickle=True)
    
    distances = [euclidean(embedding, f) for f in features]
    min_idx = np.argmin(distances)
    min_dist = distances[min_idx]
    
    if min_dist < THRESHOLD:
        return (names[min_idx], min_dist)
    else:
        return ("Unknown", min_dist)

def run_camera():
    """主循环:打开摄像头实时识别"""
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("错误:无法打开摄像头")
        return
    
    print("按 'q' 退出,按 's' 保存当前帧")
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(80, 80))
        
        for (x, y, w, h) in faces:
            face = frame[y:y+h, x:x+w]
            name, dist = recognize_face(face)
            
            # 绘制框和姓名
            color = (0, 255, 0) if name != "Unknown" else (0, 0, 255)
            cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2)
            label = f"{name} ({dist:.2f})" if dist else name
            cv2.putText(frame, label, (x, y-10), 
                       cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
        
        cv2.imshow("Face Recognition", frame)
        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break
        elif key == ord('s'):
            cv2.imwrite("captured_frame.jpg", frame)
            print("已保存当前帧")
    
    cap.release()
    cv2.destroyAllWindows()

# ========== 命令行入口 ==========
if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1 and sys.argv[1] == "register":
        # 用法: python face_recognition.py register 姓名 照片路径
        if len(sys.argv) != 4:
            print("用法: python face_recognition.py register <姓名> <照片路径>")
        else:
            register_person(sys.argv[2], sys.argv[3])
    else:
        run_camera()

运行与验证

  1. 注册身份:准备每个待识别者的正面照片(光线充足、人脸清晰),执行:
    python face_recognition.py register "张三" /path/to/zhangsan.jpg
    python face_recognition.py register "李四" /path/to/lisi.jpg
    注册成功后控制台输出“已注册 xxx,特征库现有 N 人”。
  2. 启动实时识别:
    python face_recognition.py
    摄像头窗口打开后,将已注册的人脸对准摄像头,绿色框内应显示对应姓名;未注册人脸显示红色框和“Unknown”。
  3. 验证通过标准:
    • 已注册人员在正常光照下能被正确标注姓名(距离值小于0.9);
    • 未注册人员显示为“Unknown”;
    • 程序不崩溃,帧率可接受(Jetson Nano上约5-10 FPS属正常)。

常见问题

  • 检测不到人脸:确认摄像头可用(ls /dev/video0),光线充足,人脸正对镜头。
  • 识别错误率高:注册照片与摄像头画面差异过大时,重新注册更接近实际场景的照片;或适当调低THRESHOLD(如0.8)提高判定严格度。
  • 模型加载失败:确认facenet_model.h5路径正确,且为TensorFlow 2.x可加载的完整模型文件。