bugfixes
This commit is contained in:
parent
13ec4cca54
commit
7d649525d9
@ -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))
|
||||
|
||||
|
||||
@ -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()
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user