People: Add additional face cluster config options #22
This commit is contained in:
parent
38cabca0ed
commit
57aa8811fc
9 changed files with 124 additions and 8 deletions
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
79
internal/config/face_test.go
Normal file
79
internal/config/face_test.go
Normal 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())
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
Loading…
Reference in a new issue