Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
e0af18b363 |
|
|
@ -0,0 +1,224 @@
|
||||||
|
# 人脸识别系统 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"}`
|
||||||
|
|
@ -106,4 +106,23 @@ public class UserController {
|
||||||
result.put("data", count);
|
result.put("data", count);
|
||||||
return result;
|
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,5 +58,14 @@ public interface UserService {
|
||||||
*
|
*
|
||||||
* @param id 用户ID
|
* @param id 用户ID
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 人脸搜索
|
||||||
|
*
|
||||||
|
* @param photo 人脸照片
|
||||||
|
* @param groupId 分组ID
|
||||||
|
* @return 匹配的用户(如果匹配度过低返回null)
|
||||||
|
*/
|
||||||
|
User searchUser(MultipartFile photo, Long groupId);
|
||||||
|
|
||||||
void deleteUser(Long id);
|
void deleteUser(Long id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ public class UserServiceImpl implements UserService {
|
||||||
if (userGroupMapper.selectById(groupId) == null) {
|
if (userGroupMapper.selectById(groupId) == null) {
|
||||||
throw new RuntimeException("指定的用户分组不存在");
|
throw new RuntimeException("指定的用户分组不存在");
|
||||||
}
|
}
|
||||||
|
// 检查重名
|
||||||
|
checkDuplicateName(name, groupId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保上传目录存在
|
// 确保上传目录存在
|
||||||
|
|
@ -142,6 +144,12 @@ public class UserServiceImpl implements UserService {
|
||||||
throw new RuntimeException("指定的用户分组不存在");
|
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.setName(name);
|
||||||
user.setGroupId(groupId);
|
user.setGroupId(groupId);
|
||||||
user.setUpdateTime(LocalDateTime.now());
|
user.setUpdateTime(LocalDateTime.now());
|
||||||
|
|
@ -220,4 +228,90 @@ 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 + "] 的用户");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
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.
|
|
@ -0,0 +1,103 @@
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 配置服务地址
|
||||||
|
JAVA_BACKEND_URL = "http://localhost:18080"
|
||||||
|
PYTHON_ALGO_URL = "http://localhost:18000"
|
||||||
|
|
||||||
|
def create_test_image(filename="test_face.jpg"):
|
||||||
|
"""创建一张简单的测试图片(如果不存在)"""
|
||||||
|
if not os.path.exists(filename):
|
||||||
|
print(f"Creating test image: {filename}...")
|
||||||
|
# 创建一个 640x480 的黑色图像
|
||||||
|
img = np.zeros((480, 640, 3), np.uint8)
|
||||||
|
# 画一个简单的圆代表"脸" (虽然检测不到,但可以测试文件上传流程)
|
||||||
|
cv2.circle(img, (320, 240), 100, (255, 255, 255), -1)
|
||||||
|
cv2.imwrite(filename, img)
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def test_python_health():
|
||||||
|
"""测试 Python 算法服务健康检查"""
|
||||||
|
url = f"{PYTHON_ALGO_URL}/health"
|
||||||
|
print(f"\n[Testing] Python Algorithm Service Health ({url})...")
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
print(f"✅ Success: {response.json()}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed: Status {response.status_code}, Response: {response.text}")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("❌ Failed: Connection refused. Is the service running on port 18000?")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
|
||||||
|
def test_extract_feature(image_path):
|
||||||
|
"""测试人脸特征提取接口"""
|
||||||
|
url = f"{PYTHON_ALGO_URL}/api/extract_feature"
|
||||||
|
print(f"\n[Testing] Face Feature Extraction ({url})...")
|
||||||
|
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
print("❌ Error: Test image not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(image_path, 'rb') as f:
|
||||||
|
files = {'image': f}
|
||||||
|
response = requests.post(url, files=files, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
if result.get('success'):
|
||||||
|
dim = result.get('feature_dim')
|
||||||
|
print(f"✅ Success: Feature extracted. Dimension: {dim}")
|
||||||
|
else:
|
||||||
|
# 预期内失败,因为我们的假脸可能过不了检测,但这证明接口通了
|
||||||
|
print(f"⚠️ Service Reachable (Logic Result): {result.get('message')}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed: Status {response.status_code}, Response: {response.text}")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("❌ Failed: Connection refused.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
|
||||||
|
def test_java_group_list():
|
||||||
|
"""测试 Java 后端分组列表接口"""
|
||||||
|
url = f"{JAVA_BACKEND_URL}/api/groups/list"
|
||||||
|
print(f"\n[Testing] Java Backend Group List ({url})...")
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data.get('code') == 200:
|
||||||
|
print(f"✅ Success: Retrieved {len(data.get('data', []))} groups.")
|
||||||
|
print(f" Response: {json.dumps(data, ensure_ascii=False)}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Functional Error: {data}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed: Status {response.status_code}, Response: {response.text}")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("❌ Failed: Connection refused. Is the service running on port 18080?")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=== Face Recognition System Quick Test ===")
|
||||||
|
|
||||||
|
# 1. 准备测试图片
|
||||||
|
test_img = create_test_image()
|
||||||
|
|
||||||
|
# 2. 测试算法服务
|
||||||
|
test_python_health()
|
||||||
|
test_extract_feature(test_img)
|
||||||
|
|
||||||
|
# 3. 测试Java后端
|
||||||
|
test_java_group_list()
|
||||||
|
|
||||||
|
print("\n=== Test Finished ===")
|
||||||
|
# 清理生成的测试图 (可选,这里保留以便用户查看)
|
||||||
|
# if os.path.exists(test_img):
|
||||||
|
# os.remove(test_img)
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
BASE_URL = "http://localhost:18080/api"
|
||||||
|
GROUP_NAME = "TestGroup_Verify"
|
||||||
|
USER_NAME = "DuplicateTestUser"
|
||||||
|
TEST_IMG = "verify_face.jpg"
|
||||||
|
|
||||||
|
def create_test_image():
|
||||||
|
if not os.path.exists(TEST_IMG):
|
||||||
|
# 创建一个简单的dummy文件
|
||||||
|
with open(TEST_IMG, 'wb') as f:
|
||||||
|
f.write(b'\xFF\xD8\xFF\xE0' + b'\x00' * 1000) # Fake JPG header
|
||||||
|
return TEST_IMG
|
||||||
|
|
||||||
|
def run_verification():
|
||||||
|
print("=== 开始验证 ===")
|
||||||
|
|
||||||
|
# 1. 创建测试分组
|
||||||
|
print("\n1. 创建分组...")
|
||||||
|
resp = requests.post(f"{BASE_URL}/groups/add", json={"name": GROUP_NAME})
|
||||||
|
if resp.status_code == 200 and resp.json()['code'] == 200:
|
||||||
|
group_id = resp.json()['data']['id']
|
||||||
|
print(f"✅ 分组创建成功 ID: {group_id}")
|
||||||
|
else:
|
||||||
|
print(f"❌ 分组创建失败: {resp.text}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 准备图片
|
||||||
|
create_test_image()
|
||||||
|
files = {'photo': open(TEST_IMG, 'rb')}
|
||||||
|
|
||||||
|
# 2. 添加用户 (第一次)
|
||||||
|
print("\n2. 添加用户 (预期成功)...")
|
||||||
|
data = {"name": USER_NAME, "groupId": group_id}
|
||||||
|
# Re-open file for each request
|
||||||
|
resp = requests.post(f"{BASE_URL}/users/add", data=data, files={'photo': open(TEST_IMG, 'rb')})
|
||||||
|
|
||||||
|
if resp.status_code == 200 and resp.json()['code'] == 200:
|
||||||
|
print(f"✅ 用户创建成功")
|
||||||
|
else:
|
||||||
|
# 如果是因为测试图片无法提取特征而失败,也是预期的(说明代码跑到了特征提取那一步)
|
||||||
|
# 但我们主要验证重名逻辑,所以这里假设如果失败是因为重名则是有问题,如果是因为特征提取则是环境问题
|
||||||
|
print(f"⚠️ 用户创建返回: {resp.json().get('msg')}")
|
||||||
|
|
||||||
|
# 3. 添加同名用户 (预期失败)
|
||||||
|
print("\n3. 再次添加同名用户 (预期被拦截)...")
|
||||||
|
resp = requests.post(f"{BASE_URL}/users/add", data=data, files={'photo': open(TEST_IMG, 'rb')})
|
||||||
|
|
||||||
|
res_json = resp.json()
|
||||||
|
if res_json.get('code') != 200 and "已存在" in str(res_json.get('msg')):
|
||||||
|
print(f"✅ 成功拦截重名用户: {res_json.get('msg')}")
|
||||||
|
else:
|
||||||
|
print(f"❌ 拦截失败或错误信息不匹配: {resp.text}")
|
||||||
|
|
||||||
|
# 4. 测试搜索接口
|
||||||
|
print("\n4. 测试人脸搜索接口...")
|
||||||
|
try:
|
||||||
|
resp = requests.post(f"{BASE_URL}/users/search", data={"groupId": group_id}, files={'photo': open(TEST_IMG, 'rb')})
|
||||||
|
if resp.status_code == 200:
|
||||||
|
print(f"✅ 接口调用成功: {resp.json()}")
|
||||||
|
elif resp.status_code == 404:
|
||||||
|
print(f"✅ 接口调用成功 (未找到匹配 - 符合预期): {resp.json()}")
|
||||||
|
else:
|
||||||
|
print(f"❌ 接口调用异常: {resp.status_code} - {resp.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 请求异常: {e}")
|
||||||
|
|
||||||
|
print("\n=== 验证结束 ===")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
run_verification()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fatal Error: {e}")
|
||||||
Loading…
Reference in New Issue