first commit

This commit is contained in:
Fabio 2025-12-27 15:36:20 +01:00
commit 1a05c97ca1
23 changed files with 492 additions and 0 deletions

35
.gitignore vendored Normal file
View file

@ -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

1
app/__init__.py Normal file
View file

@ -0,0 +1 @@
# App package

1
app/api/__init__.py Normal file
View file

@ -0,0 +1 @@
# API package

30
app/api/faces.py Normal file
View file

@ -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)
}

56
app/api/photos.py Normal file
View file

@ -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
}

8
app/app.py Normal file
View file

@ -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")

12
app/config.py Normal file
View file

@ -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()

1
app/core/__init__.py Normal file
View file

@ -0,0 +1 @@
# Core logic package

90
app/core/detector.py Normal file
View file

@ -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

31
app/core/embedder.py Normal file
View file

@ -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

28
app/core/matcher.py Normal file
View file

@ -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
}

38
app/core/storage.py Normal file
View file

@ -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)

View file

@ -0,0 +1 @@
{}

63
app/models/README.md Normal file
View file

@ -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 facereidentification 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 ResNet50 (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

View file

@ -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"
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

19
app/models/test_rknn.py Normal file
View file

@ -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)")

27
docker-compose.yml Normal file
View file

@ -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

6
requirements.txt Normal file
View file

@ -0,0 +1,6 @@
fastapi
uvicorn
opencv-python
numpy
rknn-toolkit2
rknn-toolkit2==1.5.2