People: Add face clustering and matching to meta worker #22

This commit is contained in:
Michael Mayer 2021-08-14 14:24:48 +02:00
parent 05daa9f7be
commit 42027962aa
6 changed files with 49 additions and 28 deletions

4
go.mod
View file

@ -63,9 +63,9 @@ require (
github.com/ugorji/go v1.2.6 // indirect github.com/ugorji/go v1.2.6 // indirect
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6 github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
github.com/urfave/cli v1.22.5 github.com/urfave/cli v1.22.5
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
gonum.org/v1/gonum v0.9.3 // indirect gonum.org/v1/gonum v0.9.3 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/photoprism/go-tz.v2 v2.1.1 gopkg.in/photoprism/go-tz.v2 v2.1.1

4
go.sum
View file

@ -314,6 +314,8 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e h1:VvfwVmMH40bpMeizC9/K7ipM5Qjucuu16RWfneFPyhQ=
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -378,6 +380,8 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

View file

@ -21,7 +21,7 @@ var FacesCommand = cli.Command{
}, },
{ {
Name: "index", Name: "index",
Usage: "Performs face clustering and recognition", Usage: "Performs face clustering and matching",
Action: facesIndexAction, Action: facesIndexAction,
}, },
}, },
@ -58,7 +58,7 @@ func facesStatsAction(ctx *cli.Context) error {
return nil return nil
} }
// facesIndexAction performs face clustering and recognition. // facesIndexAction performs face clustering and matching.
func facesIndexAction(ctx *cli.Context) error { func facesIndexAction(ctx *cli.Context) error {
start := time.Now() start := time.Now()

View file

@ -16,7 +16,7 @@ import (
"github.com/mpraski/clusters" "github.com/mpraski/clusters"
) )
// Faces represents a worker for face clustering and recognition. // Faces represents a worker for face clustering and matching.
type Faces struct { type Faces struct {
conf *config.Config conf *config.Config
} }
@ -143,19 +143,22 @@ func (w *Faces) Analyze() (err error) {
return nil return nil
} }
// Start face clustering and recognition. // Disabled tests if facial recognition is disabled.
func (w *Faces) Disabled() bool {
return !(w.conf.Experimental() && w.conf.Settings().Features.People)
}
// Start face clustering and matching.
func (w *Faces) Start() (err error) { func (w *Faces) Start() (err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err = fmt.Errorf("faces: %s (panic)\nstack: %s", r, debug.Stack()) err = fmt.Errorf("%s (panic)\nstack: %s", r, debug.Stack())
log.Error(err) log.Errorf("faces: %s", err)
} }
}() }()
if !w.conf.Experimental() { if w.Disabled() {
return fmt.Errorf("faces: experimental features disabled") return fmt.Errorf("facial recognition is disabled")
} else if !w.conf.Settings().Features.People {
return fmt.Errorf("faces: disabled in settings")
} }
if err := mutex.MainWorker.Start(); err != nil { if err := mutex.MainWorker.Start(); err != nil {
@ -266,7 +269,7 @@ func (w *Faces) Start() (err error) {
for _, marker := range markers { for _, marker := range markers {
if mutex.MainWorker.Canceled() { if mutex.MainWorker.Canceled() {
return fmt.Errorf("faces: worker canceled") return fmt.Errorf("worker canceled")
} }
var faceId string var faceId string

View file

@ -32,8 +32,8 @@ func NewMoments(conf *config.Config) *Moments {
func (w *Moments) Start() (err error) { func (w *Moments) Start() (err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err = fmt.Errorf("moments: %s (panic)\nstack: %s", r, debug.Stack()) err = fmt.Errorf("%s (panic)\nstack: %s", r, debug.Stack())
log.Error(err) log.Errorf("moments: %s", err)
} }
}() }()

View file

@ -14,23 +14,23 @@ import (
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
) )
// Meta represents a background metadata maintenance worker. // Meta represents a background metadata optimization worker.
type Meta struct { type Meta struct {
conf *config.Config conf *config.Config
} }
// NewMeta returns a new background metadata maintenance worker. // NewMeta returns a new Meta worker.
func NewMeta(conf *config.Config) *Meta { func NewMeta(conf *config.Config) *Meta {
return &Meta{conf: conf} return &Meta{conf: conf}
} }
// originalsPath returns the original media files path as string. // originalsPath returns the original media files path as string.
func (worker *Meta) originalsPath() string { func (m *Meta) originalsPath() string {
return worker.conf.OriginalsPath() return m.conf.OriginalsPath()
} }
// Start starts the metadata worker. // Start metadata optimization routine.
func (worker *Meta) Start(delay time.Duration) (err error) { func (m *Meta) Start(delay time.Duration) (err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err = fmt.Errorf("metadata: %s (panic)\nstack: %s", r, debug.Stack()) err = fmt.Errorf("metadata: %s (panic)\nstack: %s", r, debug.Stack())
@ -46,13 +46,14 @@ func (worker *Meta) Start(delay time.Duration) (err error) {
log.Debugf("metadata: starting routine check") log.Debugf("metadata: starting routine check")
settings := worker.conf.Settings() settings := m.conf.Settings()
done := make(map[string]bool) done := make(map[string]bool)
limit := 50 limit := 50
offset := 0 offset := 0
optimized := 0 optimized := 0
// Run index optimization.
for { for {
photos, err := query.PhotosCheck(limit, offset, delay) photos, err := query.PhotosCheck(limit, offset, delay)
@ -86,9 +87,9 @@ func (worker *Meta) Start(delay time.Duration) (err error) {
log.Debugf("metadata: optimized photo %s", photo.String()) log.Debugf("metadata: optimized photo %s", photo.String())
} }
for _, m := range merged { for _, p := range merged {
log.Infof("metadata: merged %s", m.PhotoUID) log.Infof("metadata: merged %s", p.PhotoUID)
done[m.PhotoUID] = true done[p.PhotoUID] = true
} }
} }
@ -105,20 +106,33 @@ func (worker *Meta) Start(delay time.Duration) (err error) {
log.Infof("metadata: optimized %d photos", optimized) log.Infof("metadata: optimized %d photos", optimized)
} }
// Explicitly set quality of photos without primary file to -1.
if err := query.ResetPhotoQuality(); err != nil { if err := query.ResetPhotoQuality(); err != nil {
log.Warnf("metadata: %s (reset photo quality)", err.Error()) log.Warnf("metadata: %s (reset photo quality)", err.Error())
} }
// Update photo counts for labels and places.
if err := entity.UpdatePhotoCounts(); err != nil { if err := entity.UpdatePhotoCounts(); err != nil {
log.Warnf("metadata: %s (update photo counts)", err.Error()) log.Warnf("metadata: %s (update photo counts)", err.Error())
} }
moments := photoprism.NewMoments(worker.conf) // Run moments worker.
if w := photoprism.NewMoments(m.conf); w == nil {
if err := moments.Start(); err != nil { log.Errorf("moments: failed creating worker")
} else if err := w.Start(); err != nil {
log.Warnf("moments: %s", err) log.Warnf("moments: %s", err)
} }
// Run faces worker.
if w := photoprism.NewFaces(m.conf); w == nil {
log.Errorf("faces: failed creating worker")
} else if w.Disabled() {
// Do nothing.
} else if err := w.Start(); err != nil {
log.Warnf("faces: %s", err)
}
// Run garbage collection.
runtime.GC() runtime.GC()
return nil return nil