Add CSV plotting and episode recording functionality

- Implemented `plot_data_csv.py` to read CSV files and generate plots for velocity and linear acceleration signals.
- Created `record1.py` for recording ESP32 IMU data, DIN/DOUT states, and RealSense camera images into episodes.
- Enhanced `ProviderWorldIMUVelocityEstimator` to include stationary detection logic, resetting velocity when stationary.
- Updated `EpisodeWriter` to save episode data with timestamped filenames for better organization.
This commit is contained in:
Brunsmeier
2026-07-10 10:23:15 +08:00
parent b4739362a5
commit 781540d3a8
16 changed files with 18282 additions and 3 deletions

200
plot_data_csv.py Normal file
View File

@ -0,0 +1,200 @@
import argparse
import csv
import os
from pathlib import Path
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
import matplotlib.pyplot as plt
TARGET_GROUPS = {
"velocity": {
"vel_x_ms": ["vel_x_ms"],
"vel_y_ms": ["vel_y_ms"],
"vel_z_ms": ["vel_z_ms"],
},
"lin_acc": {
"lin_acc_x_ms2": ["lin_acc_x_ms2", "lin_acc_xms2"],
"lin_acc_y_ms2": ["lin_acc_y_ms2", "lin_acc_yms2"],
"lin_acc_z_ms2": ["lin_acc_z_ms2", "lin_acc_zms2"],
},
}
TIME_CANDIDATES = ["t_monotonic", "t_wall", "t_esp_ms", "frame_id"]
def parse_args():
parser = argparse.ArgumentParser(
description="Read CSV files from a folder and plot velocity/linear acceleration signals."
)
parser.add_argument(
"--input-dir",
default="data_csv",
help="Directory containing CSV files (default: data_csv)",
)
parser.add_argument(
"--output-dir",
default="png",
help="Directory to save output images (default: png)",
)
parser.add_argument(
"--dpi",
type=int,
default=140,
help="Output image DPI (default: 140)",
)
return parser.parse_args()
def pick_first_existing(header, candidates):
for name in candidates:
if name in header:
return name
return None
def to_float(value):
if value is None:
return float("nan")
text = str(value).strip()
if text == "":
return float("nan")
try:
return float(text)
except ValueError:
return float("nan")
def read_csv_data(csv_path):
with csv_path.open("r", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
header = reader.fieldnames or []
time_col = pick_first_existing(header, TIME_CANDIDATES)
resolved = {}
for group in TARGET_GROUPS.values():
for canonical_name, aliases in group.items():
resolved[canonical_name] = pick_first_existing(header, aliases)
rows = list(reader)
if not rows:
return None
x = []
if time_col is None:
x = list(range(len(rows)))
x_label = "sample_index"
else:
raw_t = [to_float(r.get(time_col)) for r in rows]
t0 = raw_t[0] if raw_t else 0.0
if time_col.endswith("_ms"):
x = [((v - t0) / 1000.0) for v in raw_t]
x_label = f"{time_col} (s, relative)"
else:
x = [(v - t0) for v in raw_t]
x_label = f"{time_col} (relative)"
data = {}
for canonical_name, actual_name in resolved.items():
if actual_name is None:
data[canonical_name] = None
else:
data[canonical_name] = [to_float(r.get(actual_name)) for r in rows]
return {
"x": x,
"x_label": x_label,
"data": data,
"resolved": resolved,
}
def plot_one_csv(csv_path, output_dir, dpi):
parsed = read_csv_data(csv_path)
if parsed is None:
print(f"[SKIP] {csv_path.name}: empty file")
return False
x = parsed["x"]
x_label = parsed["x_label"]
data = parsed["data"]
resolved = parsed["resolved"]
has_velocity = any(data[name] is not None for name in TARGET_GROUPS["velocity"].keys())
has_lin_acc = any(data[name] is not None for name in TARGET_GROUPS["lin_acc"].keys())
if not has_velocity and not has_lin_acc:
print(f"[SKIP] {csv_path.name}: no target columns found")
return False
fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
fig.suptitle(f"Motion Signals - {csv_path.name}")
ax_v, ax_a = axes
for name in TARGET_GROUPS["velocity"].keys():
series = data[name]
if series is not None:
ax_v.plot(x, series, label=name, linewidth=1.1)
for name in TARGET_GROUPS["lin_acc"].keys():
series = data[name]
if series is not None:
ax_a.plot(x, series, label=name, linewidth=1.1)
ax_v.set_ylabel("velocity (m/s)")
ax_a.set_ylabel("linear acc (m/s^2)")
ax_a.set_xlabel(x_label)
ax_v.grid(True, alpha=0.25)
ax_a.grid(True, alpha=0.25)
if has_velocity:
ax_v.legend(loc="upper right")
else:
ax_v.text(0.5, 0.5, "No velocity columns", transform=ax_v.transAxes, ha="center", va="center")
if has_lin_acc:
ax_a.legend(loc="upper right")
else:
ax_a.text(0.5, 0.5, "No linear-acc columns", transform=ax_a.transAxes, ha="center", va="center")
missing = [k for k, v in resolved.items() if v is None]
if missing:
fig.text(0.01, 0.01, f"Missing columns: {', '.join(missing)}", fontsize=9)
fig.tight_layout(rect=[0, 0.03, 1, 0.97])
output_dir.mkdir(parents=True, exist_ok=True)
out_path = output_dir / f"{csv_path.stem}_motion.png"
fig.savefig(out_path, dpi=dpi)
plt.close(fig)
print(f"[OK] {csv_path.name} -> {out_path}")
return True
def main():
args = parse_args()
input_dir = Path(args.input_dir)
output_dir = Path(args.output_dir)
if not input_dir.exists() or not input_dir.is_dir():
raise SystemExit(f"Input directory does not exist: {input_dir}")
csv_files = sorted(input_dir.glob("*.csv"))
if not csv_files:
raise SystemExit(f"No CSV files found in: {input_dir}")
ok_count = 0
for csv_path in csv_files:
if plot_one_csv(csv_path, output_dir, args.dpi):
ok_count += 1
print(f"Finished. Generated charts for {ok_count}/{len(csv_files)} files.")
if __name__ == "__main__":
main()