fix: face clusters write photo_tags, raise detection threshold to 0.7
- recluster_faces now writes photo_tags rows for each face cluster so the tag count and tag_ids filter work (previously count was always 0) - Old cluster tags and photo_tags are cleaned up before re-clustering - Raise face detection threshold from 0.4 to 0.7 to reduce false positives (was detecting dog faces as people) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,7 @@ class FacesSettings(BaseModel):
|
|||||||
"""YuNet + SFace face detection/recognition settings"""
|
"""YuNet + SFace face detection/recognition settings"""
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
min_face_size: int = 40
|
min_face_size: int = 40
|
||||||
recognition_threshold: float = 0.4
|
recognition_threshold: float = 0.7
|
||||||
cluster_eps: float = 0.35
|
cluster_eps: float = 0.35
|
||||||
|
|
||||||
class VisionSettings(BaseModel):
|
class VisionSettings(BaseModel):
|
||||||
|
|||||||
@@ -294,9 +294,11 @@ def recluster_faces():
|
|||||||
return {'status': 'skipped', 'reason': 'faces disabled'}
|
return {'status': 'skipped', 'reason': 'faces disabled'}
|
||||||
|
|
||||||
from app.models.face_embedding import FaceEmbedding
|
from app.models.face_embedding import FaceEmbedding
|
||||||
from app.models.tags import Tag
|
from app.models.tags import Tag, photo_tags
|
||||||
from app.services.vision.clustering import cluster_faces
|
from app.services.vision.clustering import cluster_faces
|
||||||
|
|
||||||
|
source_name = "vision:sface"
|
||||||
|
|
||||||
session = _get_sync_session()
|
session = _get_sync_session()
|
||||||
try:
|
try:
|
||||||
face_rows = session.execute(
|
face_rows = session.execute(
|
||||||
@@ -310,37 +312,61 @@ def recluster_faces():
|
|||||||
embeddings = np.array([f.vector for f in face_rows], dtype=np.float32)
|
embeddings = np.array([f.vector for f in face_rows], dtype=np.float32)
|
||||||
labels = cluster_faces(embeddings, eps=settings.vision.faces.cluster_eps)
|
labels = cluster_faces(embeddings, eps=settings.vision.faces.cluster_eps)
|
||||||
|
|
||||||
|
# Clean up old face_cluster tags and their photo_tags
|
||||||
|
old_cluster_tags = session.execute(
|
||||||
|
select(Tag).where(Tag.kind == 'face_cluster', Tag.source == source_name)
|
||||||
|
).scalars().all()
|
||||||
|
for old_tag in old_cluster_tags:
|
||||||
|
session.execute(
|
||||||
|
delete(photo_tags).where(
|
||||||
|
photo_tags.c.tag_id == old_tag.id,
|
||||||
|
photo_tags.c.source == source_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session.delete(old_tag)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
# Build new clusters
|
||||||
cluster_tag_map: dict[int, str] = {}
|
cluster_tag_map: dict[int, str] = {}
|
||||||
source_name = "vision:sface"
|
# Track which photos belong to which cluster
|
||||||
|
cluster_photos: dict[int, set[str]] = {}
|
||||||
|
|
||||||
for i, label in enumerate(labels):
|
for i, label in enumerate(labels):
|
||||||
if label == -1:
|
if label == -1:
|
||||||
face_rows[i].cluster_id = None
|
face_rows[i].cluster_id = None
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if label not in cluster_photos:
|
||||||
|
cluster_photos[label] = set()
|
||||||
|
cluster_photos[label].add(face_rows[i].photo_id)
|
||||||
|
|
||||||
if label not in cluster_tag_map:
|
if label not in cluster_tag_map:
|
||||||
cluster_name = f"Person {label + 1}"
|
cluster_name = f"Person {label + 1}"
|
||||||
tag = session.execute(
|
tag = Tag(
|
||||||
select(Tag).where(
|
name=cluster_name,
|
||||||
Tag.kind == 'face_cluster',
|
kind='face_cluster',
|
||||||
Tag.source == source_name,
|
source=source_name,
|
||||||
Tag.name == cluster_name,
|
representative_photo_id=face_rows[i].photo_id,
|
||||||
)
|
)
|
||||||
).scalar_one_or_none()
|
session.add(tag)
|
||||||
|
session.flush()
|
||||||
if not tag:
|
|
||||||
tag = Tag(
|
|
||||||
name=cluster_name,
|
|
||||||
kind='face_cluster',
|
|
||||||
source=source_name,
|
|
||||||
representative_photo_id=face_rows[i].photo_id,
|
|
||||||
)
|
|
||||||
session.add(tag)
|
|
||||||
session.flush()
|
|
||||||
cluster_tag_map[label] = tag.id
|
cluster_tag_map[label] = tag.id
|
||||||
|
|
||||||
face_rows[i].cluster_id = cluster_tag_map[label]
|
face_rows[i].cluster_id = cluster_tag_map[label]
|
||||||
|
|
||||||
|
# Write photo_tags associations so the tag count and tag_ids
|
||||||
|
# filter work for face clusters
|
||||||
|
for label, photo_ids in cluster_photos.items():
|
||||||
|
tag_id = cluster_tag_map[label]
|
||||||
|
for pid in photo_ids:
|
||||||
|
session.execute(
|
||||||
|
photo_tags.insert().values(
|
||||||
|
photo_id=pid,
|
||||||
|
tag_id=tag_id,
|
||||||
|
source=source_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|||||||
@@ -44,6 +44,6 @@ vision:
|
|||||||
faces:
|
faces:
|
||||||
enabled: true
|
enabled: true
|
||||||
min_face_size: 40
|
min_face_size: 40
|
||||||
recognition_threshold: 0.4
|
recognition_threshold: 0.7
|
||||||
cluster_eps: 0.35
|
cluster_eps: 0.35
|
||||||
worker_concurrency: 2
|
worker_concurrency: 2
|
||||||
|
|||||||
Reference in New Issue
Block a user