# 03. 얼굴 인식 시스템 (insightface + pgvector)

## 개요

얼굴 인식 시스템은 두 가지 기능을 제공합니다:

1. **등록 (Register)**: 사진에서 얼굴을 추출 → 기존 사용자 매칭 시도 → 매칭 실패 시 새 사용자 생성
2. **식별 (Identify)**: 사진에서 얼굴을 추출 → 가장 유사한 등록 사용자 반환

## 구성 요소

```
FaceAnalyzer (utils/face_analyzer.py)
  └── insightface buffalo_l 모델
        └── 얼굴 감지 + 512차원 임베딩 추출

FaceService (services/face_service.py)
  ├── FaceAnalyzer (임베딩 추출)
  ├── pgvector (벡터 유사도 검색)
  └── DB (User, FaceEmbedding CRUD)

Router (routers/face.py)
  ├── POST /api/v1/faces/register
  └── POST /api/v1/faces/identify
```

---

## FaceAnalyzer (`app/utils/face_analyzer.py`)

### insightface란?

오픈소스 얼굴 인식 라이브러리입니다. `buffalo_l` 모델은:
- 얼굴 감지 (detection): 이미지에서 얼굴 위치 (bbox) 찾기
- 임베딩 추출 (embedding): 각 얼굴을 512차원 벡터로 변환

### 초기화

```python
class FaceAnalyzer:
    def __init__(self, model_name="buffalo_l", ctx_id=-1, det_thresh=0.3):
        self._app = FaceAnalysis(name=model_name, providers=["CPUExecutionProvider"])
        self._app.prepare(ctx_id=ctx_id, det_size=(640, 640))
```

- `CPUExecutionProvider`: GPU 없이 CPU에서 실행 (POC 환경)
- `det_size=(640, 640)`: 감지 시 이미지를 640x640으로 리사이즈
- `det_thresh=0.3`: 얼굴 감지 임계값을 낮게 설정 (다양한 조명/각도 대응)
- **모델 로딩이 무겁기 때문에** `@lru_cache`로 싱글톤 관리 (dependencies.py에서)

### 주요 메서드

```python
def get_largest_face_embedding(self, image: np.ndarray) -> np.ndarray | None:
    """가장 큰 얼굴의 512차원 임베딩 반환. 얼굴 없으면 None."""
    faces = self._app.get(image)
    if not faces:
        return None
    largest = max(faces, key=lambda f: (f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1]))
    return largest.normed_embedding.astype(np.float32)
```

- 여러 얼굴이 감지되면 **가장 큰 얼굴**(bbox 면적 기준)만 사용
- `normed_embedding`: 정규화된 임베딩 (L2 norm = 1)
- 반환값: `np.ndarray(shape=(512,), dtype=float32)` 또는 `None`

---

## FaceService (`app/services/face_service.py`)

### 등록 플로우 (`register()`)

```
이미지 바이트 수신
   │
   ▼
_extract_embedding()
   │  이미지 디코딩 (cv2.imdecode)
   │  insightface로 가장 큰 얼굴 임베딩 추출
   │  → 실패 시 ValueError("얼굴을 감지할 수 없습니다.")
   │
   ▼
_find_best_match()
   │  pgvector 코사인 거리로 가장 유사한 사용자 검색
   │  confidence = 1 - cosine_distance
   │  threshold(0.5) 이상이면 매칭 성공
   │
   ├─ 매칭 성공 → 기존 사용자에 새 임베딩 추가
   │                 is_new_user: false
   │
   └─ 매칭 실패 → 새 User 생성 + 임베딩 저장
                    is_new_user: true
```

### 식별 플로우 (`identify()`)

```
이미지 바이트 수신
   │
   ▼
_extract_embedding()
   │
   ▼
_find_best_match()
   │
   ├─ 매칭 성공 → {user_id, user_name, confidence}
   │
   └─ 매칭 실패 → None (라우터에서 404 응답)
```

### pgvector 코사인 거리 검색 (`_find_best_match()`)

```sql
SELECT fe.id, fe.user_id, fe.embedding <=> :emb AS distance
FROM face_embeddings fe
JOIN users u ON u.id = fe.user_id
WHERE u.id != :global_id           -- 글로벌 사용자 제외
ORDER BY fe.embedding <=> :emb      -- 코사인 거리 오름차순
LIMIT 1                             -- 가장 유사한 1개만
```

- `<=>` 연산자: pgvector의 코사인 거리 (0 = 동일, 2 = 반대)
- `confidence = 1 - distance`: 유사도로 변환 (1 = 동일, -1 = 반대)
- `WHERE u.id != :global_id`: unknown 사용자(고정 UUID)의 임베딩은 검색에서 제외

### 얼굴 이미지 저장

```python
async def _save_face_image(self, image_bytes, user_id):
    # storage/faces/{user_id}/{random_hex}.jpg
    rel_path = Path("faces") / str(user_id)
    # ...
```

- 사용자별 폴더로 구분
- 파일명은 UUID hex (충돌 방지)

---

## 라우터 (`app/routers/face.py`)

### POST `/api/v1/faces/register`

| 파라미터 | 타입 | 설명 |
|----------|------|------|
| `image` | File (필수) | 얼굴 사진 (JPEG/PNG) |
| `user_name` | Form (선택) | 사용자 이름. 생략 시 "새 사용자" |

**응답:**
```json
{
    "user_id": "uuid-...",
    "user_name": "홍길동",
    "is_new_user": true,
    "confidence": 1.0
}
```

### POST `/api/v1/faces/identify`

| 파라미터 | 타입 | 설명 |
|----------|------|------|
| `image` | File (필수) | 얼굴 사진 (JPEG/PNG) |

**성공 응답 (200):**
```json
{
    "user_id": "uuid-...",
    "user_name": "홍길동",
    "confidence": 0.85
}
```

**실패 응답 (404):**
```json
{
    "detail": "매칭되는 사용자를 찾을 수 없습니다."
}
```

---

## 코사인 거리 이해하기

```
동일한 사람:  distance ≈ 0.0 ~ 0.3  →  confidence ≈ 0.7 ~ 1.0
다른 사람:    distance ≈ 0.5 ~ 1.5  →  confidence ≈ -0.5 ~ 0.5

FACE_MATCH_THRESHOLD = 0.5 의미:
  confidence >= 0.5 → "같은 사람" (매칭 성공)
  confidence < 0.5  → "다른 사람" (매칭 실패)
```

- 같은 사람의 다른 사진을 여러 번 등록하면 다양한 조건에서의 매칭 정확도가 올라갑니다
- 한 User에 여러 FaceEmbedding이 연결될 수 있는 이유입니다
