HossamFakher commited on
Commit
c1ff2a6
·
verified ·
1 Parent(s): 64da477

Upload 42 files

Browse files
Files changed (42) hide show
  1. .env +5 -0
  2. Dockerfile +18 -0
  3. README.md +91 -14
  4. app/__init__.py +0 -0
  5. app/__pycache__/__init__.cpython-310.pyc +0 -0
  6. app/__pycache__/config.cpython-310.pyc +0 -0
  7. app/__pycache__/main.cpython-310.pyc +0 -0
  8. app/api/__init__.py +0 -0
  9. app/api/__pycache__/__init__.cpython-310.pyc +0 -0
  10. app/api/__pycache__/attendance.cpython-310.pyc +0 -0
  11. app/api/__pycache__/register.cpython-310.pyc +0 -0
  12. app/api/attendance.py +115 -0
  13. app/api/register.py +87 -0
  14. app/config.py +30 -0
  15. app/database/__init__.py +0 -0
  16. app/database/__pycache__/__init__.cpython-310.pyc +0 -0
  17. app/database/__pycache__/database.cpython-310.pyc +0 -0
  18. app/database/__pycache__/models.cpython-310.pyc +0 -0
  19. app/database/database.py +18 -0
  20. app/database/models.py +41 -0
  21. app/main.py +17 -0
  22. app/services/__init__.py +0 -0
  23. app/services/__pycache__/__init__.cpython-310.pyc +0 -0
  24. app/services/__pycache__/edgeface_model.cpython-310.pyc +0 -0
  25. app/services/__pycache__/embedding.cpython-310.pyc +0 -0
  26. app/services/__pycache__/face_detector.cpython-310.pyc +0 -0
  27. app/services/__pycache__/face_quality.cpython-310.pyc +0 -0
  28. app/services/__pycache__/recognition.cpython-310.pyc +0 -0
  29. app/services/edgeface_model.py +90 -0
  30. app/services/embedding.py +22 -0
  31. app/services/face_detector.py +38 -0
  32. app/services/face_quality.py +138 -0
  33. app/services/recognition.py +34 -0
  34. app/utils/__init__.py +0 -0
  35. app/utils/__pycache__/__init__.cpython-310.pyc +0 -0
  36. app/utils/__pycache__/preprocess.cpython-310.pyc +0 -0
  37. app/utils/preprocess.py +23 -0
  38. app/weights/PUT_WEIGHTS_HERE.txt +3 -0
  39. app/weights/YoloV8_Face.pt +3 -0
  40. app/weights/edgeface_xxs_q.pt +3 -0
  41. dockerignore +4 -0
  42. 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
- title: Attendance System
3
- emoji: 📉
4
- colorFrom: purple
5
- colorTo: blue
6
- sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.12'
9
- app_file: app.py
10
- pinned: false
11
- short_description: Attendance System
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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