photoprism/internal/entity/marker.go

217 lines
7 KiB
Go

package entity
import (
"encoding/json"
"fmt"
"time"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/ulule/deepcopier"
"github.com/photoprism/photoprism/internal/face"
)
const (
MarkerUnknown = ""
MarkerFace = "Face"
MarkerLabel = "Label"
)
// Marker represents an image marker point.
type Marker struct {
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
FileID uint `gorm:"index;" json:"-" yaml:"-"`
FaceID string `gorm:"type:VARBINARY(42);index;" json:"FaceID" yaml:"FaceID,omitempty"`
RefUID string `gorm:"type:VARBINARY(42);index:idx_markers_uid_type;" json:"RefUID" yaml:"RefUID,omitempty"`
RefSrc string `gorm:"type:VARBINARY(8);default:'';" json:"RefSrc" yaml:"RefSrc,omitempty"`
MarkerType string `gorm:"type:VARBINARY(8);index:idx_markers_uid_type;default:'';" json:"Type" yaml:"Type"`
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
MarkerScore int `gorm:"type:SMALLINT" json:"Score" yaml:"Score,omitempty"`
MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"`
MarkerLabel string `gorm:"type:VARCHAR(255);" json:"Label" yaml:"Label,omitempty"`
MetaJSON []byte `gorm:"type:MEDIUMBLOB;" json:"MetaJSON" yaml:"MetaJSON,omitempty"`
EmbeddingsJSON []byte `gorm:"type:MEDIUMBLOB;" json:"EmbeddingsJSON" yaml:"EmbeddingsJSON,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"`
CreatedAt time.Time
UpdatedAt time.Time
Person *Person `gorm:"foreignkey:RefUID;association_foreignkey:PersonUID;association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Person" yaml:"-"`
embeddings Embeddings `gorm:"-"`
}
// UnknownMarker can be used as a default for unknown markers.
var UnknownMarker = NewMarker(0, "", SrcDefault, MarkerUnknown, 0, 0, 0, 0)
// TableName returns the entity database table name.
func (Marker) TableName() string {
return "markers_dev2"
}
// NewMarker creates a new entity.
func NewMarker(fileUID uint, refUID, markerSrc, markerType string, x, y, w, h float32) *Marker {
m := &Marker{
FileID: fileUID,
RefUID: refUID,
MarkerSrc: markerSrc,
MarkerType: markerType,
X: x,
Y: y,
W: w,
H: h,
}
return m
}
// NewFaceMarker creates a new entity.
func NewFaceMarker(f face.Face, fileID uint, refUID string) *Marker {
pos := f.Marker()
m := NewMarker(fileID, refUID, SrcImage, MarkerFace, pos.X, pos.Y, pos.W, pos.H)
m.MarkerScore = f.Score
m.MetaJSON = f.RelativeLandmarksJSON()
m.EmbeddingsJSON = f.EmbeddingsJSON()
return m
}
// Updates multiple columns in the database.
func (m *Marker) Updates(values interface{}) error {
return UnscopedDb().Model(m).Updates(values).Error
}
// Update updates a column in the database.
func (m *Marker) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).Update(attr, value).Error
}
// SaveForm updates the entity using form data and stores it in the database.
func (m *Marker) SaveForm(f form.Marker) error {
if err := deepcopier.Copy(m).From(f); err != nil {
return err
}
if f.MarkerLabel != "" {
m.MarkerLabel = txt.Title(txt.Clip(f.MarkerLabel, txt.ClipKeyword))
}
if err := m.Save(); err != nil {
return err
}
faceId := m.FaceID
if faceId != "" && m.MarkerLabel != "" && m.RefUID == "" && m.MarkerType == MarkerFace {
if p := NewPerson(m.MarkerLabel, SrcMarker, 1); p == nil {
return fmt.Errorf("marker: person should not be nil (save form)")
} else if p = FirstOrCreatePerson(p); p == nil {
return fmt.Errorf("marker: failed adding person %s for marker %d (save form)", txt.Quote(m.MarkerLabel), m.ID)
} else if err := m.Updates(Val{"RefUID": p.PersonUID, "RefSrc": SrcManual, "FaceID": ""}); err != nil {
return fmt.Errorf("marker: %s (save form)", err)
} else if err := Db().Model(&Face{}).Where("id = ? AND person_uid = ''", faceId).Update("PersonUID", p.PersonUID).Error; err != nil {
return fmt.Errorf("marker: %s (update face)", err)
} else if err := Db().Model(&Marker{}).
Where("face_id = ?", faceId).
Updates(Val{"RefUID": p.PersonUID, "RefSrc": SrcManual, "FaceID": ""}).Error; err != nil {
return fmt.Errorf("marker: %s (update related markers)", err)
} else {
log.Infof("marker: matched person %s with label %s", p.PersonUID, txt.Quote(m.MarkerLabel))
}
} else if m.MarkerLabel != "" && m.RefUID != "" && m.MarkerType == MarkerFace {
if p := FindPerson(m.RefUID); p != nil {
p.SetName(m.MarkerLabel)
if err := p.Save(); err != nil {
return fmt.Errorf("marker: %s (update person)", err)
}
}
}
return nil
}
// Save updates the existing or inserts a new row.
func (m *Marker) Save() error {
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")
}
return Db().Save(m).Error
}
// Create inserts a new row to the database.
func (m *Marker) Create() error {
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")
}
return Db().Create(m).Error
}
// 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
}
// FindMarker returns an existing row if exists.
func FindMarker(id uint) *Marker {
result := Marker{}
if err := Db().Where("id = ?", id).First(&result).Error; err == nil {
return &result
}
return nil
}
// UpdateOrCreateMarker updates a marker in the database or creates a new one if needed.
func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
const d = 0.07
result := Marker{}
if m.ID > 0 {
err := m.Save()
log.Debugf("faces: saved marker %d for file %d", m.ID, m.FileID)
return m, err
} else if err := Db().Where(`file_id = ? AND x > ? AND x < ? AND y > ? AND y < ?`,
m.FileID, m.X-d, m.X+d, m.Y-d, m.Y+d).First(&result).Error; err == nil {
if SrcPriority[m.MarkerSrc] < SrcPriority[result.MarkerSrc] {
// Ignore.
return &result, nil
}
err := result.Updates(map[string]interface{}{
"X": m.X,
"Y": m.Y,
"W": m.W,
"H": m.H,
"MarkerScore": m.MarkerScore,
"MetaJSON": m.MetaJSON,
"EmbeddingsJSON": m.EmbeddingsJSON,
"RefUID": m.RefUID,
})
log.Debugf("faces: updated existing marker %d for file %d", result.ID, result.FileID)
return &result, err
} else if err := m.Create(); err != nil {
log.Debugf("faces: added marker %d for file %d", m.ID, m.FileID)
return m, err
}
return m, nil
}