first commit
This commit is contained in:
commit
1a05c97ca1
23 changed files with 492 additions and 0 deletions
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal 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
1
app/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# App package
|
||||
1
app/api/__init__.py
Normal file
1
app/api/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# API package
|
||||
30
app/api/faces.py
Normal file
30
app/api/faces.py
Normal 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
56
app/api/photos.py
Normal 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
8
app/app.py
Normal 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
12
app/config.py
Normal 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
1
app/core/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Core logic package
|
||||
90
app/core/detector.py
Normal file
90
app/core/detector.py
Normal 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
31
app/core/embedder.py
Normal 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
28
app/core/matcher.py
Normal 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
38
app/core/storage.py
Normal 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)
|
||||
1
app/data/faces/embeddings.json
Normal file
1
app/data/faces/embeddings.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
63
app/models/README.md
Normal file
63
app/models/README.md
Normal 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 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
|
||||
|
||||
45
app/models/convert_models.py
Normal file
45
app/models/convert_models.py
Normal 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"
|
||||
)
|
||||
BIN
app/models/onnx/det_10g.onnx
Normal file
BIN
app/models/onnx/det_10g.onnx
Normal file
Binary file not shown.
BIN
app/models/onnx/det_2.5g.onnx
Normal file
BIN
app/models/onnx/det_2.5g.onnx
Normal file
Binary file not shown.
BIN
app/models/onnx/det_500m.onnx
Normal file
BIN
app/models/onnx/det_500m.onnx
Normal file
Binary file not shown.
BIN
app/models/onnx/w600k_mbf.onnx
Normal file
BIN
app/models/onnx/w600k_mbf.onnx
Normal file
Binary file not shown.
BIN
app/models/onnx/w600k_r50.onnx
Normal file
BIN
app/models/onnx/w600k_r50.onnx
Normal file
Binary file not shown.
19
app/models/test_rknn.py
Normal file
19
app/models/test_rknn.py
Normal 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
27
docker-compose.yml
Normal 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
6
requirements.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
fastapi
|
||||
uvicorn
|
||||
opencv-python
|
||||
numpy
|
||||
rknn-toolkit2
|
||||
rknn-toolkit2==1.5.2
|
||||
Loading…
Reference in a new issue