bugfixes
This commit is contained in:
parent
13ec4cca54
commit
7d649525d9
@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from ultralytics import YOLO
|
from ultralytics import YOLO
|
||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
def clamp01(v):
|
def clamp01(v):
|
||||||
@ -18,6 +19,7 @@ def main():
|
|||||||
parser.add_argument("--image", required=True)
|
parser.add_argument("--image", required=True)
|
||||||
parser.add_argument("--conf", type=float, default=0.30)
|
parser.add_argument("--conf", type=float, default=0.30)
|
||||||
parser.add_argument("--imgsz", type=int, default=640)
|
parser.add_argument("--imgsz", type=int, default=640)
|
||||||
|
parser.add_argument("--debug-image", action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
root = Path(args.root)
|
root = Path(args.root)
|
||||||
@ -28,33 +30,52 @@ def main():
|
|||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
"available": False,
|
"available": False,
|
||||||
"source": "detector_missing",
|
"source": "detector_missing",
|
||||||
|
"modelPath": str(model_path),
|
||||||
"boxes": [],
|
"boxes": [],
|
||||||
}))
|
}, ensure_ascii=False))
|
||||||
return
|
return
|
||||||
|
|
||||||
img = Image.open(image_path).convert("RGB")
|
img = Image.open(image_path).convert("RGB")
|
||||||
img_w, img_h = img.size
|
img_w, img_h = img.size
|
||||||
|
|
||||||
|
try:
|
||||||
model = YOLO(str(model_path))
|
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(
|
results = model.predict(
|
||||||
source=str(image_path),
|
source=str(image_path),
|
||||||
conf=args.conf,
|
conf=float(args.conf),
|
||||||
imgsz=args.imgsz,
|
imgsz=int(args.imgsz),
|
||||||
verbose=False,
|
verbose=False,
|
||||||
device=0 if __import__("torch").cuda.is_available() else "cpu",
|
device=device,
|
||||||
)
|
)
|
||||||
|
|
||||||
boxes = []
|
boxes = []
|
||||||
|
model_names = {}
|
||||||
|
|
||||||
|
raw_box_count = 0
|
||||||
|
|
||||||
if results:
|
if results:
|
||||||
r = results[0]
|
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:
|
if r.boxes is not None:
|
||||||
|
raw_box_count = len(r.boxes)
|
||||||
|
|
||||||
for b in r.boxes:
|
for b in r.boxes:
|
||||||
cls_id = int(b.cls[0].item())
|
cls_id = int(b.cls[0].item())
|
||||||
score = float(b.conf[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()]
|
x1, y1, x2, y2 = [float(v) for v in b.xyxy[0].tolist()]
|
||||||
|
|
||||||
@ -75,9 +96,25 @@ def main():
|
|||||||
"h": h,
|
"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({
|
print(json.dumps({
|
||||||
"available": True,
|
"available": True,
|
||||||
"source": "yolo_detector",
|
"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,
|
"boxes": boxes,
|
||||||
}, ensure_ascii=False))
|
}, ensure_ascii=False))
|
||||||
|
|
||||||
|
|||||||
@ -158,45 +158,10 @@ def main():
|
|||||||
"bodyPartsPresent": weighted_multilabel(top_targets, "bodyPartsPresent", weights),
|
"bodyPartsPresent": weighted_multilabel(top_targets, "bodyPartsPresent", weights),
|
||||||
"objectsPresent": weighted_multilabel(top_targets, "objectsPresent", weights),
|
"objectsPresent": weighted_multilabel(top_targets, "objectsPresent", weights),
|
||||||
"clothingPresent": weighted_multilabel(top_targets, "clothingPresent", weights),
|
"clothingPresent": weighted_multilabel(top_targets, "clothingPresent", weights),
|
||||||
"boxes": predict_boxes(root, image_path),
|
"boxes": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
print(json.dumps(pred, ensure_ascii=False))
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@ -56,7 +56,7 @@ type TrainingPrediction struct {
|
|||||||
BodyPartsPresent []TrainingScoredLabel `json:"bodyPartsPresent"`
|
BodyPartsPresent []TrainingScoredLabel `json:"bodyPartsPresent"`
|
||||||
ObjectsPresent []TrainingScoredLabel `json:"objectsPresent"`
|
ObjectsPresent []TrainingScoredLabel `json:"objectsPresent"`
|
||||||
ClothingPresent []TrainingScoredLabel `json:"clothingPresent"`
|
ClothingPresent []TrainingScoredLabel `json:"clothingPresent"`
|
||||||
Boxes []TrainingBox `json:"boxes,omitempty"`
|
Boxes []TrainingBox `json:"boxes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrainingCorrection struct {
|
type TrainingCorrection struct {
|
||||||
@ -68,7 +68,7 @@ type TrainingCorrection struct {
|
|||||||
BodyPartsPresent []string `json:"bodyPartsPresent"`
|
BodyPartsPresent []string `json:"bodyPartsPresent"`
|
||||||
ObjectsPresent []string `json:"objectsPresent"`
|
ObjectsPresent []string `json:"objectsPresent"`
|
||||||
ClothingPresent []string `json:"clothingPresent"`
|
ClothingPresent []string `json:"clothingPresent"`
|
||||||
Boxes []TrainingBox `json:"boxes,omitempty"`
|
Boxes []TrainingBox `json:"boxes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrainingSample struct {
|
type TrainingSample struct {
|
||||||
@ -104,7 +104,11 @@ type TrainingAnnotation struct {
|
|||||||
Notes string `json:"notes,omitempty"`
|
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 {
|
type TrainingJobStatus struct {
|
||||||
Running bool `json:"running"`
|
Running bool `json:"running"`
|
||||||
@ -116,6 +120,11 @@ type TrainingJobStatus struct {
|
|||||||
FinishedAt string `json:"finishedAt,omitempty"`
|
FinishedAt string `json:"finishedAt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minTrainingFeedbackCount = 5
|
||||||
|
|
||||||
|
const minDetectorTrainCount = 20
|
||||||
|
const minDetectorValCount = 3
|
||||||
|
|
||||||
var trainingJob = struct {
|
var trainingJob = struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
status TrainingJobStatus
|
status TrainingJobStatus
|
||||||
@ -457,17 +466,45 @@ func trainingTrainHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
feedbackPath := filepath.Join(root, "feedback.jsonl")
|
// Falls bisher alles zufällig in train gelandet ist, erzeugen wir mindestens
|
||||||
count, _ := trainingCountAnnotations(feedbackPath)
|
// 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(
|
trainingWriteError(
|
||||||
w,
|
w,
|
||||||
http.StatusBadRequest,
|
http.StatusBadRequest,
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"Zu wenige Bewertungen. Mindestens %d, aktuell %d.",
|
"Zu wenige YOLO-Box-Labels. Benötigt: mindestens %d Train und %d Val. Aktuell: Train=%d, Val=%d. Feedback gesamt: %d.",
|
||||||
minTrainingFeedbackCount,
|
minDetectorTrainCount,
|
||||||
count,
|
minDetectorValCount,
|
||||||
|
trainCount,
|
||||||
|
valCount,
|
||||||
|
feedbackCount,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@ -477,17 +514,27 @@ func trainingTrainHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
*s = TrainingJobStatus{
|
*s = TrainingJobStatus{
|
||||||
Running: true,
|
Running: true,
|
||||||
Progress: 5,
|
Progress: 5,
|
||||||
Step: "Training wird vorbereitet…",
|
Step: "YOLO-Detector-Training wird vorbereitet…",
|
||||||
StartedAt: time.Now().UTC().Format(time.RFC3339),
|
StartedAt: time.Now().UTC().Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
go trainingRunJob(root, count)
|
go trainingRunJob(root, feedbackCount)
|
||||||
|
|
||||||
trainingWriteJSON(w, http.StatusAccepted, map[string]any{
|
trainingWriteJSON(w, http.StatusAccepted, map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"message": "Training gestartet.",
|
"message": "YOLO-Detector-Training gestartet.",
|
||||||
"training": trainingGetJobStatus(),
|
"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()
|
python := trainingPythonExe()
|
||||||
|
|
||||||
trainingSetJobStatus(func(s *TrainingJobStatus) {
|
trainingSetJobStatus(func(s *TrainingJobStatus) {
|
||||||
s.Progress = 15
|
s.Progress = 20
|
||||||
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.Step = "Detector-Daten werden geprüft…"
|
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")
|
detectorValImages := filepath.Join(root, "detector", "dataset", "images", "val")
|
||||||
detectorValLabels := filepath.Join(root, "detector", "dataset", "labels", "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)
|
trainCount := trainingCountDetectorSamples(detectorTrainImages, detectorTrainLabels)
|
||||||
valCount := trainingCountDetectorSamples(detectorValImages, detectorValLabels)
|
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) {
|
trainingSetJobStatus(func(s *TrainingJobStatus) {
|
||||||
s.Progress = 75
|
s.Progress = 35
|
||||||
s.Step = "Detector wird trainiert…"
|
s.Step = "YOLO-Detector wird trainiert…"
|
||||||
})
|
})
|
||||||
|
|
||||||
detectorScript := trainingScriptPath("train_detector_model.py")
|
detectorScript := trainingScriptPath("train_detector_model.py")
|
||||||
@ -541,7 +578,7 @@ func trainingRunJob(root string, count int) {
|
|||||||
detectorScript,
|
detectorScript,
|
||||||
"--root", root,
|
"--root", root,
|
||||||
"--base", "yolo11n.pt",
|
"--base", "yolo11n.pt",
|
||||||
"--epochs", "20",
|
"--epochs", "60",
|
||||||
"--imgsz", "640",
|
"--imgsz", "640",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -556,18 +593,25 @@ func trainingRunJob(root string, count int) {
|
|||||||
} else {
|
} else {
|
||||||
detectorStatus = "skipped_no_detector_data"
|
detectorStatus = "skipped_no_detector_data"
|
||||||
detectorOutput = fmt.Sprintf(
|
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,
|
trainCount,
|
||||||
valCount,
|
valCount,
|
||||||
|
minDetectorTrainCount,
|
||||||
|
minDetectorValCount,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fmt.Println("⚠️", detectorOutput)
|
||||||
}
|
}
|
||||||
|
|
||||||
message := "Training abgeschlossen. Neue Bilder werden jetzt mit dem Scene-Modell analysiert."
|
message := "Training abgeschlossen."
|
||||||
if detectorStatus == "trained" {
|
if detectorStatus == "trained" {
|
||||||
message = "Training abgeschlossen. Scene-Modell und Detector wurden trainiert."
|
message = "Training abgeschlossen. YOLO-Detector wurde trainiert."
|
||||||
}
|
}
|
||||||
if detectorStatus == "failed" {
|
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) {
|
trainingSetJobStatus(func(s *TrainingJobStatus) {
|
||||||
@ -592,17 +636,65 @@ func trainingStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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")
|
feedbackPath := filepath.Join(root, "feedback.jsonl")
|
||||||
count, _ := trainingCountAnnotations(feedbackPath)
|
feedbackCount, _ := trainingCountAnnotations(feedbackPath)
|
||||||
|
|
||||||
job := trainingGetJobStatus()
|
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{
|
trainingWriteJSON(w, http.StatusOK, map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"feedbackCount": count,
|
"feedbackCount": feedbackCount,
|
||||||
"requiredCount": minTrainingFeedbackCount,
|
"requiredCount": minTrainingFeedbackCount,
|
||||||
"canTrain": count >= minTrainingFeedbackCount,
|
|
||||||
|
// Für YOLO-only ist canTrain jetzt bewusst an Box-Labels gekoppelt,
|
||||||
|
// nicht mehr nur an feedback.jsonl.
|
||||||
|
"canTrain": detectorDataReady,
|
||||||
|
|
||||||
"training": job,
|
"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,14 +975,102 @@ func trainingPredictFrame(framePath string) TrainingPrediction {
|
|||||||
return trainingEmptyPrediction("root_error")
|
return trainingEmptyPrediction("root_error")
|
||||||
}
|
}
|
||||||
|
|
||||||
python := trainingPythonExe()
|
det := trainingPredictDetector(root, framePath)
|
||||||
script := trainingScriptPath("predict_scene_model.py")
|
|
||||||
|
|
||||||
|
pred := trainingPredictionFromDetector(det)
|
||||||
|
|
||||||
|
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{}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("⚠️ detector label grouping failed:", err)
|
||||||
|
return pred
|
||||||
|
}
|
||||||
|
|
||||||
|
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{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
confValues := []string{"0.30", "0.10", "0.03", "0.01"}
|
||||||
|
|
||||||
|
best := TrainingDetectorPrediction{
|
||||||
|
Available: true,
|
||||||
|
Source: "yolo_detector",
|
||||||
|
Boxes: []TrainingBox{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conf := range confValues {
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
python,
|
python,
|
||||||
script,
|
script,
|
||||||
"--root", root,
|
"--root", root,
|
||||||
"--image", framePath,
|
"--image", framePath,
|
||||||
|
"--conf", conf,
|
||||||
|
"--imgsz", "640",
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
@ -898,34 +1078,157 @@ func trainingPredictFrame(framePath string) TrainingPrediction {
|
|||||||
CreationFlags: 0x08000000,
|
CreationFlags: 0x08000000,
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := cmd.CombinedOutput()
|
var stdout strings.Builder
|
||||||
outText := strings.TrimSpace(string(out))
|
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 {
|
if err != nil {
|
||||||
fmt.Println("⚠️ training predict failed:", err, outText)
|
fmt.Println("⚠️ detector predict failed")
|
||||||
return trainingEmptyPrediction("predict_failed")
|
fmt.Println(" conf:", conf)
|
||||||
|
fmt.Println(" error:", err)
|
||||||
|
fmt.Println(" stdout:", outText)
|
||||||
|
fmt.Println(" stderr:", errText)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var pred TrainingPrediction
|
if outText == "" {
|
||||||
if err := json.Unmarshal(out, &pred); err != nil {
|
fmt.Println("⚠️ detector predict empty stdout")
|
||||||
fmt.Println("⚠️ training predict json failed:", err, outText)
|
fmt.Println(" conf:", conf)
|
||||||
return trainingEmptyPrediction("predict_json_failed")
|
fmt.Println(" stderr:", errText)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if pred.SexPosition == "" {
|
var det TrainingDetectorPrediction
|
||||||
pred.SexPosition = "unknown"
|
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 pred.BodyPartsPresent == nil {
|
|
||||||
|
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{}
|
pred.BodyPartsPresent = []TrainingScoredLabel{}
|
||||||
}
|
|
||||||
if pred.ObjectsPresent == nil {
|
|
||||||
pred.ObjectsPresent = []TrainingScoredLabel{}
|
pred.ObjectsPresent = []TrainingScoredLabel{}
|
||||||
}
|
|
||||||
if pred.ClothingPresent == nil {
|
|
||||||
pred.ClothingPresent = []TrainingScoredLabel{}
|
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
|
return pred
|
||||||
@ -946,8 +1249,7 @@ func trainingWriteDetectorSample(root string, sample *TrainingSample, boxes []Tr
|
|||||||
return fmt.Errorf("frame missing: %w", err)
|
return fmt.Errorf("frame missing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erstmal alles in train schreiben.
|
// Stabiler 80/20 Split: gleicher sampleID landet immer im gleichen Split.
|
||||||
// Später kannst du 80/20 train/val splitten.
|
|
||||||
split := trainingStableSplit(sample.SampleID)
|
split := trainingStableSplit(sample.SampleID)
|
||||||
|
|
||||||
imgDir := filepath.Join(root, "detector", "dataset", "images", split)
|
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)
|
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 {
|
func trainingStableSplit(sampleID string) string {
|
||||||
sum := sha1.Sum([]byte(sampleID))
|
sum := sha1.Sum([]byte(sampleID))
|
||||||
if int(sum[0])%5 == 0 {
|
if int(sum[0])%5 == 0 {
|
||||||
@ -1057,21 +1433,16 @@ func trainingProjectRoot() string {
|
|||||||
return "."
|
return "."
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall A: Prozess läuft im Projekt-Root:
|
if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_detector_model.py")); err == nil {
|
||||||
// ./backend/ml/predict_scene_model.py existiert
|
|
||||||
if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_scene_model.py")); err == nil {
|
|
||||||
return wd
|
return wd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall B: Prozess läuft direkt in /backend:
|
if _, err := os.Stat(filepath.Join(wd, "ml", "predict_detector_model.py")); err == nil {
|
||||||
// ./ml/predict_scene_model.py existiert
|
|
||||||
if _, err := os.Stat(filepath.Join(wd, "ml", "predict_scene_model.py")); err == nil {
|
|
||||||
return filepath.Dir(wd)
|
return filepath.Dir(wd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall C: Fallback: Parent testen
|
|
||||||
parent := filepath.Dir(wd)
|
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
|
return parent
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1123,51 +1494,35 @@ func isTempBuildDir(dir string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func trainingBackendRootDir() (string, error) {
|
func trainingBackendRootDir() (string, error) {
|
||||||
// Fall 1:
|
if script, err := resolvePathRelativeToApp(filepath.Join("ml", "predict_detector_model.py")); err == nil {
|
||||||
// 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() {
|
if st, statErr := os.Stat(script); statErr == nil && !st.IsDir() {
|
||||||
return filepath.Dir(filepath.Dir(script)), nil
|
return filepath.Dir(filepath.Dir(script)), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall 2:
|
if script, err := resolvePathRelativeToApp(filepath.Join("backend", "ml", "predict_detector_model.py")); err == nil {
|
||||||
// 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() {
|
if st, statErr := os.Stat(script); statErr == nil && !st.IsDir() {
|
||||||
return filepath.Dir(filepath.Dir(script)), nil
|
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) {
|
if dir, err := exeDir(); err == nil && strings.TrimSpace(dir) != "" && !isTempBuildDir(dir) {
|
||||||
return dir, nil
|
return dir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall 4:
|
|
||||||
// Dev-Fallback über Working Directory.
|
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn wir direkt in /backend laufen.
|
if _, err := os.Stat(filepath.Join(wd, "ml", "predict_detector_model.py")); err == nil {
|
||||||
if _, err := os.Stat(filepath.Join(wd, "ml", "predict_scene_model.py")); err == nil {
|
|
||||||
return wd, nil
|
return wd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn wir im Projekt-Root laufen.
|
if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_detector_model.py")); err == nil {
|
||||||
if _, err := os.Stat(filepath.Join(wd, "backend", "ml", "predict_scene_model.py")); err == nil {
|
|
||||||
return filepath.Join(wd, "backend"), nil
|
return filepath.Join(wd, "backend"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Letzter Fallback: bisherige Projekterkennung.
|
|
||||||
projectRoot := trainingProjectRoot()
|
projectRoot := trainingProjectRoot()
|
||||||
return filepath.Join(projectRoot, "backend"), nil
|
return filepath.Join(projectRoot, "backend"), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -516,7 +516,7 @@ export default function TrainingTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (elapsed > 25_000) {
|
if (elapsed > 25_000) {
|
||||||
setTrainingStep('Scene-Modell wird trainiert…')
|
setTrainingStep('Detector wird trainiert…')
|
||||||
return Math.min(prev + 0.8, 80)
|
return Math.min(prev + 0.8, 80)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user