| """
|
| HuggingFace Inference Endpoint용 InsightFace 핸들러
|
| 얼굴 임베딩 추출 및 유사도 계산용
|
| """
|
| import base64
|
| import io
|
| from typing import Dict, Any, Optional
|
| from PIL import Image
|
| import numpy as np
|
| import insightface
|
|
|
|
|
| class EndpointHandler:
|
| def __init__(self, path=""):
|
| """모델 초기화"""
|
| self.face_analyzer = None
|
| self._load_models(path)
|
|
|
| def _load_models(self, path: str):
|
| """모델 로드"""
|
| try:
|
|
|
| self.face_analyzer = insightface.app.FaceAnalysis(
|
| name='buffalo_l',
|
| root=path,
|
| providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
|
| )
|
| self.face_analyzer.prepare(ctx_id=0, det_size=(640, 640))
|
|
|
| print("✅ InsightFace 모델 로드 완료 (얼굴 임베딩 추출용)")
|
| except Exception as e:
|
| print(f"❌ 모델 로드 실패: {e}")
|
| raise
|
|
|
| def _base64_to_image(self, base64_str: str) -> Image.Image:
|
| """base64 문자열을 이미지로 변환"""
|
| if "," in base64_str:
|
| base64_str = base64_str.split(",")[1]
|
|
|
| img_bytes = base64.b64decode(base64_str)
|
| return Image.open(io.BytesIO(img_bytes))
|
|
|
| def _image_to_base64(self, image: Image.Image) -> str:
|
| """이미지를 base64 문자열로 변환"""
|
| buffer = io.BytesIO()
|
| image.save(buffer, format="PNG")
|
| img_bytes = buffer.getvalue()
|
| return base64.b64encode(img_bytes).decode("utf-8")
|
|
|
| def _pil_to_numpy(self, image: Image.Image) -> np.ndarray:
|
| """PIL Image를 BGR numpy 배열로 변환"""
|
| return np.array(image.convert('RGB'))[:, :, ::-1]
|
|
|
| def _numpy_to_pil(self, image: np.ndarray) -> Image.Image:
|
| """BGR numpy 배열을 PIL Image로 변환"""
|
| rgb = image[:, :, ::-1]
|
| return Image.fromarray(rgb)
|
|
|
| def face_detect(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| """
|
| 얼굴 감지
|
|
|
| Args:
|
| data: {"image": "data:image/png;base64,..."}
|
|
|
| Returns:
|
| {"faces": [{"bbox": [x1, y1, x2, y2], "embedding": [...], ...}]}
|
| """
|
| try:
|
| image_b64 = data.get("image", "")
|
| if not image_b64:
|
| return {"error": "이미지가 제공되지 않았습니다"}
|
|
|
|
|
| image = self._base64_to_image(image_b64)
|
| image_np = self._pil_to_numpy(image)
|
|
|
|
|
| faces = self.face_analyzer.get(image_np)
|
|
|
|
|
| faces_data = []
|
| for face in faces:
|
| face_data = {
|
| "bbox": face.bbox.tolist(),
|
| "kps": face.kps.tolist() if hasattr(face, 'kps') and face.kps is not None else None,
|
| "embedding": face.embedding.tolist() if hasattr(face, 'embedding') and face.embedding is not None else None,
|
| "det_score": float(face.det_score) if hasattr(face, 'det_score') else None,
|
| "gender": int(face.gender) if hasattr(face, 'gender') else None,
|
| "age": int(face.age) if hasattr(face, 'age') else None,
|
| }
|
| faces_data.append(face_data)
|
|
|
| return {"faces": faces_data}
|
|
|
| except Exception as e:
|
| return {"error": str(e)}
|
|
|
| def face_similarity(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| """
|
| 두 얼굴 간 유사도 계산 (코사인 유사도)
|
|
|
| Args:
|
| data: {
|
| "image1": "data:image/png;base64,...",
|
| "image2": "data:image/png;base64,...",
|
| "face_index1": 0, # image1에서 사용할 얼굴 인덱스
|
| "face_index2": 0 # image2에서 사용할 얼굴 인덱스
|
| }
|
|
|
| Returns:
|
| {
|
| "similarity": float, # 0.0 ~ 1.0 (1.0이 가장 유사)
|
| "embedding1": [...], # 첫 번째 얼굴 임베딩
|
| "embedding2": [...], # 두 번째 얼굴 임베딩
|
| "face1_info": {...}, # 첫 번째 얼굴 정보
|
| "face2_info": {...} # 두 번째 얼굴 정보
|
| }
|
| """
|
| try:
|
| image1_b64 = data.get("image1", "")
|
| image2_b64 = data.get("image2", "")
|
| face_index1 = data.get("face_index1", 0)
|
| face_index2 = data.get("face_index2", 0)
|
|
|
| if not image1_b64 or not image2_b64:
|
| return {"error": "이미지1 또는 이미지2가 제공되지 않았습니다"}
|
|
|
|
|
| image1 = self._base64_to_image(image1_b64)
|
| image2 = self._base64_to_image(image2_b64)
|
|
|
| image1_np = self._pil_to_numpy(image1)
|
| image2_np = self._pil_to_numpy(image2)
|
|
|
|
|
| faces1 = self.face_analyzer.get(image1_np)
|
| faces2 = self.face_analyzer.get(image2_np)
|
|
|
| if len(faces1) == 0:
|
| return {"error": "이미지1에서 얼굴을 찾을 수 없습니다"}
|
|
|
| if len(faces2) == 0:
|
| return {"error": "이미지2에서 얼굴을 찾을 수 없습니다"}
|
|
|
|
|
| if face_index1 >= len(faces1):
|
| face_index1 = 0
|
| if face_index2 >= len(faces2):
|
| face_index2 = 0
|
|
|
| face1 = faces1[face_index1]
|
| face2 = faces2[face_index2]
|
|
|
|
|
| if not hasattr(face1, 'embedding') or face1.embedding is None:
|
| return {"error": "이미지1의 얼굴에서 임베딩을 추출할 수 없습니다"}
|
|
|
| if not hasattr(face2, 'embedding') or face2.embedding is None:
|
| return {"error": "이미지2의 얼굴에서 임베딩을 추출할 수 없습니다"}
|
|
|
| emb1 = face1.embedding
|
| emb2 = face2.embedding
|
|
|
|
|
|
|
| emb1_norm = np.linalg.norm(emb1)
|
| emb2_norm = np.linalg.norm(emb2)
|
|
|
| if emb1_norm == 0 or emb2_norm == 0:
|
| return {"error": "임베딩 벡터의 크기가 0입니다"}
|
|
|
|
|
|
|
| similarity = np.dot(emb1, emb2) / (emb1_norm * emb2_norm)
|
|
|
|
|
|
|
| similarity = max(0.0, min(1.0, similarity))
|
|
|
|
|
| face1_info = {
|
| "bbox": face1.bbox.tolist(),
|
| "det_score": float(face1.det_score) if hasattr(face1, 'det_score') else None,
|
| "gender": int(face1.gender) if hasattr(face1, 'gender') else None,
|
| "age": int(face1.age) if hasattr(face1, 'age') else None,
|
| }
|
|
|
| face2_info = {
|
| "bbox": face2.bbox.tolist(),
|
| "det_score": float(face2.det_score) if hasattr(face2, 'det_score') else None,
|
| "gender": int(face2.gender) if hasattr(face2, 'gender') else None,
|
| "age": int(face2.age) if hasattr(face2, 'age') else None,
|
| }
|
|
|
| return {
|
| "similarity": float(similarity),
|
| "embedding1": emb1.tolist(),
|
| "embedding2": emb2.tolist(),
|
| "face1_info": face1_info,
|
| "face2_info": face2_info
|
| }
|
|
|
| except Exception as e:
|
| return {"error": str(e)}
|
|
|
| def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| """
|
| 메인 엔드포인트
|
|
|
| Args:
|
| data: {
|
| "task": "face-detect" | "face-similarity",
|
| "image": "data:image/png;base64,..." (face-detect용),
|
| "image1": "data:image/png;base64,..." (face-similarity용),
|
| "image2": "data:image/png;base64,..." (face-similarity용),
|
| ...
|
| }
|
|
|
| Returns:
|
| 작업 결과
|
| """
|
| task = data.get("task", "")
|
|
|
| if task == "face-detect":
|
| return self.face_detect(data)
|
| elif task == "face-similarity":
|
| return self.face_similarity(data)
|
| else:
|
| return {"error": f"알 수 없는 작업: {task}. 지원 작업: 'face-detect', 'face-similarity'"}
|
|
|
|
|