2021-05-25 11:38:04 +02:00
package entity
import (
2021-08-15 20:57:26 +02:00
"encoding/json"
2021-05-26 14:41:59 +02:00
"fmt"
2021-09-06 05:13:53 +02:00
"math"
2021-10-05 18:42:39 +02:00
"strings"
2021-05-25 11:38:04 +02:00
"time"
2021-05-31 15:40:52 +02:00
2021-10-05 18:42:39 +02:00
"github.com/dustin/go-humanize/english"
2021-09-01 20:46:15 +02:00
"github.com/jinzhu/gorm"
2021-09-05 11:05:33 +02:00
"github.com/photoprism/photoprism/internal/crop"
2021-08-24 14:27:34 +02:00
"github.com/photoprism/photoprism/internal/face"
2021-06-02 17:25:04 +02:00
"github.com/photoprism/photoprism/internal/form"
2022-04-15 09:42:07 +02:00
"github.com/photoprism/photoprism/pkg/clean"
2021-09-18 20:41:30 +02:00
"github.com/photoprism/photoprism/pkg/rnd"
2021-05-25 11:38:04 +02:00
)
const (
MarkerUnknown = ""
2021-09-30 13:44:23 +02:00
MarkerFace = "face" // MarkerType for faces (implemented).
MarkerLabel = "label" // MarkerType for labels (todo).
2021-05-25 11:38:04 +02:00
)
// Marker represents an image marker point.
type Marker struct {
2022-10-02 11:38:30 +02:00
MarkerUID string ` gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID" `
FileUID string ` gorm:"type:VARBINARY(42);index;default:'';" json:"FileUID" yaml:"FileUID" `
2021-08-22 16:14:34 +02:00
MarkerType string ` gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type" `
2021-08-16 01:45:36 +02:00
MarkerSrc string ` gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty" `
2021-09-23 23:46:17 +02:00
MarkerName string ` gorm:"type:VARCHAR(160);" json:"Name" yaml:"Name,omitempty" `
2021-09-17 14:26:12 +02:00
MarkerReview bool ` json:"Review" yaml:"Review,omitempty" `
2021-09-18 20:41:30 +02:00
MarkerInvalid bool ` json:"Invalid" yaml:"Invalid,omitempty" `
2022-10-02 11:38:30 +02:00
SubjUID string ` gorm:"type:VARBINARY(42);index:idx_markers_subj_uid_src;" json:"SubjUID" yaml:"SubjUID,omitempty" `
2021-09-17 14:26:12 +02:00
SubjSrc string ` gorm:"type:VARBINARY(8);index:idx_markers_subj_uid_src;default:'';" json:"SubjSrc" yaml:"SubjSrc,omitempty" `
subject * Subject ` gorm:"foreignkey:SubjUID;association_foreignkey:SubjUID;association_autoupdate:false;association_autocreate:false;association_save_reference:false" `
2022-09-28 09:01:17 +02:00
FaceID string ` gorm:"type:VARBINARY(64);index;" json:"FaceID" yaml:"FaceID,omitempty" `
2021-09-21 12:11:51 +02:00
FaceDist float64 ` gorm:"default:-1;" json:"FaceDist" yaml:"FaceDist,omitempty" `
2021-09-01 20:46:15 +02:00
face * Face ` gorm:"foreignkey:FaceID;association_foreignkey:ID;association_autoupdate:false;association_autocreate:false;association_save_reference:false" `
2021-08-16 01:45:36 +02:00
EmbeddingsJSON json . RawMessage ` gorm:"type:MEDIUMBLOB;" json:"-" yaml:"EmbeddingsJSON,omitempty" `
2021-09-30 13:44:23 +02:00
embeddings face . Embeddings ` gorm:"-" `
2021-08-16 01:45:36 +02:00
LandmarksJSON json . RawMessage ` gorm:"type:MEDIUMBLOB;" json:"-" yaml:"LandmarksJSON,omitempty" `
X float32 ` gorm:"type:FLOAT;" json:"X" yaml:"X,omitempty" `
Y float32 ` gorm:"type:FLOAT;" json:"Y" yaml:"Y,omitempty" `
W float32 ` gorm:"type:FLOAT;" json:"W" yaml:"W,omitempty" `
H float32 ` gorm:"type:FLOAT;" json:"H" yaml:"H,omitempty" `
2021-09-17 18:51:24 +02:00
Q int ` json:"Q" yaml:"Q,omitempty" `
2021-09-21 12:11:51 +02:00
Size int ` gorm:"default:-1;" json:"Size" yaml:"Size,omitempty" `
Score int ` gorm:"type:SMALLINT;" json:"Score" yaml:"Score,omitempty" `
Thumb string ` gorm:"type:VARBINARY(128);index;default:'';" json:"Thumb" yaml:"Thumb,omitempty" `
2021-08-24 20:15:36 +02:00
MatchedAt * time . Time ` sql:"index" json:"MatchedAt" yaml:"MatchedAt,omitempty" `
2021-08-15 20:57:26 +02:00
CreatedAt time . Time
UpdatedAt time . Time
2021-05-25 11:38:04 +02:00
}
2022-09-28 09:01:17 +02:00
// TableName returns the entity table name.
2021-05-25 11:38:04 +02:00
func ( Marker ) TableName ( ) string {
2021-09-21 12:11:51 +02:00
return "markers"
2021-09-01 20:46:15 +02:00
}
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func ( m * Marker ) BeforeCreate ( scope * gorm . Scope ) error {
2022-09-28 09:01:17 +02:00
if rnd . IsUnique ( m . MarkerUID , 'm' ) {
2021-09-01 20:46:15 +02:00
return nil
}
2022-04-15 09:42:07 +02:00
return scope . SetColumn ( "MarkerUID" , rnd . GenerateUID ( 'm' ) )
2021-05-25 11:38:04 +02:00
}
// NewMarker creates a new entity.
2021-09-20 16:17:10 +02:00
func NewMarker ( file File , area crop . Area , subjUID , markerSrc , markerType string , size , score int ) * Marker {
2021-09-22 19:33:41 +02:00
if file . FileHash == "" {
2022-04-06 17:46:41 +02:00
log . Errorf ( "markers: file hash is empty - possible bug" )
2021-09-22 19:33:41 +02:00
return nil
}
2021-05-26 14:41:59 +02:00
m := & Marker {
2021-09-20 16:17:10 +02:00
FileUID : file . FileUID ,
MarkerSrc : markerSrc ,
MarkerType : markerType ,
MarkerReview : score < 30 ,
MarkerInvalid : false ,
SubjUID : subjUID ,
FaceDist : - 1 ,
X : area . X ,
Y : area . Y ,
W : area . W ,
H : area . H ,
Q : int ( float32 ( math . Log ( float64 ( score ) ) ) * float32 ( size ) * area . W ) ,
Size : size ,
Score : score ,
Thumb : area . Thumb ( file . FileHash ) ,
MatchedAt : nil ,
2021-05-25 11:38:04 +02:00
}
2021-05-26 14:41:59 +02:00
return m
}
// NewFaceMarker creates a new entity.
2022-10-02 11:38:30 +02:00
func NewFaceMarker ( f face . Face , file File , subjUid string ) * Marker {
m := NewMarker ( file , f . CropArea ( ) , subjUid , SrcImage , MarkerFace , f . Size ( ) , f . Score )
2021-05-26 14:41:59 +02:00
2021-09-22 19:33:41 +02:00
// Failed creating new marker?
if m == nil {
return nil
}
2021-09-30 13:44:23 +02:00
m . SetEmbeddings ( f . Embeddings )
2021-09-03 16:14:09 +02:00
m . LandmarksJSON = f . RelativeLandmarksJSON ( )
2021-05-26 14:41:59 +02:00
return m
2021-05-25 11:38:04 +02:00
}
2021-09-30 13:44:23 +02:00
// SetEmbeddings assigns new face emebddings to the marker.
func ( m * Marker ) SetEmbeddings ( e face . Embeddings ) {
m . embeddings = e
m . EmbeddingsJSON = e . JSON ( )
}
2021-10-05 18:42:39 +02:00
// UpdateFile sets the file uid and thumb and updates the index if the marker already exists.
func ( m * Marker ) UpdateFile ( file * File ) ( updated bool ) {
if file . FileUID != "" && m . FileUID != file . FileUID {
m . FileUID = file . FileUID
updated = true
}
if file . FileHash != "" && ! strings . HasPrefix ( m . Thumb , file . FileHash ) {
m . Thumb = crop . NewArea ( "crop" , m . X , m . Y , m . W , m . H ) . Thumb ( file . FileHash )
updated = true
}
if ! updated || m . MarkerUID == "" {
return false
2022-04-13 01:59:32 +02:00
} else if err := UnscopedDb ( ) . Model ( m ) . UpdateColumns ( Values { "file_uid" : m . FileUID , "thumb" : m . Thumb } ) . Error ; err != nil {
log . Errorf ( "faces: failed assigning marker %s to file %s (%s)" , m . MarkerUID , m . FileUID , err )
2021-10-05 18:42:39 +02:00
return false
} else {
return true
}
}
2021-05-25 11:38:04 +02:00
// Updates multiple columns in the database.
func ( m * Marker ) Updates ( values interface { } ) error {
2021-08-12 12:05:10 +02:00
return UnscopedDb ( ) . Model ( m ) . Updates ( values ) . Error
2021-05-25 11:38:04 +02:00
}
// Update updates a column in the database.
func ( m * Marker ) Update ( attr string , value interface { } ) error {
2021-08-12 12:05:10 +02:00
return UnscopedDb ( ) . Model ( m ) . Update ( attr , value ) . Error
2021-05-25 11:38:04 +02:00
}
2021-09-29 20:09:34 +02:00
// SetName changes the marker name.
func ( m * Marker ) SetName ( name , src string ) ( changed bool , err error ) {
if src == SrcAuto || SrcPriority [ src ] < SrcPriority [ m . SubjSrc ] {
return false , nil
}
2022-04-15 09:42:07 +02:00
name = clean . Name ( name )
2021-09-29 20:09:34 +02:00
if name == "" {
return false , nil
}
if m . MarkerName == name {
// Name didn't change.
return false , nil
}
m . SubjSrc = src
m . MarkerName = name
return true , m . SyncSubject ( true )
}
2021-06-02 17:25:04 +02:00
// SaveForm updates the entity using form data and stores it in the database.
2021-09-17 18:51:24 +02:00
func ( m * Marker ) SaveForm ( f form . Marker ) ( changed bool , err error ) {
2021-08-24 14:27:34 +02:00
if m . MarkerInvalid != f . MarkerInvalid {
m . MarkerInvalid = f . MarkerInvalid
changed = true
2021-06-02 17:25:04 +02:00
}
2021-09-17 14:26:12 +02:00
if m . MarkerReview != f . MarkerReview {
m . MarkerReview = f . MarkerReview
2021-08-24 14:27:34 +02:00
changed = true
2021-06-02 17:25:04 +02:00
}
2021-09-29 20:09:34 +02:00
if nameChanged , err := m . SetName ( f . MarkerName , f . SubjSrc ) ; err != nil {
return changed , err
} else if nameChanged {
2021-08-24 14:27:34 +02:00
changed = true
}
if changed {
2021-09-17 18:51:24 +02:00
return changed , m . Save ( )
2021-06-02 17:25:04 +02:00
}
2021-09-17 18:51:24 +02:00
return changed , nil
2021-08-19 21:12:38 +02:00
}
2021-08-14 21:37:57 +02:00
2021-08-29 13:26:05 +02:00
// HasFace tests if the marker already has the best matching face.
func ( m * Marker ) HasFace ( f * Face , dist float64 ) bool {
if m . FaceID == "" {
return false
} else if f == nil {
return m . FaceID != ""
} else if m . FaceID == f . ID {
return m . FaceID != ""
} else if m . FaceDist < 0 {
return false
} else if dist < 0 {
return true
}
return m . FaceDist <= dist
}
2021-08-19 21:12:38 +02:00
// SetFace sets a new face for this marker.
2021-08-29 13:26:05 +02:00
func ( m * Marker ) SetFace ( f * Face , dist float64 ) ( updated bool , err error ) {
2021-08-19 21:12:38 +02:00
if f == nil {
return false , fmt . Errorf ( "face is nil" )
}
if m . MarkerType != MarkerFace {
return false , fmt . Errorf ( "not a face marker" )
}
2021-08-22 16:14:34 +02:00
// Any reason we don't want to set a new face for this marker?
2021-09-17 14:26:12 +02:00
if m . SubjSrc == SrcAuto || f . SubjUID == "" || m . SubjUID == "" || f . SubjUID == m . SubjUID {
2021-08-22 16:14:34 +02:00
// Don't skip if subject wasn't set manually, or subjects match.
2021-09-01 12:48:17 +02:00
} else if reported , err := f . ResolveCollision ( m . Embeddings ( ) ) ; err != nil {
2021-08-23 16:22:01 +02:00
return false , err
} else if reported {
2022-04-15 09:42:07 +02:00
log . Warnf ( "faces: marker %s face %s has ambiguous subjects %s <> %s, subject source %s" , clean . Log ( m . MarkerUID ) , clean . Log ( f . ID ) , clean . Log ( m . SubjUID ) , clean . Log ( f . SubjUID ) , SrcString ( m . SubjSrc ) )
2021-08-23 16:22:01 +02:00
return false , nil
} else {
2021-08-22 16:14:34 +02:00
return false , nil
}
// Update face with known subject from marker?
2021-09-17 14:26:12 +02:00
if m . SubjSrc == SrcAuto || m . SubjUID == "" || f . SubjUID != "" {
2021-08-22 16:14:34 +02:00
// Don't update if face has a known subject, or marker subject is unknown.
2021-09-17 14:26:12 +02:00
} else if err = f . SetSubjectUID ( m . SubjUID ) ; err != nil {
2021-08-19 21:12:38 +02:00
return false , err
}
2021-09-01 12:48:17 +02:00
// Set face.
2021-09-01 20:46:15 +02:00
m . face = f
2021-09-01 12:48:17 +02:00
2021-08-22 16:14:34 +02:00
// Skip update if the same face is already set.
2021-09-17 14:26:12 +02:00
if m . SubjUID == f . SubjUID && m . FaceID == f . ID {
2021-08-24 20:15:36 +02:00
// Update matching timestamp.
2021-08-29 13:26:05 +02:00
m . MatchedAt = TimePointer ( )
2021-08-24 20:15:36 +02:00
return false , m . Updates ( Values { "MatchedAt" : m . MatchedAt } )
2021-08-19 21:12:38 +02:00
}
2021-08-22 16:14:34 +02:00
// Remember current values for comparison.
2021-08-19 21:12:38 +02:00
faceID := m . FaceID
2021-09-17 14:26:12 +02:00
subjUID := m . SubjUID
subjSrc := m . SubjSrc
2021-08-19 21:12:38 +02:00
m . FaceID = f . ID
2021-08-29 13:26:05 +02:00
m . FaceDist = dist
if m . FaceDist < 0 {
faceEmbedding := f . Embedding ( )
2021-09-17 14:26:12 +02:00
// Calculate the smallest distance to embeddings.
2021-08-29 13:26:05 +02:00
for _ , e := range m . Embeddings ( ) {
if len ( e ) != len ( faceEmbedding ) {
continue
}
2022-04-03 17:25:37 +02:00
if d := e . Dist ( faceEmbedding ) ; d < m . FaceDist || m . FaceDist < 0 {
2021-08-29 13:26:05 +02:00
m . FaceDist = d
}
}
}
2021-08-19 21:12:38 +02:00
2021-09-17 14:26:12 +02:00
if f . SubjUID != "" {
m . SubjUID = f . SubjUID
2021-08-19 21:12:38 +02:00
}
2021-09-01 12:48:17 +02:00
if err = m . SyncSubject ( false ) ; err != nil {
2021-08-19 21:12:38 +02:00
return false , err
}
// Update face subject?
2021-09-17 14:26:12 +02:00
if m . SubjSrc == SrcAuto || m . SubjUID == "" || f . SubjUID == m . SubjUID {
2021-08-19 21:12:38 +02:00
// Not needed.
2021-09-17 14:26:12 +02:00
} else if err = f . SetSubjectUID ( m . SubjUID ) ; err != nil {
2021-08-19 21:12:38 +02:00
return false , err
}
2021-09-17 14:26:12 +02:00
updated = m . FaceID != faceID || m . SubjUID != subjUID || m . SubjSrc != subjSrc
2021-08-24 20:15:36 +02:00
// Update matching timestamp.
2021-08-29 13:26:05 +02:00
m . MatchedAt = TimePointer ( )
2021-08-19 21:12:38 +02:00
2021-09-17 14:26:12 +02:00
if err := m . Updates ( Values { "FaceID" : m . FaceID , "FaceDist" : m . FaceDist , "SubjUID" : m . SubjUID , "SubjSrc" : m . SubjSrc , "MarkerReview" : false , "MatchedAt" : m . MatchedAt } ) ; err != nil {
2021-09-06 01:16:36 +02:00
return false , err
} else if ! updated {
return false , nil
}
return true , m . RefreshPhotos ( )
2021-06-02 17:25:04 +02:00
}
2021-08-19 21:12:38 +02:00
// SyncSubject maintains the marker subject relationship.
2021-09-01 20:46:15 +02:00
func ( m * Marker ) SyncSubject ( updateRelated bool ) ( err error ) {
2021-08-19 21:12:38 +02:00
// Face marker? If not, return.
if m . MarkerType != MarkerFace {
2021-08-16 00:29:36 +02:00
return nil
}
2021-09-01 20:46:15 +02:00
subj := m . Subject ( )
2021-08-16 00:29:36 +02:00
2021-09-17 14:26:12 +02:00
if subj == nil || m . SubjSrc == SrcAuto {
2021-08-19 21:12:38 +02:00
return nil
}
// Update subject with marker name?
2021-09-17 14:26:12 +02:00
if m . MarkerName == "" || subj . SubjName == m . MarkerName {
2021-08-19 21:12:38 +02:00
// Do nothing.
2021-09-01 20:46:15 +02:00
} else if subj , err = subj . UpdateName ( m . MarkerName ) ; err != nil {
2021-08-19 21:12:38 +02:00
return err
2021-09-01 20:46:15 +02:00
} else if subj != nil {
// Update subject fields in case it was merged.
m . subject = subj
2021-09-17 14:26:12 +02:00
m . SubjUID = subj . SubjUID
m . MarkerName = subj . SubjName
2021-08-16 00:29:36 +02:00
}
2021-08-19 21:12:38 +02:00
// Create known face for subject?
2021-09-01 12:48:17 +02:00
if m . FaceID != "" {
2021-08-19 21:12:38 +02:00
// Do nothing.
2021-09-01 20:46:15 +02:00
} else if f := m . Face ( ) ; f != nil {
2021-08-19 21:12:38 +02:00
m . FaceID = f . ID
}
// Update related markers?
2021-09-17 14:26:12 +02:00
if m . FaceID == "" || m . SubjUID == "" {
2021-08-19 21:12:38 +02:00
// Do nothing.
2022-04-04 08:54:03 +02:00
} else if res := Db ( ) . Model ( & Face { } ) . Where ( "id = ? AND subj_uid = ''" , m . FaceID ) . UpdateColumn ( "subj_uid" , m . SubjUID ) ; res . Error != nil {
2021-08-19 21:12:38 +02:00
return fmt . Errorf ( "%s (update known face)" , err )
} else if ! updateRelated {
return nil
} else if err := Db ( ) . Model ( & Marker { } ) .
2021-09-01 20:46:15 +02:00
Where ( "marker_uid <> ?" , m . MarkerUID ) .
2021-08-19 21:12:38 +02:00
Where ( "face_id = ?" , m . FaceID ) .
2021-09-17 14:26:12 +02:00
Where ( "subj_src = ?" , SrcAuto ) .
Where ( "subj_uid <> ?" , m . SubjUID ) .
2022-04-04 08:54:03 +02:00
UpdateColumns ( Values { "subj_uid" : m . SubjUID , "subj_src" : SrcAuto , "marker_review" : false } ) . Error ; err != nil {
2021-08-19 21:12:38 +02:00
return fmt . Errorf ( "%s (update related markers)" , err )
2021-09-06 01:16:36 +02:00
} else if res . RowsAffected > 0 && m . face != nil {
2021-10-05 18:42:39 +02:00
log . Debugf ( "markers: matched %s with %s" , subj . SubjName , m . FaceID )
2021-09-06 01:16:36 +02:00
return m . face . RefreshPhotos ( )
2021-08-19 21:12:38 +02:00
}
return nil
2021-08-16 00:29:36 +02:00
}
2021-10-05 18:42:39 +02:00
// InvalidArea tests if the marker area is invalid or out of range.
func ( m * Marker ) InvalidArea ( ) error {
if m . MarkerType != MarkerFace {
return nil
}
// Ok?
if false == ( m . X > 1 || m . Y > 1 || m . X < 0 || m . Y < 0 || m . W <= 0 || m . H <= 0 || m . W > 1 || m . H > 1 ) {
return nil
}
2021-10-06 12:16:52 +02:00
return fmt . Errorf ( "invalid %s crop area x=%d%% y=%d%% w=%d%% h=%d%%" , TypeString ( m . MarkerType ) , int ( m . X * 100 ) , int ( m . Y * 100 ) , int ( m . W * 100 ) , int ( m . H * 100 ) )
2021-10-05 18:42:39 +02:00
}
2022-10-02 11:38:30 +02:00
// Save updates the record in the database or inserts a new record if it does not already exist.
2021-05-25 11:38:04 +02:00
func ( m * Marker ) Save ( ) error {
2021-10-05 18:42:39 +02:00
if err := m . InvalidArea ( ) ; err != nil {
return err
2021-05-26 14:41:59 +02:00
}
2021-05-25 11:38:04 +02:00
return Db ( ) . Save ( m ) . Error
}
// Create inserts a new row to the database.
func ( m * Marker ) Create ( ) error {
2021-10-05 18:42:39 +02:00
if err := m . InvalidArea ( ) ; err != nil {
return err
2021-05-26 14:41:59 +02:00
}
2021-05-25 11:38:04 +02:00
return Db ( ) . Create ( m ) . Error
}
2021-08-15 20:57:26 +02:00
// Embeddings returns parsed marker embeddings.
2021-09-30 13:44:23 +02:00
func ( m * Marker ) Embeddings ( ) face . Embeddings {
2021-08-15 20:57:26 +02:00
if len ( m . EmbeddingsJSON ) == 0 {
2021-09-30 13:44:23 +02:00
return face . Embeddings { }
2021-08-15 20:57:26 +02:00
} else if len ( m . embeddings ) > 0 {
return m . embeddings
} else if err := json . Unmarshal ( m . EmbeddingsJSON , & m . embeddings ) ; err != nil {
2021-10-05 18:42:39 +02:00
log . Errorf ( "markers: %s while parsing embeddings json" , err )
2021-08-15 20:57:26 +02:00
}
return m . embeddings
2021-08-12 04:54:20 +02:00
}
2021-09-02 11:12:42 +02:00
// SubjectName returns the matching subject's name.
func ( m * Marker ) SubjectName ( ) string {
if m . MarkerName != "" {
return m . MarkerName
} else if s := m . Subject ( ) ; s != nil {
2021-09-17 14:26:12 +02:00
return s . SubjName
2021-09-02 11:12:42 +02:00
}
return ""
}
2021-09-01 20:46:15 +02:00
// Subject returns the matching subject or nil.
func ( m * Marker ) Subject ( ) ( subj * Subject ) {
if m . subject != nil {
2021-09-17 14:26:12 +02:00
if m . SubjUID == m . subject . SubjUID {
2021-09-01 20:46:15 +02:00
return m . subject
2021-09-01 12:48:17 +02:00
}
2021-08-19 21:12:38 +02:00
}
2021-09-01 12:48:17 +02:00
// Create subject?
2021-09-17 14:26:12 +02:00
if m . SubjSrc != SrcAuto && m . MarkerName != "" && m . SubjUID == "" {
if subj = NewSubject ( m . MarkerName , SubjPerson , m . SubjSrc ) ; subj == nil {
2022-04-15 09:42:07 +02:00
log . Errorf ( "faces: marker %s has invalid subject %s" , clean . Log ( m . MarkerUID ) , clean . Log ( m . MarkerName ) )
2021-08-19 21:12:38 +02:00
return nil
} else if subj = FirstOrCreateSubject ( subj ) ; subj == nil {
2022-04-15 09:42:07 +02:00
log . Debugf ( "faces: marker %s has invalid subject %s" , clean . Log ( m . MarkerUID ) , clean . Log ( m . MarkerName ) )
2021-08-19 21:12:38 +02:00
return nil
2021-09-01 12:48:17 +02:00
} else {
2021-09-01 20:46:15 +02:00
m . subject = subj
2021-09-17 14:26:12 +02:00
m . SubjUID = subj . SubjUID
2021-08-19 21:12:38 +02:00
}
2021-09-01 20:46:15 +02:00
return m . subject
2021-08-19 21:12:38 +02:00
}
2021-09-17 14:26:12 +02:00
m . subject = FindSubject ( m . SubjUID )
2021-08-19 21:12:38 +02:00
2021-09-01 20:46:15 +02:00
return m . subject
2021-08-19 21:12:38 +02:00
}
2021-08-21 16:36:00 +02:00
// ClearSubject removes an existing subject association, and reports a collision.
2021-08-22 16:14:34 +02:00
func ( m * Marker ) ClearSubject ( src string ) error {
2021-09-03 16:14:09 +02:00
// Find the matching face.
2021-09-01 20:46:15 +02:00
if m . face == nil {
m . face = FindFace ( m . FaceID )
2021-08-22 16:14:34 +02:00
}
2021-09-29 20:09:34 +02:00
defer func ( ) {
// Find and (soft) delete unused subjects.
start := time . Now ( )
if count , err := DeleteOrphanPeople ( ) ; err != nil {
2022-04-15 09:42:07 +02:00
log . Errorf ( "faces: %s while clearing subject of marker %s [%s]" , err , clean . Log ( m . MarkerUID ) , time . Since ( start ) )
2021-09-29 20:09:34 +02:00
} else if count > 0 {
2022-04-15 09:42:07 +02:00
log . Debugf ( "faces: %s marked as missing while clearing subject of marker %s [%s]" , english . Plural ( count , "person" , "people" ) , clean . Log ( m . MarkerUID ) , time . Since ( start ) )
2021-09-29 20:09:34 +02:00
}
} ( )
2021-09-03 16:14:09 +02:00
// Update index & resolve collisions.
2021-09-17 14:26:12 +02:00
if err := m . Updates ( Values { "MarkerName" : "" , "FaceID" : "" , "FaceDist" : - 1.0 , "SubjUID" : "" , "SubjSrc" : src } ) ; err != nil {
2021-08-21 16:36:00 +02:00
return err
2021-09-03 16:14:09 +02:00
} else if m . face == nil {
m . subject = nil
return nil
} else if resolved , err := m . face . ResolveCollision ( m . Embeddings ( ) ) ; err != nil {
2021-08-21 16:36:00 +02:00
return err
2021-09-01 21:16:08 +02:00
} else if resolved {
2022-04-15 09:42:07 +02:00
log . Debugf ( "faces: marker %s resolved ambiguous subjects for face %s" , clean . Log ( m . MarkerUID ) , clean . Log ( m . face . ID ) )
2021-08-21 16:36:00 +02:00
}
2021-09-03 16:14:09 +02:00
// Clear references.
2021-09-01 20:46:15 +02:00
m . face = nil
2021-09-03 16:14:09 +02:00
m . subject = nil
2021-08-21 16:36:00 +02:00
return nil
}
2021-09-01 20:46:15 +02:00
// Face returns a matching face entity if possible.
func ( m * Marker ) Face ( ) ( f * Face ) {
2021-09-20 16:17:10 +02:00
if m . MarkerUID == "" {
2022-01-05 11:40:44 +01:00
log . Debugf ( "markers: cannot find face when uid is empty" )
2021-09-20 16:17:10 +02:00
return nil
}
2021-09-01 20:46:15 +02:00
if m . face != nil {
if m . FaceID == m . face . ID {
return m . face
2021-09-01 12:48:17 +02:00
}
2021-08-19 23:12:51 +02:00
}
2021-08-29 13:26:05 +02:00
// Add face if size
2021-09-17 14:26:12 +02:00
if m . SubjSrc != SrcAuto && m . FaceID == "" {
2021-10-05 10:12:48 +02:00
if m . Size < face . ClusterSizeThreshold || m . Score < face . ClusterScoreThreshold {
2022-04-15 09:42:07 +02:00
log . Debugf ( "faces: marker %s skipped adding face due to low-quality (size %d, score %d)" , clean . Log ( m . MarkerUID ) , m . Size , m . Score )
2021-08-29 13:26:05 +02:00
return nil
2022-04-04 21:22:31 +02:00
}
if emb := m . Embeddings ( ) ; emb . Empty ( ) {
2022-04-15 09:42:07 +02:00
log . Warnf ( "faces: marker %s has no face embeddings" , clean . Log ( m . MarkerUID ) )
2021-08-19 23:12:51 +02:00
return nil
2021-09-17 14:26:12 +02:00
} else if f = NewFace ( m . SubjUID , m . SubjSrc , emb ) ; f == nil {
2022-04-15 09:42:07 +02:00
log . Warnf ( "faces: failed assigning face to marker %s" , clean . Log ( m . MarkerUID ) )
2021-08-31 20:08:53 +02:00
return nil
2022-04-04 21:22:31 +02:00
} else if f . SkipMatching ( ) {
2022-04-15 09:42:07 +02:00
log . Infof ( "faces: skipped matching marker %s, embedding %s not distinct enough" , clean . Log ( m . MarkerUID ) , f . ID )
2021-08-31 20:08:53 +02:00
} else if f = FirstOrCreateFace ( f ) ; f == nil {
2022-04-15 09:42:07 +02:00
log . Warnf ( "faces: failed matching marker %s with subject %s" , clean . Log ( m . MarkerUID ) , SubjNames . Log ( m . SubjUID ) )
2021-08-19 23:12:51 +02:00
return nil
2021-08-29 13:26:05 +02:00
} else if err := f . MatchMarkers ( Faceless ) ; err != nil {
2022-04-15 09:42:07 +02:00
log . Errorf ( "faces: failed matching marker %s with subject %s (%s)" , clean . Log ( m . MarkerUID ) , SubjNames . Log ( m . SubjUID ) , err )
2021-08-19 23:12:51 +02:00
}
2021-09-01 20:46:15 +02:00
m . face = f
2021-08-19 23:12:51 +02:00
m . FaceID = f . ID
2021-09-01 12:48:17 +02:00
m . FaceDist = 0
2021-08-23 16:22:01 +02:00
} else {
2021-09-01 20:46:15 +02:00
m . face = FindFace ( m . FaceID )
2021-08-19 23:12:51 +02:00
}
2021-09-01 20:46:15 +02:00
return m . face
2021-08-19 23:12:51 +02:00
}
2021-08-22 21:06:44 +02:00
// ClearFace removes an existing face association.
func ( m * Marker ) ClearFace ( ) ( updated bool , err error ) {
if m . FaceID == "" {
2021-08-29 13:26:05 +02:00
return false , m . Matched ( )
2021-08-22 21:06:44 +02:00
}
updated = true
// Remove face references.
2021-09-01 20:46:15 +02:00
m . face = nil
2021-08-22 21:06:44 +02:00
m . FaceID = ""
2021-08-29 13:26:05 +02:00
m . MatchedAt = TimePointer ( )
2021-08-22 21:06:44 +02:00
// Remove subject if set automatically.
2021-09-17 14:26:12 +02:00
if m . SubjSrc == SrcAuto {
m . SubjUID = ""
err = m . Updates ( Values { "FaceID" : "" , "FaceDist" : - 1.0 , "SubjUID" : "" , "MatchedAt" : m . MatchedAt } )
2021-08-22 21:06:44 +02:00
} else {
2021-08-29 13:26:05 +02:00
err = m . Updates ( Values { "FaceID" : "" , "FaceDist" : - 1.0 , "MatchedAt" : m . MatchedAt } )
2021-08-22 21:06:44 +02:00
}
2021-09-06 00:52:10 +02:00
return updated , m . RefreshPhotos ( )
}
// RefreshPhotos flags related photos for metadata maintenance.
2021-09-06 01:16:36 +02:00
func ( m * Marker ) RefreshPhotos ( ) error {
2021-09-06 00:52:10 +02:00
if m . MarkerUID == "" {
return fmt . Errorf ( "empty marker uid" )
}
2022-01-03 11:12:08 +01:00
var err error
switch DbDialect ( ) {
case MySQL :
err = UnscopedDb ( ) . Exec ( ` UPDATE photos p JOIN files f ON f . photo_id = p . id
JOIN ? m ON m . file_uid = f . file_uid SET p . checked_at = NULL
WHERE m . marker_uid = ? ` ,
gorm . Expr ( Marker { } . TableName ( ) ) , m . MarkerUID ) . Error
default :
err = UnscopedDb ( ) . Exec ( ` UPDATE photos SET checked_at = NULL WHERE id IN
( SELECT f . photo_id FROM files f JOIN ? m ON m . file_uid = f . file_uid
WHERE m . marker_uid = ? GROUP BY f . photo_id ) ` ,
gorm . Expr ( Marker { } . TableName ( ) ) , m . MarkerUID ) . Error
}
return err
2021-08-22 21:06:44 +02:00
}
2021-08-29 13:26:05 +02:00
// Matched updates the match timestamp.
func ( m * Marker ) 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-29 13:26:05 +02:00
}
2021-09-20 22:19:54 +02:00
// Top returns the top Y coordinate as float64.
func ( m * Marker ) Top ( ) float64 {
return float64 ( m . Y )
}
2021-09-06 05:13:53 +02:00
// Left returns the left X coordinate as float64.
func ( m * Marker ) Left ( ) float64 {
return float64 ( m . X )
}
// Right returns the right X coordinate as float64.
func ( m * Marker ) Right ( ) float64 {
return float64 ( m . X + m . W )
}
// Bottom returns the bottom Y coordinate as float64.
func ( m * Marker ) Bottom ( ) float64 {
return float64 ( m . Y + m . H )
}
2021-09-20 22:19:54 +02:00
// Surface returns the surface area.
func ( m * Marker ) Surface ( ) float64 {
return float64 ( m . W * m . H )
}
// SurfaceRatio returns the surface ratio.
func ( m * Marker ) SurfaceRatio ( area float64 ) float64 {
if area <= 0 {
return 0
}
if s := m . Surface ( ) ; s <= 0 {
return 0
} else if area > s {
return s / area
} else {
return area / s
}
}
2021-09-06 05:13:53 +02:00
// Overlap calculates the overlap of two markers.
func ( m * Marker ) Overlap ( marker Marker ) ( x , y float64 ) {
x = math . Max ( 0 , math . Min ( m . Right ( ) , marker . Right ( ) ) - math . Max ( m . Left ( ) , marker . Left ( ) ) )
y = math . Max ( 0 , math . Min ( m . Bottom ( ) , marker . Bottom ( ) ) - math . Max ( m . Top ( ) , marker . Top ( ) ) )
return x , y
}
// OverlapArea calculates the overlap area of two markers.
func ( m * Marker ) OverlapArea ( marker Marker ) ( area float64 ) {
x , y := m . Overlap ( marker )
return x * y
}
2021-09-20 22:19:54 +02:00
// OverlapPercent calculates the overlap ratio of two markers in percent.
func ( m * Marker ) OverlapPercent ( marker Marker ) int {
return int ( math . Round ( marker . SurfaceRatio ( m . OverlapArea ( marker ) ) * 100 ) )
}
2021-09-23 23:46:17 +02:00
// Unsaved tests if the marker hasn't been saved yet.
func ( m * Marker ) Unsaved ( ) bool {
return m . MarkerUID == "" || m . CreatedAt . IsZero ( )
}
// ValidFace tests if the marker is a valid face.
func ( m * Marker ) ValidFace ( ) bool {
return m . MarkerType == MarkerFace && ! m . MarkerInvalid
}
// DetectedFace tests if the marker is an automatically detected face.
func ( m * Marker ) DetectedFace ( ) bool {
return m . MarkerType == MarkerFace && m . MarkerSrc == SrcImage
}
// Uncertainty returns the detection uncertainty based on the score in percent.
func ( m * Marker ) Uncertainty ( ) int {
switch {
case m . Score > 300 :
return 1
case m . Score > 200 :
return 5
case m . Score > 100 :
return 10
case m . Score > 80 :
return 15
case m . Score > 65 :
return 20
case m . Score > 50 :
return 25
case m . Score > 40 :
return 30
case m . Score > 30 :
return 35
case m . Score > 20 :
return 40
case m . Score > 10 :
return 45
}
return 50
}
2021-08-12 04:54:20 +02:00
// FindMarker returns an existing row if exists.
2021-09-19 15:59:29 +02:00
func FindMarker ( markerUid string ) * Marker {
if markerUid == "" {
return nil
}
2021-09-01 20:46:15 +02:00
var result Marker
2021-08-12 04:54:20 +02:00
2021-09-19 15:59:29 +02:00
if err := Db ( ) . Where ( "marker_uid = ?" , markerUid ) . First ( & result ) . Error ; err != nil {
2021-09-01 20:46:15 +02:00
return nil
2021-08-12 04:54:20 +02:00
}
2021-09-01 20:46:15 +02:00
return & result
2021-08-12 04:54:20 +02:00
}
2021-10-05 18:42:39 +02:00
// CreateMarkerIfNotExists updates a marker in the database or creates a new one if needed.
func CreateMarkerIfNotExists ( m * Marker ) ( * Marker , error ) {
2021-05-25 11:38:04 +02:00
result := Marker { }
2021-09-01 20:46:15 +02:00
if m . MarkerUID != "" {
2021-10-05 18:42:39 +02:00
return m , nil
2022-03-30 20:36:25 +02:00
} else if Db ( ) . Where ( "file_uid = ? AND marker_type = ? AND thumb = ?" , m . FileUID , m . MarkerType , m . Thumb ) .
2021-10-06 12:16:52 +02:00
First ( & result ) . Error == nil {
return & result , nil
2021-05-25 11:38:04 +02:00
} else if err := m . Create ( ) ; err != nil {
2021-05-26 14:41:59 +02:00
return m , err
2021-09-21 09:23:17 +02:00
} else {
2022-04-15 09:42:07 +02:00
log . Debugf ( "markers: added %s %s for file %s" , TypeString ( m . MarkerType ) , clean . Log ( m . MarkerUID ) , clean . Log ( m . FileUID ) )
2021-05-25 11:38:04 +02:00
}
2021-05-26 14:41:59 +02:00
return m , nil
2021-05-25 11:38:04 +02:00
}