"""
|
viz.py
|
|
读取 sim_log.csv(由 test.py 生成)并用 matplotlib 动画显示:
|
- 主图:路径点(`example_path.json`)和割草机当前位置/航向箭头
|
- 下方两个子图:forward_sig 与 turn_sig 随时间的曲线,当前帧用竖线标示
|
|
运行:
|
python viz.py
|
|
如果没有 `sim_log.csv`,脚本会提示先运行 `test.py` 生成数据。
|
"""
|
import os
|
import json
|
import csv
|
import sys
|
from math import cos, sin
|
import numpy as np
|
|
try:
|
import matplotlib.pyplot as plt
|
from matplotlib.animation import FuncAnimation
|
except Exception as e:
|
print('matplotlib required. Install with: pip install matplotlib')
|
raise
|
|
|
SIM_CSV = 'sim_log.csv'
|
PATH_FILE = 'example_path.json'
|
|
|
def load_sim(csv_path=SIM_CSV):
|
if not os.path.exists(csv_path):
|
print(f"Sim log '{csv_path}' not found. Run test.py first to generate it.")
|
sys.exit(1)
|
t, xs, ys, headings, forward_sig, turn_sig = [], [], [], [], [], []
|
with open(csv_path, 'r', newline='') as f:
|
reader = csv.DictReader(f)
|
for row in reader:
|
t.append(float(row['t']))
|
xs.append(float(row['x']))
|
ys.append(float(row['y']))
|
headings.append(float(row['heading']))
|
forward_sig.append(int(row['forward_sig']))
|
turn_sig.append(int(row['turn_sig']))
|
return {'t': t, 'x': xs, 'y': ys, 'heading': headings, 'forward': forward_sig, 'turn': turn_sig}
|
|
|
def load_path(pth=PATH_FILE):
|
if not os.path.exists(pth):
|
return []
|
with open(pth, 'r') as f:
|
return json.load(f)
|
|
|
def animate_sim(simdata, path):
|
t = simdata['t']
|
x = simdata['x']
|
y = simdata['y']
|
h = simdata['heading']
|
fwd = simdata['forward']
|
trn = simdata['turn']
|
|
fig = plt.figure(figsize=(10, 8))
|
gs = fig.add_gridspec(3, 1, height_ratios=[3, 1, 1])
|
|
ax_map = fig.add_subplot(gs[0])
|
ax_f = fig.add_subplot(gs[1])
|
ax_t = fig.add_subplot(gs[2])
|
|
# map plot
|
if path:
|
px = [p[0] for p in path]
|
py = [p[1] for p in path]
|
ax_map.plot(px, py, '-k', lw=1, label='planned path')
|
ax_map.scatter(px, py, c='k', s=10)
|
|
line_pos, = ax_map.plot([], [], '-r', lw=2, label='actual')
|
point_pos, = ax_map.plot([], [], 'ro')
|
# heading arrow using quiver (initialize at first pose if available)
|
if len(x) > 0 and len(h) > 0:
|
quiv = ax_map.quiver([x[0]], [y[0]], [cos(h[0])], [sin(h[0])], angles='xy', scale_units='xy', scale=4, color='b')
|
else:
|
quiv = ax_map.quiver([0], [0], [1], [0], angles='xy', scale_units='xy', scale=4, color='b')
|
|
ax_map.set_aspect('equal', 'box')
|
ax_map.set_title('Mower path and pose')
|
ax_map.legend()
|
|
# forward subplot
|
ax_f.plot(t, fwd, color='gray', alpha=0.5)
|
f_line = ax_f.axvline(t[0], color='r')
|
ax_f.set_ylabel('forward_sig')
|
|
# turn subplot
|
ax_t.plot(t, trn, color='gray', alpha=0.5)
|
t_line = ax_t.axvline(t[0], color='r')
|
ax_t.set_ylabel('turn_sig')
|
ax_t.set_xlabel('time (s)')
|
|
# set map limits
|
margin = 1.0
|
all_x = x + ( [p[0] for p in path] if path else [] )
|
all_y = y + ( [p[1] for p in path] if path else [] )
|
if all_x and all_y:
|
ax_map.set_xlim(min(all_x)-margin, max(all_x)+margin)
|
ax_map.set_ylim(min(all_y)-margin, max(all_y)+margin)
|
|
def init():
|
line_pos.set_data([], [])
|
point_pos.set_data([], [])
|
return line_pos, point_pos, quiv, f_line, t_line
|
|
def update(i):
|
# i is frame index
|
xi = x[:i+1]
|
yi = y[:i+1]
|
line_pos.set_data(xi, yi)
|
point_pos.set_data([x[i]], [y[i]])
|
|
# update quiver in-place
|
hx = cos(h[i])
|
hy = sin(h[i])
|
# set new offset and vector
|
quiv.set_offsets(np.array([[x[i], y[i]]]))
|
quiv.set_UVC(np.array([hx]), np.array([hy]))
|
|
# update vertical lines
|
f_line.set_xdata(t[i])
|
t_line.set_xdata(t[i])
|
|
return line_pos, point_pos, quiv, f_line, t_line
|
|
interval_ms = max(10, int(1000.0 / 74.0))
|
ani = FuncAnimation(fig, update, frames=len(t), init_func=init, blit=False, interval=interval_ms)
|
plt.tight_layout()
|
plt.show()
|
|
|
def main():
|
sim = load_sim()
|
path = load_path()
|
animate_sim(sim, path)
|
|
|
if __name__ == '__main__':
|
main()
|