From 345665522059e1d01ced181fb84833be5eb72e35 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:31:43 +0200 Subject: [PATCH] added detection boxes & bugfixes --- backend/ml/predict_detector_model.py | 2 +- backend/training.go | 11 +- frontend/src/components/ui/TrainingTab.tsx | 329 +++++++++++++++++---- frontend/src/components/ui/formatters.ts | 3 +- 4 files changed, 286 insertions(+), 59 deletions(-) diff --git a/backend/ml/predict_detector_model.py b/backend/ml/predict_detector_model.py index a23cfaa..cd10318 100644 --- a/backend/ml/predict_detector_model.py +++ b/backend/ml/predict_detector_model.py @@ -23,7 +23,7 @@ def main(): root = Path(args.root) image_path = Path(args.image) - model_path = root / "detector" / "best.pt" + model_path = root / "detector" / "model" / "best.pt" if not model_path.exists(): print(json.dumps({ "available": False, diff --git a/backend/training.go b/backend/training.go index 62a1a44..b71681c 100644 --- a/backend/training.go +++ b/backend/training.go @@ -916,13 +916,16 @@ func trainingWriteDetectorSample(root string, sample *TrainingSample, boxes []Tr continue } - // Erwartung: box.X/Y/W/H sind bereits YOLO-normalisiert: - // x_center, y_center, width, height. + // Frontend/Predictor nutzen x/y als linke obere Ecke. + // YOLO erwartet x_center/y_center. + xCenter := clamp01(x + w/2) + yCenter := clamp01(y + h/2) + lines = append(lines, fmt.Sprintf( "%d %.6f %.6f %.6f %.6f", classID, - x, - y, + xCenter, + yCenter, w, h, )) diff --git a/frontend/src/components/ui/TrainingTab.tsx b/frontend/src/components/ui/TrainingTab.tsx index 1032230..547e284 100644 --- a/frontend/src/components/ui/TrainingTab.tsx +++ b/frontend/src/components/ui/TrainingTab.tsx @@ -1,7 +1,7 @@ // frontend/src/components/ui/TrainingTab.tsx 'use client' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import Button from './Button' import LoadingSpinner from './LoadingSpinner' import { formatBytes, formatDuration } from './formatters' @@ -60,6 +60,7 @@ type CorrectionState = { bodyPartsPresent: string[] objectsPresent: string[] clothingPresent: string[] + boxes: TrainingBox[] } type TrainingBox = { @@ -99,20 +100,6 @@ function scoredLabelsText(items?: ScoredLabel[] | null) { .join(', ') } -function detectionBoxes(sample: TrainingSample | null) { - const boxes = sample?.prediction.boxes ?? [] - - return boxes.filter((box) => { - const label = String(box.label || '').trim() - if (!label) return false - - const isBodyPart = sample?.prediction.bodyPartsPresent?.some((x) => x.label === label) - const isObject = sample?.prediction.objectsPresent?.some((x) => x.label === label) - - return isBodyPart || isObject - }) -} - function clampPercent(v: number) { if (!Number.isFinite(v)) return 0 return Math.max(0, Math.min(100, v)) @@ -133,6 +120,40 @@ function totalPeopleFromGenderCounts(state: Pick() + const out: string[] = [] + + for (const value of values) { + const clean = String(value || '').trim() + if (!clean || seen.has(clean)) continue + seen.add(clean) + out.push(clean) + } + + return out +} + function predictionToCorrection(sample: TrainingSample | null): CorrectionState { const p = sample?.prediction @@ -148,6 +169,14 @@ function predictionToCorrection(sample: TrainingSample | null): CorrectionState bodyPartsPresent: (p?.bodyPartsPresent ?? []).map((x) => x.label), objectsPresent: (p?.objectsPresent ?? []).map((x) => x.label), clothingPresent: (p?.clothingPresent ?? []).map((x) => x.label), + boxes: (p?.boxes ?? []).map((box) => ({ + label: String(box.label || '').trim(), + score: box.score, + x: clamp01(Number(box.x)), + y: clamp01(Number(box.y)), + w: clamp01(Number(box.w)), + h: clamp01(Number(box.h)), + })).filter((box) => box.label && box.w > 0 && box.h > 0), } } @@ -184,6 +213,25 @@ export default function TrainingTab() { const [deletingTrainingData, setDeletingTrainingData] = useState(false) const [error, setError] = useState(null) const [message, setMessage] = useState(null) + + const imageBoxRef = useRef(null) + + const [drawingBox, setDrawingBox] = useState(null) + const [boxLabel, setBoxLabel] = useState('') + + const boxLabels = useMemo(() => { + return uniqStrings([ + ...labels.bodyParts, + ...labels.objects, + ...labels.clothing, + ]).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) + }, [labels.bodyParts, labels.objects, labels.clothing]) + + const correctionBoxes = correction.boxes ?? [] + + const visibleBoxes = drawingBox + ? [...correctionBoxes, drawingBox] + : correctionBoxes const computedPeopleCount = useMemo( () => totalPeopleFromGenderCounts(correction), @@ -195,8 +243,6 @@ export default function TrainingTab() { return `${sample.frameUrl}&t=${encodeURIComponent(sample.sampleId)}` }, [sample]) - const boxes = useMemo(() => detectionBoxes(sample), [sample]) - const canStartTraining = Boolean(trainingStatus?.canTrain) const feedbackCount = trainingStatus?.feedbackCount ?? 0 const requiredCount = trainingStatus?.requiredCount ?? 5 @@ -248,6 +294,11 @@ export default function TrainingTab() { }) }, []) + useEffect(() => { + if (boxLabel && boxLabels.includes(boxLabel)) return + setBoxLabel(boxLabels[0] || '') + }, [boxLabel, boxLabels]) + useEffect(() => { void loadLabels() void loadNext() @@ -267,12 +318,17 @@ export default function TrainingTab() { ...correction, peopleCount: totalPeopleFromGenderCounts(correction), unknownCount: 0, + boxes: (correction.boxes ?? []) + .map(normalizeBox) + .filter((box) => box.label && box.w > 0 && box.h > 0), } const payload = { sampleId: sample.sampleId, accepted, - correction: accepted ? undefined : correctionPayload, + correction: accepted && correctionPayload.boxes.length === 0 + ? undefined + : correctionPayload, } const res = await fetch('/api/training/feedback', { @@ -363,6 +419,86 @@ export default function TrainingTab() { } }, [loadNext, loadTrainingStatus, requiredCount]) + const getPointerPosInImage = useCallback((clientX: number, clientY: number) => { + const el = imageBoxRef.current + if (!el) return null + + const rect = el.getBoundingClientRect() + if (rect.width <= 0 || rect.height <= 0) return null + + return { + x: clamp01((clientX - rect.left) / rect.width), + y: clamp01((clientY - rect.top) / rect.height), + } + }, []) + + const startDrawBox = useCallback((e: React.PointerEvent) => { + if (!boxLabel) return + if (loading || saving) return + + const pos = getPointerPosInImage(e.clientX, e.clientY) + if (!pos) return + + e.currentTarget.setPointerCapture(e.pointerId) + + setDrawingBox({ + label: boxLabel, + x: pos.x, + y: pos.y, + w: 0, + h: 0, + }) + }, [boxLabel, getPointerPosInImage, loading, saving]) + + const moveDrawBox = useCallback((e: React.PointerEvent) => { + if (!drawingBox) return + + const pos = getPointerPosInImage(e.clientX, e.clientY) + if (!pos) return + + const x1 = drawingBox.x + const y1 = drawingBox.y + const x2 = pos.x + const y2 = pos.y + + setDrawingBox({ + label: drawingBox.label, + x: Math.min(x1, x2), + y: Math.min(y1, y2), + w: Math.abs(x2 - x1), + h: Math.abs(y2 - y1), + }) + }, [drawingBox, getPointerPosInImage]) + + const finishDrawBox = useCallback(() => { + if (!drawingBox) return + + const box = normalizeBox(drawingBox) + + setDrawingBox(null) + + if (box.w < 0.01 || box.h < 0.01) return + + setCorrection((prev) => ({ + ...prev, + boxes: [...(prev.boxes ?? []), box], + })) + }, [drawingBox]) + + const removeBox = useCallback((index: number) => { + setCorrection((prev) => ({ + ...prev, + boxes: (prev.boxes ?? []).filter((_, i) => i !== index), + })) + }, []) + + const clearBoxes = useCallback(() => { + setCorrection((prev) => ({ + ...prev, + boxes: [], + })) + }, []) + return (
{/* Sidebar links */} @@ -372,6 +508,10 @@ export default function TrainingTab() { Training
+
+ {feedbackCount} +
+ + + )) + )} + + + {correctionBoxes.length > 0 ? ( + + ) : null} +
@@ -527,39 +720,69 @@ export default function TrainingTab() { /> ) : imageSrc ? (
- Training Frame +
+ Training Frame - {boxes.length > 0 ? ( -
- {boxes.map((box, index) => { +
+ {visibleBoxes.map((box, index) => { const left = clampPercent(box.x * 100) const top = clampPercent(box.y * 100) const width = clampPercent(box.w * 100) const height = clampPercent(box.h * 100) + const isDraft = drawingBox && index === visibleBoxes.length - 1 return ( -
{ + e.preventDefault() + e.stopPropagation() + }} + onClick={(e) => { + e.preventDefault() + e.stopPropagation() + if (!isDraft) removeBox(index) + }} > -
+ {box.label} {typeof box.score === 'number' ? percent(box.score) : ''} -
-
+ + ) })}
- ) : null} +
) : (
Kein Frame geladen
diff --git a/frontend/src/components/ui/formatters.ts b/frontend/src/components/ui/formatters.ts index 834b5d4..4772eb2 100644 --- a/frontend/src/components/ui/formatters.ts +++ b/frontend/src/components/ui/formatters.ts @@ -72,4 +72,5 @@ export function formatDateTime(v?: string | number | Date | null): string { hour: '2-digit', minute: '2-digit', }) -} \ No newline at end of file +} +