feat: 新增人脸检测API和人脸检测测试脚本,并更新 API 文档。

This commit is contained in:
Yuanzq 2026-02-11 11:21:19 +08:00
parent e0af18b363
commit b5d0a66249
8 changed files with 251 additions and 1 deletions

View File

@ -222,3 +222,39 @@
* **接口地址**: `/health` * **接口地址**: `/health`
* **请求方式**: `GET` * **请求方式**: `GET`
* **返回结果**: `{"status": "healthy", "service": "Face Feature Extractor"}` * **返回结果**: `{"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
}
```

View File

@ -5,7 +5,7 @@
""" """
import uvicorn import uvicorn
from fastapi import FastAPI, File, UploadFile, HTTPException from fastapi import FastAPI, File, UploadFile, HTTPException, Form
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Optional from typing import List, Optional
@ -118,6 +118,94 @@ async def extract_feature(image: UploadFile = File(...)):
message=f"Server error: {str(e)}" 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__": if __name__ == "__main__":
import argparse import argparse
parser = argparse.ArgumentParser(description='Face Feature Extraction Microservice') parser = argparse.ArgumentParser(description='Face Feature Extraction Microservice')

BIN
face_crop_1_scale_0.3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
face_crop_1_scale_0.6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

BIN
result_detected.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

126
test_detect_face.py Normal file
View File

@ -0,0 +1,126 @@
import requests
import cv2
import numpy as np
import os
import json
import argparse
import sys
# 配置服务地址
PYTHON_ALGO_URL = "http://192.168.0.37:18000"
def get_default_image_path():
"""获取一个默认存在的测试图片路径"""
# 尝试找一个存在的真实图片
potential_paths = [
r"C:\Users\24830\Desktop\人脸.jpg",
]
for path in potential_paths:
if os.path.exists(path):
return path
return None
def detect_and_draw(image_path, expand_scale=0.0):
url = f"{PYTHON_ALGO_URL}/api/detect_face"
print(f"\n[Processing] Image: {image_path}")
print(f"[API URL] {url}")
print(f"[Expand Scale] {expand_scale}")
if not os.path.exists(image_path):
print(f"❌ Error: Image file not found: {image_path}")
return
try:
# 1. 准备发送请求
# 读取图片用于显示/画框
img_array = np.fromfile(image_path, dtype=np.uint8)
original_img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
if original_img is None:
print(f"❌ Error: Failed to read image using opencv: {image_path}")
return
# 2. 调用API
data = {'expand_scale': expand_scale}
with open(image_path, 'rb') as f:
files = {'image': f}
# 注意: 使用 data=data 发送表单数据,而不是 params=params (查询参数)
response = requests.post(url, files=files, data=data, timeout=10)
if response.status_code != 200:
print(f"❌ Failed: Status {response.status_code}, Response: {response.text}")
return
result = response.json()
print("\n=== API Response ===")
print(json.dumps(result, indent=2))
# 3. 处理结果并画图
if result.get('success'):
faces = result.get('faces', [])
count = len(faces)
print(f"\n✅ Success: Detected {count} faces.")
# 创建副本用于画图
draw_img = original_img.copy()
for i, face in enumerate(faces):
x1 = int(face['x1'])
y1 = int(face['y1'])
x2 = int(face['x2'])
y2 = int(face['y2'])
score = face['score']
# 画矩形框
# 颜色 (B, G, R) - 绿色
color = (0, 255, 0)
thickness = 2
cv2.rectangle(draw_img, (x1, y1), (x2, y2), color, thickness)
# 写文字
label = f"Face {i+1}: {score:.2f}"
cv2.putText(draw_img, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
print(f" - Face {i+1}: Box({x1}, {y1}, {x2}, {y2}), Score: {score:.4f}")
# 保存裁剪的人脸图观察效果
face_crop = original_img[y1:y2, x1:x2]
if face_crop.size > 0:
crop_filename = f"face_crop_{i+1}_scale_{expand_scale}.jpg"
cv2.imencode('.jpg', face_crop)[1].tofile(crop_filename)
print(f" Saved crop: {crop_filename}")
# 4. 保存结果图
output_filename = f"result_detected_scale_{expand_scale}.jpg"
cv2.imencode('.jpg', draw_img)[1].tofile(output_filename)
print(f"\n✅ Result image saved to: {os.path.abspath(output_filename)}")
else:
print(f"⚠️ API logic returned failure: {result.get('message')}")
except Exception as e:
print(f"❌ Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Face Detection API Test Script')
parser.add_argument('image_path', nargs='?', help='Path to the image file')
parser.add_argument('--scale', type=float, default=0.6, help='Expand scale (default: 0.0)')
args = parser.parse_args()
target_path = args.image_path
if not target_path:
default_path = get_default_image_path()
if default_path:
print(f"No image path provided, using default found: {default_path}")
target_path = default_path
else:
print("Usage: python test_detect_face.py <path_to_image> [--scale 0.3]")
print("Error: No image path provided and no default test image found.")
sys.exit(1)
detect_and_draw(target_path, args.scale)