photoprism/internal/face/embeddings.go
Michael Mayer 41b252d820 People: Add unofficial env variables to tweak face matching #1587 #2182
Adds two unofficial env variables so advanced users can experiment:

1. PHOTOPRISM_FACE_KIDS_DIST=0.6950 (range: 0.1-1.5, -1 to disable)
2. PHOTOPRISM_FACE_IGNORE_DIST=0.86 (range: 0.1-1.5, -1 to disable)
2022-04-03 17:25:37 +02:00

176 lines
3.6 KiB
Go

package face
import (
"encoding/json"
"fmt"
"strings"
"github.com/montanaflynn/stats"
"github.com/photoprism/photoprism/pkg/clusters"
)
// Embeddings represents a face embedding cluster.
type Embeddings []Embedding
// NewEmbeddings creates a new embeddings from inference results.
func NewEmbeddings(inference [][]float32) Embeddings {
result := make(Embeddings, len(inference))
var v []float32
var i int
for i, v = range inference {
e := NewEmbedding(v)
if e.CanMatch() {
result[i] = e
}
}
return result
}
// Empty tests if embeddings are empty.
func (embeddings Embeddings) Empty() bool {
if len(embeddings) < 1 {
return true
}
return len(embeddings[0]) < 1
}
// Count returns the number of embeddings.
func (embeddings Embeddings) Count() int {
if embeddings.Empty() {
return 0
}
return len(embeddings)
}
// One tests if there is exactly one embedding.
func (embeddings Embeddings) One() bool {
return embeddings.Count() == 1
}
// First returns the first face embedding.
func (embeddings Embeddings) First() Embedding {
if embeddings.Empty() {
return NullEmbedding
}
return embeddings[0]
}
// Float64 returns embeddings as a float64 slice.
func (embeddings Embeddings) Float64() [][]float64 {
result := make([][]float64, len(embeddings))
for i, e := range embeddings {
result[i] = e
}
return result
}
// Contains tests if another embeddings is contained within a radius.
func (embeddings Embeddings) Contains(other Embedding, radius float64) bool {
for _, e := range embeddings {
if d := e.Dist(other); d < radius {
return true
}
}
return false
}
// Dist returns the minimum distance to an embedding.
func (embeddings Embeddings) Dist(other Embedding) (dist float64) {
dist = -1
for _, e := range embeddings {
if d := e.Dist(other); d < dist || dist < 0 {
dist = d
}
}
return dist
}
// JSON returns the embeddings as JSON bytes.
func (embeddings Embeddings) JSON() []byte {
var noResult = []byte("")
if embeddings.Empty() {
return noResult
}
if result, err := json.Marshal(embeddings); err != nil {
return noResult
} else {
return result
}
}
// EmbeddingsMidpoint returns the embeddings vector midpoint.
func EmbeddingsMidpoint(embeddings Embeddings) (result Embedding, radius float64, count int) {
// Return if there are no embeddings.
if embeddings.Empty() {
return Embedding{}, 0, 0
}
// Count embeddings.
count = len(embeddings)
// Only one embedding?
if count == 1 {
// Return embedding if there is only one.
return embeddings[0], 0.0, 1
}
dim := len(embeddings[0])
// No embedding values?
if dim == 0 {
return Embedding{}, 0.0, count
}
result = make(Embedding, dim)
// The mean of a set of vectors is calculated component-wise.
for i := 0; i < dim; i++ {
values := make(stats.Float64Data, count)
for j := 0; j < count; j++ {
values[j] = embeddings[j][i]
}
if m, err := stats.Mean(values); err != nil {
log.Warnf("embeddings: %s", err)
} else {
result[i] = m
}
}
// Radius is the max embedding distance + 0.01 from result.
for _, emb := range embeddings {
if d := clusters.EuclideanDist(result, emb); d > radius {
radius = d + 0.01
}
}
return result, radius, count
}
// UnmarshalEmbeddings parses face embedding JSON.
func UnmarshalEmbeddings(s string) (result Embeddings, err error) {
if s == "" {
return result, fmt.Errorf("cannot unmarshal empeddings, empty string provided")
} else if !strings.HasPrefix(s, "[[") {
return result, fmt.Errorf("cannot unmarshal empeddings, invalid json provided")
}
err = json.Unmarshal([]byte(s), &result)
return result, err
}