From ee2b49ef4ba54c0a5c074b5169a36f72d451b334 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Thu, 23 Sep 2021 13:16:05 +0200 Subject: [PATCH] People: Add config options for face detection and clustering #22 --- internal/commands/config.go | 7 ++ internal/config/config.go | 31 +++-- internal/config/face.go | 48 +++++++ internal/config/flags.go | 32 +++++ internal/config/options.go | 180 +++++++++++++++------------ internal/entity/face.go | 2 +- internal/face/detector.go | 4 +- internal/face/thresholds.go | 26 ++-- internal/face/thresholds_test.go | 16 +-- internal/photoprism/faces_audit.go | 2 +- internal/photoprism/faces_cluster.go | 2 +- internal/query/faces.go | 2 +- 12 files changed, 232 insertions(+), 120 deletions(-) create mode 100644 internal/config/face.go diff --git a/internal/commands/config.go b/internal/commands/config.go index 8ff15d711..8d95523e9 100644 --- a/internal/commands/config.go +++ b/internal/commands/config.go @@ -141,5 +141,12 @@ func configAction(ctx *cli.Context) error { fmt.Printf("%-25s %d\n", "jpeg-size", conf.JpegSize()) fmt.Printf("%-25s %d\n", "jpeg-quality", conf.JpegQuality()) + // Facial recognition. + 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-core", conf.FaceClusterCore()) + fmt.Printf("%-25s %f\n", "face-cluster-dist", conf.FaceClusterDist()) + fmt.Printf("%-25s %f\n", "face-match-dist", conf.FaceMatchDist()) + return nil } diff --git a/internal/config/config.go b/internal/config/config.go index 426702c37..bc7a49575 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,27 +13,26 @@ import ( "sync" "time" - "github.com/dustin/go-humanize" - - "github.com/pbnjay/memory" - - "github.com/photoprism/photoprism/pkg/fs" - "github.com/photoprism/photoprism/pkg/txt" - - "github.com/photoprism/photoprism/internal/entity" - "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/sqlite" + + "github.com/dustin/go-humanize" "github.com/klauspost/cpuid/v2" + "github.com/pbnjay/memory" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" + + "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/internal/face" "github.com/photoprism/photoprism/internal/hub" "github.com/photoprism/photoprism/internal/hub/places" "github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/internal/thumb" + "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/rnd" - "github.com/sirupsen/logrus" - "github.com/urfave/cli" + "github.com/photoprism/photoprism/pkg/txt" ) var log = event.Log @@ -131,13 +130,23 @@ func (c *Config) Options() *Options { func (c *Config) Propagate() { log.SetLevel(c.LogLevel()) + // Set thumbnail generation parameters. thumb.SizePrecached = c.ThumbSizePrecached() thumb.SizeUncached = c.ThumbSizeUncached() thumb.Filter = c.ThumbFilter() thumb.JpegQuality = c.JpegQuality() + + // Set geocoding parameters. places.UserAgent = c.UserAgent() entity.GeoApi = c.GeoApi() + // Set facial recognition parameters. + face.ScoreThreshold = c.FaceScore() + face.OverlapThreshold = c.FaceOverlap() + face.ClusterCore = c.FaceClusterCore() + face.ClusterDist = c.FaceClusterDist() + face.MatchDist = c.FaceMatchDist() + c.Settings().Propagate() c.Hub().Propagate() } diff --git a/internal/config/face.go b/internal/config/face.go new file mode 100644 index 000000000..ec483529b --- /dev/null +++ b/internal/config/face.go @@ -0,0 +1,48 @@ +package config + +import "github.com/photoprism/photoprism/internal/face" + +// FaceScore returns the face quality score threshold. +func (c *Config) FaceScore() float64 { + if c.options.FaceScore < 1 || c.options.FaceScore > 100 { + return face.ScoreThreshold + } + + return c.options.FaceScore +} + +// FaceOverlap returns the image area overlap threshold for faces in percent. +func (c *Config) FaceOverlap() int { + if c.options.FaceOverlap < 1 || c.options.FaceOverlap > 100 { + return face.OverlapThreshold + } + + return c.options.FaceOverlap +} + +// FaceClusterCore returns the number of faces forming a cluster core. +func (c *Config) FaceClusterCore() int { + if c.options.FaceClusterCore < 1 || c.options.FaceClusterCore > 100 { + return face.ClusterCore + } + + return c.options.FaceClusterCore +} + +// FaceClusterDist returns the radius of faces forming a cluster core. +func (c *Config) FaceClusterDist() float64 { + if c.options.FaceClusterDist < 0.1 || c.options.FaceClusterDist > 1.5 { + return face.ClusterDist + } + + return c.options.FaceClusterDist +} + +// FaceMatchDist returns the offset distance when matching faces with clusters. +func (c *Config) FaceMatchDist() float64 { + if c.options.FaceMatchDist < 0.1 || c.options.FaceMatchDist > 1.5 { + return face.MatchDist + } + + return c.options.FaceMatchDist +} diff --git a/internal/config/flags.go b/internal/config/flags.go index 9c704ddab..9ac2fffbf 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -3,6 +3,8 @@ package config import ( "github.com/klauspost/cpuid/v2" "github.com/urfave/cli" + + "github.com/photoprism/photoprism/internal/face" ) // GlobalFlags describes global command-line parameters and flags. @@ -426,4 +428,34 @@ var GlobalFlags = []cli.Flag{ Value: 92, EnvVar: "PHOTOPRISM_JPEG_QUALITY", }, + cli.Float64Flag{ + Name: "face-score", + Usage: "face `QUALITY` threshold", + Value: face.ScoreThreshold, + EnvVar: "PHOTOPRISM_FACE_SCORE", + }, + cli.IntFlag{ + Name: "face-overlap", + Usage: "image area overlap threshold in `PERCENT`", + Value: face.OverlapThreshold, + EnvVar: "PHOTOPRISM_FACE_OVERLAP", + }, + cli.IntFlag{ + Name: "face-cluster-core", + Usage: "`NUMBER` of faces forming a cluster core", + Value: face.ClusterCore, + EnvVar: "PHOTOPRISM_FACE_CLUSTER_CORE", + }, + cli.Float64Flag{ + Name: "face-cluster-dist", + Usage: "`RADIUS` of faces forming a cluster core", + Value: face.ClusterDist, + EnvVar: "PHOTOPRISM_FACE_CLUSTER_DIST", + }, + cli.Float64Flag{ + Name: "face-match-dist", + Usage: "`OFFSET` distance when matching faces with clusters", + Value: face.MatchDist, + EnvVar: "PHOTOPRISM_FACE_MATCH_DIST", + }, } diff --git a/internal/config/options.go b/internal/config/options.go index 8d7f1bc0a..477264364 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -30,88 +30,93 @@ const ( // // See https://github.com/photoprism/photoprism/issues/50#issuecomment-433856358 type Options struct { - Name string `json:"-"` - Version string `json:"-"` - Copyright string `json:"-"` - Debug bool `yaml:"Debug" json:"Debug" flag:"debug"` - Test bool `yaml:"-" json:"Test,omitempty" flag:"test"` - Demo bool `yaml:"Demo" json:"-" flag:"demo"` - Sponsor bool `yaml:"-" json:"-" flag:"sponsor"` - Public bool `yaml:"Public" json:"-" flag:"public"` - ReadOnly bool `yaml:"ReadOnly" json:"ReadOnly" flag:"read-only"` - Experimental bool `yaml:"Experimental" json:"Experimental" flag:"experimental"` - ConfigPath string `yaml:"ConfigPath" json:"-" flag:"config-path"` - ConfigFile string `json:"-"` - AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"` - OriginalsPath string `yaml:"OriginalsPath" json:"-" flag:"originals-path"` - OriginalsLimit int64 `yaml:"OriginalsLimit" json:"OriginalsLimit" flag:"originals-limit"` - ImportPath string `yaml:"ImportPath" json:"-" flag:"import-path"` - StoragePath string `yaml:"StoragePath" json:"-" flag:"storage-path"` - SidecarPath string `yaml:"SidecarPath" json:"-" flag:"sidecar-path"` - TempPath string `yaml:"TempPath" json:"-" flag:"temp-path"` - BackupPath string `yaml:"BackupPath" json:"-" flag:"backup-path"` - AssetsPath string `yaml:"AssetsPath" json:"-" flag:"assets-path"` - CachePath string `yaml:"CachePath" json:"-" flag:"cache-path"` - Workers int `yaml:"Workers" json:"Workers" flag:"workers"` - WakeupInterval int `yaml:"WakeupInterval" json:"WakeupInterval" flag:"wakeup-interval"` - AutoIndex int `yaml:"AutoIndex" json:"AutoIndex" flag:"auto-index"` - AutoImport int `yaml:"AutoImport" json:"AutoImport" flag:"auto-import"` - DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"` - DisableWebDAV bool `yaml:"DisableWebDAV" json:"DisableWebDAV" flag:"disable-webdav"` - DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"` - DisablePlaces bool `yaml:"DisablePlaces" json:"DisablePlaces" flag:"disable-places"` - DisableExifTool bool `yaml:"DisableExifTool" json:"DisableExifTool" flag:"disable-exiftool"` - DisableTensorFlow bool `yaml:"DisableTensorFlow" json:"DisableTensorFlow" flag:"disable-tensorflow"` - DisableFFmpeg bool `yaml:"DisableFFmpeg" json:"DisableFFmpeg" flag:"disable-ffmpeg"` - DisableDarktable bool `yaml:"DisableDarktable" json:"DisableDarktable" flag:"disable-darktable"` - DisableRawtherapee bool `yaml:"DisableRawtherapee" json:"DisableRawtherapee" flag:"disable-rawtherapee"` - DisableSips bool `yaml:"DisableSips" json:"DisableSips" flag:"disable-sips"` - DisableHeifConvert bool `yaml:"DisableHeifConvert" json:"DisableHeifConvert" flag:"disable-heifconvert"` - DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"` - UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"` - LogLevel string `yaml:"LogLevel" json:"-" flag:"log-level"` - LogFilename string `yaml:"LogFilename" json:"-" flag:"log-filename"` - PIDFilename string `yaml:"PIDFilename" json:"-" flag:"pid-filename"` - CdnUrl string `yaml:"CdnUrl" json:"CdnUrl" flag:"cdn-url"` - SiteUrl string `yaml:"SiteUrl" json:"SiteUrl" flag:"site-url"` - SitePreview string `yaml:"SitePreview" json:"SitePreview" flag:"site-preview"` - SiteTitle string `yaml:"SiteTitle" json:"SiteTitle" flag:"site-title"` - SiteCaption string `yaml:"SiteCaption" json:"SiteCaption" flag:"site-caption"` - SiteDescription string `yaml:"SiteDescription" json:"SiteDescription" flag:"site-description"` - SiteAuthor string `yaml:"SiteAuthor" json:"SiteAuthor" flag:"site-author"` - DatabaseDriver string `yaml:"DatabaseDriver" json:"-" flag:"database-driver"` - DatabaseDsn string `yaml:"DatabaseDsn" json:"-" flag:"database-dsn"` - DatabaseServer string `yaml:"DatabaseServer" json:"-" flag:"database-server"` - DatabaseName string `yaml:"DatabaseName" json:"-" flag:"database-name"` - DatabaseUser string `yaml:"DatabaseUser" json:"-" flag:"database-user"` - DatabasePassword string `yaml:"DatabasePassword" json:"-" flag:"database-password"` - DatabaseConns int `yaml:"DatabaseConns" json:"-" flag:"database-conns"` - DatabaseConnsIdle int `yaml:"DatabaseConnsIdle" json:"-" flag:"database-conns-idle"` - HttpHost string `yaml:"HttpHost" json:"-" flag:"http-host"` - HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"` - HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"` - HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"` - RawPresets bool `yaml:"RawPresets" json:"RawPresets" flag:"raw-presets"` - DarktableBin string `yaml:"DarktableBin" json:"-" flag:"darktable-bin"` - DarktableBlacklist string `yaml:"DarktableBlacklist" json:"-" flag:"darktable-blacklist"` - RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"` - RawtherapeeBlacklist string `yaml:"RawtherapeeBlacklist" json:"-" flag:"rawtherapee-blacklist"` - SipsBin string `yaml:"SipsBin" json:"-" flag:"sips-bin"` - HeifConvertBin string `yaml:"HeifConvertBin" json:"-" flag:"heifconvert-bin"` - FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"` - FFmpegEncoder string `yaml:"FFmpegEncoder" json:"FFmpegEncoder" flag:"ffmpeg-encoder"` - FFmpegBitrate int `yaml:"FFmpegBitrate" json:"FFmpegBitrate" flag:"ffmpeg-bitrate"` - FFmpegBuffers int `yaml:"FFmpegBuffers" json:"FFmpegBuffers" flag:"ffmpeg-buffers"` - ExifToolBin string `yaml:"ExifToolBin" json:"-" flag:"exiftool-bin"` - DetachServer bool `yaml:"DetachServer" json:"-" flag:"detach-server"` - DownloadToken string `yaml:"DownloadToken" json:"-" flag:"download-token"` - PreviewToken string `yaml:"PreviewToken" json:"-" flag:"preview-token"` - ThumbFilter string `yaml:"ThumbFilter" json:"ThumbFilter" flag:"thumb-filter"` - ThumbUncached bool `yaml:"ThumbUncached" json:"ThumbUncached" flag:"thumb-uncached"` - ThumbSize int `yaml:"ThumbSize" json:"ThumbSize" flag:"thumb-size"` - ThumbSizeUncached int `yaml:"ThumbSizeUncached" json:"ThumbSizeUncached" flag:"thumb-size-uncached"` - JpegSize int `yaml:"JpegSize" json:"JpegSize" flag:"jpeg-size"` - JpegQuality int `yaml:"JpegQuality" json:"JpegQuality" flag:"jpeg-quality"` + Name string `json:"-"` + Version string `json:"-"` + Copyright string `json:"-"` + Debug bool `yaml:"Debug" json:"Debug" flag:"debug"` + Test bool `yaml:"-" json:"Test,omitempty" flag:"test"` + Demo bool `yaml:"Demo" json:"-" flag:"demo"` + Sponsor bool `yaml:"-" json:"-" flag:"sponsor"` + Public bool `yaml:"Public" json:"-" flag:"public"` + ReadOnly bool `yaml:"ReadOnly" json:"ReadOnly" flag:"read-only"` + Experimental bool `yaml:"Experimental" json:"Experimental" flag:"experimental"` + ConfigPath string `yaml:"ConfigPath" json:"-" flag:"config-path"` + ConfigFile string `json:"-"` + AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"` + OriginalsPath string `yaml:"OriginalsPath" json:"-" flag:"originals-path"` + OriginalsLimit int64 `yaml:"OriginalsLimit" json:"OriginalsLimit" flag:"originals-limit"` + ImportPath string `yaml:"ImportPath" json:"-" flag:"import-path"` + StoragePath string `yaml:"StoragePath" json:"-" flag:"storage-path"` + SidecarPath string `yaml:"SidecarPath" json:"-" flag:"sidecar-path"` + TempPath string `yaml:"TempPath" json:"-" flag:"temp-path"` + BackupPath string `yaml:"BackupPath" json:"-" flag:"backup-path"` + AssetsPath string `yaml:"AssetsPath" json:"-" flag:"assets-path"` + CachePath string `yaml:"CachePath" json:"-" flag:"cache-path"` + Workers int `yaml:"Workers" json:"Workers" flag:"workers"` + WakeupInterval int `yaml:"WakeupInterval" json:"WakeupInterval" flag:"wakeup-interval"` + AutoIndex int `yaml:"AutoIndex" json:"AutoIndex" flag:"auto-index"` + AutoImport int `yaml:"AutoImport" json:"AutoImport" flag:"auto-import"` + DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"` + DisableWebDAV bool `yaml:"DisableWebDAV" json:"DisableWebDAV" flag:"disable-webdav"` + DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"` + DisablePlaces bool `yaml:"DisablePlaces" json:"DisablePlaces" flag:"disable-places"` + DisableExifTool bool `yaml:"DisableExifTool" json:"DisableExifTool" flag:"disable-exiftool"` + DisableTensorFlow bool `yaml:"DisableTensorFlow" json:"DisableTensorFlow" flag:"disable-tensorflow"` + DisableFFmpeg bool `yaml:"DisableFFmpeg" json:"DisableFFmpeg" flag:"disable-ffmpeg"` + DisableDarktable bool `yaml:"DisableDarktable" json:"DisableDarktable" flag:"disable-darktable"` + DisableRawtherapee bool `yaml:"DisableRawtherapee" json:"DisableRawtherapee" flag:"disable-rawtherapee"` + DisableSips bool `yaml:"DisableSips" json:"DisableSips" flag:"disable-sips"` + DisableHeifConvert bool `yaml:"DisableHeifConvert" json:"DisableHeifConvert" flag:"disable-heifconvert"` + DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"` + UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"` + LogLevel string `yaml:"LogLevel" json:"-" flag:"log-level"` + LogFilename string `yaml:"LogFilename" json:"-" flag:"log-filename"` + PIDFilename string `yaml:"PIDFilename" json:"-" flag:"pid-filename"` + CdnUrl string `yaml:"CdnUrl" json:"CdnUrl" flag:"cdn-url"` + SiteUrl string `yaml:"SiteUrl" json:"SiteUrl" flag:"site-url"` + SitePreview string `yaml:"SitePreview" json:"SitePreview" flag:"site-preview"` + SiteTitle string `yaml:"SiteTitle" json:"SiteTitle" flag:"site-title"` + SiteCaption string `yaml:"SiteCaption" json:"SiteCaption" flag:"site-caption"` + SiteDescription string `yaml:"SiteDescription" json:"SiteDescription" flag:"site-description"` + SiteAuthor string `yaml:"SiteAuthor" json:"SiteAuthor" flag:"site-author"` + DatabaseDriver string `yaml:"DatabaseDriver" json:"-" flag:"database-driver"` + DatabaseDsn string `yaml:"DatabaseDsn" json:"-" flag:"database-dsn"` + DatabaseServer string `yaml:"DatabaseServer" json:"-" flag:"database-server"` + DatabaseName string `yaml:"DatabaseName" json:"-" flag:"database-name"` + DatabaseUser string `yaml:"DatabaseUser" json:"-" flag:"database-user"` + DatabasePassword string `yaml:"DatabasePassword" json:"-" flag:"database-password"` + DatabaseConns int `yaml:"DatabaseConns" json:"-" flag:"database-conns"` + DatabaseConnsIdle int `yaml:"DatabaseConnsIdle" json:"-" flag:"database-conns-idle"` + HttpHost string `yaml:"HttpHost" json:"-" flag:"http-host"` + HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"` + HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"` + HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"` + RawPresets bool `yaml:"RawPresets" json:"RawPresets" flag:"raw-presets"` + DarktableBin string `yaml:"DarktableBin" json:"-" flag:"darktable-bin"` + DarktableBlacklist string `yaml:"DarktableBlacklist" json:"-" flag:"darktable-blacklist"` + RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"` + RawtherapeeBlacklist string `yaml:"RawtherapeeBlacklist" json:"-" flag:"rawtherapee-blacklist"` + SipsBin string `yaml:"SipsBin" json:"-" flag:"sips-bin"` + HeifConvertBin string `yaml:"HeifConvertBin" json:"-" flag:"heifconvert-bin"` + FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"` + FFmpegEncoder string `yaml:"FFmpegEncoder" json:"FFmpegEncoder" flag:"ffmpeg-encoder"` + FFmpegBitrate int `yaml:"FFmpegBitrate" json:"FFmpegBitrate" flag:"ffmpeg-bitrate"` + FFmpegBuffers int `yaml:"FFmpegBuffers" json:"FFmpegBuffers" flag:"ffmpeg-buffers"` + ExifToolBin string `yaml:"ExifToolBin" json:"-" flag:"exiftool-bin"` + DetachServer bool `yaml:"DetachServer" json:"-" flag:"detach-server"` + DownloadToken string `yaml:"DownloadToken" json:"-" flag:"download-token"` + PreviewToken string `yaml:"PreviewToken" json:"-" flag:"preview-token"` + ThumbFilter string `yaml:"ThumbFilter" json:"ThumbFilter" flag:"thumb-filter"` + ThumbUncached bool `yaml:"ThumbUncached" json:"ThumbUncached" flag:"thumb-uncached"` + ThumbSize int `yaml:"ThumbSize" json:"ThumbSize" flag:"thumb-size"` + ThumbSizeUncached int `yaml:"ThumbSizeUncached" json:"ThumbSizeUncached" flag:"thumb-size-uncached"` + JpegSize int `yaml:"JpegSize" json:"JpegSize" flag:"jpeg-size"` + JpegQuality int `yaml:"JpegQuality" json:"JpegQuality" flag:"jpeg-quality"` + FaceScore float64 `yaml:"-" json:"-" flag:"face-score"` + FaceOverlap int `yaml:"-" json:"-" flag:"face-overlap"` + FaceClusterCore int `yaml:"-" json:"-" flag:"face-cluster-core"` + FaceClusterDist float64 `yaml:"-" json:"-" flag:"face-cluster-dist"` + FaceMatchDist float64 `yaml:"-" json:"-" flag:"face-match-dist"` } // NewOptions creates a new configuration entity by using two methods: @@ -187,9 +192,18 @@ func (c *Options) SetContext(ctx *cli.Context) error { tagValue := v.Type().Field(i).Tag.Get("flag") - // Automatically assign options to fields with "flag" tag. + // Assign value to field with "flag" tag. if tagValue != "" { switch t := fieldValue.Interface().(type) { + case float64: + // Only if explicitly set or current value is empty (use default). + if ctx.IsSet(tagValue) { + f := ctx.Float64(tagValue) + fieldValue.SetFloat(f) + } else if ctx.GlobalIsSet(tagValue) || fieldValue.Float() == 0 { + f := ctx.GlobalFloat64(tagValue) + fieldValue.SetFloat(f) + } case int, int64: // Only if explicitly set or current value is empty (use default). if ctx.IsSet(tagValue) { diff --git a/internal/entity/face.go b/internal/entity/face.go index 1c3d91959..e6e0794cf 100644 --- a/internal/entity/face.go +++ b/internal/entity/face.go @@ -133,7 +133,7 @@ func (m *Face) Match(embeddings Embeddings) (match bool, dist float64) { case dist < 0: // Should never happen. return false, dist - case dist > (m.SampleRadius + face.MatchRadius): + case dist > (m.SampleRadius + face.MatchDist): // Too far. return false, dist case m.CollisionRadius > 0.1 && dist > m.CollisionRadius: diff --git a/internal/face/detector.go b/internal/face/detector.go index 6a7467aad..120e29e54 100644 --- a/internal/face/detector.go +++ b/internal/face/detector.go @@ -87,7 +87,7 @@ func Detect(fileName string, findLandmarks bool, minSize int) (faces Faces, err shiftFactor: 0.1, scaleFactor: 1.1, iouThreshold: 0.2, - scoreThreshold: ScoreThreshold, + scoreThreshold: float32(ScoreThreshold), perturb: 63, } @@ -185,7 +185,7 @@ func (d *Detector) Faces(det []pigo.Detection, params pigo.CascadeParams, findLa for _, face := range det { // Skip result if quality is too low. - if face.Q < ScaleScoreThreshold(face.Scale) { + if face.Q < QualityThreshold(face.Scale) { continue } diff --git a/internal/face/thresholds.go b/internal/face/thresholds.go index b8f87a4cd..4c12ee13a 100644 --- a/internal/face/thresholds.go +++ b/internal/face/thresholds.go @@ -5,33 +5,35 @@ import ( ) var CropSize = crop.Sizes[crop.Tile160] +var MatchDist = 0.46 +var ClusterDist = 0.64 var ClusterCore = 4 var ClusterMinScore = 15 var ClusterMinSize = 95 -var ClusterRadius = 0.64 -var MatchRadius = 0.46 var SampleThreshold = 2 * ClusterCore var OverlapThreshold = 42 var OverlapThresholdFloor = OverlapThreshold - 1 -var ScoreThreshold = float32(9.0) +var ScoreThreshold = 9.0 + +// QualityThreshold returns the scale adjusted quality score threshold. +func QualityThreshold(scale int) (score float32) { + score = float32(ScoreThreshold) -// ScaleScoreThreshold returns the scale adjusted face score threshold. -func ScaleScoreThreshold(scale int) float32 { // Smaller faces require higher quality. switch { case scale < 26: - return ScoreThreshold + 26.0 + score += 26.0 case scale < 32: - return ScoreThreshold + 16.0 + score += 16.0 case scale < 40: - return ScoreThreshold + 11.0 + score += 11.0 case scale < 50: - return ScoreThreshold + 9.0 + score += 9.0 case scale < 80: - return ScoreThreshold + 6.0 + score += 6.0 case scale < 110: - return ScoreThreshold + 2.0 + score += 2.0 } - return ScoreThreshold + return score } diff --git a/internal/face/thresholds_test.go b/internal/face/thresholds_test.go index d9428d00b..5e33a537b 100644 --- a/internal/face/thresholds_test.go +++ b/internal/face/thresholds_test.go @@ -6,26 +6,26 @@ import ( "github.com/stretchr/testify/assert" ) -func TestScaleScoreThreshold(t *testing.T) { +func TestQualityThreshold(t *testing.T) { t.Run("XXS", func(t *testing.T) { - assert.Equal(t, float32(35), ScaleScoreThreshold(21)) + assert.Equal(t, float32(35), QualityThreshold(21)) }) t.Run("XS", func(t *testing.T) { - assert.Equal(t, float32(25), ScaleScoreThreshold(27)) + assert.Equal(t, float32(25), QualityThreshold(27)) }) t.Run("S", func(t *testing.T) { - assert.Equal(t, float32(20), ScaleScoreThreshold(33)) + assert.Equal(t, float32(20), QualityThreshold(33)) }) t.Run("M", func(t *testing.T) { - assert.Equal(t, float32(18), ScaleScoreThreshold(45)) + assert.Equal(t, float32(18), QualityThreshold(45)) }) t.Run("L", func(t *testing.T) { - assert.Equal(t, float32(15), ScaleScoreThreshold(75)) + assert.Equal(t, float32(15), QualityThreshold(75)) }) t.Run("XL", func(t *testing.T) { - assert.Equal(t, float32(11), ScaleScoreThreshold(100)) + assert.Equal(t, float32(11), QualityThreshold(100)) }) t.Run("XXL", func(t *testing.T) { - assert.Equal(t, float32(9), ScaleScoreThreshold(250)) + assert.Equal(t, float32(9), QualityThreshold(250)) }) } diff --git a/internal/photoprism/faces_audit.go b/internal/photoprism/faces_audit.go index f785fe472..090b6dfea 100644 --- a/internal/photoprism/faces_audit.go +++ b/internal/photoprism/faces_audit.go @@ -71,7 +71,7 @@ func (w *Faces) Audit(fix bool) (err error) { conflicts++ - r := f1.SampleRadius + face.MatchRadius + r := f1.SampleRadius + face.MatchDist log.Infof("face %s: conflict at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius) diff --git a/internal/photoprism/faces_cluster.go b/internal/photoprism/faces_cluster.go index 5146de8ee..7914f394b 100644 --- a/internal/photoprism/faces_cluster.go +++ b/internal/photoprism/faces_cluster.go @@ -38,7 +38,7 @@ func (w *Faces) Cluster(opt FacesOptions) (added entity.Faces, err error) { var c clusters.HardClusterer // See https://dl.photoprism.org/research/ for research on face clustering algorithms. - if c, err = clusters.DBSCAN(face.ClusterCore, face.ClusterRadius, w.conf.Workers(), clusters.EuclideanDistance); err != nil { + if c, err = clusters.DBSCAN(face.ClusterCore, face.ClusterDist, w.conf.Workers(), clusters.EuclideanDistance); err != nil { return added, err } else if err = c.Learn(embeddings); err != nil { return added, err diff --git a/internal/query/faces.go b/internal/query/faces.go index b76dc8544..09e2b4240 100644 --- a/internal/query/faces.go +++ b/internal/query/faces.go @@ -178,7 +178,7 @@ func ResolveFaceCollisions() (conflicts, resolved int, err error) { conflicts++ - r := f1.SampleRadius + face.MatchRadius + r := f1.SampleRadius + face.MatchDist log.Infof("face %s: conflict at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius)