Enhance Adaptive Face Clustering and Improve Face Collection Showcase#1394
Conversation
Naive 640x640 resize distorted faces and shrank small ones below detectability in large photos. Boxes are unpadded/unscaled back and clipped to image bounds.
Blur threshold 80 rejected clean studio portraits (Laplacian variance 40-75); face detection was skipped entirely for photos with 7+ people. Lower blur default to 20, min face area to 1000px2, drop the cap.
A semantic_labels table created by another schema version (no label_id) breaks the image_semantic_labels FK with 'foreign key mismatch' on any cascading delete, blocking folder deletion. Both tables are derived data, so drop and recreate them.
Faces co-occurring in one photo are different people, but degraded embeddings of tiny faces clustered together, making a single image appear multiple times in a cluster. Enforce the cannot-link constraint after clustering (centroid-closest face wins), guard incremental assignment the same way, and dedupe the cluster image listing.
Order the people view by avg detection confidence x log2(1+face_count) instead of random UUID order, so frequently photographed, clearly detected people appear first.
cluster_util_cluster_all_face_embeddings returned a bare list on an empty library, crashing the caller's tuple unpack.
Conflict in semantic_labels.py resolved in favor of upstream: the vocabulary migration (drops pre-vocabulary tables, retires image_semantic_labels) supersedes this branch's stale-schema guard.
WalkthroughClustering defaults and YOLO preprocessing were revised, face queries now preserve image identifiers, clusters receive confidence-based ranking, and full or incremental clustering prevents multiple faces from one image entering the same cluster. ChangesFace detection and clustering refinements
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant FaceClustering
participant Database
participant DBSCAN
participant ConstraintEnforcer
FaceClustering->>Database: load embeddings and image identifiers
FaceClustering->>DBSCAN: cluster validated embeddings
DBSCAN-->>FaceClustering: return cluster assignments
FaceClustering->>ConstraintEnforcer: enforce one face per image
ConstraintEnforcer-->>FaceClustering: return filtered assignments
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
backend/scripts/reset_database.py (1)
50-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep the error message within PEP 8 line length.
Line 51 is 84 characters including indentation and may trigger E501 under strict linting. Split the adjacent string literals:
print( - "Error: Database reset failed because some files could not be deleted.", + "Error: Database reset failed because some files " + "could not be deleted.", file=sys.stderr, )As per path instructions, Python files must be checked for PEP 8 violations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/scripts/reset_database.py` around lines 50 - 53, Update the error message in the database reset failure print statement to split the adjacent string literals so the source line stays within PEP 8’s maximum length, while preserving the existing message and stderr output.Source: Path instructions
backend/app/database/face_clusters.py (1)
343-347: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove the import statement outside the loop.
Importing
jsoninside the loop causes unnecessary overhead on every iteration. Please move it to the top of the file or outside the loop.♻️ Proposed fix
- seen_image_ids.add(image_id) - - import json - + seen_image_ids.add(image_id) + metadata_dict = json.loads(metadata) if metadata else None(Ensure
import jsonis added at the top of the file if not already present).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/app/database/face_clusters.py` around lines 343 - 347, Move the json import out of the loop containing the seen_image_ids check, placing it with the module-level imports at the top of the file; remove the inner-loop import while leaving the surrounding face-cluster processing unchanged.backend/app/database/faces.py (2)
222-228: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUpdate return type hints to include
ImageId.The return dictionaries for these functions were updated to include the
image_idfield, but the return type hints were not updated to reflect this addition. As per the path instructions, ensure proper use of type hints.
backend/app/database/faces.py#L222-L228: AddImageIdto theUnionfordb_get_faces_unassigned_clusters(e.g.,Union[FaceId, ImageId, FaceEmbedding]).backend/app/database/faces.py#L253-L255: AddImageIdto theUnionfordb_get_all_faces_with_cluster_names(e.g.,Union[FaceId, ImageId, FaceEmbedding, Optional[str]]).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/app/database/faces.py` around lines 222 - 228, Update the return type hints for db_get_faces_unassigned_clusters and db_get_all_faces_with_cluster_names to include ImageId in their Union types, preserving the existing FaceId, FaceEmbedding, and Optional[str] members as applicable. Apply this in backend/app/database/faces.py at lines 222-228 and 253-255.Source: Path instructions
360-362: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSpecify the types for the returned set.
As per the path instructions, ensure proper use of type hints. The
setreturn type can be made more specific to indicate its contents, such asSet[Tuple[str, str]](orset[tuple[str, str]]in Python 3.9+).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/app/database/faces.py` around lines 360 - 362, Update the return annotation of db_get_cluster_image_pairs to specify a set of two-string tuples, using the project’s supported typing syntax (Set[Tuple[str, str]] or set[tuple[str, str]]), while preserving the function’s existing behavior.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@backend/app/database/face_clusters.py`:
- Around line 343-347: Move the json import out of the loop containing the
seen_image_ids check, placing it with the module-level imports at the top of the
file; remove the inner-loop import while leaving the surrounding face-cluster
processing unchanged.
In `@backend/app/database/faces.py`:
- Around line 222-228: Update the return type hints for
db_get_faces_unassigned_clusters and db_get_all_faces_with_cluster_names to
include ImageId in their Union types, preserving the existing FaceId,
FaceEmbedding, and Optional[str] members as applicable. Apply this in
backend/app/database/faces.py at lines 222-228 and 253-255.
- Around line 360-362: Update the return annotation of
db_get_cluster_image_pairs to specify a set of two-string tuples, using the
project’s supported typing syntax (Set[Tuple[str, str]] or set[tuple[str,
str]]), while preserving the function’s existing behavior.
In `@backend/scripts/reset_database.py`:
- Around line 50-53: Update the error message in the database reset failure
print statement to split the adjacent string literals so the source line stays
within PEP 8’s maximum length, while preserving the existing message and stderr
output.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 77ec36f8-bd88-4c3f-a7f3-25cd0a163b3f
📒 Files selected for processing (7)
backend/app/config/settings.pybackend/app/database/face_clusters.pybackend/app/database/faces.pybackend/app/models/YOLO.pybackend/app/utils/face_clusters.pybackend/app/utils/images.pybackend/scripts/reset_database.py
Fixes #1271
Summary
An investigation into two long-standing face clustering symptoms — faces never being detected, and detected people missing many of their photos — traced them to specific, measurable defects in the detection pipeline and the clustering logic. This PR fixes the detection losses, adds a physical-consistency constraint to clustering, and improves how clusters are ordered in the People view.
Measured on a real 205-image library, stored faces went from 161 to 230 (+43%), and person-tagged images with zero stored faces dropped from 17.9% to 6.3% (the remainder are genuine no-face cases: masked/rear views and sub-threshold tiny faces).
Detection fixes
Clustering fixes
cluster_util_cluster_all_face_embeddingsreturning a bare list instead of the expected tuple when no faces exist, preventing a caller crash.People view ordering
Clusters were previously ordered by UUID, making the People view effectively random. They are now ranked by
avg_confidence × log2(1 + face_count), causing frequently photographed people (typically the device owner) to surface first while logarithmic damping prevents large low-quality clusters from outranking smaller, cleaner ones.Testing
main), including the existing clustering regression suite.Summary by CodeRabbit
New Features
Bug Fixes