773 lines
25 KiB
Go
773 lines
25 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestTrainingDetectorLabelContentAllowsExplicitNegative(t *testing.T) {
|
|
content, err := trainingDetectorLabelContent(nil, map[string]int{"person": 0}, true)
|
|
if err != nil {
|
|
t.Fatalf("negative label content returned error: %v", err)
|
|
}
|
|
if len(content) != 0 {
|
|
t.Fatalf("negative label content = %q, want empty", string(content))
|
|
}
|
|
}
|
|
|
|
func TestTrainingDetectorLabelContentRejectsAccidentalEmptySample(t *testing.T) {
|
|
_, err := trainingDetectorLabelContent(
|
|
[]TrainingBox{{Label: "unknown_label", X: 0, Y: 0, W: 1, H: 1}},
|
|
map[string]int{"person": 0},
|
|
false,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("invalid non-negative sample should be rejected")
|
|
}
|
|
}
|
|
|
|
func TestTrainingDetectorLabelContentWritesYOLOBox(t *testing.T) {
|
|
content, err := trainingDetectorLabelContent(
|
|
[]TrainingBox{{Label: "person", X: 0.1, Y: 0.2, W: 0.4, H: 0.6}},
|
|
map[string]int{"person": 3},
|
|
false,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("positive label content returned error: %v", err)
|
|
}
|
|
|
|
got := strings.TrimSpace(string(content))
|
|
want := "3 0.300000 0.500000 0.400000 0.600000"
|
|
if got != want {
|
|
t.Fatalf("label content = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestTrainingCountDetectorSamplesIncludesEmptyLabels(t *testing.T) {
|
|
root := t.TempDir()
|
|
imagesDir := filepath.Join(root, "images")
|
|
labelsDir := filepath.Join(root, "labels")
|
|
|
|
if err := os.MkdirAll(imagesDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(labelsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(imagesDir, "negative.jpg"), []byte("image"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(labelsDir, "negative.txt"), []byte{}, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if got := trainingCountDetectorSamples(imagesDir, labelsDir); got != 1 {
|
|
t.Fatalf("sample count = %d, want 1", got)
|
|
}
|
|
if got := trainingCountPositiveDetectorSamples(imagesDir, labelsDir); got != 0 {
|
|
t.Fatalf("positive sample count = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestTrainingEnsureDetectorValidationSampleIncludesPositiveExample(t *testing.T) {
|
|
root := t.TempDir()
|
|
trainImages := filepath.Join(root, "detector", "dataset", "images", "train")
|
|
trainLabels := filepath.Join(root, "detector", "dataset", "labels", "train")
|
|
|
|
if err := os.MkdirAll(trainImages, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(trainLabels, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for i := 0; i < minDetectorTrainCount; i++ {
|
|
id := fmt.Sprintf("sample-%02d", i)
|
|
if err := os.WriteFile(filepath.Join(trainImages, id+".jpg"), []byte("image"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
label := []byte{}
|
|
if i == minDetectorTrainCount-1 {
|
|
label = []byte("0 0.5 0.5 1 1\n")
|
|
}
|
|
if err := os.WriteFile(filepath.Join(trainLabels, id+".txt"), label, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if err := trainingEnsureDetectorValidationSample(root); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
valImages := filepath.Join(root, "detector", "dataset", "images", "val")
|
|
valLabels := filepath.Join(root, "detector", "dataset", "labels", "val")
|
|
if got := trainingCountDetectorSamples(valImages, valLabels); got < minDetectorValCount {
|
|
t.Fatalf("validation sample count = %d, want at least %d", got, minDetectorValCount)
|
|
}
|
|
if got := trainingCountPositiveDetectorSamples(valImages, valLabels); got < 1 {
|
|
t.Fatalf("positive validation sample count = %d, want at least 1", got)
|
|
}
|
|
}
|
|
|
|
func TestTrainingEffectiveCorrectionClearsNegativeAnnotation(t *testing.T) {
|
|
effective := trainingEffectiveCorrection(TrainingAnnotation{
|
|
Negative: true,
|
|
Prediction: TrainingPrediction{
|
|
SexPosition: "doggy",
|
|
Boxes: []TrainingBox{
|
|
{Label: "person", X: 0, Y: 0, W: 1, H: 1},
|
|
},
|
|
},
|
|
})
|
|
|
|
if effective.SexPosition != trainingNoSexPositionLabel {
|
|
t.Fatalf("sex position = %q, want %s", effective.SexPosition, trainingNoSexPositionLabel)
|
|
}
|
|
if len(effective.Boxes) != 0 {
|
|
t.Fatalf("negative annotation has %d boxes, want 0", len(effective.Boxes))
|
|
}
|
|
}
|
|
|
|
func TestTrainingEffectiveCorrectionClearsPosePersonsForUnknownCorrection(t *testing.T) {
|
|
effective := trainingEffectiveCorrection(TrainingAnnotation{
|
|
Correction: &TrainingCorrection{
|
|
SexPosition: "Unknown",
|
|
Boxes: []TrainingBox{
|
|
{Label: "person_female", X: 0, Y: 0, W: 1, H: 1},
|
|
},
|
|
PosePersons: []TrainingPosePerson{
|
|
{Label: "person", Score: 0.9},
|
|
},
|
|
},
|
|
})
|
|
|
|
if effective.SexPosition != trainingNoSexPositionLabel {
|
|
t.Fatalf("sex position = %q, want %s", effective.SexPosition, trainingNoSexPositionLabel)
|
|
}
|
|
if len(effective.PosePersons) != 0 {
|
|
t.Fatalf("pose persons = %d, want 0 for unknown sex position", len(effective.PosePersons))
|
|
}
|
|
if len(effective.Boxes) != 1 {
|
|
t.Fatalf("boxes = %d, want detector boxes preserved", len(effective.Boxes))
|
|
}
|
|
}
|
|
|
|
func TestTrainingEffectiveCorrectionClearsPosePersonsForNoPositionPrediction(t *testing.T) {
|
|
effective := trainingEffectiveCorrection(TrainingAnnotation{
|
|
Accepted: true,
|
|
Prediction: TrainingPrediction{
|
|
SexPosition: "unknown",
|
|
Persons: []TrainingPosePerson{
|
|
{Label: "person", Score: 0.9},
|
|
},
|
|
},
|
|
})
|
|
|
|
if effective.SexPosition != trainingNoSexPositionLabel {
|
|
t.Fatalf("sex position = %q, want %s", effective.SexPosition, trainingNoSexPositionLabel)
|
|
}
|
|
if len(effective.PosePersons) != 0 {
|
|
t.Fatalf("pose persons = %d, want 0 for unknown sex position", len(effective.PosePersons))
|
|
}
|
|
}
|
|
|
|
func TestTrainingStripPosePersonsForNoSexPositionBeforeStorage(t *testing.T) {
|
|
annotation := trainingStripPosePersonsForNoSexPosition(TrainingAnnotation{
|
|
Prediction: TrainingPrediction{
|
|
SexPosition: "keine",
|
|
Persons: []TrainingPosePerson{
|
|
{Label: "person", Score: 0.9},
|
|
},
|
|
},
|
|
Correction: &TrainingCorrection{
|
|
SexPosition: "Unknown",
|
|
PosePersons: []TrainingPosePerson{
|
|
{Label: "person", Score: 0.8},
|
|
},
|
|
},
|
|
})
|
|
|
|
if len(annotation.Prediction.Persons) != 0 {
|
|
t.Fatalf("stored prediction persons = %d, want 0", len(annotation.Prediction.Persons))
|
|
}
|
|
if annotation.Correction == nil {
|
|
t.Fatal("correction missing")
|
|
}
|
|
if len(annotation.Correction.PosePersons) != 0 {
|
|
t.Fatalf("stored correction pose persons = %d, want 0", len(annotation.Correction.PosePersons))
|
|
}
|
|
if annotation.Correction.SexPosition != trainingNoSexPositionLabel {
|
|
t.Fatalf("stored correction sex position = %q, want %s", annotation.Correction.SexPosition, trainingNoSexPositionLabel)
|
|
}
|
|
}
|
|
|
|
func TestNoSexPositionAliasesTreatUnknownAsNoPosition(t *testing.T) {
|
|
for _, value := range []string{"Unknown", "unknown", "unbekannt", "none", "no_position"} {
|
|
if !isNoSexPositionLabel(value) {
|
|
t.Fatalf("%q should be treated as no sex position", value)
|
|
}
|
|
if got := normalizeSexPositionLabel(value); got != trainingNoSexPositionLabel {
|
|
t.Fatalf("normalizeSexPositionLabel(%q) = %q, want %q", value, got, trainingNoSexPositionLabel)
|
|
}
|
|
}
|
|
}
|
|
|
|
func trainingTestPoseKeypoints(overrides map[string]TrainingKeypoint) []TrainingKeypoint {
|
|
names := []string{
|
|
"nose",
|
|
"left_eye", "right_eye",
|
|
"left_ear", "right_ear",
|
|
"left_shoulder", "right_shoulder",
|
|
"left_elbow", "right_elbow",
|
|
"left_wrist", "right_wrist",
|
|
"left_hip", "right_hip",
|
|
"left_knee", "right_knee",
|
|
"left_ankle", "right_ankle",
|
|
}
|
|
|
|
out := make([]TrainingKeypoint, 0, len(names))
|
|
for i, name := range names {
|
|
point := TrainingKeypoint{
|
|
Name: name,
|
|
X: 0.20 + float64(i)*0.01,
|
|
Y: 0.25 + float64(i)*0.01,
|
|
Conf: 0.85,
|
|
}
|
|
|
|
if override, ok := overrides[name]; ok {
|
|
override.Name = name
|
|
point = override
|
|
}
|
|
|
|
out = append(out, point)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func TestTrainingApplyPoseToPredictionAggregatesMultiplePersons(t *testing.T) {
|
|
pred := TrainingPrediction{
|
|
SexPosition: trainingNoSexPositionLabel,
|
|
Boxes: []TrainingBox{
|
|
{Label: "person_female", X: 0.10, Y: 0.10, W: 0.35, H: 0.75},
|
|
{Label: "person_male", X: 0.42, Y: 0.12, W: 0.34, H: 0.72},
|
|
},
|
|
}
|
|
|
|
pose := TrainingPosePrediction{
|
|
Available: true,
|
|
Persons: []TrainingPosePerson{
|
|
{
|
|
Label: "missionary",
|
|
Score: 0.58,
|
|
Box: TrainingBox{X: 0.10, Y: 0.10, W: 0.35, H: 0.75},
|
|
Keypoints: trainingTestPoseKeypoints(nil),
|
|
},
|
|
{
|
|
Label: "doggy",
|
|
Score: 0.55,
|
|
Box: TrainingBox{X: 0.42, Y: 0.12, W: 0.34, H: 0.72},
|
|
Keypoints: trainingTestPoseKeypoints(nil),
|
|
},
|
|
{
|
|
Label: "doggy",
|
|
Score: 0.54,
|
|
Box: TrainingBox{X: 0.52, Y: 0.14, W: 0.30, H: 0.70},
|
|
Keypoints: trainingTestPoseKeypoints(nil),
|
|
},
|
|
},
|
|
}
|
|
|
|
got := trainingApplyPoseToPrediction(pred, pose)
|
|
if got.SexPosition != "doggy" {
|
|
t.Fatalf("sex position = %q, want doggy", got.SexPosition)
|
|
}
|
|
}
|
|
|
|
func TestTrainingApplyPoseToPredictionUsesObjectPositionContext(t *testing.T) {
|
|
pred := TrainingPrediction{
|
|
SexPosition: trainingNoSexPositionLabel,
|
|
Boxes: []TrainingBox{
|
|
{Label: "person_female", X: 0.08, Y: 0.08, W: 0.42, H: 0.78},
|
|
{Label: "person_male", X: 0.42, Y: 0.12, W: 0.35, H: 0.76},
|
|
{Label: "penis", X: 0.50, Y: 0.48, W: 0.07, H: 0.08},
|
|
},
|
|
}
|
|
|
|
pose := TrainingPosePrediction{
|
|
Available: true,
|
|
Persons: []TrainingPosePerson{
|
|
{
|
|
Label: "missionary",
|
|
Score: 0.58,
|
|
Box: TrainingBox{X: 0.08, Y: 0.08, W: 0.42, H: 0.78},
|
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
|
"nose": {X: 0.18, Y: 0.18, Conf: 0.90},
|
|
}),
|
|
},
|
|
{
|
|
Label: "blowjob",
|
|
Score: 0.55,
|
|
Box: TrainingBox{X: 0.42, Y: 0.12, W: 0.35, H: 0.76},
|
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
|
"nose": {X: 0.53, Y: 0.52, Conf: 0.95},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
got := trainingApplyPoseToPrediction(pred, pose)
|
|
if got.SexPosition != "blowjob" {
|
|
t.Fatalf("sex position = %q, want blowjob", got.SexPosition)
|
|
}
|
|
}
|
|
|
|
func TestTrainingApplyPoseToPredictionUsesOcclusionTolerantCowgirlGeometry(t *testing.T) {
|
|
pred := TrainingPrediction{
|
|
SexPosition: trainingNoSexPositionLabel,
|
|
Boxes: []TrainingBox{
|
|
{Label: "person_female", X: 0.30, Y: 0.10, W: 0.28, H: 0.62},
|
|
{Label: "person_male", X: 0.20, Y: 0.42, W: 0.55, H: 0.24},
|
|
},
|
|
}
|
|
|
|
pose := TrainingPosePrediction{
|
|
Available: true,
|
|
Persons: []TrainingPosePerson{
|
|
{
|
|
Label: "person",
|
|
Score: 0.72,
|
|
Box: TrainingBox{X: 0.30, Y: 0.10, W: 0.28, H: 0.62},
|
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
|
"left_shoulder": {X: 0.40, Y: 0.22, Conf: 0.92},
|
|
"right_shoulder": {X: 0.48, Y: 0.22, Conf: 0.92},
|
|
"left_hip": {X: 0.38, Y: 0.38, Conf: 0.92},
|
|
"right_hip": {X: 0.50, Y: 0.38, Conf: 0.92},
|
|
"left_knee": {X: 0.31, Y: 0.58, Conf: 0.90},
|
|
"right_knee": {X: 0.57, Y: 0.58, Conf: 0.90},
|
|
}),
|
|
},
|
|
{
|
|
Label: "person",
|
|
Score: 0.70,
|
|
Box: TrainingBox{X: 0.20, Y: 0.42, W: 0.55, H: 0.24},
|
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
|
"left_shoulder": {X: 0.25, Y: 0.50, Conf: 0.90},
|
|
"right_shoulder": {X: 0.38, Y: 0.50, Conf: 0.90},
|
|
"left_hip": {X: 0.52, Y: 0.53, Conf: 0.90},
|
|
"right_hip": {X: 0.66, Y: 0.53, Conf: 0.90},
|
|
"left_knee": {X: 0.58, Y: 0.56, Conf: 0.88},
|
|
"right_knee": {X: 0.70, Y: 0.56, Conf: 0.88},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
got := trainingApplyPoseToPrediction(pred, pose)
|
|
if got.SexPosition != "cowgirl" {
|
|
t.Fatalf("sex position = %q, want cowgirl", got.SexPosition)
|
|
}
|
|
if got.SexPositionScore < 0.30 {
|
|
t.Fatalf("sex position score = %.3f, want >= 0.30", got.SexPositionScore)
|
|
}
|
|
}
|
|
|
|
func TestTrainingPosePairGeometryDoesNotTreatParallelAxesAsCowgirl(t *testing.T) {
|
|
positionSet := stringSet(defaultTrainingLabelsFromJSON().SexPositions)
|
|
scores := map[string]float64{}
|
|
|
|
pose := TrainingPosePrediction{
|
|
Available: true,
|
|
Persons: []TrainingPosePerson{
|
|
{
|
|
Label: "person",
|
|
Score: 0.74,
|
|
Box: TrainingBox{X: 0.30, Y: 0.10, W: 0.30, H: 0.42},
|
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
|
"left_shoulder": {X: 0.40, Y: 0.18, Conf: 0.92},
|
|
"right_shoulder": {X: 0.50, Y: 0.18, Conf: 0.92},
|
|
"left_hip": {X: 0.40, Y: 0.38, Conf: 0.92},
|
|
"right_hip": {X: 0.50, Y: 0.38, Conf: 0.92},
|
|
"left_knee": {X: 0.32, Y: 0.58, Conf: 0.90},
|
|
"right_knee": {X: 0.58, Y: 0.58, Conf: 0.90},
|
|
}),
|
|
},
|
|
{
|
|
Label: "person",
|
|
Score: 0.72,
|
|
Box: TrainingBox{X: 0.31, Y: 0.24, W: 0.30, H: 0.42},
|
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
|
"left_shoulder": {X: 0.40, Y: 0.30, Conf: 0.92},
|
|
"right_shoulder": {X: 0.50, Y: 0.30, Conf: 0.92},
|
|
"left_hip": {X: 0.40, Y: 0.50, Conf: 0.92},
|
|
"right_hip": {X: 0.50, Y: 0.50, Conf: 0.92},
|
|
"left_knee": {X: 0.39, Y: 0.62, Conf: 0.90},
|
|
"right_knee": {X: 0.51, Y: 0.62, Conf: 0.90},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
trainingAddPosePairGeometryScores(scores, positionSet, pose)
|
|
|
|
if got := scores["cowgirl"]; got > 0 {
|
|
t.Fatalf("cowgirl score = %.3f, want 0 for parallel body axes", got)
|
|
}
|
|
if got := scores["reverse_cowgirl"]; got > 0 {
|
|
t.Fatalf("reverse_cowgirl score = %.3f, want 0 for parallel body axes", got)
|
|
}
|
|
if got := scores["missionary"]; got <= 0 {
|
|
t.Fatalf("missionary score = %.3f, want > 0 for stacked parallel body axes", got)
|
|
}
|
|
}
|
|
|
|
func TestTrainingPosePersonGeometryUsesBodyAxisForAllFours(t *testing.T) {
|
|
person := TrainingPosePerson{
|
|
Label: "person",
|
|
Score: 0.84,
|
|
Box: TrainingBox{X: 0.38, Y: 0.22, W: 0.24, H: 0.58},
|
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
|
"left_shoulder": {X: 0.45, Y: 0.30, Conf: 0.92},
|
|
"right_shoulder": {X: 0.55, Y: 0.30, Conf: 0.92},
|
|
"left_hip": {X: 0.45, Y: 0.50, Conf: 0.92},
|
|
"right_hip": {X: 0.55, Y: 0.50, Conf: 0.92},
|
|
"left_knee": {X: 0.46, Y: 0.68, Conf: 0.90},
|
|
"right_knee": {X: 0.54, Y: 0.68, Conf: 0.90},
|
|
}),
|
|
}
|
|
|
|
geometry := trainingPosePersonGeometryFor(person)
|
|
if !geometry.kneesBelowHips {
|
|
t.Fatalf("kneesBelowHips = false, want true")
|
|
}
|
|
if geometry.straddling {
|
|
t.Fatalf("straddling = true, want false")
|
|
}
|
|
if !geometry.elongated {
|
|
t.Fatalf("elongated = false, want true")
|
|
}
|
|
if !geometry.allFours {
|
|
t.Fatalf("allFours = false, want true for body-axis aligned bent pose")
|
|
}
|
|
}
|
|
|
|
func TestTrainingApplyPoseToPredictionKeepsUnreliablePoseOutOfContext(t *testing.T) {
|
|
pred := TrainingPrediction{
|
|
SexPosition: trainingNoSexPositionLabel,
|
|
Boxes: []TrainingBox{
|
|
{Label: "person_female", X: 0.08, Y: 0.08, W: 0.42, H: 0.78},
|
|
{Label: "penis", X: 0.50, Y: 0.48, W: 0.07, H: 0.08},
|
|
},
|
|
}
|
|
|
|
pose := TrainingPosePrediction{
|
|
Available: true,
|
|
Persons: []TrainingPosePerson{
|
|
{
|
|
Label: "blowjob",
|
|
Score: 0.24,
|
|
Box: TrainingBox{X: 0.08, Y: 0.08, W: 0.42, H: 0.78},
|
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
|
"nose": {X: 0.53, Y: 0.52, Conf: 0.95},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
got := trainingApplyPoseToPrediction(pred, pose)
|
|
if got.SexPosition != trainingNoSexPositionLabel {
|
|
t.Fatalf("sex position = %q, want %s", got.SexPosition, trainingNoSexPositionLabel)
|
|
}
|
|
if len(got.Persons) != 1 {
|
|
t.Fatalf("persons = %d, want 1", len(got.Persons))
|
|
}
|
|
if got.Persons[0].Reliable {
|
|
t.Fatalf("low-score pose should be marked unreliable: %+v", got.Persons[0])
|
|
}
|
|
if got.Persons[0].VisibleKeypoints == 0 || got.Persons[0].Quality == 0 {
|
|
t.Fatalf("pose quality should be annotated: %+v", got.Persons[0])
|
|
}
|
|
}
|
|
|
|
func TestTrainingApplyPoseToPredictionUsesBoxContextWithoutPose(t *testing.T) {
|
|
pred := TrainingPrediction{
|
|
ModelAvailable: true,
|
|
Source: "yolo26_detector",
|
|
SexPosition: trainingNoSexPositionLabel,
|
|
Boxes: []TrainingBox{
|
|
{Label: "person_female", X: 0.18, Y: 0.12, W: 0.42, H: 0.72},
|
|
{Label: "person_male", X: 0.42, Y: 0.18, W: 0.38, H: 0.68},
|
|
{Label: "penis", X: 0.48, Y: 0.50, W: 0.08, H: 0.08},
|
|
{Label: "pussy", X: 0.54, Y: 0.51, W: 0.08, H: 0.08},
|
|
},
|
|
}
|
|
|
|
got := trainingApplyPoseToPrediction(pred, TrainingPosePrediction{Available: false})
|
|
if got.SexPosition == trainingNoSexPositionLabel {
|
|
t.Fatalf("sex position = %q, want uncertain context prediction", got.SexPosition)
|
|
}
|
|
if got.SexPositionScore < trainingPositionContextMinScore {
|
|
t.Fatalf("sex position score = %.3f, want >= %.3f", got.SexPositionScore, trainingPositionContextMinScore)
|
|
}
|
|
if got.SexPositionScore > trainingPositionContextMaxScore {
|
|
t.Fatalf("sex position score = %.3f, want <= %.3f", got.SexPositionScore, trainingPositionContextMaxScore)
|
|
}
|
|
if !strings.Contains(got.Source, "box_context") {
|
|
t.Fatalf("source = %q, want box_context", got.Source)
|
|
}
|
|
}
|
|
|
|
func TestTrainingFuseHybridPositionScoresCapsUnconfirmedPose(t *testing.T) {
|
|
positionSet := stringSet(defaultTrainingLabelsFromJSON().SexPositions)
|
|
poseScores := map[string]float64{}
|
|
contextScores := map[string]float64{}
|
|
|
|
trainingCombinePositionScore(poseScores, positionSet, "doggy", 0.82)
|
|
|
|
gotPosition, gotScore, gotPose, gotContext := trainingFuseHybridPositionScores(poseScores, contextScores)
|
|
if gotPosition != "doggy" {
|
|
t.Fatalf("position = %q, want doggy", gotPosition)
|
|
}
|
|
if !gotPose || gotContext {
|
|
t.Fatalf("signals pose=%v context=%v, want pose only", gotPose, gotContext)
|
|
}
|
|
if gotScore > trainingPoseStrongUnconfirmedMaxScore {
|
|
t.Fatalf("score = %.3f, want capped <= %.3f", gotScore, trainingPoseStrongUnconfirmedMaxScore)
|
|
}
|
|
}
|
|
|
|
func TestTrainingFuseHybridPositionScoresPrefersStrongContextOverUnconfirmedPose(t *testing.T) {
|
|
positionSet := stringSet(defaultTrainingLabelsFromJSON().SexPositions)
|
|
poseScores := map[string]float64{}
|
|
contextScores := map[string]float64{}
|
|
|
|
trainingCombinePositionScore(poseScores, positionSet, "doggy", 0.62)
|
|
trainingCombinePositionScore(contextScores, positionSet, "blowjob", trainingPositionContextMaxScore)
|
|
|
|
gotPosition, gotScore, gotPose, gotContext := trainingFuseHybridPositionScores(poseScores, contextScores)
|
|
if gotPosition != "blowjob" {
|
|
t.Fatalf("position = %q, want blowjob", gotPosition)
|
|
}
|
|
if gotPose || !gotContext {
|
|
t.Fatalf("signals pose=%v context=%v, want context only", gotPose, gotContext)
|
|
}
|
|
if gotScore < trainingPositionContextMinScore {
|
|
t.Fatalf("score = %.3f, want context score >= %.3f", gotScore, trainingPositionContextMinScore)
|
|
}
|
|
}
|
|
|
|
func TestTrainingApplyPoseToPredictionBoostsPoseWithBoxContext(t *testing.T) {
|
|
pred := TrainingPrediction{
|
|
ModelAvailable: true,
|
|
Source: "yolo26_detector",
|
|
SexPosition: trainingNoSexPositionLabel,
|
|
Boxes: []TrainingBox{
|
|
{Label: "penis", X: 0.48, Y: 0.50, W: 0.08, H: 0.08},
|
|
{Label: "pussy", X: 0.54, Y: 0.51, W: 0.08, H: 0.08},
|
|
},
|
|
}
|
|
|
|
pose := TrainingPosePrediction{
|
|
Available: true,
|
|
Persons: []TrainingPosePerson{
|
|
{
|
|
Label: "doggy",
|
|
Score: 0.31,
|
|
Box: TrainingBox{X: 0.20, Y: 0.12, W: 0.60, H: 0.76},
|
|
Keypoints: trainingTestPoseKeypoints(nil),
|
|
},
|
|
},
|
|
}
|
|
|
|
got := trainingApplyPoseToPrediction(pred, pose)
|
|
if got.SexPosition != "doggy" {
|
|
t.Fatalf("sex position = %q, want doggy", got.SexPosition)
|
|
}
|
|
if got.SexPositionScore <= 0.31 {
|
|
t.Fatalf("sex position score = %.3f, want context boost over 0.31", got.SexPositionScore)
|
|
}
|
|
if !strings.Contains(got.Source, "yolo_pose") || !strings.Contains(got.Source, "box_context") {
|
|
t.Fatalf("source = %q, want yolo_pose and box_context", got.Source)
|
|
}
|
|
}
|
|
|
|
func TestBuildClipPositionHitsFromEvidenceRequiresTemporalSupport(t *testing.T) {
|
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
|
{Time: 1, Label: "doggy", Score: 0.55, HasPose: true, PersonCount: 2},
|
|
{Time: 2, Label: "cowgirl", Score: 0.60, HasPose: true, PersonCount: 2},
|
|
{Time: 6, Label: "doggy", Score: 0.58, HasPose: true, PersonCount: 2},
|
|
}, 10)
|
|
|
|
if len(hits) != 0 {
|
|
t.Fatalf("hits = %+v, want no temporally supported position", hits)
|
|
}
|
|
}
|
|
|
|
func TestBuildClipPositionHitsFromEvidenceBuildsStablePosition(t *testing.T) {
|
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
|
{Time: 1, Label: "doggy", Score: 0.45, HasPose: true, HasContext: true, PersonCount: 2},
|
|
{Time: 2, Label: "doggy", Score: 0.47, HasPose: true, HasContext: true, PersonCount: 2},
|
|
{Time: 3, Label: "doggy", Score: 0.44, HasPose: true, HasContext: true, PersonCount: 2},
|
|
{Time: 6, Label: "cowgirl", Score: 0.62, HasPose: true, PersonCount: 2},
|
|
}, 10)
|
|
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected stable doggy position hit")
|
|
}
|
|
if hits[0].Label != "position:doggy" {
|
|
t.Fatalf("label = %q, want position:doggy", hits[0].Label)
|
|
}
|
|
if hits[0].End <= hits[0].Start {
|
|
t.Fatalf("invalid hit span: %+v", hits[0])
|
|
}
|
|
}
|
|
|
|
func TestBuildClipPositionHitsFromEvidenceCapsContextOnlyScore(t *testing.T) {
|
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
|
{Time: 1, Label: "missionary", Score: 0.90, HasContext: true, PersonCount: 2},
|
|
{Time: 2, Label: "missionary", Score: 0.88, HasContext: true, PersonCount: 2},
|
|
{Time: 3, Label: "missionary", Score: 0.92, HasContext: true, PersonCount: 2},
|
|
}, 8)
|
|
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected context-only position hit")
|
|
}
|
|
if hits[0].Score > trainingPositionContextMaxScore {
|
|
t.Fatalf("score = %.3f, want <= %.3f", hits[0].Score, trainingPositionContextMaxScore)
|
|
}
|
|
}
|
|
|
|
func TestAppendVideoFrameHighlightHitsFromPredictionOmitsRawPosition(t *testing.T) {
|
|
hits := appendVideoFrameHighlightHitsFromPrediction(nil, TrainingPrediction{
|
|
ModelAvailable: true,
|
|
SexPosition: "doggy",
|
|
SexPositionScore: 0.85,
|
|
ObjectsPresent: []TrainingScoredLabel{
|
|
{Label: "vibrator", Score: 0.90},
|
|
},
|
|
}, 4)
|
|
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected non-position video frame highlight")
|
|
}
|
|
|
|
for _, hit := range hits {
|
|
if strings.Contains(hit.Label, "position:") {
|
|
t.Fatalf("hit label = %q, want raw position removed", hit.Label)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildClipPositionHitsFromEvidencePrefersVideoMAEClipOverFramePose(t *testing.T) {
|
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
|
{Time: 1, Label: "doggy", Score: 0.60, HasPose: true, PersonCount: 2},
|
|
{Time: 2, Label: "doggy", Score: 0.60, HasPose: true, PersonCount: 2},
|
|
{Time: 3, Label: "doggy", Score: 0.60, HasPose: true, PersonCount: 2},
|
|
{Time: 2, Label: "cowgirl", Score: 0.58, HasClip: true},
|
|
}, 8)
|
|
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected VideoMAE-backed position hit")
|
|
}
|
|
if hits[0].Label != "position:cowgirl" {
|
|
t.Fatalf("label = %q, want position:cowgirl", hits[0].Label)
|
|
}
|
|
}
|
|
|
|
func TestBuildClipPositionHitsFromEvidenceUsesVideoMAEClipSpan(t *testing.T) {
|
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
|
{Time: 12, Start: 10, End: 14, Label: "cowgirl", Score: 0.62, HasClip: true},
|
|
}, 20)
|
|
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected VideoMAE clip ledger hit")
|
|
}
|
|
if hits[0].Label != "position:cowgirl" {
|
|
t.Fatalf("label = %q, want position:cowgirl", hits[0].Label)
|
|
}
|
|
if hits[0].Start > 10 || hits[0].End < 14 {
|
|
t.Fatalf("span = %.1f..%.1f, want to cover clip 10..14", hits[0].Start, hits[0].End)
|
|
}
|
|
}
|
|
|
|
func TestBuildClipPositionHitsFromEvidenceKeepsStableToyPlayTimelinePosition(t *testing.T) {
|
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
|
{Time: 12, Start: 10, End: 24, Label: "toy_play", Score: 0.90, HasClip: true},
|
|
}, 120)
|
|
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected stable toy_play timeline position")
|
|
}
|
|
if hits[0].Label != "position:toy_play" {
|
|
t.Fatalf("label = %q, want position:toy_play", hits[0].Label)
|
|
}
|
|
}
|
|
|
|
func TestBuildClipPositionHitsFromEvidenceDropsShortWeakPositionFlipInLongVideo(t *testing.T) {
|
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
|
{Time: 10, Start: 8, End: 28, Label: "missionary", Score: 0.70, HasClip: true},
|
|
{Time: 32, Start: 30, End: 34, Label: "cowgirl", Score: 0.46, HasClip: true},
|
|
{Time: 35, Start: 34, End: 38, Label: "toy_play", Score: 0.46, HasClip: true},
|
|
{Time: 50, Start: 36, End: 64, Label: "missionary", Score: 0.72, HasClip: true},
|
|
}, 120)
|
|
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected stable missionary position hits")
|
|
}
|
|
for _, hit := range hits {
|
|
if hit.Label == "position:cowgirl" {
|
|
t.Fatalf("hits = %+v, want short weak cowgirl flip omitted", hits)
|
|
}
|
|
if hit.Label == "position:toy_play" {
|
|
t.Fatalf("hits = %+v, want short weak toy_play flip omitted", hits)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildClipPositionHitsFromEvidenceDropsCloseFrameConflict(t *testing.T) {
|
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
|
{Time: 1, Label: "doggy", Score: 0.52, HasPose: true, HasContext: true, PersonCount: 2},
|
|
{Time: 2, Label: "doggy", Score: 0.52, HasPose: true, HasContext: true, PersonCount: 2},
|
|
{Time: 3, Label: "doggy", Score: 0.52, HasPose: true, HasContext: true, PersonCount: 2},
|
|
{Time: 1, Label: "cowgirl", Score: 0.50, HasPose: true, HasContext: true, PersonCount: 2},
|
|
{Time: 2, Label: "cowgirl", Score: 0.50, HasPose: true, HasContext: true, PersonCount: 2},
|
|
{Time: 3, Label: "cowgirl", Score: 0.50, HasPose: true, HasContext: true, PersonCount: 2},
|
|
}, 8)
|
|
|
|
if len(hits) != 0 {
|
|
t.Fatalf("hits = %+v, want no hard position for close conflict", hits)
|
|
}
|
|
}
|
|
|
|
func TestTrainingFilterPosePersonsByContextDropsUnannotatedPeople(t *testing.T) {
|
|
persons := []TrainingPosePerson{
|
|
{
|
|
Label: "doggy",
|
|
Score: 0.80,
|
|
Box: TrainingBox{X: 0.10, Y: 0.10, W: 0.30, H: 0.70},
|
|
Keypoints: trainingTestPoseKeypoints(nil),
|
|
},
|
|
{
|
|
Label: "doggy",
|
|
Score: 0.70,
|
|
Box: TrainingBox{X: 0.70, Y: 0.10, W: 0.20, H: 0.70},
|
|
Keypoints: trainingTestPoseKeypoints(nil),
|
|
},
|
|
}
|
|
|
|
filtered := trainingFilterPosePersonsByContext(persons, []TrainingBox{
|
|
{Label: "person_female", X: 0.08, Y: 0.08, W: 0.34, H: 0.74},
|
|
})
|
|
|
|
if len(filtered) != 1 {
|
|
t.Fatalf("filtered persons = %d, want 1", len(filtered))
|
|
}
|
|
if filtered[0].Box.X != persons[0].Box.X {
|
|
t.Fatalf("kept wrong person: %+v", filtered[0].Box)
|
|
}
|
|
}
|