Claude: Abweichungen bei 3 Kameras zählen
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# Kamera-Anzahl-Studie
|
||||
|
||||
Untersucht, wie sich die Pose-Schätzgenauigkeit verändert, wenn weniger Kameras
|
||||
verwendet werden. Ergebnis ist eine Kurve **k Kameras → mittlerer Gelenkwinkelfehler (°)**.
|
||||
verwendet werden. Hauptergebnis ist eine Kurve **k Kameras → Positionsfehler in mm**
|
||||
an zwei Punkten des Roboters (Handgelenk und Finger).
|
||||
|
||||
Hintergrundfrage und Entscheidungslogik: [`doc/camera_number_roadmap.md`](../../doc/camera_number_roadmap.md)
|
||||
|
||||
@@ -19,6 +20,39 @@ Pro Szene müssen vorhanden sein:
|
||||
| `data/robot/robot.json` | Robotermodell |
|
||||
|
||||
Neue Szenen werden **automatisch erkannt** — kein Skript anpassen nötig.
|
||||
Für die Plots zusätzlich `matplotlib` (`pip install matplotlib`); ohne läuft
|
||||
alles, nur das PNG entfällt.
|
||||
|
||||
---
|
||||
|
||||
## Metriken
|
||||
|
||||
Pro Kamera-Subset werden zwei Positionsfehler per Vorwärtskinematik berechnet —
|
||||
der euklidische Abstand zwischen geschätzter und wahrer Position eines Punktes:
|
||||
|
||||
| Metrik | Punkt | Hängt ab von | Bedeutung |
|
||||
|---|---|---|---|
|
||||
| `wrist_error_mm` | Hand-Ursprung | Armgelenke `x,y,z,a` | Wie genau ist der Arm bis zum Handgelenk? |
|
||||
| `finger_error_mm` | FingerA-Spitze | volle Kette `x..e` | Wie genau ist die ganze Pose inkl. Hand/Finger? |
|
||||
|
||||
**Unbeobachtbare Gelenke → leerer Wert (n/a).** Sieht ein Subset z. B. die Hand
|
||||
nicht, ist `b/c/e` unbeobachtbar — dann ist die *wahre* Fingerposition schlicht
|
||||
unbekannt und `finger_error_mm` bleibt **leer** (statt mit einer falschen 0
|
||||
gefüllt zu werden). Das Handgelenk hängt nur von den Armgelenken ab und ist
|
||||
deshalb meist auch mit wenigen Kameras bestimmbar.
|
||||
|
||||
Welche Gelenke einen Punkt bewegen, wird **numerisch** ermittelt (kleine
|
||||
Auslenkung je Gelenk) — funktioniert daher auch, wenn sich das Robotermodell ändert.
|
||||
|
||||
Zusätzliche Spalten:
|
||||
|
||||
- `n_unobservable` — Anzahl der 7 Gelenke, die in diesem Subset unbeobachtbar waren.
|
||||
- `mean_abs_deg` / `max_abs_deg` — Gelenkwinkelfehler (nur **beobachtbare** Gelenke).
|
||||
- `mean_abs_mm` / `max_abs_mm` — Lineargelenkfehler (`x`, `e`).
|
||||
|
||||
> Hinweis: Winkelfehler zählen unbeobachtbare Gelenke **nicht** mit, die
|
||||
> mm-Positionsfehler lassen sie als n/a aus. Beide blenden also dieselben Fälle
|
||||
> aus — die `n/a`-Spalte zeigt, wie oft das passiert.
|
||||
|
||||
---
|
||||
|
||||
@@ -43,11 +77,14 @@ run\run_camera_study.bat --k-min 3 --k-max 5
|
||||
REM Weniger Samples — schneller, weniger repräsentativ
|
||||
run\run_camera_study.bat --samples 3
|
||||
|
||||
REM Existierende Ergebnisse überschreiben
|
||||
REM Pipeline für vorhandene Subsets erneut laufen lassen
|
||||
run\run_camera_study.bat --force
|
||||
|
||||
REM Alles kombinierbar
|
||||
run\run_camera_study.bat --scenes Scene7 --k-min 3 --k-max 5 --samples 5 --force
|
||||
REM VOLLSTÄNDIGER Neustart: alles löschen und neu rechnen
|
||||
run\run_camera_study.bat --clean
|
||||
|
||||
REM Nur eine re-gerenderte Szene komplett neu rechnen (Rest bleibt erhalten)
|
||||
run\run_camera_study.bat --clean --scenes Scene10
|
||||
```
|
||||
|
||||
Oder direkt über Python (z. B. in einer Linux-Umgebung / Docker):
|
||||
@@ -56,44 +93,58 @@ Oder direkt über Python (z. B. in einer Linux-Umgebung / Docker):
|
||||
python benchmark/camera_count/run_camera_study.py --samples 10
|
||||
```
|
||||
|
||||
**Laufzeit:** Pro Kamera-Subset läuft die volle Pipeline (Schritt 1–4).
|
||||
Bei 3 Szenen × 5 k-Werte × 10 Samples = 150 Läufe — je nach Hardware mehrere Minuten.
|
||||
**Laufzeit & Wiederholläufe:** Pro Kamera-Subset läuft die volle Pipeline
|
||||
(Schritt 1–4). Bereits gerechnete Subsets werden **übersprungen** (kein `--force`
|
||||
nötig für Fortsetzung) — die mm-Auswertung wird dabei trotzdem frisch berechnet,
|
||||
ein erneuter Lauf ohne `--force` ist also schnell.
|
||||
|
||||
Bereits gerechnete Subsets werden **übersprungen** (kein `--force` nötig für Fortsetzung).
|
||||
**Drei Stufen der Wiederholung:**
|
||||
|
||||
| Flag | Wirkung |
|
||||
|---|---|
|
||||
| *(kein Flag)* | Vorhandene `robot_state.json` werden wiederverwendet, nur die Auswertung neu gerechnet. Schnell. |
|
||||
| `--force` | Pipeline läuft erneut, überschreibt `robot_state.json`. Für geänderte Pipeline-Logik. |
|
||||
| `--clean` | Löscht Zwischenergebnisse vorher komplett. **Nötig nach Re-Rendering.** Ohne `--scenes` wird alles inkl. Ergebnisdateien zurückgesetzt; mit `--scenes` nur die genannten Szenen. |
|
||||
|
||||
**Merge:** Ein Lauf über einzelne Szenen (`--scenes`) aktualisiert nur deren
|
||||
Zeilen in `camera_study.json/.csv` — die übrigen Szenen bleiben erhalten. Du
|
||||
kannst also eine re-gerenderte Szene neu rechnen, ohne die Gesamtauswertung zu verlieren.
|
||||
|
||||
---
|
||||
|
||||
## Schritt 2 — Auswertung
|
||||
|
||||
```bash
|
||||
python benchmark/camera_count/analyze.py
|
||||
python benchmark/camera_count/analyze.py # finger_error_mm (Standard)
|
||||
python benchmark/camera_count/analyze.py --metric wrist_error_mm
|
||||
python benchmark/camera_count/analyze.py --metric mean_abs_deg
|
||||
```
|
||||
|
||||
Ausgabe auf der Konsole:
|
||||
Beispielausgabe (nur Scene10):
|
||||
|
||||
```
|
||||
Kamera-Anzahl vs. mean_abs_deg
|
||||
k | n | Mittel | Median | Std | Min | Max
|
||||
-----------------------------------------------------------------
|
||||
3 | 30 | 0.842 | 0.731 | 0.312 | 0.251 | 1.843
|
||||
4 | 30 | 0.563 | 0.521 | 0.198 | 0.252 | 1.102
|
||||
...
|
||||
Kamera-Anzahl vs. finger_error_mm
|
||||
k | n | n/a | Mittel | Median | Std | Min | Max
|
||||
--------------------------------------------------------------------------
|
||||
3 | 4 | 6 | 1.775 | 1.583 | 0.986 | 0.710 | 3.225
|
||||
4 | 10 | 0 | 1.955 | 2.034 | 0.696 | 0.503 | 3.317
|
||||
5 | 10 | 0 | 1.391 | 1.322 | 0.646 | 0.614 | 2.468
|
||||
6 | 7 | 0 | 1.306 | 1.288 | 0.389 | 0.697 | 1.992
|
||||
7 | 1 | 0 | 1.251 | 1.251 | 0.000 | 1.251 | 1.251
|
||||
|
||||
Beste / schlechteste Subsets je k:
|
||||
k=3: best [Scene7] ace (0.251°) worst [Scene9] bdf (1.843°)
|
||||
n/a = Subsets, bei denen dieser Punkt unbeobachtbar war (nicht eingerechnet).
|
||||
|
||||
Beste / schlechteste Subsets je k (finger_error_mm):
|
||||
k=3: best [Scene10] bcd (0.710) worst [Scene10] acf (3.225)
|
||||
...
|
||||
```
|
||||
|
||||
Zusätzlich wird ein Boxplot gespeichert:
|
||||
`benchmark/camera_count/results/camera_count_vs_error.png`
|
||||
Hier sieht man sofort: bei **k=3 konnten 6 von 10 Subsets den Finger gar nicht
|
||||
sehen** (n/a=6). Das ist die eigentliche Aussage — nicht „der Fehler ist klein",
|
||||
sondern „die meisten 3-Kamera-Anordnungen verlieren die Hand komplett".
|
||||
|
||||
**Andere Metrik:**
|
||||
|
||||
```bash
|
||||
python benchmark/camera_count/analyze.py --metric max_abs_deg
|
||||
```
|
||||
|
||||
Verfügbare Metriken: `finger_error_mm` (Standard), `mean_abs_deg`, `max_abs_deg`, `mean_abs_mm`, `max_abs_mm`
|
||||
Verfügbare Metriken: `finger_error_mm` (Standard), `wrist_error_mm`,
|
||||
`mean_abs_deg`, `max_abs_deg`, `mean_abs_mm`, `max_abs_mm`.
|
||||
|
||||
---
|
||||
|
||||
@@ -101,9 +152,9 @@ Verfügbare Metriken: `finger_error_mm` (Standard), `mean_abs_deg`, `max_abs_deg
|
||||
|
||||
| Pfad | Inhalt |
|
||||
|---|---|
|
||||
| `benchmark/camera_count/results/camera_study.json` | Alle Einzelergebnisse (Szene, k, Subset, Fehler) |
|
||||
| `benchmark/camera_count/results/camera_study.csv` | Dasselbe als CSV — Spalten inkl. `finger_error_mm` |
|
||||
| `benchmark/camera_count/results/camera_count_vs_error.png` | Boxplot |
|
||||
| `benchmark/camera_count/results/camera_study.json` | Alle Einzelergebnisse |
|
||||
| `benchmark/camera_count/results/camera_study.csv` | Dasselbe als CSV (Spalten inkl. `wrist_error_mm`, `finger_error_mm`, `n_unobservable`) |
|
||||
| `benchmark/camera_count/results/camera_count_<metric>.png` | Boxplot je Metrik |
|
||||
| `data/camera_study/SceneX/k3_abc/` | Pipeline-Zwischenergebnisse pro Subset |
|
||||
|
||||
Die Zwischenergebnisse in `data/camera_study/` sind groß — nicht committen.
|
||||
@@ -112,18 +163,22 @@ Die Zwischenergebnisse in `data/camera_study/` sind groß — nicht committen.
|
||||
|
||||
## Ergebnis interpretieren
|
||||
|
||||
Die Studie beantwortet drei Fragen:
|
||||
Die Studie beantwortet vier Fragen:
|
||||
|
||||
1. **Ab welchem k wird der Fehler stabil?**
|
||||
1. **Wird die Hand überhaupt gesehen?**
|
||||
→ `n/a`-Spalte bei `finger_error_mm`. Viele n/a bei k=3 heißt: zu wenige
|
||||
Kameras verlieren die Hand systematisch.
|
||||
|
||||
2. **Ab welchem k wird der Fehler stabil?**
|
||||
→ Knick in der Kurve zeigt den Sättigungspunkt.
|
||||
|
||||
2. **Welche Kamera-Kombination ist bei k=3 am besten?**
|
||||
3. **Welche Kamera-Kombination ist bei kleinem k am besten?**
|
||||
→ `analyze.py` gibt beste und schlechteste Subsets aus.
|
||||
|
||||
3. **Gibt es Szenen / Posen, bei denen wenige Kameras systematisch scheitern?**
|
||||
→ CSV öffnen, nach `scene` gruppieren.
|
||||
4. **Arm vs. Hand getrennt:** Das Handgelenk (`wrist_error_mm`) ist meist schon
|
||||
mit 3 Kameras gut bestimmt — der kritische Teil ist die Hand/Finger-Orientierung.
|
||||
|
||||
Eine praktische Entscheidungsregel: 3 Kameras gelten als ausreichend, wenn
|
||||
`mean_abs_deg` bei k=3 nicht mehr als ~20 % schlechter ist als beim Maximum,
|
||||
und `max_abs_deg` keine kritischen Ausreißer zeigt.
|
||||
(a) der Finger in fast allen Subsets beobachtbar ist (wenig n/a) **und**
|
||||
(b) `finger_error_mm` bei k=3 nicht wesentlich schlechter ist als beim Maximum.
|
||||
(Schwellwert je nach Anwendung anpassen.)
|
||||
|
||||
@@ -4,13 +4,21 @@ benchmark/camera_count/analyze.py
|
||||
===================================
|
||||
Liest camera_study.json und erstellt:
|
||||
- Konsolentabelle: k → Mittelwert / Median / Std / Min / Max des Fehlers
|
||||
(inkl. n/a-Spalte: Subsets, bei denen der Punkt unbeobachtbar war)
|
||||
- Beste und schlechteste Kamera-Subsets je k
|
||||
- Boxplot: Anzahl Kameras vs. Gelenkwinkelfehler (PNG)
|
||||
- Boxplot: Anzahl Kameras vs. Fehler (PNG)
|
||||
|
||||
Metriken:
|
||||
finger_error_mm (Standard) — Fingerposition in mm (volle Kette x..e).
|
||||
Leer, wenn Hand/Palm/Finger unbeobachtbar.
|
||||
wrist_error_mm — Handgelenkposition in mm (nur Armgelenke x,y,z,a).
|
||||
mean_abs_deg / max_abs_deg — Gelenkwinkelfehler in Grad (nur beobachtbare Gelenke).
|
||||
mean_abs_mm / max_abs_mm — Lineargelenkfehler (x, e) in mm.
|
||||
|
||||
Aufruf:
|
||||
python benchmark/camera_count/analyze.py
|
||||
python benchmark/camera_count/analyze.py --input benchmark/camera_count/results/camera_study.json
|
||||
python benchmark/camera_count/analyze.py --metric max_abs_deg
|
||||
python benchmark/camera_count/analyze.py --metric wrist_error_mm
|
||||
python benchmark/camera_count/analyze.py --metric mean_abs_deg
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -29,28 +37,39 @@ def load(path: str) -> list[dict]:
|
||||
return data
|
||||
|
||||
|
||||
def group_by_k(data: list[dict], metric: str) -> dict[int, list[float]]:
|
||||
def group_by_k(data: list[dict], metric: str) -> tuple[dict[int, list[float]], dict[int, int]]:
|
||||
"""Werte je k (None übersprungen) plus Anzahl der n/a-Fälle je k."""
|
||||
by_k: dict[int, list[float]] = {}
|
||||
na_by_k: dict[int, int] = {}
|
||||
for row in data:
|
||||
k = row["k"]
|
||||
v = row.get(metric)
|
||||
if v is None:
|
||||
na_by_k[k] = na_by_k.get(k, 0) + 1
|
||||
continue
|
||||
by_k.setdefault(row["k"], []).append(v)
|
||||
return by_k
|
||||
by_k.setdefault(k, []).append(v)
|
||||
return by_k, na_by_k
|
||||
|
||||
|
||||
def print_table(by_k: dict[int, list[float]], metric: str) -> None:
|
||||
def print_table(by_k: dict[int, list[float]], na_by_k: dict[int, int], metric: str) -> None:
|
||||
print(f"\nKamera-Anzahl vs. {metric}")
|
||||
print(f"{'k':>4} | {'n':>5} | {'Mittel':>8} | {'Median':>8} | "
|
||||
print(f"{'k':>4} | {'n':>5} | {'n/a':>5} | {'Mittel':>8} | {'Median':>8} | "
|
||||
f"{'Std':>8} | {'Min':>8} | {'Max':>8}")
|
||||
print("-" * 65)
|
||||
for k in sorted(by_k):
|
||||
vals = by_k[k]
|
||||
med = statistics.median(vals)
|
||||
std = statistics.pstdev(vals)
|
||||
print(f"{k:>4} | {len(vals):>5} | "
|
||||
f"{statistics.mean(vals):8.3f} | {med:8.3f} | "
|
||||
f"{std:8.3f} | {min(vals):8.3f} | {max(vals):8.3f}")
|
||||
print("-" * 74)
|
||||
all_ks = sorted(set(by_k) | set(na_by_k))
|
||||
for k in all_ks:
|
||||
na = na_by_k.get(k, 0)
|
||||
vals = by_k.get(k, [])
|
||||
if not vals:
|
||||
print(f"{k:>4} | {0:>5} | {na:>5} | {'—':>8} | {'—':>8} | "
|
||||
f"{'—':>8} | {'—':>8} | {'—':>8}")
|
||||
continue
|
||||
print(f"{k:>4} | {len(vals):>5} | {na:>5} | "
|
||||
f"{statistics.mean(vals):8.3f} | {statistics.median(vals):8.3f} | "
|
||||
f"{statistics.pstdev(vals):8.3f} | {min(vals):8.3f} | {max(vals):8.3f}")
|
||||
if any(na_by_k.values()):
|
||||
print("\n n/a = Subsets, bei denen dieser Punkt unbeobachtbar war "
|
||||
"(Position unbekannt, nicht eingerechnet).")
|
||||
|
||||
|
||||
def print_best_worst(data: list[dict], metric: str) -> None:
|
||||
@@ -59,6 +78,7 @@ def print_best_worst(data: list[dict], metric: str) -> None:
|
||||
for k in ks:
|
||||
rows_k = [r for r in data if r["k"] == k and r.get(metric) is not None]
|
||||
if not rows_k:
|
||||
print(f" k={k}: keine beobachtbaren Werte")
|
||||
continue
|
||||
best = min(rows_k, key=lambda r: r[metric])
|
||||
worst = max(rows_k, key=lambda r: r[metric])
|
||||
@@ -67,7 +87,8 @@ def print_best_worst(data: list[dict], metric: str) -> None:
|
||||
f"worst [{worst['scene']}] {worst['subset']} ({worst[metric]:.3f})")
|
||||
|
||||
|
||||
def plot(by_k: dict[int, list[float]], metric: str, out_path: str) -> None:
|
||||
def plot(by_k: dict[int, list[float]], na_by_k: dict[int, int],
|
||||
metric: str, out_path: str) -> None:
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
except ImportError:
|
||||
@@ -75,14 +96,25 @@ def plot(by_k: dict[int, list[float]], metric: str, out_path: str) -> None:
|
||||
return
|
||||
|
||||
ks = sorted(by_k)
|
||||
if not ks:
|
||||
print("\n[INFO] Keine Werte zum Plotten (alle unbeobachtbar).")
|
||||
return
|
||||
data_plot = [by_k[k] for k in ks]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 5))
|
||||
ax.boxplot(data_plot, labels=[str(k) for k in ks], patch_artist=True)
|
||||
ax.set_xlabel("Anzahl Kameras")
|
||||
ax.set_ylabel(f"{metric}")
|
||||
ax.set_ylabel(metric)
|
||||
ax.set_title("Anzahl Kameras vs. Pose-Schätzfehler")
|
||||
ax.grid(True, axis="y", alpha=0.35)
|
||||
|
||||
# n und n/a je k über den Boxen annotieren
|
||||
ymax = max(max(v) for v in data_plot)
|
||||
for i, k in enumerate(ks, start=1):
|
||||
na = na_by_k.get(k, 0)
|
||||
note = f"n={len(by_k[k])}" + (f"\nn/a={na}" if na else "")
|
||||
ax.text(i, ymax * 1.02, note, ha="center", va="bottom", fontsize=8, color="gray")
|
||||
|
||||
fig.tight_layout()
|
||||
fig.savefig(out_path, dpi=150)
|
||||
print(f"\n[INFO] Plot gespeichert: {out_path}")
|
||||
@@ -92,26 +124,29 @@ def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="Auswertung der Kamera-Anzahl-Studie")
|
||||
ap.add_argument("--input", default=str(RESULTS_DIR / "camera_study.json"))
|
||||
ap.add_argument("--metric",
|
||||
choices=["mean_abs_deg", "max_abs_deg", "mean_abs_mm", "max_abs_mm",
|
||||
"finger_error_mm"],
|
||||
choices=["finger_error_mm", "wrist_error_mm",
|
||||
"mean_abs_deg", "max_abs_deg", "mean_abs_mm", "max_abs_mm"],
|
||||
default="finger_error_mm",
|
||||
help="Metrik für Tabelle und Plot (Standard: finger_error_mm)")
|
||||
ap.add_argument("--out-plot",
|
||||
default=str(RESULTS_DIR / "camera_count_vs_error.png"))
|
||||
default=None,
|
||||
help="Plot-Pfad (Standard: results/camera_count_<metric>.png)")
|
||||
args = ap.parse_args()
|
||||
|
||||
data = load(args.input)
|
||||
if not data:
|
||||
return
|
||||
|
||||
by_k = group_by_k(data, args.metric)
|
||||
if not by_k:
|
||||
by_k, na_by_k = group_by_k(data, args.metric)
|
||||
if not by_k and not na_by_k:
|
||||
print(f"[ERROR] Keine Werte für Metrik '{args.metric}' gefunden.")
|
||||
return
|
||||
|
||||
print_table(by_k, args.metric)
|
||||
out_plot = args.out_plot or str(RESULTS_DIR / f"camera_count_{args.metric}.png")
|
||||
|
||||
print_table(by_k, na_by_k, args.metric)
|
||||
print_best_worst(data, args.metric)
|
||||
plot(by_k, args.metric, args.out_plot)
|
||||
plot(by_k, na_by_k, args.metric, out_plot)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -26,6 +26,7 @@ import itertools
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -101,7 +102,10 @@ def main() -> None:
|
||||
ap.add_argument("--seed", type=int, default=42)
|
||||
ap.add_argument("--robot", default=str(ROOT / "data" / "robot" / "robot.json"))
|
||||
ap.add_argument("--force", action="store_true",
|
||||
help="Existierende robot_state.json überschreiben")
|
||||
help="Vorhandene robot_state.json neu rechnen (Pipeline erneut laufen)")
|
||||
ap.add_argument("--clean", action="store_true",
|
||||
help="Zwischenergebnisse vorher löschen und komplett neu rechnen. "
|
||||
"Mit --scenes nur diese Szenen, ohne --scenes alles (inkl. Ergebnisdateien).")
|
||||
ap.add_argument("--out", default=str(RESULTS_DIR / "camera_study.json"))
|
||||
ap.add_argument("--csv", default=str(RESULTS_DIR / "camera_study.csv"))
|
||||
args = ap.parse_args()
|
||||
@@ -110,6 +114,7 @@ def main() -> None:
|
||||
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
sim_dir = ROOT / "data" / "simulation"
|
||||
study_root = ROOT / "data" / "camera_study"
|
||||
scenes = discover_scenes(sim_dir)
|
||||
if args.scenes:
|
||||
want = {s if s.startswith("Scene") else f"Scene{s}" for s in args.scenes}
|
||||
@@ -118,6 +123,25 @@ def main() -> None:
|
||||
print("[ERROR] Keine Szenen mit pose.json und render_*.png gefunden.")
|
||||
sys.exit(1)
|
||||
|
||||
# --clean: Zwischenergebnisse (und bei vollem Reset auch die Aggregat-Dateien) entfernen.
|
||||
if args.clean:
|
||||
if args.scenes:
|
||||
for s in scenes:
|
||||
d = study_root / s
|
||||
if d.exists():
|
||||
shutil.rmtree(d)
|
||||
print(f"[CLEAN] entfernt {d}")
|
||||
else:
|
||||
if study_root.exists():
|
||||
shutil.rmtree(study_root)
|
||||
print(f"[CLEAN] entfernt {study_root}")
|
||||
for f in (Path(args.out), Path(args.csv)):
|
||||
if f.exists():
|
||||
f.unlink()
|
||||
print(f"[CLEAN] entfernt {f}")
|
||||
# Nach dem Löschen muss die Pipeline zwangsläufig neu laufen.
|
||||
args.force = True
|
||||
|
||||
print(f"[INFO] Szenen: {scenes}")
|
||||
print(f"[INFO] Seed={args.seed} Samples/k={args.samples}\n")
|
||||
|
||||
@@ -168,32 +192,51 @@ def main() -> None:
|
||||
"max_abs_deg": s.get("max_abs_deg"),
|
||||
"mean_abs_mm": s.get("mean_abs_mm"),
|
||||
"max_abs_mm": s.get("max_abs_mm"),
|
||||
"wrist_error_mm": s.get("wrist_error_mm"),
|
||||
"finger_error_mm": s.get("finger_error_mm"),
|
||||
"n_unobservable": s.get("n_unobservable"),
|
||||
}
|
||||
all_results.append(row)
|
||||
errs.append(row["mean_abs_deg"] or 0.0)
|
||||
fe = row["finger_error_mm"]
|
||||
fe_str = f" finger={fe:.2f}mm" if fe is not None else ""
|
||||
print(f" {label}: mean={row['mean_abs_deg']:.3f}° max={row['max_abs_deg']:.3f}°{fe_str}")
|
||||
we, fe = row["wrist_error_mm"], row["finger_error_mm"]
|
||||
we_str = f"{we:.2f}mm" if we is not None else "n/a"
|
||||
fe_str = f"{fe:.2f}mm" if fe is not None else "n/a"
|
||||
print(f" {label}: mean={row['mean_abs_deg']:.3f}° "
|
||||
f"wrist={we_str} finger={fe_str} unobs={row['n_unobservable']}")
|
||||
|
||||
if errs:
|
||||
print(f" k={k} Zusammenfassung: mean={mean(errs):.3f}° "
|
||||
f"min={min(errs):.3f}° max={max(errs):.3f}°")
|
||||
|
||||
# Ergebnisse schreiben
|
||||
Path(args.out).write_text(json.dumps(all_results, indent=2), encoding="utf-8")
|
||||
# Ergebnisse schreiben — andere Szenen erhalten (mergen statt überschreiben),
|
||||
# damit ein Lauf über einzelne Szenen die übrigen nicht aus der Aggregat-Datei kippt.
|
||||
out_path = Path(args.out)
|
||||
processed = set(scenes)
|
||||
merged: list[dict] = []
|
||||
if out_path.exists():
|
||||
try:
|
||||
existing = json.loads(out_path.read_text(encoding="utf-8"))
|
||||
merged = [r for r in existing if r.get("scene") not in processed]
|
||||
except (json.JSONDecodeError, OSError):
|
||||
merged = []
|
||||
merged.extend(all_results)
|
||||
merged.sort(key=lambda r: (r["scene"], r["k"], r["subset"]))
|
||||
out_path.write_text(json.dumps(merged, indent=2), encoding="utf-8")
|
||||
|
||||
with open(args.csv, "w", encoding="utf-8") as f:
|
||||
f.write("scene,k,subset,mean_abs_deg,max_abs_deg,mean_abs_mm,max_abs_mm,finger_error_mm\n")
|
||||
for r in all_results:
|
||||
f.write("scene,k,subset,mean_abs_deg,max_abs_deg,mean_abs_mm,max_abs_mm,"
|
||||
"wrist_error_mm,finger_error_mm,n_unobservable\n")
|
||||
for r in merged:
|
||||
f.write(f"{r['scene']},{r['k']},{r['subset']},"
|
||||
f"{r['mean_abs_deg'] or ''},"
|
||||
f"{r['max_abs_deg'] or ''},"
|
||||
f"{r['mean_abs_mm'] or ''},"
|
||||
f"{r['max_abs_mm'] or ''},"
|
||||
f"{r['finger_error_mm'] or ''}\n")
|
||||
f"{r['wrist_error_mm'] if r['wrist_error_mm'] is not None else ''},"
|
||||
f"{r['finger_error_mm'] if r['finger_error_mm'] is not None else ''},"
|
||||
f"{r['n_unobservable'] if r['n_unobservable'] is not None else ''}\n")
|
||||
|
||||
print(f"\n[DONE] {len(all_results)} Ergebnisse -> {args.out}")
|
||||
print(f"\n[DONE] {len(all_results)} neu, {len(merged)} gesamt -> {args.out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user