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