From a598ac025fb073d235ef27a742f3dd6caf7457aa Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:40:15 +0200 Subject: [PATCH] bugfixes --- backend/training.go | 114 +++++++---- backend/training_label_loader.go | 5 +- frontend/src/components/ui/TrainingTab.tsx | 225 ++++++++++++++++----- 3 files changed, 261 insertions(+), 83 deletions(-) diff --git a/backend/training.go b/backend/training.go index bd5aedc..73e0bdb 100644 --- a/backend/training.go +++ b/backend/training.go @@ -784,9 +784,9 @@ func trainingStatusHandler(w http.ResponseWriter, r *http.Request) { canTrain := feedbackCount >= minTrainingFeedbackCount // Pipeline: - // - YOLO trainiert nur BodyParts, Objects, Clothing und deren Boxen. - // - CLIP + Logistic Regression/KNN trainiert die Sexposition. - // - Personen/Gender werden manuell korrigiert und nicht per YOLO erkannt. + // - YOLO erkennt Personen/Gender für die Counts. + // - Automatisch erkannte Personenboxen werden nicht an das Frontend als sichtbare Boxen zurückgegeben. + // - Manuell gezeichnete Personenboxen werden trotzdem als Trainingsdaten gespeichert. trainingWriteJSON(w, http.StatusOK, map[string]any{ "ok": true, "feedbackCount": feedbackCount, @@ -800,8 +800,8 @@ func trainingStatusHandler(w http.ResponseWriter, r *http.Request) { "usesSceneKNN": false, "usesResNet18KNN": false, - "detectsPeople": false, - "detectsGender": false, + "detectsPeople": true, + "detectsGender": true, "detectsBodyParts": true, "detectsObjects": true, "detectsClothing": true, @@ -856,8 +856,8 @@ func trainingStatusHandler(w http.ResponseWriter, r *http.Request) { "pipeline": map[string]any{ "variant": "B", - "peopleSource": "manual", - "genderSource": "manual", + "peopleSource": "yolo_detector", + "genderSource": "yolo_detector", "bodyPartsSource": "yolo_detector", "objectsSource": "yolo_detector", "clothingSource": "yolo_detector", @@ -1304,9 +1304,9 @@ func trainingApplyScenePosition( } func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPrediction { - boxes := det.Boxes - if boxes == nil { - boxes = []TrainingBox{} + rawBoxes := det.Boxes + if rawBoxes == nil { + rawBoxes = []TrainingBox{} } pred := TrainingPrediction{ @@ -1321,7 +1321,7 @@ func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPred BodyPartsPresent: []TrainingScoredLabel{}, ObjectsPresent: []TrainingScoredLabel{}, ClothingPresent: []TrainingScoredLabel{}, - Boxes: boxes, + Boxes: []TrainingBox{}, } if pred.Source == "" { @@ -1338,36 +1338,78 @@ func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPred return pred } - allowed := map[string]bool{} - for _, label := range grouped.BodyParts { - allowed[strings.TrimSpace(label)] = true - } - for _, label := range grouped.Objects { - allowed[strings.TrimSpace(label)] = true - } - for _, label := range grouped.Clothing { - allowed[strings.TrimSpace(label)] = true - } - - filteredBoxes := []TrainingBox{} - for _, box := range boxes { - label := strings.TrimSpace(box.Label) - if allowed[label] { - filteredBoxes = append(filteredBoxes, box) + peopleSet := map[string]bool{} + for _, label := range grouped.People { + clean := strings.TrimSpace(label) + if clean != "" { + peopleSet[clean] = true } } - boxes = filteredBoxes - pred.Boxes = boxes + detectionSet := map[string]bool{} - pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.BodyParts) - pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Objects) - pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Clothing) + for _, label := range grouped.BodyParts { + clean := strings.TrimSpace(label) + if clean != "" { + detectionSet[clean] = true + } + } - pred.UnknownCount = 0 - pred.PeopleCount = 0 - pred.MaleCount = 0 - pred.FemaleCount = 0 + for _, label := range grouped.Objects { + clean := strings.TrimSpace(label) + if clean != "" { + detectionSet[clean] = true + } + } + + for _, label := range grouped.Clothing { + clean := strings.TrimSpace(label) + if clean != "" { + detectionSet[clean] = true + } + } + + visibleBoxes := []TrainingBox{} + + for _, box := range rawBoxes { + label := strings.TrimSpace(box.Label) + if label == "" { + continue + } + + box.Label = label + + // Personen erkennen und zählen. + // Personenboxen werden jetzt auch sichtbar zurückgegeben, + // damit sie im Frontend gezeichnet, verschoben, gelöscht und trainiert werden können. + if peopleSet[label] { + switch label { + case "person_male", "male_person": + pred.MaleCount++ + + case "person_female", "female_person": + pred.FemaleCount++ + + case "person", "person_unknown": + pred.UnknownCount++ + } + + visibleBoxes = append(visibleBoxes, box) + continue + } + + // Nur Bodyparts/Objects/Clothing bleiben als Boxen sichtbar. + if detectionSet[label] { + visibleBoxes = append(visibleBoxes, box) + } + } + + pred.PeopleCount = pred.MaleCount + pred.FemaleCount + pred.UnknownCount + pred.Boxes = visibleBoxes + + pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.BodyParts) + pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.Objects) + pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.Clothing) return pred } diff --git a/backend/training_label_loader.go b/backend/training_label_loader.go index 6fada92..c51a28d 100644 --- a/backend/training_label_loader.go +++ b/backend/training_label_loader.go @@ -87,7 +87,10 @@ func trainingDetectorLabels() ([]string, error) { labels := []string{} - // Bestehende Reihenfolge beibehalten, damit alte Class-IDs stabil bleiben. + // Wichtig: + // People zuerst oder zuletzt ist egal, aber die Reihenfolge bestimmt YOLO-Class-IDs. + // Wenn du schon ein bestehendes Detector-Modell hast, musst du danach neu trainieren. + labels = append(labels, grouped.People...) labels = append(labels, grouped.BodyParts...) labels = append(labels, grouped.Objects...) labels = append(labels, grouped.Clothing...) diff --git a/frontend/src/components/ui/TrainingTab.tsx b/frontend/src/components/ui/TrainingTab.tsx index 66cab17..ee32d5e 100644 --- a/frontend/src/components/ui/TrainingTab.tsx +++ b/frontend/src/components/ui/TrainingTab.tsx @@ -124,6 +124,88 @@ function percent(v: number) { return `${Math.round(v * 100)}%` } +function scoreLevel(score?: number | null): 'none' | 'low' | 'mid' | 'high' { + const n = Number(score) + + if (!Number.isFinite(n)) return 'none' + if (n < 0.5) return 'low' + if (n < 0.75) return 'mid' + return 'high' +} + +function scoreBorderClass(score?: number | null, opts?: { draft?: boolean }) { + if (opts?.draft) return 'border-amber-400' + + switch (scoreLevel(score)) { + case 'low': + return 'border-red-500' + case 'mid': + return 'border-yellow-400' + case 'high': + return 'border-emerald-400' + default: + return 'border-gray-300' + } +} + +function scoreBgClass(score?: number | null, opts?: { draft?: boolean }) { + if (opts?.draft) return 'bg-amber-400' + + switch (scoreLevel(score)) { + case 'low': + return 'bg-red-500' + case 'mid': + return 'bg-yellow-400' + case 'high': + return 'bg-emerald-400' + default: + return 'bg-gray-300' + } +} + +function scoreHoverClass(score?: number | null, opts?: { draft?: boolean }) { + if (opts?.draft) return '' + + switch (scoreLevel(score)) { + case 'low': + return 'hover:bg-red-400' + case 'mid': + return 'hover:bg-yellow-300' + case 'high': + return 'hover:bg-emerald-300' + default: + return 'hover:bg-gray-200' + } +} + +function scoreRingClass(score?: number | null, opts?: { draft?: boolean }) { + if (opts?.draft) return 'ring-amber-500' + + switch (scoreLevel(score)) { + case 'low': + return 'ring-red-500' + case 'mid': + return 'ring-yellow-400' + case 'high': + return 'ring-emerald-500' + default: + return 'ring-gray-400' + } +} + +function scoreDetectionPillClass(score?: number | null) { + switch (scoreLevel(score)) { + case 'low': + return 'bg-red-50 text-red-800 ring-red-200 dark:bg-red-500/15 dark:text-red-100 dark:ring-red-400/30' + case 'mid': + return 'bg-yellow-50 text-yellow-900 ring-yellow-200 dark:bg-yellow-500/15 dark:text-yellow-100 dark:ring-yellow-400/30' + case 'high': + return 'bg-emerald-50 text-emerald-800 ring-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-100 dark:ring-emerald-400/30' + default: + return 'bg-gray-50 text-gray-700 ring-gray-200 dark:bg-white/5 dark:text-gray-200 dark:ring-white/10' + } +} + function normalizeMovedBox(box: TrainingBox): TrainingBox { const w = clamp01(box.w) const h = clamp01(box.h) @@ -137,22 +219,6 @@ function normalizeMovedBox(box: TrainingBox): TrainingBox { } } -function scoredLabelsText(items?: ScoredLabel[] | null) { - const list = Array.isArray(items) ? items : [] - - if (list.length === 0) return '—' - - return list - .map((x) => { - const label = String(x?.label ?? '').trim() - if (!label) return null - - return `${label} (${percent(Number(x?.score ?? 0))})` - }) - .filter(Boolean) - .join(', ') -} - function clampPercent(v: number) { if (!Number.isFinite(v)) return 0 return Math.max(0, Math.min(100, v)) @@ -209,19 +275,6 @@ function uniqStrings(values: string[]) { return out } -function isPersonBoxLabel(label?: string) { - const normalized = String(label || '').trim().toLowerCase() - - return ( - normalized === 'person' || - normalized === 'person_male' || - normalized === 'person_female' || - normalized === 'person_unknown' || - normalized === 'male_person' || - normalized === 'female_person' - ) -} - function predictionToCorrection(sample: TrainingSample | null): CorrectionState { const p = sample?.prediction @@ -246,8 +299,7 @@ function predictionToCorrection(sample: TrainingSample | null): CorrectionState w: clamp01(Number(box.w)), h: clamp01(Number(box.h)), })) - .filter((box) => box.label && box.w > 0 && box.h > 0) - .filter((box) => !isPersonBoxLabel(box.label)), + .filter((box) => box.label && box.w > 0 && box.h > 0), } } @@ -532,6 +584,52 @@ function LabelToggleGrid(props: { ) } +function ScoredLabelChips(props: { + items?: ScoredLabel[] | null +}) { + const list = Array.isArray(props.items) ? props.items : [] + + if (list.length === 0) { + return — + } + + return ( +