fixed object detection

This commit is contained in:
Linrador 2026-04-29 16:38:56 +02:00
parent b6ad070ee4
commit 13ec4cca54
4 changed files with 204 additions and 20 deletions

View File

@ -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:

View File

@ -11,7 +11,7 @@
"useMyFreeCamsWatcher": true,
"autoDeleteSmallDownloads": true,
"autoDeleteSmallDownloadsBelowMB": 300,
"autoDeleteSmallDownloadsKeepFavorites": true,
"autoDeleteSmallDownloadsKeepFavorites": false,
"lowDiskPauseBelowGB": 5,
"blurPreviews": false,
"teaserPlayback": "all",

View File

@ -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

View File

@ -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 (
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/45 text-center text-white backdrop-blur-[1px]">
<LoadingSpinner
size="lg"
className="text-white"
srLabel="Bild wird geladen…"
/>
<div className="mt-3 text-sm font-semibold">
Bild wird geladen
</div>
<div className="mt-1 max-w-[260px] px-4 text-xs text-white/80">
{props.text || 'Frame wird erstellt und analysiert. Bitte warten.'}
</div>
</div>
)
}
export default function TrainingTab() {
const [labels, setLabels] = useState<TrainingLabels>(emptyLabels)
const [sample, setSample] = useState<TrainingSample | null>(null)
@ -257,6 +315,7 @@ export default function TrainingTab() {
const [trainingStep, setTrainingStep] = useState('')
const [error, setError] = useState<string | null>(null)
const [message, setMessage] = useState<string | null>(null)
const wasTrainingRunningRef = useRef(false)
const imageBoxRef = useRef<HTMLDivElement | null>(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) => ({
setCorrection((prev) => {
const next: CorrectionState = {
...prev,
boxes: [...(prev.boxes ?? []), box],
}))
}, [drawingBox])
}
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 ? (
<LoadingImageOverlay />
) : null}
</div>
) : trainingRunning ? (
@ -970,12 +1049,7 @@ export default function TrainingTab() {
progress={shownTrainingProgress}
/>
) : loading ? (
<LoadingSpinner
size="lg"
center
className="text-white/80"
srLabel="Frame wird geladen…"
/>
<LoadingImageOverlay />
) : (
<div className="text-sm text-white/80">Kein Frame geladen</div>
)}