People: Add SubjectNames() to file entity #22

This commit is contained in:
Michael Mayer 2021-09-02 11:12:42 +02:00
parent a5f8e6149f
commit 1be409d654
14 changed files with 202 additions and 50 deletions

View file

@ -69,7 +69,7 @@ type File struct {
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
Share []FileShare `json:"-" yaml:"-"`
Sync []FileSync `json:"-" yaml:"-"`
Markers Markers `json:"Markers,omitempty" yaml:"-"`
markers *Markers
}
type FileInfos struct {
@ -254,7 +254,7 @@ func (m *File) Create() error {
return err
}
if err := m.Markers.Save(m.FileUID); err != nil {
if err := m.Markers().Save(m.FileUID); err != nil {
log.Errorf("file: %s (create markers for %s)", err, m.FileUID)
return err
}
@ -282,7 +282,7 @@ func (m *File) Save() error {
return err
}
if err := m.Markers.Save(m.FileUID); err != nil {
if err := m.Markers().Save(m.FileUID); err != nil {
log.Errorf("file: %s (save markers for %s)", err, m.FileUID)
return err
}
@ -406,15 +406,18 @@ func (m *File) AddFaces(faces face.Faces) {
// AddFace adds a face marker to the file.
func (m *File) AddFace(f face.Face, subjectUID string) {
marker := NewFaceMarker(f, m.FileUID, subjectUID)
if !m.Markers.Contains(*marker) {
m.Markers = append(m.Markers, *marker)
marker := *NewFaceMarker(f, m.FileUID, subjectUID)
if markers := m.Markers(); !markers.Contains(marker) {
markers.Append(marker)
}
}
// FaceCount returns the current number of valid faces detected.
func (m *File) FaceCount() (c int) {
if err := Db().Model(Marker{}).Where("file_uid = ? AND marker_invalid = 0", m.FileUID).
if err := Db().Model(Marker{}).
Where("file_uid = ? AND marker_type = ?", m.FileUID, MarkerFace).
Where("marker_invalid = 0").
Count(&c).Error; err != nil {
log.Errorf("file: %s (count faces)", err)
return 0
@ -423,11 +426,23 @@ func (m *File) FaceCount() (c int) {
}
}
// PreloadMarkers loads existing file markers.
func (m *File) PreloadMarkers() {
// Markers finds and returns existing file markers.
func (m *File) Markers() *Markers {
if m.markers != nil {
return m.markers
}
if res, err := FindMarkers(m.FileUID); err != nil {
log.Warnf("file: %s (load markers)", err)
m.markers = &Markers{}
} else {
m.Markers = res
m.markers = &res
}
return m.markers
}
// SubjectNames returns all known subject names.
func (m *File) SubjectNames() []string {
return m.Markers().SubjectNames()
}

View file

@ -0,0 +1,83 @@
package entity
import (
"encoding/json"
"time"
)
// MarshalJSON returns the JSON encoding.
func (m *File) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
UID string
PhotoUID string
InstanceID string `json:",omitempty"`
Name string
Root string
OriginalName string `json:",omitempty"`
Hash string
Size int64
Codec string `json:",omitempty"`
Type string
Mime string `json:",omitempty"`
Primary bool
Sidecar bool `json:",omitempty"`
Missing bool `json:",omitempty"`
Portrait bool `json:",omitempty"`
Video bool `json:",omitempty"`
Duration time.Duration `json:",omitempty"`
Width int `json:",omitempty"`
Height int `json:",omitempty"`
Orientation int `json:",omitempty"`
Projection string `json:",omitempty"`
AspectRatio float32 `json:",omitempty"`
MainColor string `json:",omitempty"`
Colors string `json:",omitempty"`
Luminance string `json:",omitempty"`
Diff uint32 `json:",omitempty"`
Chroma uint8 `json:",omitempty"`
Error string `json:",omitempty"`
ModTime int64 `json:",omitempty"`
CreatedAt time.Time `json:",omitempty"`
CreatedIn int64 `json:",omitempty"`
UpdatedAt time.Time `json:",omitempty"`
UpdatedIn int64 `json:",omitempty"`
DeletedAt *time.Time `json:",omitempty"`
Markers *Markers `json:",omitempty"`
}{
UID: m.FileUID,
PhotoUID: m.PhotoUID,
InstanceID: m.InstanceID,
Name: m.FileName,
Root: m.FileRoot,
OriginalName: m.OriginalName,
Hash: m.FileHash,
Size: m.FileSize,
Codec: m.FileCodec,
Type: m.FileType,
Mime: m.FileMime,
Primary: m.FilePrimary,
Sidecar: m.FileSidecar,
Missing: m.FileMissing,
Portrait: m.FilePortrait,
Video: m.FileVideo,
Duration: m.FileDuration,
Width: m.FileWidth,
Height: m.FileHeight,
Orientation: m.FileOrientation,
Projection: m.FileProjection,
AspectRatio: m.FileAspectRatio,
MainColor: m.FileMainColor,
Colors: m.FileColors,
Luminance: m.FileLuminance,
Diff: m.FileDiff,
Chroma: m.FileChroma,
Error: m.FileError,
ModTime: m.ModTime,
CreatedAt: m.CreatedAt,
CreatedIn: m.CreatedIn,
UpdatedAt: m.UpdatedAt,
UpdatedIn: m.UpdatedIn,
DeletedAt: m.DeletedAt,
Markers: m.Markers(),
})
}

View file

@ -0,0 +1,13 @@
package entity
import "testing"
func TestFile_MarshalJSON(t *testing.T) {
if m := FileFixtures.Pointer("Video.mp4"); m == nil {
t.Fatal("must not be nil")
} else if j, err := m.MarshalJSON(); err != nil {
t.Fatal(err)
} else {
t.Logf("json: %s", j)
}
}

View file

@ -446,7 +446,7 @@ func TestFile_AddFaces(t *testing.T) {
assert.Equal(t, false, file.FileMissing)
assert.NotEmpty(t, file.FileUID)
assert.NotEmpty(t, file.Markers)
assert.NotEmpty(t, file.Markers())
})
}
@ -489,3 +489,15 @@ func TestFile_Rename(t *testing.T) {
assert.Equal(t, "27900704_070228_D6D51B6C", p.PhotoName)
})
}
func TestFile_SubjectNames(t *testing.T) {
f := FileFixtures.Get("Video.mp4")
names := f.SubjectNames()
assert.Len(t, names, 1)
if len(names) != 1 {
t.Fatal("there should be one name")
} else {
assert.Equal(t, "Actress A", names[0])
}
}

View file

@ -331,6 +331,17 @@ func (m *Marker) Embeddings() Embeddings {
return m.embeddings
}
// 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 ""
}
// Subject returns the matching subject or nil.
func (m *Marker) Subject() (subj *Subject) {
if m.subject != nil {

View file

@ -194,6 +194,7 @@ var MarkerFixtures = MarkerMap{
MarkerUID: "mt9k3pw1wowuy999",
FileUID: "ft71s39w45bnlqdw",
FaceID: FaceFixtures.Get("actress-1").ID,
FaceThumb: "ce2e128c45abb3aa73b3eecb2831cd1761fe2b4f-178-559-68",
FaceDist: 0.26852392873736236,
SubjectSrc: SrcManual,
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
@ -213,6 +214,7 @@ var MarkerFixtures = MarkerMap{
MarkerUID: "mt9k3pw1wowu1000",
FileUID: "ft72s39w45bnlqdw",
FaceID: FaceFixtures.Get("actress-1").ID,
FaceThumb: "ce2e128c45abb3aa73b3eecb2831cd1761fe2b4f-1-2-3",
FaceDist: 0.4507357278575355,
SubjectSrc: "",
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
@ -232,6 +234,7 @@ var MarkerFixtures = MarkerMap{
MarkerUID: "mt9k3pw1wowu1001",
FileUID: "ft2es39q45bnlqd0",
FaceID: FaceFixtures.Get("actress-1").ID,
FaceThumb: "ce2e128c45abb3aa73b3eecb2831cd1761fe2b4f-4-5-6",
FaceDist: 0.5099754448545762,
SubjectSrc: "",
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,

View file

@ -22,18 +22,18 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
Type string
Src string
Name string
SubjectUID string
SubjectSrc string `json:",omitempty"`
FaceID string `json:",omitempty"`
FaceThumb string
X float32
Y float32
W float32
H float32
Size int
Score int
Review bool
Invalid bool
SubjectUID string `json:",omitempty"`
SubjectSrc string `json:",omitempty"`
FaceID string `json:",omitempty"`
FaceThumb string `json:",omitempty"`
Size int `json:",omitempty"`
Score int `json:",omitempty"`
Review bool `json:",omitempty"`
Invalid bool `json:",omitempty"`
CreatedAt time.Time
}{
UID: m.MarkerUID,
@ -41,14 +41,14 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
Type: m.MarkerType,
Src: m.MarkerSrc,
Name: name,
SubjectUID: m.SubjectUID,
SubjectSrc: m.SubjectSrc,
FaceID: m.FaceID,
FaceThumb: m.FaceThumb,
X: m.X,
Y: m.Y,
W: m.W,
H: m.H,
SubjectUID: m.SubjectUID,
SubjectSrc: m.SubjectSrc,
FaceID: m.FaceID,
FaceThumb: m.FaceThumb,
Size: m.Size,
Score: m.Score,
Review: m.Review,

View file

@ -0,0 +1,15 @@
package entity
import (
"testing"
)
func TestMarker_MarshalJSON(t *testing.T) {
if m := MarkerFixtures.Pointer("actor-a-2"); m == nil {
t.Fatal("must not be nil")
} else if j, err := m.MarshalJSON(); err != nil {
t.Fatal(err)
} else {
t.Logf("json: %s", j)
}
}

View file

@ -12,16 +12,6 @@ func TestMarker_TableName(t *testing.T) {
assert.Contains(t, m.TableName(), "markers")
}
func TestMarker_MarshalJSON(t *testing.T) {
if m := MarkerFixtures.Pointer("actor-a-2"); m == nil {
t.Fatal("must not be nil")
} else if j, err := m.MarshalJSON(); err != nil {
t.Fatal(err)
} else {
t.Logf("json: %s", j)
}
}
func TestNewMarker(t *testing.T) {
m := NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
assert.IsType(t, &Marker{}, m)
@ -193,7 +183,7 @@ func TestMarker_Save(t *testing.T) {
p := PhotoFixtures.Get("19800101_000002_D640C559")
assert.Empty(t, p.Files)
p.PreloadFiles(true)
p.PreloadFiles()
assert.NotEmpty(t, p.Files)
// t.Logf("FILES: %#v", p.Files)

View file

@ -41,13 +41,31 @@ func (m Markers) FaceCount() (faces int) {
return faces
}
// SubjectNames returns known subject names.
func (m Markers) SubjectNames() (names []string) {
for _, marker := range m {
if marker.MarkerInvalid || marker.MarkerType != MarkerFace {
continue
} else if n := marker.SubjectName(); n != "" {
names = append(names, n)
}
}
return names
}
// Append adds a marker.
func (m *Markers) Append(marker Marker) {
*m = append(*m, marker)
}
// FindMarkers returns up to 1000 markers for a given file uid.
func FindMarkers(fileUID string) (Markers, error) {
m := Markers{}
err := Db().
Where(`file_uid = ?`, fileUID).
Order("marker_uid").
Order("x").
Offset(0).Limit(1000).
Find(&m).Error

View file

@ -444,7 +444,7 @@ func (m *Photo) IndexKeywords() error {
}
// PreloadFiles prepares gorm scope to retrieve photo file
func (m *Photo) PreloadFiles(markers bool) {
func (m *Photo) PreloadFiles() {
q := Db().
Table("files").
Select(`files.*`).
@ -452,12 +452,6 @@ func (m *Photo) PreloadFiles(markers bool) {
Order("files.file_name DESC")
logError(q.Scan(&m.Files))
if markers {
for i := range m.Files {
m.Files[i].PreloadMarkers()
}
}
}
// PreloadKeywords prepares gorm scope to retrieve photo keywords
@ -485,7 +479,7 @@ func (m *Photo) PreloadAlbums() {
// PreloadMany prepares gorm scope to retrieve photo file, albums and keywords
func (m *Photo) PreloadMany() {
m.PreloadFiles(true)
m.PreloadFiles()
m.PreloadKeywords()
m.PreloadAlbums()
}

View file

@ -145,7 +145,7 @@ func TestPhoto_PreloadFiles(t *testing.T) {
t.Run("success", func(t *testing.T) {
m := PhotoFixtures.Get("Photo01")
assert.Empty(t, m.Files)
m.PreloadFiles(false)
m.PreloadFiles()
assert.NotEmpty(t, m.Files)
})
}

View file

@ -11,7 +11,7 @@ import (
func TestPhoto_Yaml(t *testing.T) {
t.Run("create from fixture", func(t *testing.T) {
m := PhotoFixtures.Get("Photo01")
m.PreloadFiles(true)
m.PreloadFiles()
result, err := m.Yaml()
if err != nil {
@ -25,7 +25,7 @@ func TestPhoto_Yaml(t *testing.T) {
func TestPhoto_SaveAsYaml(t *testing.T) {
t.Run("create from fixture", func(t *testing.T) {
m := PhotoFixtures.Get("Photo01")
m.PreloadFiles(true)
m.PreloadFiles()
fileName := filepath.Join(os.TempDir(), ".photoprism_test.yml")
@ -46,7 +46,7 @@ func TestPhoto_SaveAsYaml(t *testing.T) {
func TestPhoto_YamlFileName(t *testing.T) {
t.Run("create from fixture", func(t *testing.T) {
m := PhotoFixtures.Get("Photo01")
m.PreloadFiles(false)
m.PreloadFiles()
assert.Equal(t, "xxx/2790/02/yyy/Photo01.yml", m.YamlFileName("xxx", "yyy"))
if err := os.RemoveAll("xxx"); err != nil {

View file

@ -603,13 +603,11 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
photo.AddLabels(classify.FaceLabels(faces, entity.SrcImage))
file.PreloadMarkers()
if len(faces) > 0 {
file.AddFaces(faces)
}
photo.PhotoFaces = file.Markers.FaceCount()
photo.PhotoFaces = file.Markers().FaceCount()
}
labels := photo.ClassifyLabels()