People: Improve face thumbnails on overview page #22

This commit is contained in:
Michael Mayer 2021-09-17 18:51:24 +02:00
parent 885b2b0e00
commit 8492efebcf
15 changed files with 131 additions and 56 deletions

View file

@ -38,8 +38,8 @@ export class Subject extends RestModel {
getDefaults() {
return {
UID: "",
Thumb: "",
ThumbSrc: "",
MarkerUID: "",
MarkerSrc: "",
Type: "",
Src: "",
Slug: "",
@ -51,6 +51,8 @@ export class Subject extends RestModel {
Private: false,
Excluded: false,
FileCount: 0,
FileHash: "",
CropArea: "",
Metadata: {},
CreatedAt: "",
UpdatedAt: "",
@ -82,11 +84,21 @@ export class Subject extends RestModel {
}
thumbnailUrl(size) {
if (this.Thumb) {
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
} else {
if (!this.FileHash) {
return `${config.contentUri}/svg/portrait`;
}
if (!size) {
size = "tile_160";
}
if (this.CropArea && (size === "tile_160" || size === "tile_320")) {
return `${config.contentUri}/t/${this.FileHash}/${config.previewToken()}/${size}/${
this.CropArea
}`;
} else {
return `${config.contentUri}/t/${this.FileHash}/${config.previewToken()}/${size}`;
}
}
getDateString() {

View file

@ -64,7 +64,7 @@
>
<div class="card-background accent lighten-3"></div>
<v-img
:src="model.thumbnailUrl('tile_500')"
:src="model.thumbnailUrl('tile_320')"
:alt="model.Name"
:transition="false"
aspect-ratio="1"
@ -509,8 +509,6 @@ export default {
return;
}
console.log("onUpdate", ev, data);
const type = ev.split('.')[1];
switch (type) {

View file

@ -263,6 +263,7 @@ export default [
path: "/people",
component: Subjects,
meta: { title: $gettext("People"), auth: true },
props: { staticFilter: { files: 1, type: "person" } },
},
{
name: "library",

View file

@ -89,15 +89,25 @@ func UpdateMarker(router *gin.RouterGroup) {
}
// Save marker.
if err := marker.SaveForm(markerForm); err != nil {
if changed, err := marker.SaveForm(markerForm); err != nil {
log.Errorf("photo: %s (save marker form)", err)
AbortSaveFailed(c)
return
} else if marker.SubjUID != "" && marker.SubjSrc == entity.SrcManual && marker.FaceID != "" {
if res, err := service.Faces().Optimize(); err != nil {
log.Errorf("faces: %s (optimize)", err)
} else if res.Merged > 0 {
log.Infof("faces: %d clusters merged", res.Merged)
} else if changed {
if marker.FaceID != "" && marker.SubjUID != "" && marker.SubjSrc == entity.SrcManual {
if res, err := service.Faces().Optimize(); err != nil {
log.Errorf("faces: %s (optimize)", err)
} else if res.Merged > 0 {
log.Infof("faces: %d clusters merged", res.Merged)
}
}
if err := query.UpdateSubjectPreviews(); err != nil {
log.Errorf("faces: %s (update previews)", err)
}
if err := entity.UpdateSubjectFileCounts(); err != nil {
log.Errorf("faces: %s (update counts)", err)
}
}
@ -138,6 +148,10 @@ func ClearMarkerSubject(router *gin.RouterGroup) {
log.Errorf("faces: %s (clear subject)", err)
AbortSaveFailed(c)
return
} else if err := query.UpdateSubjectPreviews(); err != nil {
log.Errorf("faces: %s (update previews)", err)
} else if err := entity.UpdateSubjectFileCounts(); err != nil {
log.Errorf("faces: %s (update counts)", err)
}
// Update photo metadata.

View file

@ -48,6 +48,7 @@ type Marker struct {
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"`
Q int `json:"Q" yaml:"Q,omitempty"`
Size int `gorm:"default:-1" json:"Size" yaml:"Size,omitempty"`
Score int `gorm:"type:SMALLINT" json:"Score" yaml:"Score,omitempty"`
MatchedAt *time.Time `sql:"index" json:"MatchedAt" yaml:"MatchedAt,omitempty"`
@ -93,6 +94,7 @@ func NewFaceMarker(f face.Face, file File, subjUID string) *Marker {
m := NewMarker(file, f.CropArea(), subjUID, SrcImage, MarkerFace)
m.Size = f.Size()
m.Q = int(float32(math.Log(float64(f.Score))) * float32(m.Size) * m.W)
m.Score = f.Score
m.MarkerReview = f.Score < 30
m.FaceDist = -1
@ -113,9 +115,7 @@ func (m *Marker) Update(attr string, value interface{}) error {
}
// SaveForm updates the entity using form data and stores it in the database.
func (m *Marker) SaveForm(f form.Marker) error {
changed := false
func (m *Marker) SaveForm(f form.Marker) (changed bool, err error) {
if m.MarkerInvalid != f.MarkerInvalid {
m.MarkerInvalid = f.MarkerInvalid
changed = true
@ -126,22 +126,22 @@ func (m *Marker) SaveForm(f form.Marker) error {
changed = true
}
if f.SubjSrc == SrcManual && strings.TrimSpace(f.MarkerName) != "" {
if f.SubjSrc == SrcManual && strings.TrimSpace(f.MarkerName) != "" && f.MarkerName != m.MarkerName {
m.SubjSrc = SrcManual
m.MarkerName = txt.Title(txt.Clip(f.MarkerName, txt.ClipDefault))
if err := m.SyncSubject(true); err != nil {
return err
return changed, err
}
changed = true
}
if changed {
return m.Save()
return changed, m.Save()
}
return nil
return changed, nil
}
// HasFace tests if the marker already has the best matching face.
@ -551,8 +551,9 @@ func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
"Y": m.Y,
"W": m.W,
"H": m.H,
"Score": m.Score,
"Q": m.Q,
"Size": m.Size,
"Score": m.Score,
"LandmarksJSON": m.LandmarksJSON,
"EmbeddingsJSON": m.EmbeddingsJSON,
})

View file

@ -33,6 +33,7 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
Y float32
W float32 `json:",omitempty"`
H float32 `json:",omitempty"`
Q int `json:",omitempty"`
Size int `json:",omitempty"`
Score int `json:",omitempty"`
CreatedAt time.Time
@ -53,6 +54,7 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
Y: m.Y,
W: m.W,
H: m.H,
Q: m.Q,
Size: m.Size,
Score: m.Score,
CreatedAt: m.CreatedAt,

View file

@ -49,12 +49,13 @@ func TestMarker_SaveForm(t *testing.T) {
f := form.Marker{SubjSrc: SrcManual, MarkerName: "Jane Doe", MarkerInvalid: false}
err := m.SaveForm(f)
changed, err := m.SaveForm(f)
if err != nil {
t.Fatal(err)
}
assert.True(t, changed)
assert.NotEmpty(t, m.SubjUID)
if s := m.Subject(); s != nil {
@ -72,8 +73,10 @@ func TestMarker_SaveForm(t *testing.T) {
if m := FindMarker("mt9k3pw1wowuy777"); m == nil {
t.Fatal("result is nil")
} else if err := m.SaveForm(f3); err != nil {
} else if changed, err := m.SaveForm(f3); err != nil {
t.Fatal(err)
} else {
assert.True(t, changed)
}
if m := FindMarker("mt9k3pw1wowuy666"); m != nil {

View file

@ -43,8 +43,8 @@ func LabelCounts() LabelPhotoCounts {
return result
}
// UpdatePhotoCounts updates static photos counts and visibilities.
func UpdatePhotoCounts() (err error) {
// UpdatePlacesPhotoCounts updates the places photo counts.
func UpdatePlacesPhotoCounts() (err error) {
start := time.Now()
// Update places.
@ -57,9 +57,14 @@ func UpdatePhotoCounts() (err error) {
return err
}
log.Debugf("places: updating photo counts completed in %s", time.Since(start))
log.Debugf("counts: updated places [%s]", time.Since(start))
start = time.Now()
return nil
}
// UpdateSubjectFileCounts updates the subject file counts.
func UpdateSubjectFileCounts() (err error) {
start := time.Now()
// Update subjects.
if err = Db().Table(Subject{}.TableName()).
@ -72,11 +77,15 @@ func UpdatePhotoCounts() (err error) {
return err
}
log.Debugf("subjects: updating file counts completed in %s", time.Since(start))
log.Debugf("counts: updated subjects [%s]", time.Since(start))
start = time.Now()
return nil
}
// UpdateLabelPhotoCounts updates the label photo counts.
func UpdateLabelPhotoCounts() (err error) {
start := time.Now()
// Update labels.
if IsDialect(MySQL) {
if err = Db().
Table("labels").
@ -129,7 +138,24 @@ func UpdatePhotoCounts() (err error) {
return fmt.Errorf("unknown sql dialect %s", DbDialect())
}
log.Debugf("labels: updating photo counts completed in %s", time.Since(start))
log.Debugf("counts: updated labels [%s]", time.Since(start))
return nil
}
// UpdatePhotoCounts updates static photos counts and visibilities.
func UpdatePhotoCounts() (err error) {
if err = UpdatePlacesPhotoCounts(); err != nil {
return err
}
if err = UpdateSubjectFileCounts(); err != nil {
return err
}
if err = UpdateLabelPhotoCounts(); err != nil {
return err
}
/* TODO: Slow with many photos due to missing index.
start = time.Now()
@ -151,7 +177,7 @@ func UpdatePhotoCounts() (err error) {
}
}
log.Debugf("calendar: updating visibility completed in %s", time.Since(start))
log.Debugf("calendar: updating visibility completed [%s]", time.Since(start))
*/
return nil

View file

@ -21,8 +21,8 @@ type Subjects []Subject
// Subject represents a named photo subject, typically a person.
type Subject struct {
SubjUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
MarkerUID string `gorm:"type:VARBINARY(42);index" json:"MarkerUID" yaml:"MarkerUID,omitempty"`
MarkerSrc string `gorm:"type:VARBINARY(8);default:''" json:"MarkerSrc,omitempty" yaml:"MarkerSrc,omitempty"`
SubjType string `gorm:"type:VARBINARY(8);default:''" json:"Type,omitempty" yaml:"Type,omitempty"`
SubjSrc string `gorm:"type:VARBINARY(8);default:''" json:"Src,omitempty" yaml:"Src,omitempty"`
SubjSlug string `gorm:"type:VARBINARY(255);index;default:''" json:"Slug" yaml:"-"`

View file

@ -19,7 +19,6 @@ type Person struct {
SubjName string `json:"Name"`
SubjAlias string `json:"Alias"`
SubjFavorite bool `json:"Favorite"`
Thumb string `json:"Thumb"`
}
// NewPerson returns a new entity.
@ -29,7 +28,6 @@ func NewPerson(subj Subject) *Person {
SubjName: subj.SubjName,
SubjAlias: subj.SubjAlias,
SubjFavorite: subj.SubjFavorite,
Thumb: subj.Thumb,
}
return result
@ -42,12 +40,10 @@ func (m *Person) MarshalJSON() ([]byte, error) {
Name string
Keywords []string `json:",omitempty"`
Favorite bool `json:",omitempty"`
Thumb string `json:",omitempty"`
}{
UID: m.SubjUID,
Name: m.SubjName,
Keywords: txt.NameKeywords(m.SubjName, m.SubjAlias),
Favorite: m.SubjFavorite,
Thumb: m.Thumb,
})
}

View file

@ -13,7 +13,6 @@ func TestNewPerson(t *testing.T) {
SubjName: "William Henry Gates III",
SubjAlias: "Windows Guru",
SubjFavorite: true,
Thumb: "622c7287967f2800e873fbc55f0328973056ce1d",
}
m := NewPerson(subj)
@ -22,7 +21,6 @@ func TestNewPerson(t *testing.T) {
assert.Equal(t, "William Henry Gates III", m.SubjName)
assert.Equal(t, "Windows Guru", m.SubjAlias)
assert.Equal(t, true, m.SubjFavorite)
assert.Equal(t, "622c7287967f2800e873fbc55f0328973056ce1d", m.Thumb)
if j, err := m.MarshalJSON(); err != nil {
t.Fatal(err)
@ -31,7 +29,7 @@ func TestNewPerson(t *testing.T) {
expected := "{\"UID\":\"jqytw12v8jjeu3e6\",\"Name\":\"William Henry Gates III\"," +
"\"Keywords\":[\"william\",\"henry\",\"gates\",\"iii\",\"windows\",\"guru\"]," +
"\"Favorite\":true,\"Thumb\":\"622c7287967f2800e873fbc55f0328973056ce1d\"}"
"\"Favorite\":true}"
assert.Equal(t, expected, s)
t.Logf("person json: %s", s)

View file

@ -10,6 +10,7 @@ type SubjectSearch struct {
Favorite bool `form:"favorite"`
Private bool `form:"private"`
Excluded bool `form:"excluded"`
Files int `form:"files"`
Count int `form:"count" binding:"required" serialize:"-"`
Offset int `form:"offset" serialize:"-"`
Order string `form:"order" serialize:"-"`

View file

@ -21,7 +21,7 @@ func UpdateAlbumDefaultPreviews() (err error) {
ORDER BY p.taken_at DESC LIMIT 1
) WHERE thumb_src='' AND album_type = 'album' AND deleted_at IS NULL`)).Error
log.Debugf("albums: updating previews completed in %s", time.Since(start))
log.Debugf("previews: updated albums [%s]", time.Since(start))
return err
}
@ -39,7 +39,7 @@ func UpdateAlbumFolderPreviews() (err error) {
) WHERE thumb_src = '' AND album_type = 'folder' AND deleted_at IS NULL`)).
Error
log.Debugf("folders: updating previews completed in %s", time.Since(start))
log.Debugf("previews: updated folders [%s]", time.Since(start))
return err
}
@ -80,7 +80,7 @@ func UpdateAlbumMonthPreviews() (err error) {
return nil
}
*/
log.Debugf("calendar: updating previews completed in %s", time.Since(start))
log.Debugf("previews: updated calendar [%s]", time.Since(start))
return err
}
@ -122,7 +122,7 @@ func UpdateLabelPreviews() (err error) {
return err
}
log.Debugf("labels: updating previews completed in %s", time.Since(start))
log.Debugf("previews: updated labels [%s]", time.Since(start))
return nil
}
@ -145,7 +145,7 @@ func UpdateCategoryPreviews() (err error) {
return err
}
log.Debugf("categories: updating previews completed in %s", time.Since(start))
log.Debugf("previews: updated categories [%s]", time.Since(start))
return nil
}
@ -170,16 +170,28 @@ func UpdateSubjectPreviews() (err error) {
Error */
err = Db().Table(entity.Subject{}.TableName()).
UpdateColumn("thumb", gorm.Expr("(SELECT m.file_hash FROM "+
UpdateColumn("marker_uid", gorm.Expr("(SELECT m.marker_uid FROM "+
fmt.Sprintf(
"%s m WHERE m.subj_uid = %s.subj_uid AND m.subj_src = 'manual' ",
entity.Marker{}.TableName(),
entity.Subject{}.TableName())+
` AND m.file_hash <> '' ORDER BY m.size DESC LIMIT 1)
WHERE thumb_src='' AND deleted_at IS NULL`)).
` AND m.file_hash <> '' ORDER BY m.q DESC LIMIT 1)
WHERE marker_src = '' AND deleted_at IS NULL`)).
Error
log.Debugf("subjects: updating previews completed in %s", time.Since(start))
/** err = Db().Table(entity.Subject{}.TableName()).
UpdateColumn("thumb", gorm.Expr("(SELECT m.file_hash FROM "+
fmt.Sprintf(
"%s m WHERE m.subj_uid = %s.subj_uid AND m.subj_src = 'manual' ",
entity.Marker{}.TableName(),
entity.Subject{}.TableName())+
` AND m.file_hash <> '' ORDER BY m.w DESC LIMIT 1)
WHERE thumb_src = '' AND deleted_at IS NULL`)).
Error
*/
log.Debugf("previews: updated subjects [%s]", time.Since(start))
return err
}

View file

@ -14,6 +14,8 @@ import (
// SubjectResult represents a subject search result.
type SubjectResult struct {
SubjUID string `json:"UID"`
MarkerUID string `json:"MarkerUID"`
MarkerSrc string `json:"MarkerSrc,omitempty"`
SubjType string `json:"Type"`
SubjSlug string `json:"Slug"`
SubjName string `json:"Name"`
@ -22,7 +24,8 @@ type SubjectResult struct {
SubjPrivate bool `json:"Private"`
SubjExcluded bool `json:"Excluded"`
FileCount int `json:"FileCount"`
Thumb string `json:"Thumb"`
FileHash string `json:"FileHash"`
CropArea string `json:"CropArea"`
}
// SubjectResults represents subject search results.
@ -38,7 +41,10 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
// Base query.
s := UnscopedDb().Table(entity.Subject{}.TableName()).
Select("subj_uid, subj_slug, subj_name, subj_alias, subj_type, thumb, subj_favorite, subj_private, subj_excluded, file_count")
Select(fmt.Sprintf("%s.*, m.file_hash, m.crop_area", entity.Subject{}.TableName()))
// Join markers table for face thumbs.
s = s.Joins(fmt.Sprintf("LEFT JOIN %s m ON m.marker_uid = %s.marker_uid", entity.Marker{}.TableName(), entity.Subject{}.TableName()))
// Limit result count.
if f.Count > 0 && f.Count <= MaxResults {
@ -54,7 +60,7 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
case "count":
s = s.Order("file_count DESC")
case "added":
s = s.Order("created_at DESC")
s = s.Order(fmt.Sprintf("%s.created_at DESC", entity.Subject{}.TableName()))
case "relevance":
s = s.Order("subj_favorite DESC, subj_name")
default:
@ -62,7 +68,7 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
}
if f.ID != "" {
s = s.Where("subj_uid IN (?)", strings.Split(f.ID, Or))
s = s.Where(fmt.Sprintf("%s.subj_uid IN (?)", entity.Subject{}.TableName()), strings.Split(f.ID, Or))
if result := s.Scan(&results); result.Error != nil {
return results, result.Error
@ -77,6 +83,10 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
}
}
if f.Files > 0 {
s = s.Where("file_count >= ?", f.Files)
}
if f.Type != "" {
s = s.Where("subj_type IN (?)", strings.Split(f.Type, Or))
}
@ -94,7 +104,7 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
}
// Omit deleted rows.
s = s.Where("deleted_at IS NULL")
s = s.Where(fmt.Sprintf("%s.deleted_at IS NULL", entity.Subject{}.TableName()))
if result := s.Scan(&results); result.Error != nil {
return results, result.Error

View file

@ -14,6 +14,7 @@ func TestSubjectSearch(t *testing.T) {
t.Run("FindAll", func(t *testing.T) {
results, err := SubjectSearch(form.SubjectSearch{Type: entity.SubjPerson})
assert.NoError(t, err)
// t.Logf("Subjects: %#v", results)
assert.LessOrEqual(t, 3, len(results))
})