feat: 新增人脸检测API和人脸检测测试脚本,并更新 API 文档。
This commit is contained in:
parent
e0af18b363
commit
b5d0a66249
|
|
@ -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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 275 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 380 KiB |
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 |
|
|
@ -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)
|
||||||
Loading…
Reference in New Issue