Compare commits
No commits in common. "dev" and "main" have entirely different histories.
|
|
@ -1,260 +0,0 @@
|
|||
# 人脸识别系统 API 接口文档
|
||||
|
||||
本文档详细说明了人脸识别系统的后端业务接口与算法服务接口,供开发人员集成使用。
|
||||
|
||||
## 1. 系统业务接口 (Java Backend)
|
||||
|
||||
后端服务主要提供分组管理和人员信息管理功能。所有接口统一通过 HTTP 协议调用,返回 JSON 格式数据。
|
||||
|
||||
**基础路径**: `/api` (例如: `http://<server_ip>:<port>/api`)
|
||||
|
||||
### 1.1 分组管理
|
||||
|
||||
#### 1.1.1 创建/添加分组
|
||||
* **接口地址**: `/groups/add`
|
||||
* **请求方式**: `POST`
|
||||
* **请求类型**: `application/json`
|
||||
* **请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| name | String | 是 | 分组名称 |
|
||||
| description | String | 否 | 分组描述 (如果有) |
|
||||
|
||||
**请求示例**:
|
||||
```json
|
||||
{
|
||||
"name": "VIP人员",
|
||||
"description": "重要客户分组"
|
||||
}
|
||||
```
|
||||
|
||||
* **返回结果**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "VIP人员",
|
||||
"createTime": "2023-12-30T10:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.1.2 获取分组列表
|
||||
* **接口地址**: `/groups/list`
|
||||
* **请求方式**: `GET`
|
||||
* **返回结果**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "默认分组"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "员工分组"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 人员管理
|
||||
|
||||
#### 1.2.1 新增人员 (带照片)
|
||||
该接口用于注册新用户,同时上传人脸照片用于提取特征。
|
||||
|
||||
* **接口地址**: `/users/add`
|
||||
* **请求方式**: `POST`
|
||||
* **请求类型**: `multipart/form-data` (表单上传)
|
||||
* **请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| name | String | 是 | 人员姓名 |
|
||||
| groupId | Long | 否 | 所属分组ID |
|
||||
| photo | File | 是 | 人脸照片文件 (jpg/png) |
|
||||
|
||||
* **返回结果**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 101,
|
||||
"name": "张三",
|
||||
"groupId": 2,
|
||||
"feature": "..." // (内部可能不直接返回长特征,视具体实现而定)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2.2 编辑人员信息
|
||||
用于更新人员姓名、分组或重新上传人脸照片。
|
||||
|
||||
* **接口地址**: `/users/update/{id}`
|
||||
* **请求方式**: `POST`
|
||||
* **请求类型**: `multipart/form-data`
|
||||
* **路径参数**:
|
||||
* `id`: 人员ID (Long)
|
||||
|
||||
* **请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| name | String | 是 | 人员姓名 |
|
||||
| groupId | Long | 否 | 所属分组ID (不传则不修改) |
|
||||
| photo | File | 否 | 新的人脸照片 (不传则不修改) |
|
||||
|
||||
* **返回结果**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2.3 获取人员列表 (查询)
|
||||
* **接口地址**: `/users/list`
|
||||
* **请求方式**: `GET`
|
||||
* **请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| name | String | 否 | 按姓名模糊查询 |
|
||||
| groupId | Long | 否 | 按分组ID筛选 |
|
||||
|
||||
* **返回结果**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [ ...User Objects... ]
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2.4 人脸搜索 (1:N 检索)
|
||||
用于上传一张人脸照片,并在指定分组内检索最相似的人员。
|
||||
|
||||
* **接口地址**: `/users/search`
|
||||
* **请求方式**: `POST`
|
||||
* **请求类型**: `multipart/form-data`
|
||||
* **请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| photo | File | 是 | 待检索的人脸照片 |
|
||||
| groupId | Long | 是 | 目标分组ID |
|
||||
|
||||
* **返回结果**:
|
||||
|
||||
**成功找到匹配**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 101,
|
||||
"name": "张三",
|
||||
"groupId": 2,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**未找到匹配 (相似度低于阈值 0.6)**:
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"msg": "未找到匹配用户"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 算法服务接口 (Python - FaceFeatureExtractor)
|
||||
|
||||
该接口通常由 Java 后端内部调用,但如果开发人员需要独立调试算法或进行集成,可直接调用该微服务。
|
||||
|
||||
**功能**: 接收图片,进行质量检测(人脸检测、模糊度、亮度、姿态等),合格后提取 1024 维人脸特征向量。
|
||||
|
||||
**默认端口**: `8000` (具体取决于 docker-compose 配置)
|
||||
|
||||
### 2.1 提取人脸特征
|
||||
* **接口地址**: `/api/extract_feature`
|
||||
* **请求方式**: `POST`
|
||||
* **请求类型**: `multipart/form-data`
|
||||
* **请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| image | File | 是 | 图片文件 (支持 jpg, png 等常见格式) |
|
||||
|
||||
* **返回结果 (JSON)**:
|
||||
|
||||
**成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Success",
|
||||
"feature": [0.123, -0.456, ...], // 1024维浮点数组
|
||||
"feature_dim": 1024,
|
||||
"processing_time": 0.045
|
||||
}
|
||||
```
|
||||
|
||||
**失败/质量不合格响应**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "No face detected" // 或 "Face quality check failed...", "Server error..."
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 健康检查
|
||||
* **接口地址**: `/health`
|
||||
* **请求方式**: `GET`
|
||||
* **返回结果**: `{"status": "healthy", "service": "Face Feature Extractor"}`
|
||||
|
||||
### 2.3 人脸检测 (获取坐标)
|
||||
* **接口地址**: `/api/detect_face`
|
||||
* **请求方式**: `POST`
|
||||
* **请求类型**: `multipart/form-data`
|
||||
* **请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| image | File | 是 | 图片文件 |
|
||||
| expand_scale | Float | 否 | 扩充比例,默认 0.0。例如 0.3 表示长宽各扩充 30% |
|
||||
|
||||
* **返回结果**:
|
||||
|
||||
> **坐标说明**:
|
||||
> * `x1`, `y1`: 人脸检测框 **左上角** 的像素坐标。
|
||||
> * `x2`, `y2`: 人脸检测框 **右下角** 的像素坐标。
|
||||
> * `score`: 检测置信度 (0-1之间)。
|
||||
> * **注意**: 即使设置了 `expand_scale`,返回的坐标也会被限制在图片边界内 (Clip to bounds)。
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Success",
|
||||
"faces": [
|
||||
{
|
||||
"x1": 100.0,
|
||||
"y1": 50.0,
|
||||
"x2": 200.0,
|
||||
"y2": 150.0,
|
||||
"score": 0.98
|
||||
}
|
||||
],
|
||||
"processing_time": 0.02
|
||||
}
|
||||
```
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
"""
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, File, UploadFile, HTTPException, Form
|
||||
from fastapi import FastAPI, File, UploadFile, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
|
@ -118,94 +118,6 @@ async def extract_feature(image: UploadFile = File(...)):
|
|||
message=f"Server error: {str(e)}"
|
||||
)
|
||||
|
||||
# 新增人脸检测响应模型
|
||||
class FaceRect(BaseModel):
|
||||
x1: float
|
||||
y1: float
|
||||
x2: float
|
||||
y2: float
|
||||
score: float
|
||||
|
||||
class DetectFaceResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
faces: List[FaceRect] = []
|
||||
processing_time: Optional[float] = None
|
||||
|
||||
@app.post("/api/detect_face", response_model=DetectFaceResponse)
|
||||
async def detect_face(image: UploadFile = File(...), expand_scale: float = Form(0.0)):
|
||||
"""
|
||||
人脸检测接口
|
||||
输入: 图片文件, 扩充比例(expand_scale)
|
||||
输出: 人脸坐标列表 (x1, y1, x2, y2)
|
||||
"""
|
||||
import time
|
||||
start_time = time.time()
|
||||
try:
|
||||
img = decode_image(image)
|
||||
if img is None:
|
||||
raise HTTPException(status_code=400, detail="Invalid image file")
|
||||
|
||||
# 获取图片尺寸用于坐标截断
|
||||
h_img, w_img = img.shape[:2]
|
||||
|
||||
ext = get_extractor()
|
||||
# 直接调用检测器,不进行旋转校正,保证坐标对应原图
|
||||
boxes = ext.detect_faces(img)
|
||||
|
||||
face_rects = []
|
||||
if boxes:
|
||||
for box in boxes:
|
||||
# 原始坐标
|
||||
x1 = float(box.x1)
|
||||
y1 = float(box.y1)
|
||||
x2 = float(box.x2)
|
||||
y2 = float(box.y2)
|
||||
|
||||
# 应用扩充逻辑 (如果 expand_scale > 0)
|
||||
if expand_scale > 0:
|
||||
w = x2 - x1
|
||||
h = y2 - y1
|
||||
cx = x1 + w / 2
|
||||
cy = y1 + h / 2
|
||||
|
||||
new_w = w * (1 + expand_scale)
|
||||
new_h = h * (1 + expand_scale)
|
||||
|
||||
x1 = cx - new_w / 2
|
||||
y1 = cy - new_h / 2
|
||||
x2 = cx + new_w / 2
|
||||
y2 = cy + new_h / 2
|
||||
|
||||
# 强制限制坐标在图片范围内,防止出现负数或越界
|
||||
x1 = max(0.0, min(x1, float(w_img)))
|
||||
y1 = max(0.0, min(y1, float(h_img)))
|
||||
x2 = max(0.0, min(x2, float(w_img)))
|
||||
y2 = max(0.0, min(y2, float(h_img)))
|
||||
|
||||
face_rects.append(FaceRect(
|
||||
x1=x1,
|
||||
y1=y1,
|
||||
x2=x2,
|
||||
y2=y2,
|
||||
score=float(box.score)
|
||||
))
|
||||
|
||||
return DetectFaceResponse(
|
||||
success=True if face_rects else False,
|
||||
message="Success" if face_rects else "No face detected",
|
||||
faces=face_rects,
|
||||
processing_time=time.time() - start_time
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Detection failed: {e}", exc_info=True)
|
||||
return DetectFaceResponse(
|
||||
success=False,
|
||||
message=f"Server error: {str(e)}",
|
||||
processing_time=time.time() - start_time
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='Face Feature Extraction Microservice')
|
||||
|
|
|
|||
|
|
@ -106,23 +106,4 @@ public class UserController {
|
|||
result.put("data", count);
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/search")
|
||||
public Map<String, Object> searchUser(
|
||||
@RequestParam("photo") MultipartFile photo,
|
||||
@RequestParam("groupId") Long groupId) {
|
||||
|
||||
User user = userService.searchUser(photo, groupId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
if (user != null) {
|
||||
result.put("code", 200);
|
||||
result.put("msg", "success");
|
||||
result.put("data", user);
|
||||
} else {
|
||||
result.put("code", 404);
|
||||
result.put("msg", "未找到匹配用户");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,14 +58,5 @@ public interface UserService {
|
|||
*
|
||||
* @param id 用户ID
|
||||
*/
|
||||
/**
|
||||
* 人脸搜索
|
||||
*
|
||||
* @param photo 人脸照片
|
||||
* @param groupId 分组ID
|
||||
* @return 匹配的用户(如果匹配度过低返回null)
|
||||
*/
|
||||
User searchUser(MultipartFile photo, Long groupId);
|
||||
|
||||
void deleteUser(Long id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,6 @@ public class UserServiceImpl implements UserService {
|
|||
if (userGroupMapper.selectById(groupId) == null) {
|
||||
throw new RuntimeException("指定的用户分组不存在");
|
||||
}
|
||||
// 检查重名
|
||||
checkDuplicateName(name, groupId, null);
|
||||
}
|
||||
|
||||
// 确保上传目录存在
|
||||
|
|
@ -144,12 +142,6 @@ public class UserServiceImpl implements UserService {
|
|||
throw new RuntimeException("指定的用户分组不存在");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果修改了名字或分组,检查重名
|
||||
if (!user.getName().equals(name) || (groupId != null && !groupId.equals(user.getGroupId()))) {
|
||||
checkDuplicateName(name, groupId != null ? groupId : user.getGroupId(), id);
|
||||
}
|
||||
|
||||
user.setName(name);
|
||||
user.setGroupId(groupId);
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
|
|
@ -228,90 +220,4 @@ public class UserServiceImpl implements UserService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public User searchUser(MultipartFile photo, Long groupId) {
|
||||
if (groupId == null) {
|
||||
throw new RuntimeException("必须指定分组ID");
|
||||
}
|
||||
|
||||
// 1. 保存上传的照片 (临时)
|
||||
String fileName = IdUtil.fastSimpleUUID() + "_" + photo.getOriginalFilename();
|
||||
File uploadDir = new File(uploadPath);
|
||||
if (!uploadDir.exists()) {
|
||||
uploadDir.mkdirs();
|
||||
}
|
||||
File dest = new File(uploadDir, fileName);
|
||||
|
||||
try {
|
||||
photo.transferTo(dest);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("照片处理失败", e);
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 提取特征
|
||||
List<Float> targetFeature = faceFeatureExtractorClient.extractFeature(dest);
|
||||
if (targetFeature == null || targetFeature.isEmpty()) {
|
||||
throw new RuntimeException("未能从上传图片中提取到人脸特征");
|
||||
}
|
||||
|
||||
// 3. 获取分组下所有用户
|
||||
List<User> users = getUsers(null, groupId);
|
||||
if (users == null || users.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. 比对特征
|
||||
User bestMatch = null;
|
||||
float maxSimilarity = 0f;
|
||||
float threshold = 0.6f; // 相似度阈值
|
||||
|
||||
for (User user : users) {
|
||||
if (user.getFeatureData() == null)
|
||||
continue;
|
||||
|
||||
List<Float> dbFeature = com.bonuos.face.util.FaceUtils.parseFeatureData(user.getFeatureData());
|
||||
if (dbFeature != null) {
|
||||
float similarity = com.bonuos.face.util.FaceUtils.cosineSimilarity(targetFeature, dbFeature);
|
||||
if (similarity > maxSimilarity) {
|
||||
maxSimilarity = similarity;
|
||||
bestMatch = user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 返回结果
|
||||
if (maxSimilarity >= threshold) {
|
||||
return bestMatch;
|
||||
}
|
||||
return null; // 未找到匹配
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("人脸搜索失败: " + e.getMessage(), e);
|
||||
} finally {
|
||||
// 清理临时文件
|
||||
if (dest.exists()) {
|
||||
dest.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查分组下是否存在同名用户
|
||||
*/
|
||||
private void checkDuplicateName(String name, Long groupId, Long excludeId) {
|
||||
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User> wrapper = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
|
||||
wrapper.eq("group_id", groupId)
|
||||
.eq("name", name)
|
||||
.eq("status", 1); // 只检查有效用户
|
||||
|
||||
if (excludeId != null) {
|
||||
wrapper.ne("id", excludeId);
|
||||
}
|
||||
|
||||
if (userMapper.selectCount(wrapper) > 0) {
|
||||
throw new RuntimeException("该分组下已存在名为 [" + name + "] 的用户");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
package com.bonuos.face.util;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FaceUtils {
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 计算两个特征向量的余弦相似度
|
||||
*
|
||||
* @param feature1 特征向量1
|
||||
* @param feature2 特征向量2
|
||||
* @return 相似度 [-1, 1]
|
||||
*/
|
||||
public static float cosineSimilarity(List<Float> feature1, List<Float> feature2) {
|
||||
if (feature1 == null || feature2 == null || feature1.size() != feature2.size() || feature1.isEmpty()) {
|
||||
return -1f;
|
||||
}
|
||||
|
||||
float dotProduct = 0.0f;
|
||||
float normA = 0.0f;
|
||||
float normB = 0.0f;
|
||||
|
||||
for (int i = 0; i < feature1.size(); i++) {
|
||||
dotProduct += feature1.get(i) * feature2.get(i);
|
||||
normA += Math.pow(feature1.get(i), 2);
|
||||
normB += Math.pow(feature2.get(i), 2);
|
||||
}
|
||||
|
||||
if (normA == 0 || normB == 0) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return (float) (dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析特征数据字符串
|
||||
*
|
||||
* @param featureDataJson JSON字符串 [0.1, 0.2, ...]
|
||||
* @return 特征列表
|
||||
*/
|
||||
public static List<Float> parseFeatureData(String featureDataJson) {
|
||||
if (featureDataJson == null || featureDataJson.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(featureDataJson, new TypeReference<List<Float>>() {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue