2021-08-12 04:54:20 +02:00
package entity
import (
"crypto/sha1"
2021-08-12 12:05:10 +02:00
"encoding/base32"
2021-08-15 20:57:26 +02:00
"encoding/json"
2021-08-21 16:36:00 +02:00
"fmt"
2021-09-18 15:32:39 +02:00
"strings"
2021-08-16 00:29:36 +02:00
"sync"
2021-08-12 04:54:20 +02:00
"time"
2021-08-21 16:36:00 +02:00
"github.com/photoprism/photoprism/internal/face"
2021-09-22 19:33:41 +02:00
"github.com/photoprism/photoprism/pkg/rnd"
2021-08-12 04:54:20 +02:00
)
2021-08-16 00:29:36 +02:00
var faceMutex = sync . Mutex { }
// Face represents the face of a Subject.
2021-08-13 20:31:41 +02:00
type Face struct {
2022-09-28 09:01:17 +02:00
ID string ` gorm:"type:VARBINARY(64);primary_key;auto_increment:false;" json:"ID" yaml:"ID" `
2021-08-21 16:36:00 +02:00
FaceSrc string ` gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src,omitempty" `
2022-04-04 21:22:31 +02:00
FaceKind int ` json:"Kind" yaml:"Kind,omitempty" `
2021-09-18 20:41:30 +02:00
FaceHidden bool ` json:"Hidden" yaml:"Hidden,omitempty" `
2022-09-28 09:01:17 +02:00
SubjUID string ` gorm:"type:VARBINARY(64);index;default:'';" json:"SubjUID" yaml:"SubjUID,omitempty" `
2021-08-21 16:36:00 +02:00
Samples int ` json:"Samples" yaml:"Samples,omitempty" `
SampleRadius float64 ` json:"SampleRadius" yaml:"SampleRadius,omitempty" `
Collisions int ` json:"Collisions" yaml:"Collisions,omitempty" `
CollisionRadius float64 ` json:"CollisionRadius" yaml:"CollisionRadius,omitempty" `
EmbeddingJSON json . RawMessage ` gorm:"type:MEDIUMBLOB;" json:"-" yaml:"EmbeddingJSON,omitempty" `
2021-09-30 13:44:23 +02:00
embedding face . Embedding ` gorm:"-" `
2021-08-23 16:22:01 +02:00
MatchedAt * time . Time ` json:"MatchedAt" yaml:"MatchedAt,omitempty" `
2021-08-21 16:36:00 +02:00
CreatedAt time . Time ` json:"CreatedAt" yaml:"CreatedAt,omitempty" `
UpdatedAt time . Time ` json:"UpdatedAt" yaml:"UpdatedAt,omitempty" `
2021-08-12 04:54:20 +02:00
}
2021-08-29 13:26:05 +02:00
// Faceless can be used as argument to match unmatched face markers.
var Faceless = [ ] string { "" }
2022-09-28 09:01:17 +02:00
// TableName returns the entity table name.
2021-08-13 20:31:41 +02:00
func ( Face ) TableName ( ) string {
2021-09-21 12:11:51 +02:00
return "faces"
2021-08-12 04:54:20 +02:00
}
2021-08-14 15:45:51 +02:00
// NewFace returns a new face.
2021-09-30 13:44:23 +02:00
func NewFace ( subjUID , faceSrc string , embeddings face . Embeddings ) * Face {
2021-08-13 20:31:41 +02:00
result := & Face {
2021-09-17 14:26:12 +02:00
SubjUID : subjUID ,
FaceSrc : faceSrc ,
2021-08-15 20:57:26 +02:00
}
if err := result . SetEmbeddings ( embeddings ) ; err != nil {
log . Errorf ( "face: failed setting embeddings (%s)" , err )
2021-08-12 04:54:20 +02:00
}
return result
}
2022-04-13 01:59:32 +02:00
// MatchId returns a compound id for matching.
func ( m * Face ) MatchId ( f Face ) string {
if m . ID == "" || f . ID == "" {
return ""
}
if m . ID < f . ID {
return fmt . Sprintf ( "%s-%s" , m . ID , f . ID )
} else {
return fmt . Sprintf ( "%s-%s" , f . ID , m . ID )
}
}
2022-04-04 21:22:31 +02:00
// SkipMatching checks whether the face should be skipped when matching.
func ( m * Face ) SkipMatching ( ) bool {
return m . Embedding ( ) . SkipMatching ( )
2021-10-07 09:32:17 +02:00
}
2021-08-15 20:57:26 +02:00
// SetEmbeddings assigns face embeddings.
2021-09-30 13:44:23 +02:00
func ( m * Face ) SetEmbeddings ( embeddings face . Embeddings ) ( err error ) {
2022-04-04 21:22:31 +02:00
if len ( embeddings ) == 0 {
2022-09-28 09:01:17 +02:00
return fmt . Errorf ( "invalid embedding" )
2022-04-04 21:22:31 +02:00
}
2021-09-30 13:44:23 +02:00
m . embedding , m . SampleRadius , m . Samples = face . EmbeddingsMidpoint ( embeddings )
2021-09-06 03:24:11 +02:00
2022-04-04 21:22:31 +02:00
if len ( m . embedding ) != len ( face . NullEmbedding ) {
2022-09-28 09:01:17 +02:00
return fmt . Errorf ( "embedding has invalid number of values" )
2022-04-04 21:22:31 +02:00
}
2021-09-06 05:25:20 +02:00
// Limit sample radius to reduce false positives.
if m . SampleRadius > 0.35 {
m . SampleRadius = 0.35
2021-09-06 03:24:11 +02:00
}
2021-08-15 20:57:26 +02:00
m . EmbeddingJSON , err = json . Marshal ( m . embedding )
if err != nil {
return err
}
s := sha1 . Sum ( m . EmbeddingJSON )
2021-08-29 13:26:05 +02:00
2022-04-04 21:22:31 +02:00
// Update Face ID, Kind, and reset match timestamp,
m . ID = base32 . StdEncoding . EncodeToString ( s [ : ] )
m . FaceKind = int ( m . embedding . Kind ( ) )
2021-08-29 13:26:05 +02:00
m . MatchedAt = nil
2021-08-15 20:57:26 +02:00
return nil
}
2021-08-29 13:26:05 +02:00
// Matched updates the match timestamp.
func ( m * Face ) Matched ( ) error {
m . MatchedAt = TimePointer ( )
2022-04-04 08:54:03 +02:00
return UnscopedDb ( ) . Model ( m ) . UpdateColumns ( Values { "MatchedAt" : m . MatchedAt } ) . Error
2021-08-23 16:22:01 +02:00
}
2021-08-15 20:57:26 +02:00
// Embedding returns parsed face embedding.
2021-09-30 13:44:23 +02:00
func ( m * Face ) Embedding ( ) face . Embedding {
2021-08-15 20:57:26 +02:00
if len ( m . EmbeddingJSON ) == 0 {
2021-09-30 13:44:23 +02:00
return face . Embedding { }
2021-08-15 20:57:26 +02:00
} else if len ( m . embedding ) > 0 {
return m . embedding
} else if err := json . Unmarshal ( m . EmbeddingJSON , & m . embedding ) ; err != nil {
log . Errorf ( "failed parsing face embedding json: %s" , err )
}
return m . embedding
2021-08-12 04:54:20 +02:00
}
2021-08-21 16:36:00 +02:00
// Match tests if embeddings match this face.
2021-09-30 13:44:23 +02:00
func ( m * Face ) Match ( embeddings face . Embeddings ) ( match bool , dist float64 ) {
2021-08-21 16:36:00 +02:00
dist = - 1
2021-09-30 13:44:23 +02:00
if embeddings . Empty ( ) {
2021-08-21 16:36:00 +02:00
// Np embeddings, no match.
return false , dist
}
faceEmbedding := m . Embedding ( )
if len ( faceEmbedding ) == 0 {
// Should never happen.
return false , dist
}
2021-09-30 13:44:23 +02:00
// Calculate the smallest distance to embeddings.
2021-08-21 16:36:00 +02:00
for _ , e := range embeddings {
2022-04-03 17:25:37 +02:00
if d := e . Dist ( faceEmbedding ) ; d < dist || dist < 0 {
2021-08-21 16:36:00 +02:00
dist = d
}
}
// Any reasons embeddings do not match this face?
switch {
case dist < 0 :
// Should never happen.
return false , dist
2021-09-23 13:16:05 +02:00
case dist > ( m . SampleRadius + face . MatchDist ) :
2021-08-21 16:36:00 +02:00
// Too far.
return false , dist
2021-08-22 16:14:34 +02:00
case m . CollisionRadius > 0.1 && dist > m . CollisionRadius :
2021-08-21 16:36:00 +02:00
// Within radius of reported collisions.
return false , dist
}
// If not, at least one of the embeddings match!
return true , dist
}
2021-09-01 12:48:17 +02:00
// ResolveCollision resolves a collision with a different subject's face.
2021-09-30 13:44:23 +02:00
func ( m * Face ) ResolveCollision ( embeddings face . Embeddings ) ( resolved bool , err error ) {
2021-09-17 14:26:12 +02:00
if m . SubjUID == "" {
2021-08-21 16:36:00 +02:00
// Ignore reports for anonymous faces.
return false , nil
2021-08-26 16:00:11 +02:00
} else if m . ID == "" {
2021-08-21 16:36:00 +02:00
return false , fmt . Errorf ( "invalid face id" )
} else if len ( m . EmbeddingJSON ) == 0 {
2021-08-22 16:14:34 +02:00
return false , fmt . Errorf ( "embedding must not be empty" )
2021-08-21 16:36:00 +02:00
}
if match , dist := m . Match ( embeddings ) ; ! match {
// Embeddings don't match this face. Ignore.
return false , nil
} else if dist < 0 {
// Should never happen.
return false , fmt . Errorf ( "collision distance must be positive" )
2021-09-01 21:16:08 +02:00
} else if dist < 0.02 {
// Ignore if distance is very small as faces may belong to the same person.
2022-04-13 01:59:32 +02:00
log . Warnf ( "faces: clearing ambiguous subject %s from face %s, similar face at dist %f with source %s" , SubjNames . Log ( m . SubjUID ) , m . ID , dist , SrcString ( m . FaceSrc ) )
2021-09-01 21:16:08 +02:00
// Reset subject UID just in case.
2021-09-17 14:26:12 +02:00
m . SubjUID = ""
2021-09-01 21:16:08 +02:00
2022-04-13 01:59:32 +02:00
return true , m . Updates ( Values { "SubjUID" : m . SubjUID } )
2021-09-01 21:16:08 +02:00
} else {
2021-08-29 13:26:05 +02:00
m . MatchedAt = nil
2021-08-21 16:36:00 +02:00
m . Collisions ++
2021-09-01 12:48:17 +02:00
m . CollisionRadius = dist - 0.01
2021-08-21 16:36:00 +02:00
}
2021-08-29 13:26:05 +02:00
err = m . Updates ( Values { "Collisions" : m . Collisions , "CollisionRadius" : m . CollisionRadius , "MatchedAt" : m . MatchedAt } )
2021-08-22 21:06:44 +02:00
2021-09-01 21:16:08 +02:00
if err != nil {
return true , err
}
2021-08-23 16:22:01 +02:00
2021-09-01 21:16:08 +02:00
if revised , err := m . ReviseMatches ( ) ; err != nil {
return true , err
} else if r := len ( revised ) ; r > 0 {
2022-04-04 21:22:31 +02:00
log . Infof ( "faces: resolved %d conflicts" , r )
2021-08-22 21:06:44 +02:00
}
2021-09-01 21:16:08 +02:00
return true , nil
2021-08-22 21:06:44 +02:00
}
// ReviseMatches updates marker matches after face parameters have been changed.
func ( m * Face ) ReviseMatches ( ) ( revised Markers , err error ) {
2021-09-01 12:48:17 +02:00
if m . ID == "" {
return revised , fmt . Errorf ( "empty face id" )
}
2021-08-22 21:06:44 +02:00
var matches Markers
if err := Db ( ) . Where ( "face_id = ?" , m . ID ) . Where ( "marker_type = ?" , MarkerFace ) .
Find ( & matches ) . Error ; err != nil {
2022-04-04 21:22:31 +02:00
log . Debugf ( "faces: found no matching markers for conflict resolution (%s)" , err )
2021-08-22 21:06:44 +02:00
return revised , err
} else {
for _ , marker := range matches {
if ok , _ := m . Match ( marker . Embeddings ( ) ) ; ! ok {
if updated , err := marker . ClearFace ( ) ; err != nil {
2022-04-04 21:22:31 +02:00
log . Debugf ( "faces: failed to remove match with marker (%s)" , err ) // Conflict resolution
2021-08-22 21:06:44 +02:00
return revised , err
} else if updated {
revised = append ( revised , marker )
}
}
}
}
return revised , nil
2021-08-21 16:36:00 +02:00
}
2021-08-23 16:22:01 +02:00
// MatchMarkers finds and references matching markers.
2021-08-29 13:26:05 +02:00
func ( m * Face ) MatchMarkers ( faceIds [ ] string ) error {
2021-08-23 16:22:01 +02:00
var markers Markers
err := Db ( ) .
2021-08-29 13:26:05 +02:00
Where ( "marker_invalid = 0 AND marker_type = ? AND face_id IN (?)" , MarkerFace , faceIds ) .
2021-08-23 16:22:01 +02:00
Find ( & markers ) . Error
if err != nil {
2022-04-04 21:22:31 +02:00
log . Debugf ( "faces: failed fetching markers matching face id %s (%s)" , strings . Join ( faceIds , ", " ) , err )
2021-08-23 16:22:01 +02:00
return err
}
for _ , marker := range markers {
2021-08-29 13:26:05 +02:00
if ok , dist := m . Match ( marker . Embeddings ( ) ) ; ! ok {
2021-08-23 16:22:01 +02:00
// Ignore.
2021-08-29 13:26:05 +02:00
} else if _ , err = marker . SetFace ( m , dist ) ; err != nil {
2021-08-23 16:22:01 +02:00
return err
}
}
return nil
}
2021-09-01 12:48:17 +02:00
// SetSubjectUID updates the face's subject uid and related markers.
2021-09-17 14:26:12 +02:00
func ( m * Face ) SetSubjectUID ( subjUID string ) ( err error ) {
2021-09-01 12:48:17 +02:00
// Update face.
2021-09-17 14:26:12 +02:00
if err = m . Update ( "SubjUID" , subjUID ) ; err != nil {
2021-09-01 12:48:17 +02:00
return err
} else {
2021-09-17 14:26:12 +02:00
m . SubjUID = subjUID
2021-09-01 12:48:17 +02:00
}
// Update related markers.
if err = Db ( ) . Model ( & Marker { } ) .
Where ( "face_id = ?" , m . ID ) .
2021-09-17 14:26:12 +02:00
Where ( "subj_src = ?" , SrcAuto ) .
Where ( "subj_uid <> ?" , m . SubjUID ) .
2021-09-18 15:32:39 +02:00
Where ( "marker_invalid = 0" ) .
2022-04-04 08:54:03 +02:00
UpdateColumns ( Values { "subj_uid" : m . SubjUID , "marker_review" : false } ) . Error ; err != nil {
2021-09-01 12:48:17 +02:00
return err
}
2021-09-06 00:52:10 +02:00
return m . RefreshPhotos ( )
}
// RefreshPhotos flags related photos for metadata maintenance.
2021-09-06 01:16:36 +02:00
func ( m * Face ) RefreshPhotos ( ) error {
2021-09-06 00:52:10 +02:00
if m . ID == "" {
return fmt . Errorf ( "empty face id" )
}
2022-01-03 11:12:08 +01:00
var err error
switch DbDialect ( ) {
case MySQL :
update := fmt . Sprintf ( ` UPDATE photos p JOIN files f ON f . photo_id = p . id JOIN % s m ON m . file_uid = f . file_uid
SET p . checked_at = NULL WHERE m . face_id = ? ` , Marker { } . TableName ( ) )
err = UnscopedDb ( ) . Exec ( update , m . ID ) . Error
default :
update := fmt . Sprintf ( ` UPDATE photos SET checked_at = NULL WHERE id IN ( SELECT f . photo_id FROM files f
JOIN % s m ON m . file_uid = f . file_uid WHERE m . face_id = ? ) ` , Marker { } . TableName ( ) )
err = UnscopedDb ( ) . Exec ( update , m . ID ) . Error
}
2021-09-24 22:46:03 +02:00
2022-01-03 11:12:08 +01:00
return err
2021-09-01 12:48:17 +02:00
}
2021-09-18 20:41:30 +02:00
// Hide hides the face by default.
func ( m * Face ) Hide ( ) ( err error ) {
return m . Update ( "FaceHidden" , true )
}
// Show shows the face by default.
func ( m * Face ) Show ( ) ( err error ) {
return m . Update ( "FaceHidden" , false )
}
2021-08-12 04:54:20 +02:00
// Create inserts the face to the database.
2021-08-13 20:31:41 +02:00
func ( m * Face ) Create ( ) error {
2022-04-04 21:22:31 +02:00
if m . ID == "" {
return fmt . Errorf ( "empty id" )
}
2021-08-16 00:29:36 +02:00
faceMutex . Lock ( )
defer faceMutex . Unlock ( )
2021-08-12 04:54:20 +02:00
return Db ( ) . Create ( m ) . Error
}
// Delete removes the face from the database.
2021-08-13 20:31:41 +02:00
func ( m * Face ) Delete ( ) error {
2022-04-04 21:22:31 +02:00
if m . ID == "" {
return fmt . Errorf ( "empty id" )
}
2021-09-29 20:09:34 +02:00
// Remove face id from markers before deleting.
if err := Db ( ) . Model ( & Marker { } ) .
Where ( "face_id = ?" , m . ID ) .
2022-04-04 08:54:03 +02:00
UpdateColumns ( Values { "face_id" : "" , "face_dist" : - 1 } ) . Error ; err != nil {
2021-09-29 20:09:34 +02:00
return err
}
2021-08-12 04:54:20 +02:00
return Db ( ) . Delete ( m ) . Error
}
// Update a face property in the database.
2021-08-13 20:31:41 +02:00
func ( m * Face ) Update ( attr string , value interface { } ) error {
2022-04-04 21:22:31 +02:00
if m . ID == "" {
return fmt . Errorf ( "empty id" )
}
2021-08-12 12:05:10 +02:00
return UnscopedDb ( ) . Model ( m ) . Update ( attr , value ) . Error
}
// Updates face properties in the database.
2021-08-13 20:31:41 +02:00
func ( m * Face ) Updates ( values interface { } ) error {
2022-04-04 21:22:31 +02:00
if m . ID == "" {
return fmt . Errorf ( "empty id" )
}
2021-08-12 12:05:10 +02:00
return UnscopedDb ( ) . Model ( m ) . Updates ( values ) . Error
2021-08-12 04:54:20 +02:00
}
2021-08-19 23:12:51 +02:00
// FirstOrCreateFace returns the existing entity, inserts a new entity or nil in case of errors.
func FirstOrCreateFace ( m * Face ) * Face {
2022-04-04 21:22:31 +02:00
if m == nil {
return nil
}
if m . ID == "" {
return nil
}
2021-08-19 23:12:51 +02:00
result := Face { }
2022-04-04 21:22:31 +02:00
// Search existing face with the same ID. Report if found and it belongs to another person.
if findErr := UnscopedDb ( ) . Where ( "id = ?" , m . ID ) . First ( & result ) . Error ; findErr == nil && result . ID != "" {
if m . SubjUID != result . SubjUID {
log . Warnf ( "faces: %s has ambiguous subjects %s and %s" , m . ID , SubjNames . Log ( m . SubjUID ) , SubjNames . Log ( result . SubjUID ) )
}
2021-08-19 23:12:51 +02:00
return & result
2022-04-04 21:22:31 +02:00
} else if err := m . Create ( ) ; err == nil {
2021-08-19 23:12:51 +02:00
return m
2022-04-04 21:22:31 +02:00
} else if findErr = UnscopedDb ( ) . Where ( "id = ?" , m . ID ) . First ( & result ) . Error ; findErr == nil && result . ID != "" {
if m . SubjUID != result . SubjUID {
log . Warnf ( "faces: %s has ambiguous subjects %s and %s" , m . ID , SubjNames . Log ( m . SubjUID ) , SubjNames . Log ( result . SubjUID ) )
}
2021-08-19 23:12:51 +02:00
return & result
} else {
2022-04-04 21:22:31 +02:00
log . Errorf ( "faces: failed adding %s (%s)" , m . ID , err )
2021-08-19 23:12:51 +02:00
}
return nil
}
// FindFace returns an existing entity if exists.
func FindFace ( id string ) * Face {
if id == "" {
return nil
}
2021-09-18 15:32:39 +02:00
f := Face { }
2021-08-19 23:12:51 +02:00
2021-09-18 15:32:39 +02:00
if err := Db ( ) . Where ( "id = ?" , strings . ToUpper ( id ) ) . First ( & f ) . Error ; err != nil {
2021-08-19 23:12:51 +02:00
return nil
}
2021-09-18 15:32:39 +02:00
return & f
2021-08-19 23:12:51 +02:00
}
2021-09-22 19:33:41 +02:00
2021-09-23 23:46:17 +02:00
// ValidFaceCount counts the number of valid face markers for a file uid.
func ValidFaceCount ( fileUID string ) ( c int ) {
2022-09-30 00:42:19 +02:00
if ! rnd . IsUID ( fileUID , FileUID ) {
2021-09-22 19:33:41 +02:00
return
}
if err := Db ( ) . Model ( Marker { } ) .
Where ( "file_uid = ? AND marker_type = ?" , fileUID , MarkerFace ) .
Where ( "marker_invalid = 0" ) .
Count ( & c ) . Error ; err != nil {
log . Errorf ( "file: %s (count faces)" , err )
return 0
} else {
return c
}
}