Claude: Studie - Wie viele Kameras brauchts
This commit is contained in:
129
benchmark/camera_count/README.md
Normal file
129
benchmark/camera_count/README.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 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 (°)**.
|
||||
|
||||
Hintergrundfrage und Entscheidungslogik: [`doc/camera_number_roadmap.md`](../../doc/camera_number_roadmap.md)
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Pro Szene müssen vorhanden sein:
|
||||
|
||||
| Datei | Woher |
|
||||
|---|---|
|
||||
| `data/simulation/SceneX/render_*.png` | Blender-Renderer |
|
||||
| `data/simulation/SceneX/render_*.npz` | Kamera-Intrinsik |
|
||||
| `data/simulation/SceneX/pose.json` | Ground-Truth-Pose |
|
||||
| `data/robot/robot.json` | Robotermodell |
|
||||
|
||||
Neue Szenen werden **automatisch erkannt** — kein Skript anpassen nötig.
|
||||
|
||||
---
|
||||
|
||||
## Schritt 1 — Studie laufen lassen
|
||||
|
||||
```bat
|
||||
run\run_camera_study.bat
|
||||
```
|
||||
|
||||
Das wertet alle verfügbaren Szenen aus, k = 3 bis (alle verfügbaren Kameras),
|
||||
10 zufällige Subsets pro k.
|
||||
|
||||
**Optionen:**
|
||||
|
||||
```bat
|
||||
REM Nur bestimmte Szenen
|
||||
run\run_camera_study.bat --scenes Scene7 Scene9
|
||||
|
||||
REM Kamerabereich einschränken
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
Oder direkt über Python (z. B. in einer Linux-Umgebung / Docker):
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
Bereits gerechnete Subsets werden **übersprungen** (kein `--force` nötig für Fortsetzung).
|
||||
|
||||
---
|
||||
|
||||
## Schritt 2 — Auswertung
|
||||
|
||||
```bash
|
||||
python benchmark/camera_count/analyze.py
|
||||
```
|
||||
|
||||
Ausgabe auf der Konsole:
|
||||
|
||||
```
|
||||
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
|
||||
...
|
||||
|
||||
Beste / schlechteste Subsets je k:
|
||||
k=3: best [Scene7] ace (0.251°) worst [Scene9] bdf (1.843°)
|
||||
...
|
||||
```
|
||||
|
||||
Zusätzlich wird ein Boxplot gespeichert:
|
||||
`benchmark/camera_count/results/camera_count_vs_error.png`
|
||||
|
||||
**Andere Metrik:**
|
||||
|
||||
```bash
|
||||
python benchmark/camera_count/analyze.py --metric max_abs_deg
|
||||
```
|
||||
|
||||
Verfügbare Metriken: `mean_abs_deg`, `max_abs_deg`, `mean_abs_mm`, `max_abs_mm`
|
||||
|
||||
---
|
||||
|
||||
## Ausgabedateien
|
||||
|
||||
| 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 (Excel, Pandas) |
|
||||
| `benchmark/camera_count/results/camera_count_vs_error.png` | Boxplot |
|
||||
| `data/camera_study/SceneX/k3_abc/` | Pipeline-Zwischenergebnisse pro Subset |
|
||||
|
||||
Die Zwischenergebnisse in `data/camera_study/` sind groß — nicht committen.
|
||||
|
||||
---
|
||||
|
||||
## Ergebnis interpretieren
|
||||
|
||||
Die Studie beantwortet drei Fragen:
|
||||
|
||||
1. **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?**
|
||||
→ `analyze.py` gibt beste und schlechteste Subsets aus.
|
||||
|
||||
3. **Gibt es Szenen / Posen, bei denen wenige Kameras systematisch scheitern?**
|
||||
→ CSV öffnen, nach `scene` gruppieren.
|
||||
|
||||
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.
|
||||
(Schwellwert je nach Anwendung anpassen.)
|
||||
117
benchmark/camera_count/analyze.py
Normal file
117
benchmark/camera_count/analyze.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
benchmark/camera_count/analyze.py
|
||||
===================================
|
||||
Liest camera_study.json und erstellt:
|
||||
- Konsolentabelle: k → Mittelwert / Median / Std / Min / Max des Fehlers
|
||||
- Beste und schlechteste Kamera-Subsets je k
|
||||
- Boxplot: Anzahl Kameras vs. Gelenkwinkelfehler (PNG)
|
||||
|
||||
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
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import statistics
|
||||
from pathlib import Path
|
||||
|
||||
RESULTS_DIR = Path(__file__).resolve().parent / "results"
|
||||
|
||||
|
||||
def load(path: str) -> list[dict]:
|
||||
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
if not data:
|
||||
print("[ERROR] Keine Ergebnisse in der Datei.")
|
||||
return data
|
||||
|
||||
|
||||
def group_by_k(data: list[dict], metric: str) -> dict[int, list[float]]:
|
||||
by_k: dict[int, list[float]] = {}
|
||||
for row in data:
|
||||
v = row.get(metric)
|
||||
if v is None:
|
||||
continue
|
||||
by_k.setdefault(row["k"], []).append(v)
|
||||
return by_k
|
||||
|
||||
|
||||
def print_table(by_k: dict[int, list[float]], metric: str) -> None:
|
||||
print(f"\nKamera-Anzahl vs. {metric}")
|
||||
print(f"{'k':>4} | {'n':>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}")
|
||||
|
||||
|
||||
def print_best_worst(data: list[dict], metric: str) -> None:
|
||||
ks = sorted({r["k"] for r in data})
|
||||
print(f"\nBeste / schlechteste Subsets je k ({metric}):")
|
||||
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:
|
||||
continue
|
||||
best = min(rows_k, key=lambda r: r[metric])
|
||||
worst = max(rows_k, key=lambda r: r[metric])
|
||||
print(f" k={k}: best [{best['scene']}] {best['subset']} "
|
||||
f"({best[metric]:.3f}) "
|
||||
f"worst [{worst['scene']}] {worst['subset']} ({worst[metric]:.3f})")
|
||||
|
||||
|
||||
def plot(by_k: dict[int, list[float]], metric: str, out_path: str) -> None:
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
except ImportError:
|
||||
print("\n[INFO] matplotlib nicht installiert — Plot übersprungen.")
|
||||
return
|
||||
|
||||
ks = sorted(by_k)
|
||||
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_title("Anzahl Kameras vs. Pose-Schätzfehler")
|
||||
ax.grid(True, axis="y", alpha=0.35)
|
||||
fig.tight_layout()
|
||||
fig.savefig(out_path, dpi=150)
|
||||
print(f"\n[INFO] Plot gespeichert: {out_path}")
|
||||
|
||||
|
||||
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"],
|
||||
default="mean_abs_deg",
|
||||
help="Metrik für Tabelle und Plot (Standard: mean_abs_deg)")
|
||||
ap.add_argument("--out-plot",
|
||||
default=str(RESULTS_DIR / "camera_count_vs_error.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:
|
||||
print(f"[ERROR] Keine Werte für Metrik '{args.metric}' gefunden.")
|
||||
return
|
||||
|
||||
print_table(by_k, args.metric)
|
||||
print_best_worst(data, args.metric)
|
||||
plot(by_k, args.metric, args.out_plot)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
benchmark/camera_count/results/.gitkeep
Normal file
0
benchmark/camera_count/results/.gitkeep
Normal file
195
benchmark/camera_count/run_camera_study.py
Normal file
195
benchmark/camera_count/run_camera_study.py
Normal file
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
benchmark/camera_count/run_camera_study.py
|
||||
==========================================
|
||||
Untersucht, wie sich die Pose-Genauigkeit (Gelenkwinkelfehler) mit der
|
||||
Kameraanzahl verändert.
|
||||
|
||||
Für jede Szene mit bekannter Ground-Truth (pose.json) und jede Kameraanzahl k
|
||||
wird eine zufällige Stichprobe von k-Kamera-Subsets durch die volle Pipeline
|
||||
geschickt und ausgewertet. Ergebnisse landen in benchmark/camera_count/results/.
|
||||
|
||||
Erweiterbar: funktioniert mit beliebig vielen Szenen und Kameras — neue Szenen
|
||||
werden automatisch erkannt, sobald sie pose.json haben.
|
||||
|
||||
Aufruf:
|
||||
python benchmark/camera_count/run_camera_study.py
|
||||
python benchmark/camera_count/run_camera_study.py --scenes Scene7 Scene9
|
||||
python benchmark/camera_count/run_camera_study.py --k-min 3 --k-max 5 --samples 15
|
||||
python benchmark/camera_count/run_camera_study.py --force # existierende Ergebnisse überschreiben
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import itertools
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from statistics import mean
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
RESULTS_DIR = Path(__file__).resolve().parent / "results"
|
||||
PY = sys.executable
|
||||
|
||||
|
||||
def discover_scenes(sim_dir: Path) -> list[str]:
|
||||
"""Alle Szenen mit render_*.png und pose.json."""
|
||||
scenes = []
|
||||
for d in sorted(sim_dir.iterdir()):
|
||||
if not d.name.startswith("Scene"):
|
||||
continue
|
||||
if not (d / "pose.json").exists():
|
||||
continue
|
||||
if not list(d.glob("render_*.png")):
|
||||
continue
|
||||
scenes.append(d.name)
|
||||
return scenes
|
||||
|
||||
|
||||
def camera_ids(scene_dir: Path) -> list[str]:
|
||||
"""Sortierte Kamera-IDs (a, b, c ...) einer Szene."""
|
||||
ids = []
|
||||
for f in sorted(scene_dir.glob("render_*.png")):
|
||||
m = re.match(r"render_([A-Za-z0-9]+)\.png", f.name)
|
||||
if m:
|
||||
ids.append(m.group(1))
|
||||
return ids
|
||||
|
||||
|
||||
def run_pipeline(scene_dir: Path, cams: list[str], eval_dir: Path, robot: str) -> bool:
|
||||
cmd = [
|
||||
PY, str(ROOT / "pipeline" / "run_pipeline.py"),
|
||||
str(scene_dir),
|
||||
"--robot", robot,
|
||||
"--evalDir", str(eval_dir),
|
||||
"--cameras", ",".join(cams),
|
||||
]
|
||||
r = subprocess.run(cmd, cwd=str(ROOT), capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
snippet = (r.stderr or r.stdout).strip()[-300:]
|
||||
print(f" [WARN] Pipeline fehlgeschlagen: {snippet}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def eval_pose(robot_state: Path, gt: Path) -> dict | None:
|
||||
out = robot_state.with_suffix(".eval.json")
|
||||
cmd = [
|
||||
PY, str(ROOT / "benchmark" / "eval_pose.py"),
|
||||
str(robot_state), str(gt),
|
||||
"--out", str(out),
|
||||
"--tolDeg", "999", "--tolMm", "999",
|
||||
]
|
||||
subprocess.run(cmd, cwd=str(ROOT), capture_output=True, text=True)
|
||||
return json.loads(out.read_text(encoding="utf-8")) if out.exists() else None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="Kamera-Anzahl vs. Pose-Genauigkeit")
|
||||
ap.add_argument("--scenes", nargs="*", default=None,
|
||||
help="Szenen-Namen oder Nummern (Standard: alle mit pose.json)")
|
||||
ap.add_argument("--k-min", type=int, default=3, help="Minimale Kameraanzahl (Standard: 3)")
|
||||
ap.add_argument("--k-max", type=int, default=None,
|
||||
help="Maximale Kameraanzahl (Standard: alle verfügbaren)")
|
||||
ap.add_argument("--samples", type=int, default=10,
|
||||
help="Zufällige Subsets pro (Szene, k) — Standard: 10")
|
||||
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")
|
||||
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()
|
||||
|
||||
random.seed(args.seed)
|
||||
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
sim_dir = ROOT / "data" / "simulation"
|
||||
scenes = discover_scenes(sim_dir)
|
||||
if args.scenes:
|
||||
want = {s if s.startswith("Scene") else f"Scene{s}" for s in args.scenes}
|
||||
scenes = [s for s in scenes if s in want]
|
||||
if not scenes:
|
||||
print("[ERROR] Keine Szenen mit pose.json und render_*.png gefunden.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[INFO] Szenen: {scenes}")
|
||||
print(f"[INFO] Seed={args.seed} Samples/k={args.samples}\n")
|
||||
|
||||
all_results: list[dict] = []
|
||||
|
||||
for scene in scenes:
|
||||
scene_dir = sim_dir / scene
|
||||
cams = camera_ids(scene_dir)
|
||||
k_max = min(args.k_max or len(cams), len(cams))
|
||||
k_range = range(args.k_min, k_max + 1)
|
||||
gt = scene_dir / "pose.json"
|
||||
|
||||
print(f"[SZENE] {scene} Kameras={cams} k={list(k_range)}")
|
||||
|
||||
for k in k_range:
|
||||
all_combos = list(itertools.combinations(cams, k))
|
||||
n_sample = min(args.samples, len(all_combos))
|
||||
sample = random.sample(all_combos, n_sample)
|
||||
|
||||
errs: list[float] = []
|
||||
print(f" k={k}: {n_sample}/{len(all_combos)} Subsets")
|
||||
|
||||
for combo in sample:
|
||||
label = "".join(combo)
|
||||
eval_dir = ROOT / "data" / "camera_study" / scene / f"k{k}_{label}"
|
||||
rs = eval_dir / "robot_state.json"
|
||||
|
||||
if rs.exists() and not args.force:
|
||||
print(f" {label}: übersprungen (robot_state.json existiert)")
|
||||
else:
|
||||
eval_dir.mkdir(parents=True, exist_ok=True)
|
||||
ok = run_pipeline(scene_dir, list(combo), eval_dir, args.robot)
|
||||
if not ok or not rs.exists():
|
||||
print(f" {label}: FEHLER — übersprungen")
|
||||
continue
|
||||
|
||||
ev = eval_pose(rs, gt)
|
||||
if not ev:
|
||||
print(f" {label}: Auswertung fehlgeschlagen")
|
||||
continue
|
||||
|
||||
s = ev["summary"]
|
||||
row = {
|
||||
"scene": scene,
|
||||
"k": k,
|
||||
"subset": label,
|
||||
"mean_abs_deg": s.get("mean_abs_deg"),
|
||||
"max_abs_deg": s.get("max_abs_deg"),
|
||||
"mean_abs_mm": s.get("mean_abs_mm"),
|
||||
"max_abs_mm": s.get("max_abs_mm"),
|
||||
}
|
||||
all_results.append(row)
|
||||
errs.append(row["mean_abs_deg"] or 0.0)
|
||||
print(f" {label}: mean={row['mean_abs_deg']:.3f}° max={row['max_abs_deg']:.3f}°")
|
||||
|
||||
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")
|
||||
|
||||
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\n")
|
||||
for r in all_results:
|
||||
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 ''}\n")
|
||||
|
||||
print(f"\n[DONE] {len(all_results)} Ergebnisse -> {args.out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user