People: Add config options for face detection and clustering #22

This commit is contained in:
Michael Mayer 2021-09-23 13:16:05 +02:00
parent 4988bec841
commit ee2b49ef4b
12 changed files with 232 additions and 120 deletions

View file

@ -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
}

View file

@ -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()
}

48
internal/config/face.go Normal file
View file

@ -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
}

View file

@ -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",
},
}

View file

@ -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) {

View file

@ -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:

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
})
}

View file

@ -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)

View file

@ -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

View file

@ -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)