"""Visual cone placement editor. Click on the 5m×5m map to place cones.""" import json import os import tkinter as tk from PIL import Image, ImageTk MAP_FILE = "智能车地图源文件.avif" OUTPUT = "cones.json" MAP_SIZE_M = 5.0 # meters CANVAS_W = 800 CANVAS_H = 800 class ConeEditor: def __init__(self, root): self.root = root root.title("锥桶编辑器 — 左键放置 / 右键删除 / 滚轮保存") self.cones = [] # list of {"x": world_x, "y": world_y} # Load map image img = Image.open(MAP_FILE) self.img_w, self.img_h = img.size img = img.resize((CANVAS_W, CANVAS_H), Image.LANCZOS) self.photo = ImageTk.PhotoImage(img) # Canvas self.canvas = tk.Canvas(root, width=CANVAS_W, height=CANVAS_H, cursor="crosshair") self.canvas.pack() self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo) # Info label self.info = tk.Label(root, text="左键放置 | 右键删除最近 | 鼠标查看坐标") self.info.pack() # Buttons btn_frame = tk.Frame(root) btn_frame.pack(pady=5) tk.Button(btn_frame, text="保存 (cones.json)", command=self.save).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="清除全部", command=self.clear).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="加载已有", command=self.load_existing).pack(side=tk.LEFT, padx=5) tk.Label(btn_frame, text=f"地图: {MAP_SIZE_M}m×{MAP_SIZE_M}m | (-2.5,-2.5) → (2.5,2.5)").pack(side=tk.LEFT, padx=10) # Bindings self.canvas.bind("", self.on_click) self.canvas.bind("", self.on_right_click) self.canvas.bind("", self.on_move) # Load existing if available self.load_existing() def px_to_world(self, px, py): x = px / CANVAS_W * MAP_SIZE_M - MAP_SIZE_M / 2 y = MAP_SIZE_M / 2 - py / CANVAS_H * MAP_SIZE_M return x, y def world_to_px(self, wx, wy): px = (wx + MAP_SIZE_M / 2) / MAP_SIZE_M * CANVAS_W py = (MAP_SIZE_M / 2 - wy) / MAP_SIZE_M * CANVAS_H return px, py def on_move(self, event): wx, wy = self.px_to_world(event.x, event.y) self.info.config(text=f"坐标: ({wx:.3f}, {wy:.3f}) | 锥桶数量: {len(self.cones)}") def on_click(self, event): wx, wy = self.px_to_world(event.x, event.y) # Clamp to map bounds half = MAP_SIZE_M / 2 wx = max(-half, min(half, wx)) wy = max(-half, min(half, wy)) self.cones.append({"x": round(wx, 3), "y": round(wy, 3)}) self.redraw() def on_right_click(self, event): if not self.cones: return wx, wy = self.px_to_world(event.x, event.y) # Remove nearest cone idx = min(range(len(self.cones)), key=lambda i: (self.cones[i]["x"] - wx) ** 2 + (self.cones[i]["y"] - wy) ** 2) self.cones.pop(idx) self.redraw() def redraw(self): self.canvas.delete("cone") r = 5 for c in self.cones: px, py = self.world_to_px(c["x"], c["y"]) self.canvas.create_oval(px - r, py - r, px + r, py + r, fill="blue", outline="white", width=2, tags="cone") def save(self): with open(OUTPUT, "w") as f: json.dump(self.cones, f, indent=2) print(f"已保存 {len(self.cones)} 个锥桶到 {OUTPUT}") self.info.config(text=f"已保存 {len(self.cones)} 个锥桶") def clear(self): self.cones.clear() self.redraw() print("已清除所有锥桶") def load_existing(self): if os.path.exists(OUTPUT): with open(OUTPUT) as f: self.cones = json.load(f) self.redraw() print(f"已加载 {len(self.cones)} 个锥桶") if __name__ == "__main__": root = tk.Tk() app = ConeEditor(root) root.mainloop()