Skip to content

Commit ea693e9

Browse files
committed
Faces: Improve "photoprism faces audit --fix" command
Signed-off-by: Michael Mayer <[email protected]>
1 parent 00d4114 commit ea693e9

File tree

13 files changed

+191
-105
lines changed

13 files changed

+191
-105
lines changed

internal/ai/face/README.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## Face Detection and Embedding Guidelines
22

3-
**Last Updated:** October 2, 2025
3+
**Last Updated:** October 5, 2025
44

55
### Overview
66

@@ -44,7 +44,7 @@ All embeddings, regardless of origin, are normalized to unit length (‖x‖₂
4444
- `NewEmbedding` normalizes the raw float32 inference output.
4545
- `EmbeddingsMidpoint` normalizes each contributor, averages component-wise, and renormalizes the centroid.
4646
- `UnmarshalEmbedding` and `UnmarshalEmbeddings` normalize data when loading from persisted JSON.
47-
- Static datasets (`KidsEmbeddings`, `IgnoredEmbeddings`) and random generators now normalize their entries after perturbation.
47+
- Static datasets (children/background samples) and random generators now normalize their entries after perturbation.
4848
- `photoprism faces audit --fix` re-normalizes persisted embeddings, rekeys face IDs, and re-links markers (ID + `FaceDist`) so historical data adopts the canonical unit-length vectors.
4949
- `Faces.Match` pre-filters matchable clusters, keeps an in-memory veto list for freshly cleared markers, and caches embeddings to avoid redundant distance checks; `BenchmarkSelectBestFace` (1024 faces) now reports a bucket size of ~16 candidates out of 1024 (≈98 % fewer distance evaluations) at ≈0.55 ms/op with zero allocations.
5050
- Face clusters update their sample statistics (`Samples`, `SampleRadius`) from the latest matches via `Face.UpdateMatchStats`, avoiding stale radii during optimize loops.
@@ -75,13 +75,11 @@ This guarantees that Euclidean distance comparisons are equivalent to cosine com
7575

7676
### Configuration Summary
7777

78-
| Setting | Default | Description |
79-
|---------------------|------------------------------|---------------------------------------------------------------------------------|
80-
| `FACE_ANGLE` | `-0.3,0,0.3` | Detection angles (radians) swept by Pigo. |
81-
| `FACE_SCORE` | `9.0` (with dynamic offsets) | Base quality threshold before scale adjustments. |
82-
| `FACE_OVERLAP` | `42` | Maximum allowed IoU when deduplicating markers. |
83-
| `FACE_KIDS_DIST` | `0.695` | Distance cutoff for kids-face detection (still interpreted in Euclidean space). |
84-
| `FACE_IGNORED_DIST` | `0.86` | Distance cutoff for ignoring generic embeddings. |
78+
| Setting | Default | Description |
79+
|------------------------|------------------------------|--------------------------------------------------------------------------------------|
80+
| `FACE_ANGLE` | `-0.3,0,0.3` | Detection angles (radians) swept by Pigo. |
81+
| `FACE_SCORE` | `9.0` (with dynamic offsets) | Base quality threshold before scale adjustments. |
82+
| `FACE_OVERLAP` | `42` | Maximum allowed IoU when deduplicating markers. |
8583

8684
### Benchmark Reference
8785

internal/ai/face/background.go

Lines changed: 9 additions & 9 deletions
Large diffs are not rendered by default.

internal/ai/face/background_test.go

Lines changed: 24 additions & 24 deletions
Large diffs are not rendered by default.

internal/ai/face/clusters_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func TestClustersContains(t *testing.T) {
2828
assert.False(t, clusters.Contains(embedding))
2929
})
3030

31-
t.Run("DisabledClusterIgnored", func(t *testing.T) {
31+
t.Run("DisabledClusterBackground", func(t *testing.T) {
3232
clusters := Clusters{
3333
{Radius: 1, Embedding: Embedding{0, 0}, Disabled: true},
3434
}

internal/ai/face/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ var (
1313

1414
func init() {
1515
// Disable ignore/skip for background and children if legacy env variables are set.
16-
if os.Getenv("PHOTOPRISM_FACE_KIDS_DIST") != "" {
16+
if os.Getenv("PHOTOPRISM_FACE_CHILDREN_DIST") != "" || os.Getenv("PHOTOPRISM_FACE_KIDS_DIST") != "" {
1717
SkipChildren = false
1818
}
19-
if os.Getenv("PHOTOPRISM_FACE_IGNORED_DIST") != "" {
19+
if os.Getenv("PHOTOPRISM_FACE_BACKGROUND_DIST") != "" || os.Getenv("PHOTOPRISM_FACE_IGNORED_DIST") != "" {
2020
IgnoreBackground = false
2121
}
2222
}

internal/ai/face/embedding.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ func NewEmbedding(inference []float32) Embedding {
2828
return result
2929
}
3030

31-
// Kind returns the type of face e.g. regular, kids, or ignored.
31+
// Kind returns the type of face e.g. regular, children, or background.
3232
func (m Embedding) Kind() Kind {
3333
if m.IsChild() {
34-
return KidsFace
34+
return ChildrenFace
3535
} else if m.IsBackground() {
36-
return IgnoredFace
36+
return BackgroundFace
3737
}
3838

3939
return RegularFace

internal/ai/face/embedding_test.go

Lines changed: 7 additions & 7 deletions
Large diffs are not rendered by default.

internal/ai/face/embeddings_random.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ type Kind int
88

99
const (
1010
RegularFace Kind = iota + 1
11-
KidsFace
12-
IgnoredFace
11+
ChildrenFace
12+
BackgroundFace
1313
AmbiguousFace
1414
)
1515

@@ -35,10 +35,10 @@ func RandomEmbeddings(n int, k Kind) (result Embeddings) {
3535
switch k {
3636
case RegularFace:
3737
result[i] = RandomEmbedding()
38-
case KidsFace:
39-
result[i] = RandomKidsEmbedding()
40-
case IgnoredFace:
41-
result[i] = RandomIgnoredEmbedding()
38+
case ChildrenFace:
39+
result[i] = RandomChildrenEmbedding()
40+
case BackgroundFace:
41+
result[i] = RandomBackgroundEmbedding()
4242
}
4343

4444
}
@@ -67,8 +67,8 @@ func RandomEmbedding() (result Embedding) {
6767
return result
6868
}
6969

70-
// RandomKidsEmbedding returns a random kids embedding for testing.
71-
func RandomKidsEmbedding() (result Embedding) {
70+
// RandomChildrenEmbedding returns a random children embedding for testing.
71+
func RandomChildrenEmbedding() (result Embedding) {
7272
result = make(Embedding, 512)
7373

7474
if len(Children) == 0 {
@@ -88,13 +88,17 @@ func RandomKidsEmbedding() (result Embedding) {
8888
return result
8989
}
9090

91-
// RandomIgnoredEmbedding returns a random ignored embedding for testing.
92-
func RandomIgnoredEmbedding() (result Embedding) {
91+
// RandomBackgroundEmbedding returns a random background embedding for testing.
92+
func RandomBackgroundEmbedding() (result Embedding) {
9393
result = make(Embedding, 512)
9494

95+
if len(Background) == 0 {
96+
return result
97+
}
98+
9599
d := 0.1 / 512.0
96-
n := 1 + rand.IntN(len(TestEmbeddings)-1)
97-
e := TestEmbeddings[n]
100+
n := rand.IntN(len(Background))
101+
e := Background[n].Embedding
98102

99103
for i := range result {
100104
result[i] = RandomFloat64(e[i], d)

internal/ai/face/embeddings_random_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ func TestRandomEmbeddings(t *testing.T) {
2323
assert.False(t, e[i].IsBackground())
2424
}
2525
})
26-
t.Run("Kids", func(t *testing.T) {
27-
e := RandomEmbeddings(2, KidsFace)
26+
t.Run("Children", func(t *testing.T) {
27+
e := RandomEmbeddings(2, ChildrenFace)
2828
for i := range e {
2929
assert.False(t, e[i].IsBackground())
3030
assert.True(t, e[i].IsChild())
3131
}
3232
})
33-
t.Run("Ignored", func(t *testing.T) {
34-
e := RandomEmbeddings(2, IgnoredFace)
33+
t.Run("Background", func(t *testing.T) {
34+
e := RandomEmbeddings(2, BackgroundFace)
3535
for i := range e {
3636
assert.True(t, e[i].IsBackground())
3737
assert.False(t, e[i].IsChild())

0 commit comments

Comments
 (0)