commit 1a05c97ca1a9a76c90c30b07f1c336549f96d643 Author: Fabio Date: Sat Dec 27 15:36:20 2025 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..750298f --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# --- Python cache --- +__pycache__/ +*.py[cod] +*$py.class + +# --- Virtual environments --- +venv/ +.env/ +.venv/ +env/ + +# --- Build / packaging --- +build/ +dist/ +*.egg-info/ +.eggs/ + +# --- IDE / editor --- +.vscode/ +.idea/ + +# --- Logs --- +*.log + +# --- Test / coverage --- +.coverage +htmlcov/ +.cache/ + +# --- Jupyter --- +.ipynb_checkpoints/ + +# --- OS files --- +.DS_Store +Thumbs.db diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..edabda9 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +# App package diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..28b07ef --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1 @@ +# API package diff --git a/app/api/faces.py b/app/api/faces.py new file mode 100644 index 0000000..6ae2d27 --- /dev/null +++ b/app/api/faces.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter, UploadFile +import cv2 +import numpy as np +from app.core.embedder import get_embedding +from app.storage import save_known_face, load_embeddings, save_embeddings + +router = APIRouter() + +@router.post("/add") +async def add_face(name: str, file: UploadFile): + img_bytes = await file.read() + img = np.frombuffer(img_bytes, np.uint8) + img = cv2.imdecode(img, cv2.IMREAD_COLOR) + + # Salva foto del volto noto + save_known_face(name, img) + + # Genera embedding + emb = get_embedding(img) + + # Aggiorna database + db = load_embeddings() + db[name] = emb.tolist() + save_embeddings(db) + + return { + "status": "ok", + "name": name, + "embedding_len": len(emb) + } diff --git a/app/api/photos.py b/app/api/photos.py new file mode 100644 index 0000000..63fce67 --- /dev/null +++ b/app/api/photos.py @@ -0,0 +1,56 @@ +from fastapi import APIRouter, UploadFile +import cv2 +import numpy as np +import uuid +from app.core.detector import detect_faces +from app.core.embedder import get_embedding +from app.core.matcher import match_embedding +from app.storage import save_raw_photo, save_processed_photo + +router = APIRouter() + +@router.post("/upload") +async def upload_photo(file: UploadFile): + # Leggi immagine + img_bytes = await file.read() + img = np.frombuffer(img_bytes, np.uint8) + img = cv2.imdecode(img, cv2.IMREAD_COLOR) + + # Salva raw + filename = f"{uuid.uuid4().hex}.jpg" + save_raw_photo(filename, img) + + # Detection + boxes = detect_faces(img) + results = [] + + # Disegna bounding box + processed = img.copy() + + for box in boxes: + x1, y1, x2, y2 = box + face = img[y1:y2, x1:x2] + + emb = get_embedding(face) + match = match_embedding(emb) + + # Disegna box + color = (0, 255, 0) if match else (0, 0, 255) + cv2.rectangle(processed, (x1, y1), (x2, y2), color, 2) + + if match: + cv2.putText(processed, match["name"], (x1, y1 - 10), + cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) + + results.append({ + "box": box, + "match": match + }) + + # Salva immagine processata + save_processed_photo(filename, processed) + + return { + "file": filename, + "faces": results + } diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..ad25d7c --- /dev/null +++ b/app/app.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI +from app.api.faces import router as faces_router +from app.api.photos import router as photos_router + +app = FastAPI(title="Face Recognition Server") + +app.include_router(faces_router, prefix="/faces") +app.include_router(photos_router, prefix="/photos") diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..681f1eb --- /dev/null +++ b/app/config.py @@ -0,0 +1,12 @@ +import os + +class Config: + MODEL_DIR = "app/models" + SCRFD_MODEL = os.path.join(MODEL_DIR, "scrfd.rknn") + ARCFACE_MODEL = os.path.join(MODEL_DIR, "arcface.rknn") + + EMBEDDING_THRESHOLD = 0.45 # puoi regolarlo + DETECTION_SIZE = (640, 640) + EMBEDDING_SIZE = (112, 112) + +config = Config() diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..6790eda --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1 @@ +# Core logic package diff --git a/app/core/detector.py b/app/core/detector.py new file mode 100644 index 0000000..8b9bf86 --- /dev/null +++ b/app/core/detector.py @@ -0,0 +1,90 @@ +import cv2 +import numpy as np +from rknn.api import RKNN +from app.config import config + +# ----------------------------- +# Load RKNN model +# ----------------------------- +rknn = RKNN() +rknn.load_rknn(config.SCRFD_MODEL) +rknn.init_runtime() + +# ----------------------------- +# SCRFD decoding utilities +# ----------------------------- + +def nms(boxes, scores, thresh=0.45): + if len(boxes) == 0: + return [] + + boxes = np.array(boxes) + scores = np.array(scores) + + x1 = boxes[:, 0] + y1 = boxes[:, 1] + x2 = boxes[:, 2] + y2 = boxes[:, 3] + + areas = (x2 - x1) * (y2 - y1) + order = scores.argsort()[::-1] + + keep = [] + + while order.size > 0: + i = order[0] + keep.append(i) + + xx1 = np.maximum(x1[i], x1[order[1:]]) + yy1 = np.maximum(y1[i], y1[order[1:]]) + xx2 = np.minimum(x2[i], x2[order[1:]]) + yy2 = np.minimum(y2[i], y2[order[1:]]) + + w = np.maximum(0.0, xx2 - xx1) + h = np.maximum(0.0, yy2 - yy1) + + inter = w * h + ovr = inter / (areas[i] + areas[order[1:]] - inter) + + inds = np.where(ovr <= thresh)[0] + order = order[inds + 1] + + return keep + + +def decode_scrfd(outputs, img_shape): + # outputs = [scores, bboxes] + scores = outputs[0].reshape(-1) + bboxes = outputs[1].reshape(-1, 4) + + h, w, _ = img_shape + + # Filter by confidence + mask = scores > 0.5 + scores = scores[mask] + bboxes = bboxes[mask] + + # Convert to absolute coords + boxes = [] + for box in bboxes: + x1 = int(box[0] * w) + y1 = int(box[1] * h) + x2 = int(box[2] * w) + y2 = int(box[3] * h) + boxes.append([x1, y1, x2, y2]) + + # Apply NMS + keep = nms(boxes, scores) + return [boxes[i] for i in keep] + + +# ----------------------------- +# Main detection function +# ----------------------------- +def detect_faces(img): + resized = cv2.resize(img, config.DETECTION_SIZE) + input_data = np.expand_dims(resized, 0) + + outputs = rknn.inference(inputs=[input_data]) + boxes = decode_scrfd(outputs, img.shape) + return boxes diff --git a/app/core/embedder.py b/app/core/embedder.py new file mode 100644 index 0000000..9f65e96 --- /dev/null +++ b/app/core/embedder.py @@ -0,0 +1,31 @@ +import cv2 +import numpy as np +from rknn.api import RKNN +from app.config import config + +# ----------------------------- +# Load RKNN model +# ----------------------------- +rknn = RKNN() +rknn.load_rknn(config.ARCFACE_MODEL) +rknn.init_runtime() + +# ----------------------------- +# Preprocessing +# ----------------------------- +def preprocess(face): + face = cv2.resize(face, config.EMBEDDING_SIZE) + face = cv2.cvtColor(face, cv2.COLOR_BGR2RGB) + face = (face - 127.5) / 128.0 + return np.expand_dims(face, 0).astype(np.float32) + +# ----------------------------- +# Embedding extraction +# ----------------------------- +def get_embedding(face): + inp = preprocess(face) + emb = rknn.inference(inputs=[inp])[0] + + # Normalize L2 + emb = emb / np.linalg.norm(emb) + return emb diff --git a/app/core/matcher.py b/app/core/matcher.py new file mode 100644 index 0000000..07ab698 --- /dev/null +++ b/app/core/matcher.py @@ -0,0 +1,28 @@ +import numpy as np +from app.storage import load_embeddings +from app.config import config + +def match_embedding(emb): + db = load_embeddings() + + if not db: + return None + + best_name = None + best_score = -1 + + for name, vec in db.items(): + vec = np.array(vec) + score = float(np.dot(emb, vec)) + + if score > best_score: + best_score = score + best_name = name + + if best_score < config.EMBEDDING_THRESHOLD: + return None + + return { + "name": best_name, + "score": best_score + } diff --git a/app/core/storage.py b/app/core/storage.py new file mode 100644 index 0000000..3179845 --- /dev/null +++ b/app/core/storage.py @@ -0,0 +1,38 @@ +import os +import json +import cv2 + +BASE_DIR = "app/data" +PHOTOS_RAW = os.path.join(BASE_DIR, "photos/raw") +PHOTOS_PROCESSED = os.path.join(BASE_DIR, "photos/processed") +FACES_KNOWN = os.path.join(BASE_DIR, "faces/known") +EMBEDDINGS_FILE = os.path.join(BASE_DIR, "faces/embeddings.json") + +# Assicura che tutte le cartelle esistano +for path in [PHOTOS_RAW, PHOTOS_PROCESSED, FACES_KNOWN]: + os.makedirs(path, exist_ok=True) + +def save_raw_photo(filename: str, img): + path = os.path.join(PHOTOS_RAW, filename) + cv2.imwrite(path, img) + return path + +def save_processed_photo(filename: str, img): + path = os.path.join(PHOTOS_PROCESSED, filename) + cv2.imwrite(path, img) + return path + +def save_known_face(name: str, img): + path = os.path.join(FACES_KNOWN, f"{name}.jpg") + cv2.imwrite(path, img) + return path + +def load_embeddings(): + if not os.path.exists(EMBEDDINGS_FILE): + return {} + with open(EMBEDDINGS_FILE, "r") as f: + return json.load(f) + +def save_embeddings(data): + with open(EMBEDDINGS_FILE, "w") as f: + json.dump(data, f, indent=2) diff --git a/app/data/faces/embeddings.json b/app/data/faces/embeddings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/app/data/faces/embeddings.json @@ -0,0 +1 @@ +{} diff --git a/app/models/README.md b/app/models/README.md new file mode 100644 index 0000000..8c4622f --- /dev/null +++ b/app/models/README.md @@ -0,0 +1,63 @@ +git clone https://github.com/Daedaluz/rknn-docker.git +cd rknn-docker +sudo docker build -t rknn-lite . +docker run -it --rm \ + -v $(pwd):/workspace \ + rknn-lite \ + bash +cd wirkspace +python3 convert_models.py + + +poi +python3 test_rknn.py +risultato +Runtime init OK (CPU mode) + +SCRFD (Face Detector) +https://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/det_2.5g.onnx +opz +https://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/det_500m.onnx +https://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/det_10g.onnx + +ArcFace +leggero +https://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/w600k_mbf.onnx +pesante e piu accurato +https://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/w600k_r50.onnx + + +SCRFD (Face Detector) – ONNX diretto + +Dalla release del progetto face‑reidentification che include i modelli SCRFD: + +SCRFD 2.5G + +https://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/det_2.5g.onnx + +(Opzionali) + +SCRFD 500Mhttps://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/det_500m.onnx + +SCRFD 10Ghttps://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/det_10g.onnx + +🔹 ArcFace (Face Recognition) – ONNX diretto + +Dalla stessa release, modelli ArcFace in ONNX: + +ArcFace MobileFace (veloce, leggero) + +https://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/w600k_mbf.onnx + +ArcFace ResNet‑50 (più pesante, più accurato) + +https://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/w600k_r50.onnx + +📌 Consiglio tecnico per RK3588 + +Per Orange Pi 5 Plus: + +SCRFD 2.5G → miglior compromesso velocità/precisione + +ArcFace MobileFace (w600k_mbf.onnx) → più leggero, conversione RKNN più stabile + diff --git a/app/models/convert_models.py b/app/models/convert_models.py new file mode 100644 index 0000000..b24adb7 --- /dev/null +++ b/app/models/convert_models.py @@ -0,0 +1,45 @@ +from rknn.api import RKNN + +def convert(onnx_path, rknn_path): + print(f"\n=== Converting {onnx_path} → {rknn_path} ===") + + rknn = RKNN() + + # Ottimizzazioni consigliate da Rockchip + rknn.config( + mean_values=[[127.5, 127.5, 127.5]], + std_values=[[128.0, 128.0, 128.0]], + target_platform="rk3588" + ) + + print("[1] Loading ONNX model...") + ret = rknn.load_onnx(model=onnx_path) + if ret != 0: + print("Error loading ONNX") + return + + print("[2] Building RKNN model...") + ret = rknn.build(do_quantization=False) + if ret != 0: + print("Error building RKNN") + return + + print("[3] Exporting RKNN model...") + ret = rknn.export_rknn(rknn_path) + if ret != 0: + print("Error exporting RKNN") + return + + print("[OK] Conversion completed!") + +# Convert SCRFD +convert( + "models/onnx/scrfd_2.5g.onnx", + "models/rknn/scrfd.rknn" +) + +# Convert ArcFace +convert( + "models/onnx/arcface_r100.onnx", + "models/rknn/arcface.rknn" +) diff --git a/app/models/onnx/det_10g.onnx b/app/models/onnx/det_10g.onnx new file mode 100644 index 0000000..bbfebba Binary files /dev/null and b/app/models/onnx/det_10g.onnx differ diff --git a/app/models/onnx/det_2.5g.onnx b/app/models/onnx/det_2.5g.onnx new file mode 100644 index 0000000..611a71f Binary files /dev/null and b/app/models/onnx/det_2.5g.onnx differ diff --git a/app/models/onnx/det_500m.onnx b/app/models/onnx/det_500m.onnx new file mode 100644 index 0000000..60d1c0f Binary files /dev/null and b/app/models/onnx/det_500m.onnx differ diff --git a/app/models/onnx/w600k_mbf.onnx b/app/models/onnx/w600k_mbf.onnx new file mode 100644 index 0000000..e6ce6e3 Binary files /dev/null and b/app/models/onnx/w600k_mbf.onnx differ diff --git a/app/models/onnx/w600k_r50.onnx b/app/models/onnx/w600k_r50.onnx new file mode 100644 index 0000000..2c9df77 Binary files /dev/null and b/app/models/onnx/w600k_r50.onnx differ diff --git a/app/models/test_rknn.py b/app/models/test_rknn.py new file mode 100644 index 0000000..230aeda --- /dev/null +++ b/app/models/test_rknn.py @@ -0,0 +1,19 @@ +from rknn.api import RKNN + +print("Testing RKNN model load...") + +rknn = RKNN() +ret = rknn.load_rknn("models/rknn/scrfd.rknn") + +if ret != 0: + print("❌ Failed to load RKNN model") +else: + print("✅ RKNN model loaded successfully") + +print("Init runtime (CPU fallback)...") +ret = rknn.init_runtime() + +if ret != 0: + print("❌ Runtime init failed") +else: + print("✅ Runtime init OK (CPU mode)") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d5db443 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3.9" + +services: + face-server: + container_name: face-server + image: python:3.10-slim + privileged: true + restart: unless-stopped + + volumes: + - ./:/app + + working_dir: /app + + command: > + bash -c " + pip install --no-cache-dir -r requirements.txt && + uvicorn app.app:app --host 0.0.0.0 --port 8000 + " + + ports: + - "8000:8000" + + devices: + - /dev/rknn0 + - /dev/rknn1 + - /dev/rknn2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5272714 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn +opencv-python +numpy +rknn-toolkit2 +rknn-toolkit2==1.5.2