Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions app/api/users.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,58 @@
from fastapi import APIRouter, File, Form, UploadFile
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from app.models.user import UserResponse
from app.services.user_service import (
authenticate_user,
delete_user,
get_user,
register_user,
)



router = APIRouter()


@router.post("/users/register", response_model=UserResponse)
async def register(user_id: str = Form(...), image: UploadFile = File(...)):
"""회원 등록 엔드포인트"""
pass
image_data = await image.read()
registered_user = register_user(user_id, image_data)

return UserResponse(
user_id=registered_user["user_id"],
registered_at=registered_user["registered_at"],
)


@router.post("/users/authenticate")
async def authenticate(image: UploadFile = File(...)):
"""회원 인증 엔드포인트"""
pass
image_data = await image.read()
user_id = authenticate_user(image_data)

if user_id is None:
raise HTTPException(status_code=401, detail="인증 실패: 사용자 없음")

return {"user_id": user_id}



@router.get("/users/{user_id}")
def get_user_info(user_id: str):
"""회원 정보 조회 엔드포인트"""
pass
user_info = get_user(user_id)

if user_info is None:
raise HTTPException(status_code=404, detail="사용자가 존재하지 않습니다.")

return user_info


@router.delete("/users/{user_id}")
def delete_user_info(user_id: str):
"""회원 삭제 엔드포인트"""
pass
result = delete_user(user_id)
if not result:
raise HTTPException(status_code=404, detail="사용자가 존재하지 않습니다.")

return {"message": "사용자가 성공적으로 삭제되었습니다."}
17 changes: 14 additions & 3 deletions app/face/face_db.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import json
import os
from datetime import datetime

DB_PATH = "face_db.json"


def save_user(user_id, embedding):
"""사용자 등록"""
pass
db = load_db()
db[user_id] = {"embedding": embedding, "registered_at": str(datetime.now())}
with open(DB_PATH, "w") as f:
json.dump(db, f)


def load_db():
"""데이터베이스 로드"""
pass
if not os.path.exists(DB_PATH):
return {}
with open(DB_PATH, "r") as f:
return json.load(f)


def save_db(db):
"""데이터베이스 저장"""
pass
with open(DB_PATH, "w") as f:
json.dump(db, f)
15 changes: 11 additions & 4 deletions app/face/face_embedding.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
def extract_embedding(image):
import numpy as np
from typing import Union
from deepface import DeepFace


def extract_embedding(img_path: Union[str, np.ndarray]) -> list:
"""이미지에서 임베딩 추출"""
pass
embedding = DeepFace.represent(img_path)[0]["embedding"]
return embedding


def verify_embedding(embedding1, embedding2):
def verify_embedding(embedding1: list, embedding2: list) -> bool:
"""두 임베딩이 같은 사람인지 검증"""
pass
is_same_person = DeepFace.verify(embedding1, embedding2)["verified"]
return is_same_person
29 changes: 26 additions & 3 deletions app/services/user_service.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
from datetime import datetime

import cv2
import numpy as np

from app.face.face_db import load_db, save_user
from app.face.face_embedding import extract_embedding, verify_embedding


def register_user(user_id: str, image_bytes: bytes) -> dict:
"""이미지와 ID로 회원 등록"""
pass
image_np = np.frombuffer(image_bytes, dtype=np.uint8)
image = cv2.imdecode(image_np, cv2.IMREAD_COLOR)
embedding = extract_embedding(image)
save_user(user_id, embedding)
return {"user_id": user_id, "registered_at": str(datetime.now())}


def authenticate_user(image_bytes: bytes):
"""이미지로 회원 인증"""
pass
image_np = np.frombuffer(image_bytes, dtype=np.uint8)
image = cv2.imdecode(image_np, cv2.IMREAD_COLOR)
db = load_db()

embedding_to_check = extract_embedding(image)

for user_id, data in db.items():
if verify_embedding(data["embedding"], embedding_to_check):
return user_id

return None # 인증 실패


def get_user(user_id):
Expand All @@ -15,4 +38,4 @@ def get_user(user_id):

def delete_user(user_id):
"""user_id로 회원 삭제"""
pass
pass
22 changes: 22 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import pytest
from fastapi.testclient import TestClient

from app.face.face_db import save_user
from app.face.face_embedding import extract_embedding
from app.main import app


@pytest.fixture(scope="module")
def client():
with TestClient(app) as c:
yield c


@pytest.fixture(scope="module")
def setup_user_db():
image_paths = ["images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg"]
user_ids = ["aaron_peirsol"]
for image_path, user_id in zip(image_paths, user_ids):
embedding = extract_embedding(image_path)
save_user(user_id, embedding)
yield
37 changes: 37 additions & 0 deletions tests/test_api_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
def test_register_user_api(client):
# Given: 사용자의 ID와 얼굴 이미지가 주어짐
# Given: API 테스트 클라이언트가 fixture로 제공됨
image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg"
user_id = "aaron_peirsol"

# When: 사용자 등록 API (POST /users/register)를 호출
with open(image_path, "rb") as img_file:
response = client.post(
"/users/register",
data={"user_id": user_id},
files={"image": ("user1.jpg", img_file, "image/jpeg")},
)

# Then: 사용자 등록 기능이 성공(200)
# Then: 응답 내용은 등록된 사용자 ID 및 등록 시각이 포함
assert response.status_code == 200
data = response.json()
assert data["user_id"] == user_id
assert data["registered_at"] is not None


def test_authenticate_user_api(client, setup_user_db):
# Given: 등록된 사용자의 얼굴 이미지가 주어짐
image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg"

# When: 사용자 인증 API (POST /users/authenticate)를 호출
with open(image_path, "rb") as img_file:
response = client.post(
"/users/authenticate",
files={"image": ("user1.jpg", img_file, "image/jpeg")},
)

# Then: 사용자 인증 기능이 성공(200)
assert response.status_code == 200
data = response.json()
assert data["user_id"] == "aaron_peirsol"
41 changes: 41 additions & 0 deletions tests/test_face.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import pytest

from app.face.face_embedding import extract_embedding, verify_embedding


def test_extract_embedding():
# Given: 얼굴이 명확하게 보이는 이미지 파일이 주어짐
image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg"

# When: 얼굴 임베딩 추출 기능을 실행
embedding = extract_embedding(image_path)

# Then: 임베딩이 추출된다. 임베딩 길이는 0보다 큼
assert embedding is not None
assert len(embedding) > 0


def test_no_face_exception():
# Given: 얼굴이 명확하게 보이지 않는 이미지 파일이 주어짐
image_path = "tests/images/no_face.jpg"

# When: 얼굴 임베딩 추출 기능을 실행
# Then: 예외가 발생함
with pytest.raises(ValueError):
extract_embedding(image_path)


def test_verify_embedding():
# Given: 얼굴 임베딩이 주어짐
image_path1 = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg"
image_path2 = "images/Aaron_Peirsol/Aaron_Peirsol_0002.jpg"
image_path3 = "images/Olivia_Newton-John/Olivia_Newton-John_0001.jpg"

embedding1 = extract_embedding(image_path1)
embedding2 = extract_embedding(image_path2)
embedding3 = extract_embedding(image_path3)

# When: 얼굴 임베딩 검증 기능을 실행
# Then: 동일 사람인 경우 검증 결과가 True, 다른 사람인 경우 검증 결과가 False
assert verify_embedding(embedding1, embedding2)
assert not verify_embedding(embedding1, embedding3)
42 changes: 42 additions & 0 deletions tests/test_service_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from app.services.user_service import authenticate_user, register_user


def test_register_user():
# Given: 사용자의 ID와 얼굴 이미지가 주어짐
image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg"
user_id = "aaron_peirsol"

# When: 사용자 등록 기능을 실행
with open(image_path, "rb") as f:
image_data = f.read()
result = register_user(user_id, image_data)

# Then: 사용자 등록 기능이 성공
assert result["user_id"] == "aaron_peirsol"
assert result["registered_at"] is not None


def test_authenticate_registered_user(setup_user_db):
# Given: 이미 등록된 사용자의 얼굴이미지가 주어짐
image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg"

# When: 등록된 사용자와 동일한 인물의 얼굴 이미지를 제출
with open(image_path, "rb") as f:
image_data = f.read()
user_id = authenticate_user(image_data)

# Then: 인증 기능이 성공하여 사용자 ID를 반환
assert user_id == "aaron_peirsol"


def test_authenticate_unregistered_user(setup_user_db):
# Given: 등록되지 않은 사용자의 얼굴 이미지
image_path = "images/Natasha_McElhone/Natasha_McElhone_0001.jpg"
with open(image_path, "rb") as f:
image_data = f.read()

# When: 인증 서비스를 호출
user_id = authenticate_user(image_data)

# Then: 인증 실패(None 반환) 해야 함
assert user_id is None