package query import ( "fmt" "github.com/photoprism/photoprism/internal/face" "github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/internal/entity" ) // Faces returns all (known / unmatched) faces from the index. func Faces(knownOnly, unmatched bool) (result entity.Faces, err error) { stmt := Db() if unmatched { stmt = stmt.Where("matched_at IS NULL") } if knownOnly { stmt = stmt.Where("subj_uid <> ''") } err = stmt.Order("subj_uid, samples DESC").Find(&result).Error return result, err } // ManuallyAddedFaces returns all manually added face clusters. func ManuallyAddedFaces() (result entity.Faces, err error) { err = Db(). Where("face_src = ?", entity.SrcManual). Where("subj_uid <> ''").Order("subj_uid, samples DESC"). Find(&result).Error return result, err } // MatchFaceMarkers matches markers with known faces. func MatchFaceMarkers() (affected int64, err error) { faces, err := Faces(true, false) if err != nil { return affected, err } for _, f := range faces { if res := Db().Model(&entity.Marker{}). Where("face_id = ?", f.ID). Where("subj_src = ?", entity.SrcAuto). Where("subj_uid <> ?", f.SubjUID). Updates(entity.Values{"SubjUID": f.SubjUID, "MarkerReview": false}); res.Error != nil { return affected, err } else if res.RowsAffected > 0 { affected += res.RowsAffected } } return affected, nil } // RemoveAnonymousFaceClusters removes anonymous faces from the index. func RemoveAnonymousFaceClusters() (removed int64, err error) { res := UnscopedDb().Delete( entity.Face{}, "face_src = ? AND subj_uid = ''", entity.SrcAuto) return res.RowsAffected, res.Error } // RemoveAutoFaceClusters removes automatically added face clusters from the index. func RemoveAutoFaceClusters() (removed int64, err error) { res := UnscopedDb(). Delete(entity.Face{}, "face_src = ?", entity.SrcAuto) return res.RowsAffected, res.Error } // CountNewFaceMarkers counts the number of new face markers in the index. func CountNewFaceMarkers(size, score int) (n int) { var f entity.Face if err := Db().Where("face_src = ?", entity.SrcAuto). Order("created_at DESC").Limit(1).Take(&f).Error; err != nil { log.Debugf("faces: no existing clusters") } q := Db().Model(&entity.Markers{}). Where("marker_type = ?", entity.MarkerFace). Where("face_id = '' AND marker_invalid = 0 AND embeddings_json <> ''") if size > 0 { q = q.Where("size >= ?", size) } if score > 0 { q = q.Where("score >= ?", score) } if !f.CreatedAt.IsZero() { q = q.Where("created_at > ?", f.CreatedAt) } if err := q.Count(&n).Error; err != nil { log.Errorf("faces: %s (count new markers)", err) } return n } // PurgeOrphanFaces removes unused faces from the index. func PurgeOrphanFaces(faceIds []string) (removed int64, err error) { // Remove invalid face IDs. if res := Db(). Where("id IN (?)", faceIds). Where(fmt.Sprintf("id NOT IN (SELECT face_id FROM %s)", entity.Marker{}.TableName())). Delete(&entity.Face{}); res.Error != nil { return removed, fmt.Errorf("faces: %s while purging orphans", res.Error) } else { removed += res.RowsAffected } return removed, nil } // MergeFaces returns a new face that replaces multiple others. func MergeFaces(merge entity.Faces) (merged *entity.Face, err error) { if len(merge) < 2 { // Nothing to merge. return merged, fmt.Errorf("faces: two or more clusters required for merging") } subjUID := merge[0].SubjUID for i := 1; i < len(merge); i++ { if merge[i].SubjUID != subjUID { return merged, fmt.Errorf("faces: can't merge clusters with conflicting subjects %s <> %s", txt.Quote(subjUID), txt.Quote(merge[i].SubjUID)) } } // Find or create merged face cluster. if merged = entity.NewFace(merge[0].SubjUID, merge[0].FaceSrc, merge.Embeddings()); merged == nil { return merged, fmt.Errorf("faces: new cluster is nil for subject %s", txt.Quote(subjUID)) } else if merged = entity.FirstOrCreateFace(merged); merged == nil { return merged, fmt.Errorf("faces: failed creating new cluster for subject %s", txt.Quote(subjUID)) } else if err := merged.MatchMarkers(append(merge.IDs(), "")); err != nil { return merged, err } // PurgeOrphanFaces removes unused faces from the index. if removed, err := PurgeOrphanFaces(merge.IDs()); err != nil { return merged, err } else if removed > 0 { log.Debugf("faces: removed %d orphans for subject %s", removed, txt.Quote(subjUID)) } else { log.Warnf("faces: failed removing merged clusters for subject %s", txt.Quote(subjUID)) } return merged, err } // ResolveFaceCollisions resolves collisions of different subject's faces. func ResolveFaceCollisions() (conflicts, resolved int, err error) { faces, err := Faces(true, false) if err != nil { return conflicts, resolved, err } for _, f1 := range faces { for _, f2 := range faces { if matched, dist := f1.Match(entity.Embeddings{f2.Embedding()}); matched { if f1.SubjUID == f2.SubjUID { continue } conflicts++ r := f1.SampleRadius + face.ClusterRadius log.Infof("face %s: conflict at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius) if f1.SubjUID != "" { log.Debugf("face %s: subject %s (%s %s)", f1.ID, txt.Quote(f1.SubjUID), f1.SubjUID, entity.SrcString(f1.FaceSrc)) } else { log.Debugf("face %s: no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc)) } if f2.SubjUID != "" { log.Debugf("face %s: subject %s (%s %s)", f2.ID, txt.Quote(f2.SubjUID), f2.SubjUID, entity.SrcString(f2.FaceSrc)) } else { log.Debugf("face %s: no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc)) } if ok, err := f1.ResolveCollision(entity.Embeddings{f2.Embedding()}); err != nil { log.Errorf("face %s: %s", f1.ID, err) } else if ok { log.Infof("face %s: collision has been resolved", f1.ID) resolved++ } else { log.Debugf("face %s: collision could not be resolved", f1.ID) } } } } return conflicts, resolved, nil }