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-08-24 14:27:34 +02:00
|
|
|
"strings"
|
2021-05-25 11:38:04 +02:00
|
|
|
"time"
|
2021-05-31 15:40:52 +02:00
|
|
|
|
2021-09-01 20:46:15 +02:00
|
|
|
"github.com/jinzhu/gorm"
|
|
|
|
|
2021-08-29 13:26:05 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/clusters"
|
2021-09-05 11:05:33 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/rnd"
|
2021-08-29 13:26:05 +02:00
|
|
|
|
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"
|
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
2021-05-25 11:38:04 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
MarkerUnknown = ""
|
2021-08-16 00:29:36 +02:00
|
|
|
MarkerFace = "face"
|
|
|
|
MarkerLabel = "label"
|
2021-05-25 11:38:04 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Marker represents an image marker point.
|
|
|
|
type Marker struct {
|
2021-09-01 20:46:15 +02:00
|
|
|
MarkerUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
|
|
|
|
FileUID string `gorm:"type:VARBINARY(42);index;" json:"FileUID" yaml:"FileUID"`
|
2021-09-03 16:14:09 +02:00
|
|
|
FileHash string `gorm:"type:VARBINARY(128);index" json:"FileHash" yaml:"FileHash,omitempty"`
|
2021-09-05 17:10:52 +02:00
|
|
|
CropArea string `gorm:"type:VARBINARY(16);default:''" json:"CropArea" yaml:"CropArea,omitempty"`
|
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"`
|
|
|
|
MarkerName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name,omitempty"`
|
2021-09-02 23:47:37 +02:00
|
|
|
MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"`
|
2021-09-03 17:42:37 +02:00
|
|
|
SubjectUID string `gorm:"type:VARBINARY(42);index:idx_markers_subject_uid_src;" json:"SubjectUID" yaml:"SubjectUID,omitempty"`
|
|
|
|
SubjectSrc string `gorm:"type:VARBINARY(8);index:idx_markers_subject_uid_src;default:'';" json:"SubjectSrc" yaml:"SubjectSrc,omitempty"`
|
2021-09-01 20:46:15 +02:00
|
|
|
subject *Subject `gorm:"foreignkey:SubjectUID;association_foreignkey:SubjectUID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
2021-08-16 01:45:36 +02:00
|
|
|
FaceID string `gorm:"type:VARBINARY(42);index;" json:"FaceID" yaml:"FaceID,omitempty"`
|
2021-08-29 13:26:05 +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"`
|
|
|
|
embeddings Embeddings `gorm:"-"`
|
|
|
|
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-08-29 13:26:05 +02:00
|
|
|
Size int `gorm:"default:-1" json:"Size" yaml:"Size,omitempty"`
|
2021-08-16 01:45:36 +02:00
|
|
|
Score int `gorm:"type:SMALLINT" json:"Score" yaml:"Score,omitempty"`
|
2021-09-01 20:46:15 +02:00
|
|
|
Review bool `json:"Review" yaml:"Review,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
|
|
|
}
|
|
|
|
|
|
|
|
// TableName returns the entity database table name.
|
|
|
|
func (Marker) TableName() string {
|
2021-09-03 16:26:01 +02:00
|
|
|
return "markers_dev7"
|
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 {
|
|
|
|
if rnd.IsUID(m.MarkerUID, 'm') {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return scope.SetColumn("MarkerUID", rnd.PPID('m'))
|
2021-05-25 11:38:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewMarker creates a new entity.
|
2021-09-03 16:14:09 +02:00
|
|
|
func NewMarker(file File, area crop.Area, subjectUID, markerSrc, markerType string) *Marker {
|
2021-05-26 14:41:59 +02:00
|
|
|
m := &Marker{
|
2021-09-03 16:14:09 +02:00
|
|
|
FileUID: file.FileUID,
|
|
|
|
FileHash: file.FileHash,
|
2021-09-05 17:10:52 +02:00
|
|
|
CropArea: area.String(),
|
2021-05-25 11:38:04 +02:00
|
|
|
MarkerSrc: markerSrc,
|
|
|
|
MarkerType: markerType,
|
2021-09-03 16:14:09 +02:00
|
|
|
SubjectUID: subjectUID,
|
|
|
|
X: area.X,
|
|
|
|
Y: area.Y,
|
|
|
|
W: area.W,
|
|
|
|
H: area.H,
|
|
|
|
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.
|
2021-09-03 16:14:09 +02:00
|
|
|
func NewFaceMarker(f face.Face, file File, subjectUID string) *Marker {
|
|
|
|
m := NewMarker(file, f.CropArea(), subjectUID, SrcImage, MarkerFace)
|
2021-05-26 14:41:59 +02:00
|
|
|
|
2021-08-29 13:26:05 +02:00
|
|
|
m.Size = f.Size()
|
2021-08-16 01:45:36 +02:00
|
|
|
m.Score = f.Score
|
2021-09-01 20:46:15 +02:00
|
|
|
m.Review = f.Score < 30
|
2021-09-03 16:14:09 +02:00
|
|
|
m.FaceDist = -1
|
|
|
|
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
|
|
|
m.LandmarksJSON = f.RelativeLandmarksJSON()
|
2021-05-26 14:41:59 +02:00
|
|
|
|
|
|
|
return m
|
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-06-02 17:25:04 +02:00
|
|
|
// SaveForm updates the entity using form data and stores it in the database.
|
|
|
|
func (m *Marker) SaveForm(f form.Marker) error {
|
2021-08-24 14:27:34 +02:00
|
|
|
changed := false
|
|
|
|
|
|
|
|
if m.MarkerInvalid != f.MarkerInvalid {
|
|
|
|
m.MarkerInvalid = f.MarkerInvalid
|
|
|
|
changed = true
|
2021-06-02 17:25:04 +02:00
|
|
|
}
|
|
|
|
|
2021-09-01 20:46:15 +02:00
|
|
|
if m.Review != f.Review {
|
|
|
|
m.Review = f.Review
|
2021-08-24 14:27:34 +02:00
|
|
|
changed = true
|
2021-06-02 17:25:04 +02:00
|
|
|
}
|
|
|
|
|
2021-08-24 14:27:34 +02:00
|
|
|
if f.SubjectSrc == SrcManual && strings.TrimSpace(f.MarkerName) != "" {
|
|
|
|
m.SubjectSrc = SrcManual
|
|
|
|
m.MarkerName = txt.Title(txt.Clip(f.MarkerName, txt.ClipDefault))
|
|
|
|
|
|
|
|
if err := m.SyncSubject(true); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
changed = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if changed {
|
|
|
|
return m.Save()
|
2021-06-02 17:25:04 +02:00
|
|
|
}
|
|
|
|
|
2021-08-24 14:27:34 +02:00
|
|
|
return 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-01 12:48:17 +02:00
|
|
|
if m.SubjectSrc == SrcAuto || f.SubjectUID == "" || m.SubjectUID == "" || f.SubjectUID == m.SubjectUID {
|
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 {
|
2021-09-01 20:46:15 +02:00
|
|
|
log.Infof("faces: collision of marker %s, subject %s, face %s, subject %s, source %s", m.MarkerUID, m.SubjectUID, f.ID, f.SubjectUID, m.SubjectSrc)
|
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-01 12:48:17 +02:00
|
|
|
if m.SubjectSrc == SrcAuto || m.SubjectUID == "" || f.SubjectUID != "" {
|
2021-08-22 16:14:34 +02:00
|
|
|
// Don't update if face has a known subject, or marker subject is unknown.
|
2021-09-01 12:48:17 +02:00
|
|
|
} else if err = f.SetSubjectUID(m.SubjectUID); 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.
|
|
|
|
if m.SubjectUID == f.SubjectUID && 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
|
|
|
|
subjectUID := m.SubjectUID
|
|
|
|
SubjectSrc := m.SubjectSrc
|
|
|
|
|
|
|
|
m.FaceID = f.ID
|
2021-08-29 13:26:05 +02:00
|
|
|
m.FaceDist = dist
|
|
|
|
|
|
|
|
if m.FaceDist < 0 {
|
|
|
|
faceEmbedding := f.Embedding()
|
|
|
|
|
|
|
|
// Calculate smallest distance to embeddings.
|
|
|
|
for _, e := range m.Embeddings() {
|
|
|
|
if len(e) != len(faceEmbedding) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if d := clusters.EuclideanDistance(e, faceEmbedding); d < m.FaceDist || m.FaceDist < 0 {
|
|
|
|
m.FaceDist = d
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-08-19 21:12:38 +02:00
|
|
|
|
|
|
|
if f.SubjectUID != "" {
|
|
|
|
m.SubjectUID = f.SubjectUID
|
|
|
|
}
|
|
|
|
|
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-01 12:48:17 +02:00
|
|
|
if m.SubjectSrc == SrcAuto || m.SubjectUID == "" || f.SubjectUID == m.SubjectUID {
|
2021-08-19 21:12:38 +02:00
|
|
|
// Not needed.
|
2021-09-01 12:48:17 +02:00
|
|
|
} else if err = f.SetSubjectUID(m.SubjectUID); err != nil {
|
2021-08-19 21:12:38 +02:00
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2021-08-24 20:15:36 +02:00
|
|
|
updated = m.FaceID != faceID || m.SubjectUID != subjectUID || m.SubjectSrc != SubjectSrc
|
|
|
|
|
|
|
|
// Update matching timestamp.
|
2021-08-29 13:26:05 +02:00
|
|
|
m.MatchedAt = TimePointer()
|
2021-08-19 21:12:38 +02:00
|
|
|
|
2021-09-05 21:34:51 +02:00
|
|
|
return updated, m.Updates(Values{"FaceID": m.FaceID, "FaceDist": m.FaceDist, "SubjectUID": m.SubjectUID, "SubjectSrc": m.SubjectSrc, "Review": false, "MatchedAt": m.MatchedAt})
|
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-01 12:48:17 +02:00
|
|
|
if subj == nil || m.SubjectSrc == SrcAuto {
|
2021-08-19 21:12:38 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update subject with marker name?
|
2021-09-01 12:48:17 +02:00
|
|
|
if m.MarkerName == "" || subj.SubjectName == 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
|
|
|
|
m.SubjectUID = subj.SubjectUID
|
|
|
|
m.MarkerName = subj.SubjectName
|
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?
|
|
|
|
if m.FaceID == "" || m.SubjectUID == "" {
|
|
|
|
// Do nothing.
|
|
|
|
} else if err := Db().Model(&Face{}).Where("id = ? AND subject_uid = ''", m.FaceID).Update("SubjectUID", m.SubjectUID).Error; err != nil {
|
|
|
|
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).
|
|
|
|
Where("subject_src = ?", SrcAuto).
|
|
|
|
Where("subject_uid <> ?", m.SubjectUID).
|
2021-09-05 21:34:51 +02:00
|
|
|
Updates(Values{"SubjectUID": m.SubjectUID, "SubjectSrc": SrcAuto, "Review": false}).Error; err != nil {
|
2021-08-19 21:12:38 +02:00
|
|
|
return fmt.Errorf("%s (update related markers)", err)
|
|
|
|
} else {
|
2021-08-24 14:27:34 +02:00
|
|
|
log.Debugf("marker: matched %s with %s", subj.SubjectName, m.FaceID)
|
2021-08-19 21:12:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2021-08-16 00:29:36 +02:00
|
|
|
}
|
|
|
|
|
2021-05-25 11:38:04 +02:00
|
|
|
// Save updates the existing or inserts a new row.
|
|
|
|
func (m *Marker) Save() error {
|
2021-05-26 14:41:59 +02:00
|
|
|
if m.X == 0 || m.Y == 0 || m.X > 1 || m.Y > 1 || m.X < -1 || m.Y < -1 {
|
|
|
|
return fmt.Errorf("marker: invalid position")
|
|
|
|
}
|
|
|
|
|
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-05-26 14:41:59 +02:00
|
|
|
if m.X == 0 || m.Y == 0 || m.X > 1 || m.Y > 1 || m.X < -1 || m.Y < -1 {
|
|
|
|
return fmt.Errorf("marker: invalid position")
|
|
|
|
}
|
|
|
|
|
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.
|
|
|
|
func (m *Marker) Embeddings() Embeddings {
|
|
|
|
if len(m.EmbeddingsJSON) == 0 {
|
|
|
|
return Embeddings{}
|
|
|
|
} else if len(m.embeddings) > 0 {
|
|
|
|
return m.embeddings
|
|
|
|
} else if err := json.Unmarshal(m.EmbeddingsJSON, &m.embeddings); err != nil {
|
|
|
|
log.Errorf("failed parsing marker embeddings json: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
return s.SubjectName
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
if m.SubjectUID == m.subject.SubjectUID {
|
|
|
|
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?
|
|
|
|
if m.SubjectSrc != SrcAuto && m.MarkerName != "" && m.SubjectUID == "" {
|
|
|
|
if subj = NewSubject(m.MarkerName, SubjectPerson, m.SubjectSrc); subj == nil {
|
2021-08-19 21:12:38 +02:00
|
|
|
return nil
|
|
|
|
} else if subj = FirstOrCreateSubject(subj); subj == nil {
|
|
|
|
log.Debugf("marker: invalid subject %s", txt.Quote(m.MarkerName))
|
|
|
|
return nil
|
2021-09-01 12:48:17 +02:00
|
|
|
} else {
|
2021-09-01 20:46:15 +02:00
|
|
|
m.subject = subj
|
2021-09-01 12:48:17 +02:00
|
|
|
m.SubjectUID = subj.SubjectUID
|
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-01 20:46:15 +02:00
|
|
|
m.subject = FindSubject(m.SubjectUID)
|
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-03 16:14:09 +02:00
|
|
|
// Update index & resolve collisions.
|
|
|
|
if err := m.Updates(Values{"MarkerName": "", "FaceID": "", "FaceDist": -1.0, "SubjectUID": "", "SubjectSrc": 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 {
|
|
|
|
log.Debugf("faces: resolved collision with %s", 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) {
|
|
|
|
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-01 12:48:17 +02:00
|
|
|
if m.SubjectSrc != SrcAuto && m.FaceID == "" {
|
2021-08-29 13:26:05 +02:00
|
|
|
if m.Size < face.ClusterMinSize || m.Score < face.ClusterMinScore {
|
2021-09-01 20:46:15 +02:00
|
|
|
log.Debugf("faces: skipped adding face for low-quality marker %s, size %d, score %d", m.MarkerUID, m.Size, m.Score)
|
2021-08-29 13:26:05 +02:00
|
|
|
return nil
|
2021-08-31 20:08:53 +02:00
|
|
|
} else if emb := m.Embeddings(); len(emb) == 0 {
|
2021-09-01 20:46:15 +02:00
|
|
|
log.Warnf("marker: %s has no embeddings", m.MarkerUID)
|
2021-08-19 23:12:51 +02:00
|
|
|
return nil
|
2021-09-01 12:48:17 +02:00
|
|
|
} else if f = NewFace(m.SubjectUID, m.SubjectSrc, emb); f == nil {
|
2021-09-01 20:46:15 +02:00
|
|
|
log.Warnf("marker: failed adding face for id %s", m.MarkerUID)
|
2021-08-31 20:08:53 +02:00
|
|
|
return nil
|
|
|
|
} else if f = FirstOrCreateFace(f); f == nil {
|
2021-09-01 20:46:15 +02:00
|
|
|
log.Warnf("marker: failed adding face for id %s", m.MarkerUID)
|
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 {
|
2021-08-23 16:22:01 +02:00
|
|
|
log.Errorf("faces: %s (match markers)", 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.
|
|
|
|
if m.SubjectSrc == SrcAuto {
|
|
|
|
m.SubjectUID = ""
|
2021-08-29 13:26:05 +02:00
|
|
|
err = m.Updates(Values{"FaceID": "", "FaceDist": -1.0, "SubjectUID": "", "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
|
|
|
}
|
|
|
|
|
|
|
|
return updated, err
|
|
|
|
}
|
|
|
|
|
2021-08-29 13:26:05 +02:00
|
|
|
// Matched updates the match timestamp.
|
|
|
|
func (m *Marker) Matched() error {
|
|
|
|
m.MatchedAt = TimePointer()
|
|
|
|
return UnscopedDb().Model(m).UpdateColumns(Values{"MatchedAt": m.MatchedAt}).Error
|
|
|
|
}
|
|
|
|
|
2021-08-12 04:54:20 +02:00
|
|
|
// FindMarker returns an existing row if exists.
|
2021-09-01 20:46:15 +02:00
|
|
|
func FindMarker(uid string) *Marker {
|
|
|
|
var result Marker
|
2021-08-12 04:54:20 +02:00
|
|
|
|
2021-09-01 20:46:15 +02:00
|
|
|
if err := Db().Where("marker_uid = ?", uid).First(&result).Error; err != nil {
|
|
|
|
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-05-26 14:41:59 +02:00
|
|
|
// UpdateOrCreateMarker updates a marker in the database or creates a new one if needed.
|
|
|
|
func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
|
2021-06-01 17:39:03 +02:00
|
|
|
const d = 0.07
|
|
|
|
|
2021-05-25 11:38:04 +02:00
|
|
|
result := Marker{}
|
|
|
|
|
2021-09-01 20:46:15 +02:00
|
|
|
if m.MarkerUID != "" {
|
2021-05-26 14:41:59 +02:00
|
|
|
err := m.Save()
|
2021-09-01 20:46:15 +02:00
|
|
|
log.Debugf("faces: saved marker %s for file %s", m.MarkerUID, m.FileUID)
|
2021-05-26 14:41:59 +02:00
|
|
|
return m, err
|
2021-09-01 20:46:15 +02:00
|
|
|
} else if err := Db().Where(`file_uid = ? AND x > ? AND x < ? AND y > ? AND y < ?`,
|
|
|
|
m.FileUID, m.X-d, m.X+d, m.Y-d, m.Y+d).First(&result).Error; err == nil {
|
2021-05-26 14:41:59 +02:00
|
|
|
|
|
|
|
if SrcPriority[m.MarkerSrc] < SrcPriority[result.MarkerSrc] {
|
|
|
|
// Ignore.
|
|
|
|
return &result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err := result.Updates(map[string]interface{}{
|
2021-09-01 12:48:17 +02:00
|
|
|
"MarkerType": m.MarkerType,
|
|
|
|
"MarkerSrc": m.MarkerSrc,
|
2021-09-05 17:10:52 +02:00
|
|
|
"CropArea": m.CropArea,
|
2021-08-15 20:57:26 +02:00
|
|
|
"X": m.X,
|
|
|
|
"Y": m.Y,
|
|
|
|
"W": m.W,
|
|
|
|
"H": m.H,
|
2021-08-16 01:45:36 +02:00
|
|
|
"Score": m.Score,
|
2021-09-01 12:48:17 +02:00
|
|
|
"Size": m.Size,
|
2021-08-16 01:45:36 +02:00
|
|
|
"LandmarksJSON": m.LandmarksJSON,
|
2021-08-15 20:57:26 +02:00
|
|
|
"EmbeddingsJSON": m.EmbeddingsJSON,
|
2021-05-26 14:41:59 +02:00
|
|
|
})
|
|
|
|
|
2021-09-01 20:46:15 +02:00
|
|
|
log.Debugf("faces: updated existing marker %s for file %s", result.MarkerUID, result.FileUID)
|
2021-05-26 14:41:59 +02:00
|
|
|
|
|
|
|
return &result, err
|
2021-05-25 11:38:04 +02:00
|
|
|
} else if err := m.Create(); err != nil {
|
2021-09-01 20:46:15 +02:00
|
|
|
log.Debugf("faces: added marker %s for file %s", m.MarkerUID, m.FileUID)
|
2021-05-26 14:41:59 +02:00
|
|
|
return m, err
|
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
|
|
|
}
|