File size: 9,467 Bytes
7db31a5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
"""
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:
# InsightFace FaceAnalysis 초기화 (얼굴 감지 및 임베딩 추출용)
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] # BGR -> RGB
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
# 코사인 유사도 계산
# similarity = dot(emb1, emb2) / (norm(emb1) * norm(emb2))
emb1_norm = np.linalg.norm(emb1)
emb2_norm = np.linalg.norm(emb2)
if emb1_norm == 0 or emb2_norm == 0:
return {"error": "임베딩 벡터의 크기가 0입니다"}
# 코사인 유사도 계산
# InsightFace 임베딩은 이미 L2 정규화되어 있으므로 내적만으로 유사도 계산 가능
similarity = np.dot(emb1, emb2) / (emb1_norm * emb2_norm)
# 코사인 유사도는 -1 ~ 1 범위이지만, 얼굴 임베딩은 보통 0 ~ 1 범위
# 음수 값이 나올 수 있으므로 0 ~ 1로 정규화 (선택사항)
# 실제로는 InsightFace 임베딩이 정규화되어 있어 0 ~ 1 범위가 일반적
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'"}
|