41b252d820
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)
176 lines
3.6 KiB
Go
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
|
|
}
|