基于ONNX Runtime的人脸识别比对系统全栈实战

1. 项目简介

2. 整体架构

3. 模型准备与加密加载

4. 人脸检测与图像预处理

5. 多任务属性推理

6. 人脸比对与相似度映射

7. 前后端交互设计

8. 前端视觉与体验优化

9. 部署与演示

10. 总结与展望

        一个完整的在线人脸识别比对Web应用,涵盖人脸检测、属性分析、活体特征提取与相似度计算。本文将带你一步步拆解其技术实现,并附上在线Demo方便体验。

1. 项目简介

        这是一个轻量级的人脸识别比对工具,用户上传两张含有人脸的图片,系统自动完成人脸检测、年龄/性别估计、姿态角计算、活体特征提取,并输出两张人脸的相似度分数。整个系统采用 **Python Tornado** 作为后端框架,**ONNX Runtime** 进行模型推理,前端使用原生 JavaScript 配合 jQuery 交互,部署简单,适合个人学习和小规模应用。

源码地址:源码

2. 整体架构

系统分为三层:

- **前端**:HTML5 + CSS3 双栏布局,通过 AJAX 调用后端 RESTful API。
- **后端**:Tornado Web 服务,提供图片上传、人脸检测、相似度比对三个接口。
- **模型层**:使用 **ONNX Runtime** 加载加密的 ONNX 模型文件,进行高效推理。

核心流程图:
上传图片 → 图片接收与格式校验 → 人脸检测(Detect) → 人脸裁剪 → 
多模型推理(性别/年龄/活体/角度) → 返回结果(特征、属性、框) → 
前端展示 & 用户触发比对 → 余弦相似度计算 → 得分映射 → 显示相似度

3. 模型准备与加载

为了保护模型资产,本项目将 ONNX 模型文件使用 AES-CBC 加密存储,运行时动态解密并加载到内存中。

加密方式:

```python
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.padding import PKCS7

def decrypt_file_to_bytes(input_path, key, iv):
    key = bytes.fromhex(key)
    iv = bytes.fromhex(iv)
    with open(input_path, 'rb') as fin:
        ct_bytes = fin.read()
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
    decryptor = cipher.decryptor()
    unpadder = PKCS7(algorithms.AES.block_size).unpadder()
    plain_text = unpadder.update(decryptor.update(ct_bytes)) + unpadder.finalize()
    return plain_text
```

加载模型时直接传入解密后的字节流,避免模型文件在磁盘上以明文形式存在:

```python
import onnxruntime as ort

face_live_model = ort.InferenceSession(
    decrypt_file_to_bytes('model/face_live.onnx.enc', key, iv),
    providers=['CPUExecutionProvider']
)
```

4. 人脸检测与图像预处理

4.1 检测模型预处理

采用标准的 `detect.onnx` 模型(例如 YOLO 变体),输入尺寸为 640x640。为了保持宽高比,使用 **letterbox** 填充方式,填充区域使用灰色 (128,128,128):

```python
def detect_pre_process(img, output_size=(640, 640)):
    # 等比例缩放 + 居中填充
    width, height = img.size
    new_img = Image.new('RGB', output_size, color=(128,128,128))
    ratio = min(output_size[0]/width, output_size[1]/height)
    new_size = (int(width*ratio), int(height*ratio))
    resized = img.resize(new_size, Image.BILINEAR)
    left = (output_size[0] - new_size[0]) // 2
    top = (output_size[1] - new_size[1]) // 2
    new_img.paste(resized, (left, top))
    img_array = np.array(new_img) / 255.0
    img_array = img_array.transpose((2, 0, 1))  # CHW
    return img_array, width, height
```

4.2 检测后处理与坐标映射

模型输出的检测框是相对于 640x640 画布的归一化坐标(中心点 x, y,宽 w,高 h,置信度 conf)。需要根据原始图片的宽高和填充情况,反算到原始图像坐标系:

```python
if width > height:
    midx = box[0] * (width/640)
    midy = box[1] * (width/640) - (width-height)/2
    w = box[2] * (width/640)
    h = box[3] * (width/640)
else:
    midx = box[0] * (height/640) - (height-width)/2
    midy = box[1] * (height/640)
    w = box[2] * (height/640)
    h = box[3] * (height/640)
x1 = midx - w/2
y1 = midy - h/2
x2 = midx + w/2
y2 = midy + h/2
```

为了后续属性分析更准确,将检测框向外扩展一定比例作为裁剪区域:

```python
x1_crop = x1 - w * 0.3
y1_crop = y1 - h * 0.6
x2_crop = x2 + w * 0.3
y2_crop = y2 + h * 0.2
# 边界裁剪
x1_crop = max(0, x1_crop)
y1_crop = max(0, y1_crop)
x2_crop = min(width, x2_crop)
y2_crop = min(height, y2_crop)
face_crop = img.crop((x1_crop, y1_crop, x2_crop, y2_crop))
```

5. 多任务属性推理

裁剪后的人脸区域统一缩放到各自模型要求的尺寸,并进行标准化。

- **性别、活体、年龄**:输入 224x224,使用 ImageNet 均值和标准差归一化。
- **角度**:输入 64x64,同样的归一化处理。
- **性别分类**:输出两个类别的概率,取 argmax 得到男女。
- **年龄回归**:输出值加 1 并四舍五入,得到整数值。
- **活体特征**:直接返回模型输出的浮点数向量,用于后续比对。
- **姿态角**:模型输出三个角度值(yaw, pitch, roll)。

预处理代码示例:

```python
def pre_process(img, size=(224, 224)):
    mean = np.array([0.485, 0.456, 0.406]).reshape(3,1,1)
    std  = np.array([0.229, 0.224, 0.225]).reshape(3,1,1)
    img_array = np.array(img.resize(size, Image.BILINEAR)) / 255.0
    img_array = img_array.transpose((2,0,1))
    return ((img_array - mean) / std).astype('float32')
```

6. 人脸比对与相似度映射

从两张图片中分别提取到活体特征向量后,计算**余弦相似度**:

```python
def cosine_similarity(vec1, vec2):
    vec1, vec2 = np.array(vec1), np.array(vec2)
    dot = np.dot(vec1, vec2)
    norm = np.linalg.norm(vec1) * np.linalg.norm(vec2)
    return dot / norm if norm != 0 else 0
```

但余弦相似度的范围是 [-1, 1],而我们希望输出 0~100 的分值,更直观。本项目采用了一个分段线性映射函数:

```python
def get_score(x):
    if x <= 0.5:
        score = 120 * x
    elif x >= 0.616:
        score = 26.041666 * x + 73.95833
    else:
        score = 258.62069 * x - 69.31034
    score = max(0, min(100, score))
    return score
```

该映射将阈值 0.5 映射到 60 分,0.616 映射到 90 分,整个函数是单调递增的,保证分数越高表示越相似。实际应用中,80 分以上通常可认为是同一人。

7. 前后端交互设计

7.1 后端接口

- **上传接口** `/face-api/v1/face/upload`  
  POST 接收 `image` 文件,校验格式与大小,使用 Pillow 的 `ImageOps.exif_transpose` 自动纠正图片方向,存储到 `static/images/`,返回文件路径。

- **人脸检测与属性接口** `/face-api/v1/face/detect_list`  
  接收 JSON,内容为多个图片路径数组 `params`,批量处理并返回每个人脸的属性、特征(已加密)以及人脸框。活体特征使用简单的异或加密后经 Base64 编码,确保传输中不被篡改。

- **人脸比对接口** `/face-api/v1/face/match`  
  接收两个加密特征字符串,解密后计算余弦相似度并映射为 0~100 分数。

7.2 前端逻辑

前端使用 jQuery + layer 弹窗,流程如下:

1. 用户选择图片 → 调用上传接口获取服务器路径。
2. 调用 detect_list 接口,传入图片路径,获取年龄、性别、姿态角、框坐标及加密特征,并显示在原图上(后端将框绘制在图片上)。
3. 用户点击“开始比对” → 取出两张图片的加密特征,调用 match 接口,将返回的分数更新到页面中央的相似度显示区。

前端使用 `$.ajax` 发送请求,同时显示加载动画,并对网络异常、超时等进行了友好提示。

8. 前端视觉与体验优化

界面采用渐变背景、圆角卡片和悬浮动画,让整体风格现代且亲和。双栏布局中间放置 VS 标志和实时相似度,一目了然。

底部提供了详细的使用说明,解释姿态角(Yaw/Pitch/Roll)的含义和推荐范围,帮助用户理解人脸质量对识别的影响。同时,页面绘制了人脸框,并将框坐标、置信度等信息清晰展示。

9. 部署与演示

源码地址:源码

项目依赖较少,所有模型均加密存储,启动脚本直接运行即可(默认 80 端口):

```bash
pip install tornado pillow onnxruntime numpy cryptography
python app.py
```

若需修改端口,可通过命令行参数传入:

```bash
python app.py 8080
```

10. 总结与展望

本文展示了一个面向实用场景的轻量级人脸识别比对系统的全栈实现。通过 ONNX Runtime 的跨平台推理能力和 Tornado 的异步高性能,我们可以在普通服务器上流畅运行多个模型。模型加密、特征加密等措施也在一定程度上保护了核心资产。

未来可扩展方向:
- 接入人脸特征数据库,实现 1:N 的人脸检索;
- 增加活体检测动作指令,提升防攻击能力;
- 前端使用 WebSocket 实现更流畅的实时摄像头识别。

希望这篇实战分享能帮助你快速搭建自己的人脸识别应用,如果有任何问题或建议,欢迎在评论区交流!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐