From d4574b03e7fee094983a56b9a4e52ab22ed639b2 Mon Sep 17 00:00:00 2001 From: willook Date: Mon, 19 May 2025 08:40:19 +0900 Subject: [PATCH] =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=EC=84=9C=20=E2=80=9C=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EB=90=9C=20=ED=9A=8C=EC=9B=90=EC=9D=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=A0=20=EC=88=98=20=EC=97=86=EB=8B=A4.=E2=80=9D?= =?UTF-8?q?=EC=97=90=20=ED=95=B4=EB=8B=B9=ED=95=98=EB=8A=94=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=EB=A5=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/users.py | 28 ++++++++++++++ app/face/face_db.py | 10 +++++ app/face/face_embedding.py | 10 ++++- app/models/user.py | 56 ++++++++++++++++++++++++--- app/services/user_service.py | 36 +++++++++++++++++ tests/conftest.py | 20 ++++++++++ tests/images/no_face.jpg | Bin 0 -> 1651 bytes tests/test_api_users.py | 59 ++++++++++++++++++++++++++++ tests/test_face.py | 29 ++++++++++++++ tests/test_service_users.py | 73 +++++++++++++++++++++++++++++++++++ 10 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 tests/images/no_face.jpg diff --git a/app/api/users.py b/app/api/users.py index 043dd98..6c7ae43 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -1,5 +1,7 @@ from fastapi import APIRouter, File, Form, UploadFile +from fastapi import APIRouter, File, Form, UploadFile, HTTPException from app.models.user import UserResponse +from app.services.user_service import * router = APIRouter() @@ -8,21 +10,47 @@ 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': '사용자가 성공적으로 삭제되었습니다.'} \ No newline at end of file diff --git a/app/face/face_db.py b/app/face/face_db.py index 380ae3a..c3da3f3 100644 --- a/app/face/face_db.py +++ b/app/face/face_db.py @@ -1,16 +1,26 @@ DB_PATH = "face_db.json" +import json, 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) \ No newline at end of file diff --git a/app/face/face_embedding.py b/app/face/face_embedding.py index 3acfb0c..6409f4a 100644 --- a/app/face/face_embedding.py +++ b/app/face/face_embedding.py @@ -1,8 +1,16 @@ def extract_embedding(image): """이미지에서 임베딩 추출""" pass +import numpy as np +from typing import Union +from deepface import DeepFace +def extract_embedding(img_path: Union[str, np.ndarray]) -> list: + 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 \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py index 3958dd5..108d5dd 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,8 +1,54 @@ -from pydantic import BaseModel +import cv2 +import numpy as np +from datetime import datetime +from app.face.face_embedding import * +from app.face.face_db import * -class UserResponse(BaseModel): - """회원 응답 모델""" +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())} - user_id: str - registered_at: str + +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): + """user_id로 회원 정보 조회""" + pass + db = load_db() + user_data = db.get(user_id) + + if user_data: + return {'user_id': user_id, 'registered_at': user_data['registered_at']} + else: + return None # 사용자 없음 + + +def delete_user(user_id): + """user_id로 회원 삭제""" + pass + db = load_db() + if user_id in db: + del db[user_id] + save_db(db) + return True + return False \ No newline at end of file diff --git a/app/services/user_service.py b/app/services/user_service.py index 410b807..108d5dd 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -1,18 +1,54 @@ +import cv2 +import numpy as np +from datetime import datetime + +from app.face.face_embedding import * +from app.face.face_db import * + 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): """user_id로 회원 정보 조회""" pass + db = load_db() + user_data = db.get(user_id) + + if user_data: + return {'user_id': user_id, 'registered_at': user_data['registered_at']} + else: + return None # 사용자 없음 def delete_user(user_id): """user_id로 회원 삭제""" pass + db = load_db() + if user_id in db: + del db[user_id] + save_db(db) + return True + return False \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..cfead72 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,20 @@ +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 \ No newline at end of file diff --git a/tests/images/no_face.jpg b/tests/images/no_face.jpg new file mode 100644 index 0000000000000000000000000000000000000000..61f331519a601c39ce5de585431feb5d528022b6 GIT binary patch literal 1651 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<HEAm;@P_1sVSzVUP#9la&z+7@&ZWiJ66!jh%y&iyNq5 zs{jKNBQrA-3o|P#3ky(nEl{3;MUYiU(a@1iI53f2sZhkIapFP_Wv7h?MT0JWP%%y_ zYU1P)6PJ*bQdLve(9|+9H8Z!cv~qTFb#wRd^a>6M4GWKmj7m;PO-s+n%qlJ^Ei136 ztZHs)ZENr7?3y%r%G7DoXUv?nXz`Mz%a*TLxoXqqEnBy3-?4Mop~FXx9y@;G&P778mFHFAhJO8sPtb69C$qBOL$$ literal 0 HcmV?d00001 diff --git a/tests/test_api_users.py b/tests/test_api_users.py index e69de29..70d00a3 100644 --- a/tests/test_api_users.py +++ b/tests/test_api_users.py @@ -0,0 +1,59 @@ +def test_register_user_api(client): + # Given + image_path = 'images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg' + user_id = 'aaron_peirsol' + + # When + 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 + 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 + with open(image_path, 'rb') as img_file: + response = client.post( + '/users/authenticate', + files={'image': ('user1.jpg', img_file, 'image/jpeg')}, + ) + + assert response.status_code == 200 + data = response.json() + assert data['user_id'] == 'aaron_peirsol' + +def test_get_registered_user_api(client, setup_user_db): + # Given + user_id = 'aaron_peirsol' + + # When + response = client.get(f'/users/{user_id}') + + # Then + assert response.status_code == 200 + data = response.json() + assert data['user_id'] == user_id + assert 'registered_at' in data + +def test_delete_registered_user_api(client, setup_user_db): + # Given + user_id = 'aaron_peirsol' + + # When + response = client.delete(f'/users/{user_id}') + + # Then + assert response.status_code == 200 + + response = client.get(f'/users/{user_id}') + assert response.status_code == 404 \ No newline at end of file diff --git a/tests/test_face.py b/tests/test_face.py index e69de29..8de4e03 100644 --- a/tests/test_face.py +++ b/tests/test_face.py @@ -0,0 +1,29 @@ +import pytest + +from app.face.face_embedding import extract_embedding, verify_embedding + +def test_extract_embedding(): + image_path = "images/Aaron_Peirsol/Aaron_Peirsol_0001.jpg" + + embedding = extract_embedding(image_path) + + assert embedding is not None + assert len(embedding) > 0 + +def test_no_face_exception(): + image_path = "tests/images/no_face.jpg" + + with pytest.raises(ValueError): + extract_embedding(image_path) + +def test_verify_embedding(): + 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) + + assert verify_embedding(embedding1, embedding2) + assert not verify_embedding(embedding1, embedding3) \ No newline at end of file diff --git a/tests/test_service_users.py b/tests/test_service_users.py index e69de29..d861f62 100644 --- a/tests/test_service_users.py +++ b/tests/test_service_users.py @@ -0,0 +1,73 @@ +from app.services.user_service import * + +def test_register_user(): + # Given + 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 + 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 + assert user_id is None + +def test_get_registered_user(setup_user_db): + # Given + user_id = 'aaron_peirsol' + + # When + user_info = get_user(user_id) + + # Then + assert user_info['user_id'] == user_id + assert 'registered_at' in user_info + +def test_delete_registered_user(setup_user_db): + # Given + user_id = 'aaron_peirsol' + + # When + result = delete_user(user_id) + + # Then + assert result is True + + user_id = get_user(user_id) + assert user_id is None + +def test_non_authenticate_user(setup_user_db): + # Given: 등록되지 않은 회원 + user_id = 'non_existent_user' + + # When + user = get_user(user_id) # 등록되지 않은 사용자를 + + # Then + assert user is None # 사용자가 존재하지 않음 \ No newline at end of file