Spaces:
Configuration error
Configuration error
Upload 42 files
Browse files- .env +5 -0
- Dockerfile +18 -0
- README.md +91 -14
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-310.pyc +0 -0
- app/__pycache__/config.cpython-310.pyc +0 -0
- app/__pycache__/main.cpython-310.pyc +0 -0
- app/api/__init__.py +0 -0
- app/api/__pycache__/__init__.cpython-310.pyc +0 -0
- app/api/__pycache__/attendance.cpython-310.pyc +0 -0
- app/api/__pycache__/register.cpython-310.pyc +0 -0
- app/api/attendance.py +115 -0
- app/api/register.py +87 -0
- app/config.py +30 -0
- app/database/__init__.py +0 -0
- app/database/__pycache__/__init__.cpython-310.pyc +0 -0
- app/database/__pycache__/database.cpython-310.pyc +0 -0
- app/database/__pycache__/models.cpython-310.pyc +0 -0
- app/database/database.py +18 -0
- app/database/models.py +41 -0
- app/main.py +17 -0
- app/services/__init__.py +0 -0
- app/services/__pycache__/__init__.cpython-310.pyc +0 -0
- app/services/__pycache__/edgeface_model.cpython-310.pyc +0 -0
- app/services/__pycache__/embedding.cpython-310.pyc +0 -0
- app/services/__pycache__/face_detector.cpython-310.pyc +0 -0
- app/services/__pycache__/face_quality.cpython-310.pyc +0 -0
- app/services/__pycache__/recognition.cpython-310.pyc +0 -0
- app/services/edgeface_model.py +90 -0
- app/services/embedding.py +22 -0
- app/services/face_detector.py +38 -0
- app/services/face_quality.py +138 -0
- app/services/recognition.py +34 -0
- app/utils/__init__.py +0 -0
- app/utils/__pycache__/__init__.cpython-310.pyc +0 -0
- app/utils/__pycache__/preprocess.cpython-310.pyc +0 -0
- app/utils/preprocess.py +23 -0
- app/weights/PUT_WEIGHTS_HERE.txt +3 -0
- app/weights/YoloV8_Face.pt +3 -0
- app/weights/edgeface_xxs_q.pt +3 -0
- dockerignore +4 -0
- requirements.txt +12 -0
.env
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DB_USER=postgres
|
| 2 |
+
DB_PASSWORD=HOSAMfakher1!
|
| 3 |
+
DB_HOST=db.lczwuudofcmpetlnqfwp.supabase.co
|
| 4 |
+
DB_PORT=5432
|
| 5 |
+
DB_NAME=postgres
|
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
libgl1 \
|
| 9 |
+
libglib2.0-0 \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
|
| 18 |
+
CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","7860"]
|
README.md
CHANGED
|
@@ -1,14 +1,91 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Face Attendance API
|
| 2 |
+
|
| 3 |
+
نظام حضور بالتعرف على الوجه، مبني على **FastAPI** + **PostgreSQL** + **YOLOv8 (كشف الوجه)** + **EdgeFace (embedding)**.
|
| 4 |
+
|
| 5 |
+
## هيكل المشروع
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
face_attendance/
|
| 9 |
+
├── app/
|
| 10 |
+
│ ├── main.py # نقطة تشغيل التطبيق
|
| 11 |
+
│ ├── config.py # الإعدادات (DB, threshold, مسارات الموديلات)
|
| 12 |
+
│ ├── api/
|
| 13 |
+
│ │ ├── register.py # POST /register → تسجيل شخص جديد
|
| 14 |
+
│ │ └── attendance.py # POST /attendance → تسجيل الحضور
|
| 15 |
+
│ ├── database/
|
| 16 |
+
│ │ ├── database.py # اتصال SQLAlchemy
|
| 17 |
+
│ │ └── models.py # جدول Person + جدول Attendance
|
| 18 |
+
│ ├── services/
|
| 19 |
+
│ │ ├── edgeface_model.py # تعريف موديل EdgeFace (بدون تعديل)
|
| 20 |
+
│ │ ├── embedding.py # تحميل EdgeFace وحساب embedding
|
| 21 |
+
│ │ ├── face_detector.py # تحميل YOLO وكشف/تتبع الوجوه
|
| 22 |
+
│ │ └── recognition.py # مقارنة embeddings وتحديد الشخص
|
| 23 |
+
│ ├── utils/
|
| 24 |
+
│ │ └── preprocess.py # تجهيز الصورة + cosine similarity
|
| 25 |
+
│ └── weights/ # حط هنا ملفات الموديلات (.pt)
|
| 26 |
+
├── requirements.txt
|
| 27 |
+
└── .env.example
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
## 1) تجهيز البيئة
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
python -m venv venv
|
| 34 |
+
source venv/bin/activate # ويندوز: venv\Scripts\activate
|
| 35 |
+
|
| 36 |
+
pip install -r requirements.txt
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
## 2) تجهيز PostgreSQL
|
| 40 |
+
|
| 41 |
+
```sql
|
| 42 |
+
CREATE DATABASE face_attendance;
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
انسخ `.env.example` إلى `.env` وعدّل البيانات حسب السيرفر بتاعك:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
cp .env.example .env
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
الجداول (`person`, `attendance`) هتتعمل تلقائيًا أول ما السيرفر يشتغل، مفيش حاجة تعملها يدوي.
|
| 52 |
+
|
| 53 |
+
## 3) حط ملفات الموديلات
|
| 54 |
+
|
| 55 |
+
حط الملفين دول جوه `app/weights/`:
|
| 56 |
+
- `YoloV8_Face.pt`
|
| 57 |
+
- `edgeface_xxs_q.pt`
|
| 58 |
+
|
| 59 |
+
(لو الاسم مختلف عندك، عدّله فى `app/config.py`)
|
| 60 |
+
|
| 61 |
+
## 4) تشغيل السيرفر
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
uvicorn app.main:app --reload
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
هيفتح على: `http://127.0.0.1:8000/docs` (Swagger UI تلقائي لتجربة الـ APIs).
|
| 68 |
+
|
| 69 |
+
## 5) استخدام الـ APIs
|
| 70 |
+
|
| 71 |
+
### تسجيل شخص جديد
|
| 72 |
+
```bash
|
| 73 |
+
curl -X POST "http://127.0.0.1:8000/register/?name=Hossam"
|
| 74 |
+
```
|
| 75 |
+
هتفتح الكاميرا:
|
| 76 |
+
- اضغط **c** لما وجهك يبقى واضح فى المربع الأخضر → يسجل فى الداتابيز.
|
| 77 |
+
- اضغط **q** للإلغاء.
|
| 78 |
+
|
| 79 |
+
### تسجيل الحضور (Real-time)
|
| 80 |
+
```bash
|
| 81 |
+
curl -X POST "http://127.0.0.1:8000/attendance/"
|
| 82 |
+
```
|
| 83 |
+
هتفتح الكاميرا وتتعرف على كل الوجوه اللي قدامها وتسجلهم فى جدول `attendance`
|
| 84 |
+
(مرة واحدة بس لكل شخص فى اليوم). اضغط **q** لإنهاء الجلسة.
|
| 85 |
+
|
| 86 |
+
## ملاحظات للتعديل لاحقًا
|
| 87 |
+
|
| 88 |
+
- **threshold التعرف**: غيّره من `RECOGNITION_THRESHOLD` فى `app/config.py`.
|
| 89 |
+
- **رقم الكاميرا**: غيّره من `CAMERA_INDEX` فى نفس الملف (لو عندك أكتر من كاميرا).
|
| 90 |
+
- **تسجيل شخص من صورة بدل الكاميرا مباشرة**: عدّل `app/api/register.py` فقط، باقي الملفات مش هتتأثر.
|
| 91 |
+
- **الترقية إلى pgvector** (بحث أسرع للـ embeddings عند زيادة عدد الأشخاص): غيّر نوع عمود `embedding` فى `app/database/models.py` من `ARRAY(Float)` إلى `Vector(512)` بعد تفعيل extension `pgvector` فى بوستجريس.
|
app/__init__.py
ADDED
|
File without changes
|
app/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (159 Bytes). View file
|
|
|
app/__pycache__/config.cpython-310.pyc
ADDED
|
Binary file (832 Bytes). View file
|
|
|
app/__pycache__/main.cpython-310.pyc
ADDED
|
Binary file (644 Bytes). View file
|
|
|
app/api/__init__.py
ADDED
|
File without changes
|
app/api/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (163 Bytes). View file
|
|
|
app/api/__pycache__/attendance.cpython-310.pyc
ADDED
|
Binary file (2.5 kB). View file
|
|
|
app/api/__pycache__/register.cpython-310.pyc
ADDED
|
Binary file (2.36 kB). View file
|
|
|
app/api/attendance.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
| 6 |
+
from sqlalchemy.orm import Session
|
| 7 |
+
|
| 8 |
+
from app.database.database import get_db
|
| 9 |
+
from app.database.models import Attendance
|
| 10 |
+
from app.services.face_detector import detect_faces
|
| 11 |
+
from app.services.embedding import get_embedding
|
| 12 |
+
from app.services.recognition import load_all_persons, identify_person
|
| 13 |
+
|
| 14 |
+
router = APIRouter(prefix="/attendance", tags=["Attendance"])
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@router.post("/")
|
| 18 |
+
async def take_attendance(
|
| 19 |
+
file: UploadFile = File(...),
|
| 20 |
+
db: Session = Depends(get_db),
|
| 21 |
+
):
|
| 22 |
+
"""
|
| 23 |
+
Register attendance from an uploaded image.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
known_persons = load_all_persons(db)
|
| 27 |
+
|
| 28 |
+
if not known_persons:
|
| 29 |
+
raise HTTPException(
|
| 30 |
+
status_code=400,
|
| 31 |
+
detail="لا يوجد أشخاص مسجلين"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# قراءة الصورة
|
| 35 |
+
contents = await file.read()
|
| 36 |
+
|
| 37 |
+
image = cv2.imdecode(
|
| 38 |
+
np.frombuffer(contents, np.uint8),
|
| 39 |
+
cv2.IMREAD_COLOR,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
if image is None:
|
| 43 |
+
raise HTTPException(
|
| 44 |
+
status_code=400,
|
| 45 |
+
detail="الصورة غير صالحة"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# كشف جميع الوجوه
|
| 49 |
+
boxes = detect_faces(image)
|
| 50 |
+
|
| 51 |
+
if len(boxes) == 0:
|
| 52 |
+
raise HTTPException(
|
| 53 |
+
status_code=400,
|
| 54 |
+
detail="لم يتم العثور على أي وجه"
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
present = []
|
| 58 |
+
|
| 59 |
+
today = datetime.date.today()
|
| 60 |
+
|
| 61 |
+
for box in boxes:
|
| 62 |
+
|
| 63 |
+
x1, y1, x2, y2 = box
|
| 64 |
+
|
| 65 |
+
face_crop = image[y1:y2, x1:x2]
|
| 66 |
+
|
| 67 |
+
if face_crop.size == 0:
|
| 68 |
+
continue
|
| 69 |
+
|
| 70 |
+
embedding = get_embedding(face_crop)
|
| 71 |
+
|
| 72 |
+
person_id, name, score = identify_person(
|
| 73 |
+
embedding,
|
| 74 |
+
known_persons
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
if person_id is None:
|
| 78 |
+
present.append({
|
| 79 |
+
"name": "Unknown",
|
| 80 |
+
"score": round(float(score), 4)
|
| 81 |
+
})
|
| 82 |
+
continue
|
| 83 |
+
|
| 84 |
+
already_exists = (
|
| 85 |
+
db.query(Attendance)
|
| 86 |
+
.filter(
|
| 87 |
+
Attendance.person_id == person_id,
|
| 88 |
+
Attendance.date == today,
|
| 89 |
+
)
|
| 90 |
+
.first()
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
if not already_exists:
|
| 94 |
+
|
| 95 |
+
attendance = Attendance(
|
| 96 |
+
person_id=person_id,
|
| 97 |
+
date=today,
|
| 98 |
+
time=datetime.datetime.now().time(),
|
| 99 |
+
status="Present",
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
db.add(attendance)
|
| 103 |
+
db.commit()
|
| 104 |
+
|
| 105 |
+
present.append({
|
| 106 |
+
"id": person_id,
|
| 107 |
+
"name": name,
|
| 108 |
+
"score": round(float(score), 4),
|
| 109 |
+
"status": "Present",
|
| 110 |
+
})
|
| 111 |
+
|
| 112 |
+
return {
|
| 113 |
+
"count": len(present),
|
| 114 |
+
"results": present,
|
| 115 |
+
}
|
app/api/register.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
| 5 |
+
from sqlalchemy.orm import Session
|
| 6 |
+
|
| 7 |
+
from app.database.database import get_db
|
| 8 |
+
from app.database.models import Person
|
| 9 |
+
from app.services.face_detector import detect_faces
|
| 10 |
+
from app.services.embedding import get_embedding
|
| 11 |
+
from app.services.face_quality import check_face_quality
|
| 12 |
+
|
| 13 |
+
router = APIRouter(prefix="/register", tags=["Register"])
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@router.post("/")
|
| 17 |
+
async def register_person(
|
| 18 |
+
name: str,
|
| 19 |
+
file: UploadFile = File(...),
|
| 20 |
+
db: Session = Depends(get_db),
|
| 21 |
+
):
|
| 22 |
+
"""
|
| 23 |
+
Register a new person from an uploaded image.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
# قراءة الصورة
|
| 27 |
+
contents = await file.read()
|
| 28 |
+
|
| 29 |
+
image = cv2.imdecode(
|
| 30 |
+
np.frombuffer(contents, np.uint8),
|
| 31 |
+
cv2.IMREAD_COLOR,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
if image is None:
|
| 35 |
+
raise HTTPException(
|
| 36 |
+
status_code=400,
|
| 37 |
+
detail="الصورة غير صالحة"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# كشف الوجه
|
| 41 |
+
boxes = detect_faces(image)
|
| 42 |
+
|
| 43 |
+
if len(boxes) == 0:
|
| 44 |
+
raise HTTPException(
|
| 45 |
+
status_code=400,
|
| 46 |
+
detail="لم يتم العثور على أي وجه"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
# أول وجه فقط
|
| 50 |
+
x1, y1, x2, y2 = boxes[0]
|
| 51 |
+
|
| 52 |
+
face_crop = image[y1:y2, x1:x2]
|
| 53 |
+
|
| 54 |
+
if face_crop.size == 0:
|
| 55 |
+
raise HTTPException(
|
| 56 |
+
status_code=400,
|
| 57 |
+
detail="فشل استخراج الوجه"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
# فحص الجودة
|
| 61 |
+
quality = check_face_quality(face_crop)
|
| 62 |
+
|
| 63 |
+
if not quality["ok"]:
|
| 64 |
+
raise HTTPException(
|
| 65 |
+
status_code=400,
|
| 66 |
+
detail=quality["message"]
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# استخراج الـ Embedding
|
| 70 |
+
embedding = get_embedding(face_crop)
|
| 71 |
+
|
| 72 |
+
# حفظه في قاعدة البيانات
|
| 73 |
+
person = Person(
|
| 74 |
+
name=name,
|
| 75 |
+
embedding=embedding.tolist(),
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
db.add(person)
|
| 79 |
+
db.commit()
|
| 80 |
+
db.refresh(person)
|
| 81 |
+
|
| 82 |
+
return {
|
| 83 |
+
"id": person.id,
|
| 84 |
+
"name": person.name,
|
| 85 |
+
"quality": quality,
|
| 86 |
+
"message": "تم التسجيل بنجاح"
|
| 87 |
+
}
|
app/config.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
# ---------------- Database ----------------
|
| 7 |
+
DB_USER = os.getenv("DB_USER", "postgres")
|
| 8 |
+
DB_PASSWORD = os.getenv("DB_PASSWORD", "postgres")
|
| 9 |
+
DB_HOST = os.getenv("DB_HOST", "localhost")
|
| 10 |
+
DB_PORT = os.getenv("DB_PORT", "5432")
|
| 11 |
+
DB_NAME = os.getenv("DB_NAME", "face_attendance")
|
| 12 |
+
|
| 13 |
+
DATABASE_URL = (
|
| 14 |
+
f"postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# ---------------- Models weights ----------------
|
| 18 |
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 19 |
+
WEIGHTS_DIR = os.path.join(BASE_DIR, "app", "weights")
|
| 20 |
+
|
| 21 |
+
YOLO_FACE_WEIGHTS = os.path.join(WEIGHTS_DIR, "YoloV8_Face.pt")
|
| 22 |
+
EDGEFACE_WEIGHTS = os.path.join(WEIGHTS_DIR, "edgeface_xxs_q.pt")
|
| 23 |
+
EDGEFACE_MODEL_NAME = "edgeface_xxs_q"
|
| 24 |
+
|
| 25 |
+
# ---------------- Recognition ----------------
|
| 26 |
+
# لو الـ similarity بين الوجه واللي مخزن اقل من الرقم ده -> Unknown
|
| 27 |
+
RECOGNITION_THRESHOLD = 0.25
|
| 28 |
+
|
| 29 |
+
# ---------------- Camera ----------------
|
| 30 |
+
CAMERA_INDEX = 0
|
app/database/__init__.py
ADDED
|
File without changes
|
app/database/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (168 Bytes). View file
|
|
|
app/database/__pycache__/database.cpython-310.pyc
ADDED
|
Binary file (676 Bytes). View file
|
|
|
app/database/__pycache__/models.cpython-310.pyc
ADDED
|
Binary file (1.45 kB). View file
|
|
|
app/database/database.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from sqlalchemy.orm import sessionmaker, declarative_base
|
| 3 |
+
|
| 4 |
+
from app.config import DATABASE_URL
|
| 5 |
+
|
| 6 |
+
engine = create_engine(DATABASE_URL)
|
| 7 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 8 |
+
|
| 9 |
+
Base = declarative_base()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_db():
|
| 13 |
+
"""Dependency بتفتح جلسة DB وتقفلها لوحدها بعد كل request"""
|
| 14 |
+
db = SessionLocal()
|
| 15 |
+
try:
|
| 16 |
+
yield db
|
| 17 |
+
finally:
|
| 18 |
+
db.close()
|
app/database/models.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import (
|
| 2 |
+
Column,
|
| 3 |
+
Integer,
|
| 4 |
+
String,
|
| 5 |
+
Float,
|
| 6 |
+
Date,
|
| 7 |
+
Time,
|
| 8 |
+
DateTime,
|
| 9 |
+
ForeignKey,
|
| 10 |
+
UniqueConstraint,
|
| 11 |
+
func,
|
| 12 |
+
)
|
| 13 |
+
from sqlalchemy.dialects.postgresql import ARRAY
|
| 14 |
+
from sqlalchemy.orm import relationship
|
| 15 |
+
|
| 16 |
+
from app.database.database import Base
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Person(Base):
|
| 20 |
+
__tablename__ = "person"
|
| 21 |
+
|
| 22 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 23 |
+
name = Column(String, nullable=False, index=True)
|
| 24 |
+
embedding = Column(ARRAY(Float), nullable=False) # 512-d embedding
|
| 25 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 26 |
+
|
| 27 |
+
attendances = relationship("Attendance", back_populates="person")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class Attendance(Base):
|
| 31 |
+
__tablename__ = "attendance"
|
| 32 |
+
# شخص واحد يتسجل مرة واحدة بس فى اليوم
|
| 33 |
+
__table_args__ = (UniqueConstraint("person_id", "date", name="uq_person_date"),)
|
| 34 |
+
|
| 35 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 36 |
+
person_id = Column(Integer, ForeignKey("person.id"), nullable=False)
|
| 37 |
+
date = Column(Date, nullable=False)
|
| 38 |
+
time = Column(Time, nullable=False)
|
| 39 |
+
status = Column(String, default="Present")
|
| 40 |
+
|
| 41 |
+
person = relationship("Person", back_populates="attendances")
|
app/main.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
|
| 3 |
+
from app.database.database import engine, Base
|
| 4 |
+
from app.api import register, attendance
|
| 5 |
+
|
| 6 |
+
# ينشئ الجداول لو مش موجودة (person + attendance)
|
| 7 |
+
Base.metadata.create_all(bind=engine)
|
| 8 |
+
|
| 9 |
+
app = FastAPI(title="Face Attendance API")
|
| 10 |
+
|
| 11 |
+
app.include_router(register.router)
|
| 12 |
+
app.include_router(attendance.router)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@app.get("/")
|
| 16 |
+
def root():
|
| 17 |
+
return {"message": "Face Attendance API is running. Check /docs"}
|
app/services/__init__.py
ADDED
|
File without changes
|
app/services/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (168 Bytes). View file
|
|
|
app/services/__pycache__/edgeface_model.cpython-310.pyc
ADDED
|
Binary file (3.08 kB). View file
|
|
|
app/services/__pycache__/embedding.cpython-310.pyc
ADDED
|
Binary file (931 Bytes). View file
|
|
|
app/services/__pycache__/face_detector.cpython-310.pyc
ADDED
|
Binary file (1.35 kB). View file
|
|
|
app/services/__pycache__/face_quality.cpython-310.pyc
ADDED
|
Binary file (2 kB). View file
|
|
|
app/services/__pycache__/recognition.cpython-310.pyc
ADDED
|
Binary file (1.51 kB). View file
|
|
|
app/services/edgeface_model.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
تعريف موديل EdgeFace (بدون أي تعديل عن الكود الأصلي)
|
| 3 |
+
"""
|
| 4 |
+
import timm
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class LoRaLin(nn.Module):
|
| 10 |
+
def __init__(self, in_features, out_features, rank, bias=True):
|
| 11 |
+
super(LoRaLin, self).__init__()
|
| 12 |
+
self.in_features = in_features
|
| 13 |
+
self.out_features = out_features
|
| 14 |
+
self.rank = rank
|
| 15 |
+
self.linear1 = nn.Linear(in_features, rank, bias=False)
|
| 16 |
+
self.linear2 = nn.Linear(rank, out_features, bias=bias)
|
| 17 |
+
|
| 18 |
+
def forward(self, input):
|
| 19 |
+
x = self.linear1(input)
|
| 20 |
+
x = self.linear2(x)
|
| 21 |
+
return x
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def replace_linear_with_lowrank_recursive_2(model, rank_ratio=0.2):
|
| 25 |
+
for name, module in model.named_children():
|
| 26 |
+
if isinstance(module, nn.Linear) and "head" not in name:
|
| 27 |
+
in_features = module.in_features
|
| 28 |
+
out_features = module.out_features
|
| 29 |
+
rank = max(2, int(min(in_features, out_features) * rank_ratio))
|
| 30 |
+
bias = False
|
| 31 |
+
if module.bias is not None:
|
| 32 |
+
bias = True
|
| 33 |
+
lowrank_module = LoRaLin(in_features, out_features, rank, bias)
|
| 34 |
+
setattr(model, name, lowrank_module)
|
| 35 |
+
else:
|
| 36 |
+
replace_linear_with_lowrank_recursive_2(module, rank_ratio)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def replace_linear_with_lowrank_2(model, rank_ratio=0.2):
|
| 40 |
+
replace_linear_with_lowrank_recursive_2(model, rank_ratio)
|
| 41 |
+
return model
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class TimmFRWrapperV2(nn.Module):
|
| 45 |
+
"""Wraps timm model"""
|
| 46 |
+
|
| 47 |
+
def __init__(self, model_name="edgenext_x_small", featdim=512, batchnorm=False):
|
| 48 |
+
super().__init__()
|
| 49 |
+
self.featdim = featdim
|
| 50 |
+
self.model_name = model_name
|
| 51 |
+
|
| 52 |
+
self.model = timm.create_model(self.model_name)
|
| 53 |
+
self.model.reset_classifier(self.featdim)
|
| 54 |
+
|
| 55 |
+
def forward(self, x):
|
| 56 |
+
x = self.model(x)
|
| 57 |
+
return x
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def get_timmfrv2(model_name, **kwargs):
|
| 61 |
+
return TimmFRWrapperV2(model_name=model_name, **kwargs)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def get_model(name, **kwargs):
|
| 65 |
+
if name == "edgeface_xs_gamma_06":
|
| 66 |
+
return replace_linear_with_lowrank_2(
|
| 67 |
+
get_timmfrv2("edgenext_x_small", batchnorm=False), rank_ratio=0.6
|
| 68 |
+
)
|
| 69 |
+
elif name == "edgeface_xs_q":
|
| 70 |
+
model = get_timmfrv2("edgenext_x_small", batchnorm=False)
|
| 71 |
+
model = torch.quantization.quantize_dynamic(
|
| 72 |
+
model, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8
|
| 73 |
+
)
|
| 74 |
+
return model
|
| 75 |
+
elif name == "edgeface_xxs":
|
| 76 |
+
return get_timmfrv2("edgenext_xx_small", batchnorm=False)
|
| 77 |
+
elif name == "edgeface_base":
|
| 78 |
+
return get_timmfrv2("edgenext_base", batchnorm=False)
|
| 79 |
+
elif name == "edgeface_xxs_q":
|
| 80 |
+
model = get_timmfrv2("edgenext_xx_small", batchnorm=False)
|
| 81 |
+
model = torch.quantization.quantize_dynamic(
|
| 82 |
+
model, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8
|
| 83 |
+
)
|
| 84 |
+
return model
|
| 85 |
+
elif name == "edgeface_s_gamma_05":
|
| 86 |
+
return replace_linear_with_lowrank_2(
|
| 87 |
+
get_timmfrv2("edgenext_small", batchnorm=False), rank_ratio=0.5
|
| 88 |
+
)
|
| 89 |
+
else:
|
| 90 |
+
raise ValueError(f"Unknown model name: {name}")
|
app/services/embedding.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
from app.config import EDGEFACE_WEIGHTS, EDGEFACE_MODEL_NAME
|
| 5 |
+
from app.services.edgeface_model import get_model
|
| 6 |
+
from app.utils.preprocess import preprocess_face
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# ---------------- تحميل الموديل مرة واحدة بس ----------------
|
| 10 |
+
_net = get_model(EDGEFACE_MODEL_NAME, fp16=False)
|
| 11 |
+
_net.load_state_dict(torch.load(EDGEFACE_WEIGHTS, map_location="cpu"))
|
| 12 |
+
_net.eval()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def get_embedding(face_bgr: np.ndarray) -> np.ndarray:
|
| 16 |
+
"""بياخد صورة وجه (crop بصيغة BGR) ويرجع الـ embedding بتاعه (512,)"""
|
| 17 |
+
face_tensor = preprocess_face(face_bgr)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
with torch.no_grad():
|
| 21 |
+
emb = _net(face_tensor).squeeze().numpy().astype(np.float32)
|
| 22 |
+
return emb
|
app/services/face_detector.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ultralytics import YOLO
|
| 2 |
+
|
| 3 |
+
from app.config import YOLO_FACE_WEIGHTS
|
| 4 |
+
|
| 5 |
+
_TRACKER_YAML = "bytetrack.yaml"
|
| 6 |
+
|
| 7 |
+
# هيتحمل مرة واحدة بس لما السيرفر يشتغل
|
| 8 |
+
_yolo_model = YOLO(YOLO_FACE_WEIGHTS)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def get_yolo_model():
|
| 12 |
+
return _yolo_model
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def detect_faces(frame, conf=0.25):
|
| 16 |
+
"""كشف الوجوه من غير tracking (بتستخدم فى /register)"""
|
| 17 |
+
results = _yolo_model.predict(frame, conf=conf, verbose=False)[0]
|
| 18 |
+
boxes = []
|
| 19 |
+
if results.boxes is not None:
|
| 20 |
+
for box in results.boxes.xyxy.cpu():
|
| 21 |
+
boxes.append(tuple(map(int, box.tolist())))
|
| 22 |
+
return boxes
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def track_faces(frame, conf=0.25):
|
| 26 |
+
"""كشف + تتبع الوجوه (بتستخدم فى /attendance)"""
|
| 27 |
+
results = _yolo_model.track(
|
| 28 |
+
frame, persist=True, tracker=_TRACKER_YAML, conf=conf, verbose=False
|
| 29 |
+
)[0]
|
| 30 |
+
|
| 31 |
+
tracked = []
|
| 32 |
+
if results.boxes is not None and results.boxes.id is not None:
|
| 33 |
+
boxes = results.boxes.xyxy.cpu()
|
| 34 |
+
track_ids = results.boxes.id.int().cpu().tolist()
|
| 35 |
+
for box, track_id in zip(boxes, track_ids):
|
| 36 |
+
x1, y1, x2, y2 = map(int, box.tolist())
|
| 37 |
+
tracked.append((track_id, (x1, y1, x2, y2)))
|
| 38 |
+
return tracked
|
app/services/face_quality.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import math
|
| 3 |
+
import mediapipe as mp
|
| 4 |
+
|
| 5 |
+
mp_face_mesh = mp.solutions.face_mesh
|
| 6 |
+
|
| 7 |
+
# إعدادات يمكن تعديلها
|
| 8 |
+
MIN_FACE_SIZE = 120
|
| 9 |
+
MIN_BRIGHTNESS = 60
|
| 10 |
+
MAX_BRIGHTNESS = 220
|
| 11 |
+
MIN_SHARPNESS = 80
|
| 12 |
+
MAX_ROLL_ANGLE = 10
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def check_face_quality(face_img):
|
| 16 |
+
"""
|
| 17 |
+
Parameters
|
| 18 |
+
----------
|
| 19 |
+
face_img : BGR image (cropped face)
|
| 20 |
+
|
| 21 |
+
Returns
|
| 22 |
+
-------
|
| 23 |
+
dict
|
| 24 |
+
{
|
| 25 |
+
"ok": bool,
|
| 26 |
+
"message": "...",
|
| 27 |
+
"brightness": float,
|
| 28 |
+
"sharpness": float,
|
| 29 |
+
"roll": float
|
| 30 |
+
}
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
h, w = face_img.shape[:2]
|
| 34 |
+
|
| 35 |
+
# -----------------------------
|
| 36 |
+
# حجم الوجه
|
| 37 |
+
# -----------------------------
|
| 38 |
+
if h < MIN_FACE_SIZE or w < MIN_FACE_SIZE:
|
| 39 |
+
return {
|
| 40 |
+
"ok": False,
|
| 41 |
+
"message": "اقترب من الكاميرا",
|
| 42 |
+
"brightness": 0,
|
| 43 |
+
"sharpness": 0,
|
| 44 |
+
"roll": 0,
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
# -----------------------------
|
| 48 |
+
# الإضاءة
|
| 49 |
+
# -----------------------------
|
| 50 |
+
gray = cv2.cvtColor(face_img, cv2.COLOR_BGR2GRAY)
|
| 51 |
+
|
| 52 |
+
brightness = gray.mean()
|
| 53 |
+
|
| 54 |
+
if brightness < MIN_BRIGHTNESS:
|
| 55 |
+
return {
|
| 56 |
+
"ok": False,
|
| 57 |
+
"message": "الإضاءة ضعيفة",
|
| 58 |
+
"brightness": brightness,
|
| 59 |
+
"sharpness": 0,
|
| 60 |
+
"roll": 0,
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
if brightness > MAX_BRIGHTNESS:
|
| 64 |
+
return {
|
| 65 |
+
"ok": False,
|
| 66 |
+
"message": "الإضاءة قوية جدًا",
|
| 67 |
+
"brightness": brightness,
|
| 68 |
+
"sharpness": 0,
|
| 69 |
+
"roll": 0,
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
# -----------------------------
|
| 73 |
+
# وضوح الصورة
|
| 74 |
+
# -----------------------------
|
| 75 |
+
sharpness = cv2.Laplacian(gray, cv2.CV_64F).var()
|
| 76 |
+
|
| 77 |
+
if sharpness < MIN_SHARPNESS:
|
| 78 |
+
return {
|
| 79 |
+
"ok": False,
|
| 80 |
+
"message": "الصورة غير واضحة",
|
| 81 |
+
"brightness": brightness,
|
| 82 |
+
"sharpness": sharpness,
|
| 83 |
+
"roll": 0,
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# -----------------------------
|
| 87 |
+
# Face Mesh
|
| 88 |
+
# -----------------------------
|
| 89 |
+
rgb = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB)
|
| 90 |
+
|
| 91 |
+
with mp_face_mesh.FaceMesh(
|
| 92 |
+
static_image_mode=True,
|
| 93 |
+
max_num_faces=1,
|
| 94 |
+
refine_landmarks=True
|
| 95 |
+
) as mesh:
|
| 96 |
+
|
| 97 |
+
result = mesh.process(rgb)
|
| 98 |
+
|
| 99 |
+
if not result.multi_face_landmarks:
|
| 100 |
+
return {
|
| 101 |
+
"ok": False,
|
| 102 |
+
"message": "لم يتم اكتشاف معالم الوجه",
|
| 103 |
+
"brightness": brightness,
|
| 104 |
+
"sharpness": sharpness,
|
| 105 |
+
"roll": 0,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
lm = result.multi_face_landmarks[0].landmark
|
| 109 |
+
|
| 110 |
+
left_eye = lm[33]
|
| 111 |
+
right_eye = lm[263]
|
| 112 |
+
|
| 113 |
+
left = (left_eye.x * w, left_eye.y * h)
|
| 114 |
+
right = (right_eye.x * w, right_eye.y * h)
|
| 115 |
+
|
| 116 |
+
roll = math.degrees(
|
| 117 |
+
math.atan2(
|
| 118 |
+
right[1] - left[1],
|
| 119 |
+
right[0] - left[0]
|
| 120 |
+
)
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
if abs(roll) > MAX_ROLL_ANGLE:
|
| 124 |
+
return {
|
| 125 |
+
"ok": False,
|
| 126 |
+
"message": "اجعل رأسك مستقيمًا",
|
| 127 |
+
"brightness": brightness,
|
| 128 |
+
"sharpness": sharpness,
|
| 129 |
+
"roll": roll,
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
return {
|
| 133 |
+
"ok": True,
|
| 134 |
+
"message": "Face OK",
|
| 135 |
+
"brightness": brightness,
|
| 136 |
+
"sharpness": sharpness,
|
| 137 |
+
"roll": roll,
|
| 138 |
+
}
|
app/services/recognition.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
|
| 4 |
+
from app.database.models import Person
|
| 5 |
+
from app.utils.preprocess import cosine_similarity
|
| 6 |
+
from app.config import RECOGNITION_THRESHOLD
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def load_all_persons(db: Session):
|
| 10 |
+
"""بترجع كل الأشخاص المسجلين مع الـ embeddings بتاعتهم"""
|
| 11 |
+
persons = db.query(Person).all()
|
| 12 |
+
return [(p.id, p.name, np.array(p.embedding, dtype=np.float32)) for p in persons]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def identify_person(embedding: np.ndarray, known_persons: list):
|
| 16 |
+
"""
|
| 17 |
+
بتقارن embedding بكل الأشخاص المعروفين وترجع أقرب واحد
|
| 18 |
+
known_persons: list of (id, name, embedding)
|
| 19 |
+
"""
|
| 20 |
+
best_id, best_name, best_score = None, "Unknown", -1.0
|
| 21 |
+
|
| 22 |
+
for person_id, name, known_emb in known_persons:
|
| 23 |
+
score = cosine_similarity(embedding, known_emb)
|
| 24 |
+
print(f"Comparing with {name}: score={score:.4f}")
|
| 25 |
+
if score > best_score:
|
| 26 |
+
best_score = score
|
| 27 |
+
best_name = name
|
| 28 |
+
best_id = person_id
|
| 29 |
+
|
| 30 |
+
print(f"Best match: {best_name} with score={best_score:.4f}")
|
| 31 |
+
if best_score < RECOGNITION_THRESHOLD:
|
| 32 |
+
return None, "Unknown", best_score
|
| 33 |
+
|
| 34 |
+
return best_id, best_name, best_score
|
app/utils/__init__.py
ADDED
|
File without changes
|
app/utils/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (165 Bytes). View file
|
|
|
app/utils/__pycache__/preprocess.cpython-310.pyc
ADDED
|
Binary file (1.07 kB). View file
|
|
|
app/utils/preprocess.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def preprocess_face(face_bgr: np.ndarray) -> torch.Tensor:
|
| 7 |
+
"""
|
| 8 |
+
بياخد صورة وجه (BGR من OpenCV) ويرجعها Tensor جاهز للموديل
|
| 9 |
+
"""
|
| 10 |
+
face = cv2.resize(face_bgr, (112, 112))
|
| 11 |
+
face = cv2.cvtColor(face, cv2.COLOR_BGR2RGB)
|
| 12 |
+
face = np.transpose(face, (2, 0, 1))
|
| 13 |
+
face_tensor = torch.from_numpy(face).unsqueeze(0).float()
|
| 14 |
+
face_tensor.div_(255).sub_(0.5).div_(0.5)
|
| 15 |
+
return face_tensor
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
|
| 19 |
+
"""Cosine similarity بين embedding واحد وembedding تاني"""
|
| 20 |
+
a = a.flatten()
|
| 21 |
+
b = b.flatten()
|
| 22 |
+
denom = (np.linalg.norm(a) * np.linalg.norm(b)) + 1e-8
|
| 23 |
+
return float(np.dot(a, b) / denom)
|
app/weights/PUT_WEIGHTS_HERE.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
حط هنا ملفات الموديلات:
|
| 2 |
+
- YoloV8_Face.pt
|
| 3 |
+
- edgeface_xxs_q.pt
|
app/weights/YoloV8_Face.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d545bf1add5aa736a4febac4f4f9245a6d596cd0fe70d5d57989fe0cb9e626ca
|
| 3 |
+
size 6389512
|
app/weights/edgeface_xxs_q.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:46be7cc7e8d6e23501c0627fe5fbef101922ee9b85ccbef476987ab1a320b053
|
| 3 |
+
size 1734661
|
dockerignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv
|
| 2 |
+
.git
|
| 3 |
+
__pycache__
|
| 4 |
+
*.pyc
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
sqlalchemy
|
| 4 |
+
psycopg2-binary
|
| 5 |
+
python-dotenv
|
| 6 |
+
numpy
|
| 7 |
+
opencv-python-headless
|
| 8 |
+
torch
|
| 9 |
+
timm
|
| 10 |
+
ultralytics
|
| 11 |
+
pillow
|
| 12 |
+
mediapipe==0.10.21
|