People: Add additional face cluster config options #22

This commit is contained in:
Michael Mayer 2021-10-05 10:12:48 +02:00
parent 38cabca0ed
commit 57aa8811fc
9 changed files with 124 additions and 8 deletions

View file

@ -147,6 +147,8 @@ func configAction(ctx *cli.Context) error {
fmt.Printf("%-25s %d\n", "face-size", conf.FaceSize())
fmt.Printf("%-25s %f\n", "face-score", conf.FaceScore())
fmt.Printf("%-25s %d\n", "face-overlap", conf.FaceOverlap())
fmt.Printf("%-25s %d\n", "face-cluster-size", conf.FaceClusterSize())
fmt.Printf("%-25s %d\n", "face-cluster-score", conf.FaceClusterScore())
fmt.Printf("%-25s %d\n", "face-cluster-core", conf.FaceClusterCore())
fmt.Printf("%-25s %f\n", "face-cluster-dist", conf.FaceClusterDist())
fmt.Printf("%-25s %f\n", "face-match-dist", conf.FaceMatchDist())

View file

@ -143,6 +143,8 @@ func (c *Config) Propagate() {
// Set facial recognition parameters.
face.ScoreThreshold = c.FaceScore()
face.OverlapThreshold = c.FaceOverlap()
face.ClusterScoreThreshold = c.FaceClusterScore()
face.ClusterSizeThreshold = c.FaceClusterSize()
face.ClusterCore = c.FaceClusterCore()
face.ClusterDist = c.FaceClusterDist()
face.MatchDist = c.FaceMatchDist()

View file

@ -5,7 +5,7 @@ import "github.com/photoprism/photoprism/internal/face"
// FaceSize returns the face size threshold in pixels.
func (c *Config) FaceSize() int {
if c.options.FaceSize < 20 || c.options.FaceSize > 10000 {
return 50
return face.SizeThreshold
}
return c.options.FaceSize
@ -29,6 +29,24 @@ func (c *Config) FaceOverlap() int {
return c.options.FaceOverlap
}
// FaceClusterSize returns the size threshold for faces forming a cluster in pixels.
func (c *Config) FaceClusterSize() int {
if c.options.FaceClusterSize < 20 || c.options.FaceClusterSize > 10000 {
return face.ClusterSizeThreshold
}
return c.options.FaceClusterSize
}
// FaceClusterScore returns the quality threshold for faces forming a cluster.
func (c *Config) FaceClusterScore() int {
if c.options.FaceClusterScore < 1 || c.options.FaceClusterScore > 100 {
return face.ClusterScoreThreshold
}
return c.options.FaceClusterScore
}
// FaceClusterCore returns the number of faces forming a cluster core.
func (c *Config) FaceClusterCore() int {
if c.options.FaceClusterCore < 1 || c.options.FaceClusterCore > 100 {

View file

@ -0,0 +1,79 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfig_FaceSize(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 40, c.FaceSize())
c.options.FaceSize = 30
assert.Equal(t, 30, c.FaceSize())
c.options.FaceSize = 1
assert.Equal(t, 40, c.FaceSize())
}
func TestConfig_FaceScore(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 9.0, c.FaceScore())
c.options.FaceScore = 8.5
assert.Equal(t, 8.5, c.FaceScore())
c.options.FaceScore = 0.1
assert.Equal(t, 9.0, c.FaceScore())
}
func TestConfig_FaceOverlap(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 42, c.FaceOverlap())
c.options.FaceOverlap = 300
assert.Equal(t, 42, c.FaceOverlap())
c.options.FaceOverlap = 1
assert.Equal(t, 1, c.FaceOverlap())
}
func TestConfig_FaceClusterSize(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 80, c.FaceClusterSize())
c.options.FaceClusterSize = 10
assert.Equal(t, 80, c.FaceClusterSize())
c.options.FaceClusterSize = 66
assert.Equal(t, 66, c.FaceClusterSize())
}
func TestConfig_FaceClusterScore(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 15, c.FaceClusterScore())
c.options.FaceClusterScore = 0
assert.Equal(t, 15, c.FaceClusterScore())
c.options.FaceClusterScore = 55
assert.Equal(t, 55, c.FaceClusterScore())
}
func TestConfig_FaceClusterCore(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 4, c.FaceClusterCore())
c.options.FaceClusterCore = 1000
assert.Equal(t, 4, c.FaceClusterCore())
c.options.FaceClusterCore = 1
assert.Equal(t, 1, c.FaceClusterCore())
}
func TestConfig_FaceClusterDist(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 0.64, c.FaceClusterDist())
c.options.FaceClusterDist = 0.01
assert.Equal(t, 0.64, c.FaceClusterDist())
c.options.FaceClusterDist = 0.34
assert.Equal(t, 0.34, c.FaceClusterDist())
}
func TestConfig_FaceMatchDist(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 0.46, c.FaceMatchDist())
c.options.FaceMatchDist = 0.1
assert.Equal(t, 0.1, c.FaceMatchDist())
c.options.FaceMatchDist = 0.01
assert.Equal(t, 0.46, c.FaceMatchDist())
}

View file

@ -440,8 +440,8 @@ var GlobalFlags = []cli.Flag{
},
cli.IntFlag{
Name: "face-size",
Usage: "min face size in `PIXELS`",
Value: 50,
Usage: "face size threshold in `PIXELS`",
Value: face.SizeThreshold,
EnvVar: "PHOTOPRISM_FACE_SIZE",
},
cli.Float64Flag{
@ -456,6 +456,18 @@ var GlobalFlags = []cli.Flag{
Value: face.OverlapThreshold,
EnvVar: "PHOTOPRISM_FACE_OVERLAP",
},
cli.IntFlag{
Name: "face-cluster-size",
Usage: "size threshold for faces forming a cluster in `PIXELS`",
Value: face.ClusterSizeThreshold,
EnvVar: "PHOTOPRISM_FACE_CLUSTER_SIZE",
},
cli.IntFlag{
Name: "face-cluster-score",
Usage: "`QUALITY` threshold for faces forming a cluster",
Value: face.ClusterScoreThreshold,
EnvVar: "PHOTOPRISM_FACE_CLUSTER_SCORE",
},
cli.IntFlag{
Name: "face-cluster-core",
Usage: "`NUMBER` of faces forming a cluster core",

View file

@ -117,6 +117,8 @@ type Options struct {
FaceSize int `yaml:"-" json:"-" flag:"face-size"`
FaceScore float64 `yaml:"-" json:"-" flag:"face-score"`
FaceOverlap int `yaml:"-" json:"-" flag:"face-overlap"`
FaceClusterSize int `yaml:"-" json:"-" flag:"face-cluster-size"`
FaceClusterScore int `yaml:"-" json:"-" flag:"face-cluster-score"`
FaceClusterCore int `yaml:"-" json:"-" flag:"face-cluster-core"`
FaceClusterDist float64 `yaml:"-" json:"-" flag:"face-cluster-dist"`
FaceMatchDist float64 `yaml:"-" json:"-" flag:"face-match-dist"`

View file

@ -458,7 +458,7 @@ func (m *Marker) Face() (f *Face) {
// Add face if size
if m.SubjSrc != SrcAuto && m.FaceID == "" {
if m.Size < face.ClusterMinSize || m.Score < face.ClusterMinScore {
if m.Size < face.ClusterSizeThreshold || m.Score < face.ClusterScoreThreshold {
log.Debugf("marker: skipped adding face due to low-quality (uid %s, size %d, score %d)", txt.Quote(m.MarkerUID), m.Size, m.Score)
return nil
} else if emb := m.Embeddings(); emb.Empty() {

View file

@ -8,12 +8,13 @@ var CropSize = crop.Sizes[crop.Tile160]
var MatchDist = 0.46
var ClusterDist = 0.64
var ClusterCore = 4
var ClusterMinScore = 15
var ClusterMinSize = 80
var ClusterScoreThreshold = 15
var ClusterSizeThreshold = 80
var SampleThreshold = 2 * ClusterCore
var OverlapThreshold = 42
var OverlapThresholdFloor = OverlapThreshold - 1
var ScoreThreshold = 9.0
var SizeThreshold = 40
// QualityThreshold returns the scale adjusted quality score threshold.
func QualityThreshold(scale int) (score float32) {

View file

@ -20,13 +20,13 @@ func (w *Faces) Cluster(opt FacesOptions) (added entity.Faces, err error) {
// Skip clustering if index contains no new face markers, and force option isn't set.
if opt.Force {
log.Infof("faces: forced clustering")
} else if n := query.CountNewFaceMarkers(face.ClusterMinSize, face.ClusterMinScore); n < opt.SampleThreshold() {
} else if n := query.CountNewFaceMarkers(face.ClusterSizeThreshold, face.ClusterScoreThreshold); n < opt.SampleThreshold() {
log.Debugf("faces: skipping clustering")
return added, nil
}
// Fetch unclustered face embeddings.
embeddings, err := query.Embeddings(false, true, face.ClusterMinSize, face.ClusterMinScore)
embeddings, err := query.Embeddings(false, true, face.ClusterSizeThreshold, face.ClusterScoreThreshold)
log.Debugf("faces: %d unclustered samples found", len(embeddings))