fixed object detection
This commit is contained in:
parent
b6ad070ee4
commit
13ec4cca54
@ -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:
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
"useMyFreeCamsWatcher": true,
|
||||
"autoDeleteSmallDownloads": true,
|
||||
"autoDeleteSmallDownloadsBelowMB": 300,
|
||||
"autoDeleteSmallDownloadsKeepFavorites": true,
|
||||
"autoDeleteSmallDownloadsKeepFavorites": false,
|
||||
"lowDiskPauseBelowGB": 5,
|
||||
"blurPreviews": false,
|
||||
"teaserPlayback": "all",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) => ({
|
||||
...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 ? (
|
||||
<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>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user