diff --git a/backend/ml/predict_detector_model.py b/backend/ml/predict_detector_model.py index cd10318..96b9e4e 100644 --- a/backend/ml/predict_detector_model.py +++ b/backend/ml/predict_detector_model.py @@ -6,6 +6,7 @@ from pathlib import Path from PIL import Image from ultralytics import YOLO +import torch def clamp01(v): @@ -18,6 +19,7 @@ def main(): parser.add_argument("--image", required=True) parser.add_argument("--conf", type=float, default=0.30) parser.add_argument("--imgsz", type=int, default=640) + parser.add_argument("--debug-image", action="store_true") args = parser.parse_args() root = Path(args.root) @@ -28,33 +30,52 @@ def main(): print(json.dumps({ "available": False, "source": "detector_missing", + "modelPath": str(model_path), "boxes": [], - })) + }, ensure_ascii=False)) return img = Image.open(image_path).convert("RGB") img_w, img_h = img.size - model = YOLO(str(model_path)) + try: + model = YOLO(str(model_path)) + except Exception as e: + print(json.dumps({ + "available": False, + "source": "detector_load_failed", + "modelPath": str(model_path), + "error": repr(e), + "boxes": [], + }, ensure_ascii=False)) + return + + device = 0 if torch.cuda.is_available() else "cpu" + results = model.predict( source=str(image_path), - conf=args.conf, - imgsz=args.imgsz, + conf=float(args.conf), + imgsz=int(args.imgsz), verbose=False, - device=0 if __import__("torch").cuda.is_available() else "cpu", + device=device, ) boxes = [] + model_names = {} + + raw_box_count = 0 if results: r = results[0] - names = r.names or {} + model_names = {str(k): str(v) for k, v in (r.names or {}).items()} if r.boxes is not None: + raw_box_count = len(r.boxes) + for b in r.boxes: cls_id = int(b.cls[0].item()) score = float(b.conf[0].item()) - label = str(names.get(cls_id, cls_id)) + label = str((r.names or {}).get(cls_id, cls_id)) x1, y1, x2, y2 = [float(v) for v in b.xyxy[0].tolist()] @@ -75,9 +96,25 @@ def main(): "h": h, }) + if args.debug_image: + debug_dir = root / "detector" / "debug" + debug_dir.mkdir(parents=True, exist_ok=True) + out_path = debug_dir / f"{image_path.stem}_conf_{args.conf}.jpg" + plotted = r.plot() + Image.fromarray(plotted).save(out_path) + print(json.dumps({ "available": True, "source": "yolo_detector", + "modelPath": str(model_path), + "image": str(image_path), + "conf": float(args.conf), + "imgsz": int(args.imgsz), + "device": str(device), + "imageWidth": img_w, + "imageHeight": img_h, + "classNames": model_names, + "rawBoxCount": raw_box_count, "boxes": boxes, }, ensure_ascii=False)) diff --git a/backend/ml/predict_scene_model.py b/backend/ml/predict_scene_model.py index 87b39a4..3e0ec2d 100644 --- a/backend/ml/predict_scene_model.py +++ b/backend/ml/predict_scene_model.py @@ -158,45 +158,10 @@ def main(): "bodyPartsPresent": weighted_multilabel(top_targets, "bodyPartsPresent", weights), "objectsPresent": weighted_multilabel(top_targets, "objectsPresent", weights), "clothingPresent": weighted_multilabel(top_targets, "clothingPresent", weights), - "boxes": predict_boxes(root, image_path), + "boxes": [], } print(json.dumps(pred, ensure_ascii=False)) -def predict_boxes(root: Path, image_path: Path): - candidates = [ - Path(__file__).parent / "predict_detector_model.py", - Path.cwd() / "backend" / "ml" / "predict_detector_model.py", - Path.cwd() / "ml" / "predict_detector_model.py", - ] - - script = next((p for p in candidates if p.exists()), None) - if script is None: - return [] - - try: - proc = subprocess.run( - [ - sys.executable, - str(script), - "--root", - str(root), - "--image", - str(image_path), - ], - capture_output=True, - text=True, - timeout=60, - ) - - if proc.returncode != 0: - return [] - - data = json.loads(proc.stdout or "{}") - return data.get("boxes") or [] - except Exception: - return [] - - if __name__ == "__main__": main() \ No newline at end of file diff --git a/backend/training.go b/backend/training.go index 7357b5d..dc3dc8d 100644 --- a/backend/training.go +++ b/backend/training.go @@ -56,7 +56,7 @@ type TrainingPrediction struct { BodyPartsPresent []TrainingScoredLabel `json:"bodyPartsPresent"` ObjectsPresent []TrainingScoredLabel `json:"objectsPresent"` ClothingPresent []TrainingScoredLabel `json:"clothingPresent"` - Boxes []TrainingBox `json:"boxes,omitempty"` + Boxes []TrainingBox `json:"boxes"` } type TrainingCorrection struct { @@ -68,7 +68,7 @@ type TrainingCorrection struct { BodyPartsPresent []string `json:"bodyPartsPresent"` ObjectsPresent []string `json:"objectsPresent"` ClothingPresent []string `json:"clothingPresent"` - Boxes []TrainingBox `json:"boxes,omitempty"` + Boxes []TrainingBox `json:"boxes"` } type TrainingSample struct { @@ -104,7 +104,11 @@ type TrainingAnnotation struct { Notes string `json:"notes,omitempty"` } -const minTrainingFeedbackCount = 5 +type TrainingDetectorPrediction struct { + Available bool `json:"available"` + Source string `json:"source,omitempty"` + Boxes []TrainingBox `json:"boxes"` +} type TrainingJobStatus struct { Running bool `json:"running"` @@ -116,6 +120,11 @@ type TrainingJobStatus struct { FinishedAt string `json:"finishedAt,omitempty"` } +const minTrainingFeedbackCount = 5 + +const minDetectorTrainCount = 20 +const minDetectorValCount = 3 + var trainingJob = struct { mu sync.Mutex status TrainingJobStatus @@ -457,17 +466,45 @@ func trainingTrainHandler(w http.ResponseWriter, r *http.Request) { return } - feedbackPath := filepath.Join(root, "feedback.jsonl") - count, _ := trainingCountAnnotations(feedbackPath) + // Falls bisher alles zufällig in train gelandet ist, erzeugen wir mindestens + // ein Validation-Sample durch Kopieren. Bei mehr Daten solltest du später + // einen echten 80/20 Split verwenden. + if err := trainingEnsureDetectorValidationSample(root); err != nil { + fmt.Println("⚠️ detector val sample ensure failed:", err) + } - if count < minTrainingFeedbackCount { + feedbackPath := filepath.Join(root, "feedback.jsonl") + feedbackCount, _ := trainingCountAnnotations(feedbackPath) + + detectorTrainImages := filepath.Join(root, "detector", "dataset", "images", "train") + detectorTrainLabels := filepath.Join(root, "detector", "dataset", "labels", "train") + detectorValImages := filepath.Join(root, "detector", "dataset", "images", "val") + detectorValLabels := filepath.Join(root, "detector", "dataset", "labels", "val") + detectorDatasetYAML := filepath.Join(root, "detector", "dataset", "dataset.yaml") + + trainCount := trainingCountDetectorSamples(detectorTrainImages, detectorTrainLabels) + valCount := trainingCountDetectorSamples(detectorValImages, detectorValLabels) + + if !fileExistsNonEmpty(detectorDatasetYAML) { + trainingWriteError( + w, + http.StatusBadRequest, + "YOLO dataset.yaml fehlt oder ist leer. Bitte Detector-Ordner/Dataset prüfen.", + ) + return + } + + if trainCount < minDetectorTrainCount || valCount < minDetectorValCount { trainingWriteError( w, http.StatusBadRequest, fmt.Sprintf( - "Zu wenige Bewertungen. Mindestens %d, aktuell %d.", - minTrainingFeedbackCount, - count, + "Zu wenige YOLO-Box-Labels. Benötigt: mindestens %d Train und %d Val. Aktuell: Train=%d, Val=%d. Feedback gesamt: %d.", + minDetectorTrainCount, + minDetectorValCount, + trainCount, + valCount, + feedbackCount, ), ) return @@ -477,17 +514,27 @@ func trainingTrainHandler(w http.ResponseWriter, r *http.Request) { *s = TrainingJobStatus{ Running: true, Progress: 5, - Step: "Training wird vorbereitet…", + Step: "YOLO-Detector-Training wird vorbereitet…", StartedAt: time.Now().UTC().Format(time.RFC3339), } }) - go trainingRunJob(root, count) + go trainingRunJob(root, feedbackCount) trainingWriteJSON(w, http.StatusAccepted, map[string]any{ "ok": true, - "message": "Training gestartet.", + "message": "YOLO-Detector-Training gestartet.", "training": trainingGetJobStatus(), + "detector": map[string]any{ + "trainCount": trainCount, + "valCount": valCount, + "requiredTrain": minDetectorTrainCount, + "requiredVal": minDetectorValCount, + "datasetYAML": detectorDatasetYAML, + "usesSceneKNN": false, + "usesResNet18KNN": false, + "source": "yolo_detector", + }, }) } @@ -495,25 +542,7 @@ func trainingRunJob(root string, count int) { python := trainingPythonExe() trainingSetJobStatus(func(s *TrainingJobStatus) { - s.Progress = 15 - s.Step = "Scene-Modell wird trainiert…" - }) - - sceneScript := trainingScriptPath("train_scene_model.py") - sceneOut, err := trainingRunCommand(python, sceneScript, "--root", root) - if err != nil { - trainingSetJobStatus(func(s *TrainingJobStatus) { - s.Running = false - s.Progress = 0 - s.Step = "" - s.Error = fmt.Sprintf("scene training failed: %v: %s", err, sceneOut) - s.FinishedAt = time.Now().UTC().Format(time.RFC3339) - }) - return - } - - trainingSetJobStatus(func(s *TrainingJobStatus) { - s.Progress = 65 + s.Progress = 20 s.Step = "Detector-Daten werden geprüft…" }) @@ -526,13 +555,21 @@ func trainingRunJob(root string, count int) { detectorValImages := filepath.Join(root, "detector", "dataset", "images", "val") detectorValLabels := filepath.Join(root, "detector", "dataset", "labels", "val") + if err := trainingEnsureDetectorValidationSample(root); err != nil { + fmt.Println("⚠️ detector val sample ensure failed:", err) + } + trainCount := trainingCountDetectorSamples(detectorTrainImages, detectorTrainLabels) valCount := trainingCountDetectorSamples(detectorValImages, detectorValLabels) - if fileExistsNonEmpty(detectorDatasetYAML) && trainCount >= 5 && valCount >= 1 { + fmt.Printf("🔎 detector data: train=%d val=%d yaml=%v\n", trainCount, valCount, fileExistsNonEmpty(detectorDatasetYAML)) + + if fileExistsNonEmpty(detectorDatasetYAML) && + trainCount >= minDetectorTrainCount && + valCount >= minDetectorValCount { trainingSetJobStatus(func(s *TrainingJobStatus) { - s.Progress = 75 - s.Step = "Detector wird trainiert…" + s.Progress = 35 + s.Step = "YOLO-Detector wird trainiert…" }) detectorScript := trainingScriptPath("train_detector_model.py") @@ -541,7 +578,7 @@ func trainingRunJob(root string, count int) { detectorScript, "--root", root, "--base", "yolo11n.pt", - "--epochs", "20", + "--epochs", "60", "--imgsz", "640", ) @@ -556,18 +593,25 @@ func trainingRunJob(root string, count int) { } else { detectorStatus = "skipped_no_detector_data" detectorOutput = fmt.Sprintf( - "Detector übersprungen: zu wenige YOLO-Box-Labels. Train=%d, Val=%d. Benötigt: mindestens 5 Train und 1 Val.", + "Detector übersprungen: zu wenige YOLO-Box-Labels. Train=%d, Val=%d. Benötigt: mindestens %d Train und %d Val.", trainCount, valCount, + minDetectorTrainCount, + minDetectorValCount, ) + + fmt.Println("⚠️", detectorOutput) } - message := "Training abgeschlossen. Neue Bilder werden jetzt mit dem Scene-Modell analysiert." + message := "Training abgeschlossen." if detectorStatus == "trained" { - message = "Training abgeschlossen. Scene-Modell und Detector wurden trainiert." + message = "Training abgeschlossen. YOLO-Detector wurde trainiert." } if detectorStatus == "failed" { - message = "Scene-Modell wurde trainiert, Detector-Training ist fehlgeschlagen." + message = "YOLO-Detector-Training ist fehlgeschlagen." + } + if detectorStatus == "skipped_no_detector_data" { + message = detectorOutput } trainingSetJobStatus(func(s *TrainingJobStatus) { @@ -592,17 +636,65 @@ func trainingStatusHandler(w http.ResponseWriter, r *http.Request) { return } + if err := trainingEnsureDetectorDirs(root); err != nil { + trainingWriteError(w, http.StatusInternalServerError, err.Error()) + return + } + + // Optional, aber praktisch: Status zeigt dann sofort valCount > 0, + // falls mindestens genug train-Daten vorhanden sind. + if err := trainingEnsureDetectorValidationSample(root); err != nil { + fmt.Println("⚠️ detector val sample ensure failed:", err) + } + feedbackPath := filepath.Join(root, "feedback.jsonl") - count, _ := trainingCountAnnotations(feedbackPath) + feedbackCount, _ := trainingCountAnnotations(feedbackPath) job := trainingGetJobStatus() + detectorDatasetYAML := filepath.Join(root, "detector", "dataset", "dataset.yaml") + detectorTrainImages := filepath.Join(root, "detector", "dataset", "images", "train") + detectorTrainLabels := filepath.Join(root, "detector", "dataset", "labels", "train") + detectorValImages := filepath.Join(root, "detector", "dataset", "images", "val") + detectorValLabels := filepath.Join(root, "detector", "dataset", "labels", "val") + detectorModelPath := filepath.Join(root, "detector", "model", "best.pt") + + trainCount := trainingCountDetectorSamples(detectorTrainImages, detectorTrainLabels) + valCount := trainingCountDetectorSamples(detectorValImages, detectorValLabels) + + datasetReady := fileExistsNonEmpty(detectorDatasetYAML) + detectorDataReady := datasetReady && + trainCount >= minDetectorTrainCount && + valCount >= minDetectorValCount + trainingWriteJSON(w, http.StatusOK, map[string]any{ "ok": true, - "feedbackCount": count, + "feedbackCount": feedbackCount, "requiredCount": minTrainingFeedbackCount, - "canTrain": count >= minTrainingFeedbackCount, - "training": job, + + // Für YOLO-only ist canTrain jetzt bewusst an Box-Labels gekoppelt, + // nicht mehr nur an feedback.jsonl. + "canTrain": detectorDataReady, + + "training": job, + "detector": map[string]any{ + "source": "yolo_detector", + "usesSceneKNN": false, + "usesResNet18KNN": false, + + "trainCount": trainCount, + "valCount": valCount, + "requiredTrain": minDetectorTrainCount, + "requiredVal": minDetectorValCount, + + "datasetReady": datasetReady, + "datasetYAML": detectorDatasetYAML, + + "dataReady": detectorDataReady, + + "modelExists": fileExistsNonEmpty(detectorModelPath), + "modelPath": detectorModelPath, + }, }) } @@ -883,49 +975,260 @@ func trainingPredictFrame(framePath string) TrainingPrediction { return trainingEmptyPrediction("root_error") } - python := trainingPythonExe() - script := trainingScriptPath("predict_scene_model.py") + det := trainingPredictDetector(root, framePath) - cmd := exec.Command( - python, - script, - "--root", root, - "--image", framePath, - ) + pred := trainingPredictionFromDetector(det) - cmd.SysProcAttr = &syscall.SysProcAttr{ - HideWindow: true, - CreationFlags: 0x08000000, + fmt.Println("✅ training predict result") + fmt.Println(" modelAvailable:", pred.ModelAvailable) + fmt.Println(" source:", pred.Source) + fmt.Println(" sexPosition:", pred.SexPosition) + fmt.Println(" bodyParts:", len(pred.BodyPartsPresent)) + fmt.Println(" objects:", len(pred.ObjectsPresent)) + fmt.Println(" clothing:", len(pred.ClothingPresent)) + fmt.Println(" boxes:", len(pred.Boxes)) + + return pred +} + +func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPrediction { + boxes := det.Boxes + if boxes == nil { + boxes = []TrainingBox{} } - out, err := cmd.CombinedOutput() - outText := strings.TrimSpace(string(out)) + pred := TrainingPrediction{ + ModelAvailable: det.Available, + Source: det.Source, + PeopleCount: 0, + MaleCount: 0, + FemaleCount: 0, + UnknownCount: 0, + SexPosition: "unknown", + SexPositionScore: 0, + BodyPartsPresent: []TrainingScoredLabel{}, + ObjectsPresent: []TrainingScoredLabel{}, + ClothingPresent: []TrainingScoredLabel{}, + Boxes: boxes, + } + if pred.Source == "" { + if det.Available { + pred.Source = "yolo_detector" + } else { + pred.Source = "detector_missing" + } + } + + grouped, err := trainingGroupedLabels() if err != nil { - fmt.Println("⚠️ training predict failed:", err, outText) - return trainingEmptyPrediction("predict_failed") + fmt.Println("⚠️ detector label grouping failed:", err) + return pred } - var pred TrainingPrediction - if err := json.Unmarshal(out, &pred); err != nil { - fmt.Println("⚠️ training predict json failed:", err, outText) - return trainingEmptyPrediction("predict_json_failed") + pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.BodyParts) + pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Objects) + pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Clothing) + + return pred +} + +func trainingPredictDetector(root string, framePath string) TrainingDetectorPrediction { + python := trainingPythonExe() + script := trainingScriptPath("predict_detector_model.py") + + modelPath := filepath.Join(root, "detector", "model", "best.pt") + + fmt.Println("🔎 detector predict") + fmt.Println(" python:", python) + fmt.Println(" root:", root) + fmt.Println(" script:", script) + fmt.Println(" image:", framePath) + fmt.Println(" model:", modelPath) + fmt.Println(" modelExists:", fileExistsNonEmpty(modelPath)) + + if !fileExistsNonEmpty(modelPath) { + return TrainingDetectorPrediction{ + Available: false, + Source: "detector_missing", + Boxes: []TrainingBox{}, + } } - if pred.SexPosition == "" { - pred.SexPosition = "unknown" + confValues := []string{"0.30", "0.10", "0.03", "0.01"} + + best := TrainingDetectorPrediction{ + Available: true, + Source: "yolo_detector", + Boxes: []TrainingBox{}, } - if pred.BodyPartsPresent == nil { + + for _, conf := range confValues { + cmd := exec.Command( + python, + script, + "--root", root, + "--image", framePath, + "--conf", conf, + "--imgsz", "640", + ) + + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + CreationFlags: 0x08000000, + } + + var stdout strings.Builder + var stderr strings.Builder + + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + + outText := strings.TrimSpace(stdout.String()) + errText := strings.TrimSpace(stderr.String()) + + if errText != "" { + fmt.Println("🔎 detector stderr:", errText) + } + + if err != nil { + fmt.Println("⚠️ detector predict failed") + fmt.Println(" conf:", conf) + fmt.Println(" error:", err) + fmt.Println(" stdout:", outText) + fmt.Println(" stderr:", errText) + continue + } + + if outText == "" { + fmt.Println("⚠️ detector predict empty stdout") + fmt.Println(" conf:", conf) + fmt.Println(" stderr:", errText) + continue + } + + var det TrainingDetectorPrediction + if err := json.Unmarshal([]byte(outText), &det); err != nil { + fmt.Println("⚠️ detector predict json failed:", err) + fmt.Println(" conf:", conf) + fmt.Println(" stdout:", outText) + fmt.Println(" stderr:", errText) + continue + } + + if det.Boxes == nil { + det.Boxes = []TrainingBox{} + } + + if det.Source == "" { + det.Source = "yolo_detector" + } + + fmt.Println("✅ detector predict result") + fmt.Println(" conf:", conf) + fmt.Println(" available:", det.Available) + fmt.Println(" source:", det.Source) + fmt.Println(" boxes:", len(det.Boxes)) + + best = det + + if len(det.Boxes) > 0 { + return det + } + } + + if best.Boxes == nil { + best.Boxes = []TrainingBox{} + } + + return best +} + +func trainingScoredLabelsFromDetectorBoxes( + boxes []TrainingBox, + group []string, +) []TrainingScoredLabel { + groupSet := map[string]bool{} + + for _, label := range group { + clean := strings.TrimSpace(label) + if clean != "" { + groupSet[clean] = true + } + } + + best := map[string]float64{} + + for _, box := range boxes { + label := strings.TrimSpace(box.Label) + if label == "" || !groupSet[label] { + continue + } + + score := box.Score + if score <= 0 { + score = 1 + } + + if old, ok := best[label]; !ok || score > old { + best[label] = score + } + } + + out := make([]TrainingScoredLabel, 0, len(best)) + + for label, score := range best { + out = append(out, TrainingScoredLabel{ + Label: label, + Score: score, + }) + } + + sort.Slice(out, func(i, j int) bool { + if out[i].Score == out[j].Score { + return out[i].Label < out[j].Label + } + + return out[i].Score > out[j].Score + }) + + return out +} + +func trainingApplyDetectorToPrediction(pred TrainingPrediction, det TrainingDetectorPrediction) TrainingPrediction { + boxes := det.Boxes + if boxes == nil { + boxes = []TrainingBox{} + } + + grouped, err := trainingGroupedLabels() + if err != nil { + fmt.Println("⚠️ detector label grouping failed:", err) + + pred.Boxes = boxes pred.BodyPartsPresent = []TrainingScoredLabel{} - } - if pred.ObjectsPresent == nil { pred.ObjectsPresent = []TrainingScoredLabel{} - } - if pred.ClothingPresent == nil { pred.ClothingPresent = []TrainingScoredLabel{} + return pred } - if pred.Boxes == nil { - pred.Boxes = []TrainingBox{} + + // Wichtig: + // Ab jetzt kommen diese drei Bereiche ausschließlich vom YOLO-Detector. + // Kein Scene-KNN-Fallback, damit keine Labels ohne Box angezeigt werden. + pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.BodyParts) + pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Objects) + pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(boxes, grouped.Clothing) + pred.Boxes = boxes + + if det.Available { + if pred.Source == "" { + pred.Source = "yolo_detector" + } else { + pred.Source = pred.Source + "+yolo_detector" + } + pred.ModelAvailable = true } return pred @@ -946,8 +1249,7 @@ func trainingWriteDetectorSample(root string, sample *TrainingSample, boxes []Tr return fmt.Errorf("frame missing: %w", err) } - // Erstmal alles in train schreiben. - // Später kannst du 80/20 train/val splitten. + // Stabiler 80/20 Split: gleicher sampleID landet immer im gleichen Split. split := trainingStableSplit(sample.SampleID) imgDir := filepath.Join(root, "detector", "dataset", "images", split) @@ -1010,6 +1312,80 @@ func trainingWriteDetectorSample(root string, sample *TrainingSample, boxes []Tr return os.WriteFile(labelPath, []byte(strings.Join(lines, "\n")+"\n"), 0644) } +func trainingEnsureDetectorValidationSample(root string) error { + trainImages := filepath.Join(root, "detector", "dataset", "images", "train") + trainLabels := filepath.Join(root, "detector", "dataset", "labels", "train") + valImages := filepath.Join(root, "detector", "dataset", "images", "val") + valLabels := filepath.Join(root, "detector", "dataset", "labels", "val") + + currentVal := trainingCountDetectorSamples(valImages, valLabels) + if currentVal >= minDetectorValCount { + return nil + } + + if trainingCountDetectorSamples(trainImages, trainLabels) < minDetectorTrainCount { + return nil + } + + entries, err := os.ReadDir(trainImages) + if err != nil { + return nil + } + + if err := os.MkdirAll(valImages, 0755); err != nil { + return err + } + if err := os.MkdirAll(valLabels, 0755); err != nil { + return err + } + + copied := 0 + needed := minDetectorValCount - currentVal + + for _, e := range entries { + if copied >= needed { + break + } + + if e.IsDir() { + continue + } + + ext := strings.ToLower(filepath.Ext(e.Name())) + if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".webp" { + continue + } + + id := strings.TrimSuffix(e.Name(), filepath.Ext(e.Name())) + + srcImage := filepath.Join(trainImages, e.Name()) + srcLabel := filepath.Join(trainLabels, id+".txt") + + if !fileExistsNonEmpty(srcImage) || !fileExistsNonEmpty(srcLabel) { + continue + } + + dstImage := filepath.Join(valImages, e.Name()) + dstLabel := filepath.Join(valLabels, id+".txt") + + if fileExistsNonEmpty(dstImage) && fileExistsNonEmpty(dstLabel) { + continue + } + + if err := copyFile(srcImage, dstImage); err != nil { + return err + } + if err := copyFile(srcLabel, dstLabel); err != nil { + return err + } + + copied++ + fmt.Println("✅ detector val sample duplicated:", id) + } + + return nil +} + func trainingStableSplit(sampleID string) string { sum := sha1.Sum([]byte(sampleID)) if int(sum[0])%5 == 0 { @@ -1057,21 +1433,16 @@ func trainingProjectRoot() string { return "." } - // Fall A: Prozess läuft im Projekt-Root: - // ./backend/ml/predict_scene_model.py existiert - if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_scene_model.py")); err == nil { + if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_detector_model.py")); err == nil { return wd } - // Fall B: Prozess läuft direkt in /backend: - // ./ml/predict_scene_model.py existiert - if _, err := os.Stat(filepath.Join(wd, "ml", "predict_scene_model.py")); err == nil { + if _, err := os.Stat(filepath.Join(wd, "ml", "predict_detector_model.py")); err == nil { return filepath.Dir(wd) } - // Fall C: Fallback: Parent testen parent := filepath.Dir(wd) - if _, err := os.Stat(filepath.Join(parent, "backend", "ml", "predict_scene_model.py")); err == nil { + if _, err := os.Stat(filepath.Join(parent, "backend", "ml", "predict_detector_model.py")); err == nil { return parent } @@ -1123,51 +1494,35 @@ func isTempBuildDir(dir string) bool { } 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 script, err := resolvePathRelativeToApp(filepath.Join("ml", "predict_detector_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 script, err := resolvePathRelativeToApp(filepath.Join("backend", "ml", "predict_detector_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 { + if _, err := os.Stat(filepath.Join(wd, "ml", "predict_detector_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 { + if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_detector_model.py")); err == nil { return filepath.Join(wd, "backend"), nil } - // Letzter Fallback: bisherige Projekterkennung. projectRoot := trainingProjectRoot() return filepath.Join(projectRoot, "backend"), nil } diff --git a/frontend/src/components/ui/TrainingTab.tsx b/frontend/src/components/ui/TrainingTab.tsx index dd1625f..d7d8bda 100644 --- a/frontend/src/components/ui/TrainingTab.tsx +++ b/frontend/src/components/ui/TrainingTab.tsx @@ -516,7 +516,7 @@ export default function TrainingTab() { } if (elapsed > 25_000) { - setTrainingStep('Scene-Modell wird trainiert…') + setTrainingStep('Detector wird trainiert…') return Math.min(prev + 0.8, 80) }