114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
"""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("<Button-1>", self.on_click)
|
||
self.canvas.bind("<Button-3>", self.on_right_click)
|
||
self.canvas.bind("<Motion>", 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()
|