Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 38 additions & 15 deletions backend/app/database/faces.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class FaceData(TypedDict):
embeddings: FaceEmbedding # Numpy array in application, stored as JSON string in DB
confidence: Optional[float]
bbox: Optional[BoundingBox]
quality: Optional[float] # Face quality score (0.0-1.0)


FaceClusterMapping = Dict[FaceId, Optional[ClusterId]]
Expand All @@ -41,6 +42,7 @@ def db_create_faces_table() -> None:
embeddings TEXT,
confidence REAL,
bbox TEXT,
quality REAL DEFAULT 0.5,
FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE,
FOREIGN KEY (cluster_id) REFERENCES face_clusters(cluster_id) ON DELETE SET NULL
)
Expand All @@ -58,6 +60,7 @@ def db_insert_face_embeddings(
confidence: Optional[float] = None,
bbox: Optional[BoundingBox] = None,
cluster_id: Optional[ClusterId] = None,
quality: Optional[float] = None,
) -> FaceId:
"""
Insert face embeddings with additional metadata.
Expand All @@ -69,6 +72,7 @@ def db_insert_face_embeddings(
confidence: Confidence score for face detection (optional)
bbox: Bounding box coordinates as dict with keys: x, y, width, height (optional)
cluster_id: ID of the face cluster this face belongs to (optional)
quality: Face quality score 0.0-1.0 (optional)
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
Expand All @@ -81,10 +85,10 @@ def db_insert_face_embeddings(

cursor.execute(
"""
INSERT INTO faces (image_id, cluster_id, embeddings, confidence, bbox)
VALUES (?, ?, ?, ?, ?)
INSERT INTO faces (image_id, cluster_id, embeddings, confidence, bbox, quality)
VALUES (?, ?, ?, ?, ?, ?)
""",
(image_id, cluster_id, embeddings_json, confidence, bbox_json),
(image_id, cluster_id, embeddings_json, confidence, bbox_json, quality),
)

face_id = cursor.lastrowid
Expand All @@ -100,6 +104,7 @@ def db_insert_face_embeddings_by_image_id(
confidence: Optional[Union[float, List[float]]] = None,
bbox: Optional[Union[BoundingBox, List[BoundingBox]]] = None,
cluster_id: Optional[Union[ClusterId, List[ClusterId]]] = None,
quality: Optional[Union[float, List[float]]] = None,
) -> Union[FaceId, List[FaceId]]:
"""
Insert face embeddings using image path (convenience function).
Expand All @@ -110,6 +115,7 @@ def db_insert_face_embeddings_by_image_id(
confidence: Confidence score(s) for face detection (optional)
bbox: Bounding box coordinates or list of bounding boxes (optional)
cluster_id: Cluster ID(s) for the face(s) (optional)
quality: Face quality score(s) 0.0-1.0 (optional)
"""

# Handle multiple faces in one image
Expand All @@ -131,13 +137,18 @@ def db_insert_face_embeddings_by_image_id(
if isinstance(cluster_id, list) and i < len(cluster_id)
else cluster_id
)
face_id = db_insert_face_embeddings(image_id, emb, conf, bb, cid)
qual = (
quality[i]
if isinstance(quality, list) and i < len(quality)
else quality
)
face_id = db_insert_face_embeddings(image_id, emb, conf, bb, cid, qual)
face_ids.append(face_id)
return face_ids
else:
# Single face
return db_insert_face_embeddings(
image_id, embeddings, confidence, bbox, cluster_id
image_id, embeddings, confidence, bbox, cluster_id, quality
)


Expand Down Expand Up @@ -227,16 +238,20 @@ def db_get_faces_unassigned_clusters() -> List[Dict[str, Union[FaceId, FaceEmbed
cursor = conn.cursor()

try:
cursor.execute("SELECT face_id, embeddings FROM faces WHERE cluster_id IS NULL")
cursor.execute(
"SELECT face_id, embeddings, COALESCE(quality, 0.5) as quality FROM faces WHERE cluster_id IS NULL"
)

rows = cursor.fetchall()

faces = []
for row in rows:
face_id, embeddings_json = row
face_id, embeddings_json, quality = row
# Convert JSON string back to numpy array
embeddings = np.array(json.loads(embeddings_json))
faces.append({"face_id": face_id, "embeddings": embeddings})
faces.append(
{"face_id": face_id, "embeddings": embeddings, "quality": quality}
)

return faces
finally:
Expand All @@ -258,7 +273,7 @@ def db_get_all_faces_with_cluster_names() -> (
try:
cursor.execute(
"""
SELECT f.face_id, f.embeddings, fc.cluster_name
SELECT f.face_id, f.embeddings, fc.cluster_name, COALESCE(f.quality, 0.5) as quality
FROM faces f
LEFT JOIN face_clusters fc ON f.cluster_id = fc.cluster_id
ORDER BY f.face_id
Expand All @@ -269,14 +284,15 @@ def db_get_all_faces_with_cluster_names() -> (

faces = []
for row in rows:
face_id, embeddings_json, cluster_name = row
face_id, embeddings_json, cluster_name, quality = row
# Convert JSON string back to numpy array
embeddings = np.array(json.loads(embeddings_json))
faces.append(
{
"face_id": face_id,
"embeddings": embeddings,
"cluster_name": cluster_name,
"quality": quality,
}
)

Expand Down Expand Up @@ -344,7 +360,7 @@ def db_get_cluster_mean_embeddings() -> List[Dict[str, Union[str, FaceEmbedding]
try:
cursor.execute(
"""
SELECT f.cluster_id, f.embeddings
SELECT f.cluster_id, f.embeddings, COALESCE(f.quality, 0.5) as quality
FROM faces f
WHERE f.cluster_id IS NOT NULL
ORDER BY f.cluster_id
Expand All @@ -356,26 +372,33 @@ def db_get_cluster_mean_embeddings() -> List[Dict[str, Union[str, FaceEmbedding]
if not rows:
return []

# Group embeddings by cluster_id
# Group embeddings and quality by cluster_id
cluster_embeddings = {}
cluster_qualities = {}
for row in rows:
cluster_id, embeddings_json = row
cluster_id, embeddings_json, quality = row
# Convert JSON string back to numpy array
embeddings = np.array(json.loads(embeddings_json))

if cluster_id not in cluster_embeddings:
cluster_embeddings[cluster_id] = []
cluster_qualities[cluster_id] = []
cluster_embeddings[cluster_id].append(embeddings)
cluster_qualities[cluster_id].append(quality)

# Calculate mean embeddings for each cluster
cluster_means = []
for cluster_id, embeddings_list in cluster_embeddings.items():
# Stack all embeddings for this cluster and calculate mean
stacked_embeddings = np.stack(embeddings_list)
mean_embedding = np.mean(stacked_embeddings, axis=0)
quality_list = cluster_qualities[cluster_id]

cluster_means.append(
{"cluster_id": cluster_id, "mean_embedding": mean_embedding}
{
"cluster_id": cluster_id,
"embeddings": stacked_embeddings,
"quality_scores": np.array(quality_list),
}
)

return cluster_means
Expand Down
34 changes: 32 additions & 2 deletions backend/app/models/FaceDetector.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from app.utils.YOLO import YOLO_util_get_model_path
from app.models.YOLO import YOLO
from app.database.faces import db_insert_face_embeddings_by_image_id
from app.utils.face_quality import calculate_face_quality
from app.logging.setup_logging import get_logger

# Initialize logger
Expand Down Expand Up @@ -33,7 +34,7 @@ def detect_faces(self, image_id: str, image_path: str, forSearch: bool = False):
logger.debug(f"Face detection boxes: {boxes}")
logger.info(f"Detected {len(boxes)} faces in image {image_id}.")

processed_faces, embeddings, bboxes, confidences = [], [], [], []
processed_faces, embeddings, bboxes, confidences, qualities = [], [], [], [], []

for box, score in zip(boxes, scores):
if score > self.yolo_detector.conf_threshold:
Expand All @@ -49,17 +50,46 @@ def detect_faces(self, image_id: str, image_path: str, forSearch: bool = False):
max(0, y1 - padding) : min(img.shape[0], y2 + padding),
max(0, x1 - padding) : min(img.shape[1], x2 + padding),
]

# Calculate face quality
quality_result = calculate_face_quality(face_img)
quality_score = quality_result["quality"]
qualities.append(quality_score)

# Log quality metrics for debugging
logger.debug(
f"Face quality: {quality_score:.3f} "
f"(sharpness: {quality_result['sharpness']:.3f}, "
f"brightness: {quality_result['brightness']:.3f}, "
f"size: {quality_result['size']:.3f})"
)

# Process face for embedding generation
processed_face = FaceNet_util_preprocess_image(face_img)
processed_faces.append(processed_face)

embedding = self.facenet.get_embedding(processed_face)
embeddings.append(embedding)

if not forSearch and embeddings:
# Store faces with quality scores
db_insert_face_embeddings_by_image_id(
image_id, embeddings, confidence=confidences, bbox=bboxes
image_id,
embeddings,
confidence=confidences,
bbox=bboxes,
quality=qualities,
)

# Log quality statistics
if qualities:
avg_quality = sum(qualities) / len(qualities)
high_quality_count = sum(1 for q in qualities if q >= 0.7)
logger.info(
f"Face quality stats: avg={avg_quality:.3f}, "
f"high_quality={high_quality_count}/{len(qualities)}"
)

return {
"ids": f"{class_ids}",
"processed_faces": processed_faces,
Expand Down
Loading