From 13ec4cca5415daa0e5be8adb0c30f3cde4893cf3 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:38:56 +0200 Subject: [PATCH] fixed object detection --- backend/ml/predict_scene_model.py | 11 +- backend/recorder_settings.json | 2 +- backend/training.go | 113 ++++++++++++++++++++- frontend/src/components/ui/TrainingTab.tsx | 98 +++++++++++++++--- 4 files changed, 204 insertions(+), 20 deletions(-) diff --git a/backend/ml/predict_scene_model.py b/backend/ml/predict_scene_model.py index ed0091e..87b39a4 100644 --- a/backend/ml/predict_scene_model.py +++ b/backend/ml/predict_scene_model.py @@ -164,9 +164,14 @@ def main(): print(json.dumps(pred, ensure_ascii=False)) def predict_boxes(root: Path, image_path: Path): - script = Path(__file__).parent / "predict_detector_model.py" + candidates = [ + Path(__file__).parent / "predict_detector_model.py", + Path.cwd() / "backend" / "ml" / "predict_detector_model.py", + Path.cwd() / "ml" / "predict_detector_model.py", + ] - if not script.exists(): + script = next((p for p in candidates if p.exists()), None) + if script is None: return [] try: @@ -181,7 +186,7 @@ def predict_boxes(root: Path, image_path: Path): ], capture_output=True, text=True, - timeout=20, + timeout=60, ) if proc.returncode != 0: diff --git a/backend/recorder_settings.json b/backend/recorder_settings.json index b62ac00..fc67600 100644 --- a/backend/recorder_settings.json +++ b/backend/recorder_settings.json @@ -11,7 +11,7 @@ "useMyFreeCamsWatcher": true, "autoDeleteSmallDownloads": true, "autoDeleteSmallDownloadsBelowMB": 300, - "autoDeleteSmallDownloadsKeepFavorites": true, + "autoDeleteSmallDownloadsKeepFavorites": false, "lowDiskPauseBelowGB": 5, "blurPreviews": false, "teaserPlayback": "all", diff --git a/backend/training.go b/backend/training.go index fe0c0cc..7357b5d 100644 --- a/backend/training.go +++ b/backend/training.go @@ -255,6 +255,11 @@ func trainingLatestOpenSample(root string) (*TrainingSample, bool, error) { continue } + // Wichtig: Prediction aktualisieren, damit alte "model_missing"-Samples + // nach einem Training nicht stale bleiben. + sample.Prediction = trainingPredictFrame(framePath) + _ = trainingWriteSample(root, sample) + return sample, true, nil } @@ -874,6 +879,7 @@ func trainingExtractFrame(videoPath string, framePath string, second float64) er func trainingPredictFrame(framePath string) TrainingPrediction { root, err := trainingRootDir() if err != nil { + fmt.Println("⚠️ training predict root error:", err) return trainingEmptyPrediction("root_error") } @@ -893,14 +899,16 @@ func trainingPredictFrame(framePath string) TrainingPrediction { } out, err := cmd.CombinedOutput() + outText := strings.TrimSpace(string(out)) + if err != nil { - fmt.Println("⚠️ training predict failed:", err, strings.TrimSpace(string(out))) + fmt.Println("⚠️ training predict failed:", err, outText) return trainingEmptyPrediction("predict_failed") } var pred TrainingPrediction if err := json.Unmarshal(out, &pred); err != nil { - fmt.Println("⚠️ training predict json failed:", err, strings.TrimSpace(string(out))) + fmt.Println("⚠️ training predict json failed:", err, outText) return trainingEmptyPrediction("predict_json_failed") } @@ -1079,7 +1087,15 @@ func trainingScriptPath(name string) string { } } - // 2) Dev-Fallback: Projektstruktur. + // 2) App-/Backend-relativ wie record_paths.go. + if backendRoot, err := trainingBackendRootDir(); err == nil { + p := filepath.Join(backendRoot, "ml", name) + if _, err := os.Stat(p); err == nil { + return p + } + } + + // 3) Dev-Fallback. root := trainingProjectRoot() p := filepath.Join(root, "backend", "ml", name) @@ -1095,8 +1111,97 @@ func trainingScriptPath(name string) string { return filepath.Join(root, "backend", "ml", name) } +func isTempBuildDir(dir string) bool { + low := strings.ToLower(filepath.Clean(dir)) + + return strings.Contains(low, `\appdata\local\temp`) || + strings.Contains(low, `\temp\`) || + strings.Contains(low, `\tmp\`) || + strings.Contains(low, `\go-build`) || + strings.Contains(low, `/tmp/`) || + strings.Contains(low, `/go-build`) +} + +func trainingBackendRootDir() (string, error) { + // Fall 1: + // App läuft aus /backend oder EXE liegt neben /ml. + // Dann ist "ml/predict_scene_model.py" app-relativ korrekt. + if script, err := resolvePathRelativeToApp(filepath.Join("ml", "predict_scene_model.py")); err == nil { + if st, statErr := os.Stat(script); statErr == nil && !st.IsDir() { + return filepath.Dir(filepath.Dir(script)), nil + } + } + + // Fall 2: + // Dev-Start aus Projekt-Root. + // Dann ist "backend/ml/predict_scene_model.py" app-relativ korrekt. + if script, err := resolvePathRelativeToApp(filepath.Join("backend", "ml", "predict_scene_model.py")); err == nil { + if st, statErr := os.Stat(script); statErr == nil && !st.IsDir() { + return filepath.Dir(filepath.Dir(script)), nil + } + } + + // Fall 3: + // Gebaute App mit embedded ML-Scripts. + // Dann gibt es ggf. kein /ml neben der EXE. + // In diesem Fall nehmen wir den EXE-Ordner als App/Backend-Root, + // solange es nicht go-run Temp ist. + if dir, err := exeDir(); err == nil && strings.TrimSpace(dir) != "" && !isTempBuildDir(dir) { + return dir, nil + } + + // Fall 4: + // Dev-Fallback über Working Directory. + wd, err := os.Getwd() + if err != nil { + return "", err + } + + // Wenn wir direkt in /backend laufen. + if _, err := os.Stat(filepath.Join(wd, "ml", "predict_scene_model.py")); err == nil { + return wd, nil + } + + // Wenn wir im Projekt-Root laufen. + if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_scene_model.py")); err == nil { + return filepath.Join(wd, "backend"), nil + } + + // Letzter Fallback: bisherige Projekterkennung. + projectRoot := trainingProjectRoot() + return filepath.Join(projectRoot, "backend"), nil +} + func trainingRootDir() (string, error) { - root := filepath.Join("generated", "training") + // Optionaler Override, falls du später explizit einen anderen Speicherort willst. + // Relative Pfade werden wie in record_paths.go app-relativ aufgelöst. + if override := strings.TrimSpace(os.Getenv("TRAINING_ROOT")); override != "" { + root, err := resolvePathRelativeToApp(override) + if err != nil { + return "", err + } + + root, err = filepath.Abs(root) + if err != nil { + return "", err + } + + if err := os.MkdirAll(root, 0755); err != nil { + return "", err + } + + return root, nil + } + + backendRoot, err := trainingBackendRootDir() + if err != nil { + return "", err + } + + root, err := filepath.Abs(filepath.Join(backendRoot, "generated", "training")) + if err != nil { + return "", err + } if err := os.MkdirAll(root, 0755); err != nil { return "", err diff --git a/frontend/src/components/ui/TrainingTab.tsx b/frontend/src/components/ui/TrainingTab.tsx index b938820..dd1625f 100644 --- a/frontend/src/components/ui/TrainingTab.tsx +++ b/frontend/src/components/ui/TrainingTab.tsx @@ -191,6 +191,44 @@ function predictionToCorrection(sample: TrainingSample | null): CorrectionState } } +function applyBoxLabelToCorrection( + state: CorrectionState, + label: string, + labels: TrainingLabels +): CorrectionState { + const clean = String(label || '').trim() + if (!clean) return state + + if (labels.bodyParts.includes(clean)) { + return { + ...state, + bodyPartsPresent: state.bodyPartsPresent.includes(clean) + ? state.bodyPartsPresent + : [...state.bodyPartsPresent, clean], + } + } + + if (labels.objects.includes(clean)) { + return { + ...state, + objectsPresent: state.objectsPresent.includes(clean) + ? state.objectsPresent + : [...state.objectsPresent, clean], + } + } + + if (labels.clothing.includes(clean)) { + return { + ...state, + clothingPresent: state.clothingPresent.includes(clean) + ? state.clothingPresent + : [...state.clothingPresent, clean], + } + } + + return state +} + function sortLabelList(values?: string[], opts?: { keepUnknownFirst?: boolean }) { const list = [...(values ?? [])].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }) @@ -244,6 +282,26 @@ function TrainingOverlay(props: { step: string; progress: number }) { ) } +function LoadingImageOverlay(props: { text?: string }) { + return ( +
+ + +
+ Bild wird geladen… +
+ +
+ {props.text || 'Frame wird erstellt und analysiert. Bitte warten.'} +
+
+ ) +} + export default function TrainingTab() { const [labels, setLabels] = useState(emptyLabels) const [sample, setSample] = useState(null) @@ -257,6 +315,7 @@ export default function TrainingTab() { const [trainingStep, setTrainingStep] = useState('') const [error, setError] = useState(null) const [message, setMessage] = useState(null) + const wasTrainingRunningRef = useRef(false) const imageBoxRef = useRef(null) @@ -375,6 +434,16 @@ export default function TrainingTab() { setBoxLabel(boxLabels[0] || '') }, [boxLabel, boxLabels]) + useEffect(() => { + const wasRunning = wasTrainingRunningRef.current + + if (wasRunning && !trainingRunning && trainingStatus?.training?.finishedAt) { + void loadNext({ forceNew: true }) + } + + wasTrainingRunningRef.current = trainingRunning + }, [trainingRunning, trainingStatus?.training?.finishedAt, loadNext]) + useEffect(() => { let cancelled = false @@ -527,6 +596,10 @@ export default function TrainingTab() { } await loadTrainingStatus() + + // WICHTIG: + // Hier NICHT direkt loadNext() aufrufen. + // Das Training läuft im Backend asynchron weiter. } catch (e) { setTraining(false) setTrainingProgress(0) @@ -636,11 +709,15 @@ export default function TrainingTab() { if (box.w < 0.01 || box.h < 0.01) return - setCorrection((prev) => ({ - ...prev, - boxes: [...(prev.boxes ?? []), box], - })) - }, [drawingBox]) + setCorrection((prev) => { + const next: CorrectionState = { + ...prev, + boxes: [...(prev.boxes ?? []), box], + } + + return applyBoxLabelToCorrection(next, box.label, labels) + }) + }, [drawingBox, labels]) const removeBox = useCallback((index: number) => { setCorrection((prev) => ({ @@ -883,7 +960,7 @@ export default function TrainingTab() { ref={imageBoxRef} className={[ 'relative inline-block max-h-[72dvh] max-w-full select-none transition', - trainingRunning ? 'pointer-events-none blur-sm' : '', + trainingRunning || loading ? 'pointer-events-none blur-sm' : '', ].join(' ')} onPointerDown={startDrawBox} onPointerMove={moveDrawBox} @@ -962,6 +1039,8 @@ export default function TrainingTab() { step={shownTrainingStep} progress={shownTrainingProgress} /> + ) : loading ? ( + ) : null} ) : trainingRunning ? ( @@ -970,12 +1049,7 @@ export default function TrainingTab() { progress={shownTrainingProgress} /> ) : loading ? ( - + ) : (
Kein Frame geladen
)}