bump project

This commit is contained in:
2026-05-01 21:24:21 +03:30
parent dc4b1a1400
commit 139b3e26da
83 changed files with 2455 additions and 18349 deletions

125
app/services/recognition.py Normal file
View File

@@ -0,0 +1,125 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from werkzeug.datastructures import FileStorage
from app.models import Person
class RecognitionUnavailable(RuntimeError):
pass
@dataclass(frozen=True)
class DetectedFace:
person: "Person | None"
confidence: float | None
@dataclass(frozen=True)
class RecognitionResult:
faces: list[DetectedFace]
annotated_image: str
def encode_uploaded_image(file: "FileStorage") -> list[float]:
cv2, np, face_recognition = _load_dependencies()
raw = file.read()
image = _decode_bytes(raw, cv2, np)
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
encodings = face_recognition.face_encodings(rgb)
if len(encodings) == 0:
raise ValueError(f"No face was detected in {file.filename}.")
if len(encodings) > 1:
raise ValueError(f"Upload a single-face photo for {file.filename}.")
return [float(value) for value in encodings[0]]
def recognize_faces(data_url: str, people: list["Person"], *, tolerance: float) -> RecognitionResult:
cv2, np, face_recognition = _load_dependencies()
image = _decode_data_url(data_url, cv2, np)
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
locations = face_recognition.face_locations(rgb)
encodings = face_recognition.face_encodings(rgb, locations)
candidates = [
(person, encoding.vector)
for person in people
for encoding in person.face_encodings
]
faces: list[DetectedFace] = []
for location, face_encoding in zip(locations, encodings, strict=False):
person = None
confidence = None
label = "Unknown"
if candidates:
vectors = [candidate[1] for candidate in candidates]
distances = face_recognition.face_distance(vectors, face_encoding)
best_index = int(np.argmin(distances))
best_distance = float(distances[best_index])
if best_distance <= tolerance:
person = candidates[best_index][0]
confidence = max(0.0, 1.0 - best_distance)
label = person.full_name
faces.append(DetectedFace(person=person, confidence=confidence))
_draw_face_label(image, location, label, cv2)
if not faces:
return RecognitionResult(faces=[DetectedFace(person=None, confidence=None)], annotated_image=_encode_png(image, cv2))
return RecognitionResult(faces=faces, annotated_image=_encode_png(image, cv2))
def _load_dependencies():
try:
import cv2
import face_recognition
import numpy as np
except ImportError as exc:
raise RecognitionUnavailable(
"Face recognition dependencies are not installed. Install requirements on a supported Python version."
) from exc
return cv2, np, face_recognition
def _decode_data_url(data_url: str, cv2, np):
if "," not in data_url:
raise ValueError("Invalid image payload.")
header, encoded = data_url.split(",", 1)
if not header.startswith("data:image/"):
raise ValueError("Only image uploads are supported.")
try:
raw = base64.b64decode(encoded, validate=True)
except ValueError as exc:
raise ValueError("Invalid image encoding.") from exc
return _decode_bytes(raw, cv2, np)
def _decode_bytes(raw: bytes, cv2, np):
if not raw:
raise ValueError("The uploaded image is empty.")
array = np.frombuffer(raw, np.uint8)
image = cv2.imdecode(array, cv2.IMREAD_COLOR)
if image is None:
raise ValueError("The uploaded file is not a readable image.")
return image
def _draw_face_label(image, location, label: str, cv2) -> None:
top, right, bottom, left = location
cv2.rectangle(image, (left, top), (right, bottom), (30, 136, 229), 2)
cv2.rectangle(image, (left, bottom - 28), (right, bottom), (30, 136, 229), cv2.FILLED)
cv2.putText(image, label[:32], (left + 6, bottom - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1)
def _encode_png(image, cv2) -> str:
ok, buffer = cv2.imencode(".png", image)
if not ok:
raise ValueError("Could not encode processed image.")
return "data:image/png;base64," + base64.b64encode(buffer).decode("ascii")