diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 2c5d226..a7ff3dd 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -222,3 +222,39 @@ * **接口地址**: `/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 + } + ``` diff --git a/FaceFeatureExtractorAPI/feature_server.py b/FaceFeatureExtractorAPI/feature_server.py index 63f607a..2436f8a 100644 --- a/FaceFeatureExtractorAPI/feature_server.py +++ b/FaceFeatureExtractorAPI/feature_server.py @@ -5,7 +5,7 @@ """ import uvicorn -from fastapi import FastAPI, File, UploadFile, HTTPException +from fastapi import FastAPI, File, UploadFile, HTTPException, Form from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import List, Optional @@ -118,6 +118,94 @@ 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') diff --git a/face_crop_1_scale_0.3.jpg b/face_crop_1_scale_0.3.jpg new file mode 100644 index 0000000..b1ebc77 Binary files /dev/null and b/face_crop_1_scale_0.3.jpg differ diff --git a/face_crop_1_scale_0.6.jpg b/face_crop_1_scale_0.6.jpg new file mode 100644 index 0000000..171acc5 Binary files /dev/null and b/face_crop_1_scale_0.6.jpg differ diff --git a/result_detected.jpg b/result_detected.jpg new file mode 100644 index 0000000..a9bc226 Binary files /dev/null and b/result_detected.jpg differ diff --git a/result_detected_scale_0.3.jpg b/result_detected_scale_0.3.jpg new file mode 100644 index 0000000..6691c05 Binary files /dev/null and b/result_detected_scale_0.3.jpg differ diff --git a/result_detected_scale_0.6.jpg b/result_detected_scale_0.6.jpg new file mode 100644 index 0000000..f995fe5 Binary files /dev/null and b/result_detected_scale_0.6.jpg differ diff --git a/test_detect_face.py b/test_detect_face.py new file mode 100644 index 0000000..1ce6d11 --- /dev/null +++ b/test_detect_face.py @@ -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 [--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)