图片浏览器#
如果中文显示异常,可以考虑:
sudo apt-get install fonts-wqy-zenhei fonts-arphic-ukai fonts-arphic-uming
from tkinter import ttk, Tk, StringVar
from tkinter import filedialog
from pathlib import Path
from PIL import Image
import numpy as np
# from matplotlib import rcParams
from matplotlib.backend_bases import key_press_handler
from matplotlib.patches import Circle
from matplotlib.collections import PatchCollection
from matplotlib.backend_bases import MouseButton
from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk, FigureCanvasTkAgg
from matplotlib.figure import Figure
import toml
# rcParams['font.family'] = 'SimHei' # 替换为你选择的字体 'SimHei' 'DejaVu Sans', 'Noto Sans CJK JP', 'Noto Sans TC'
# rcParams['axes.unicode_minus'] = False # 用来正常显示负号
def fake_labels(cfg):
cfg.eyes.labels = [np.random.uniform(0, 100) for _ in range(len(cfg.eyes.centers))]
return cfg
def update_cfg(toml_path):
cfgs = toml.load(toml_path)
cfgs = Bunch(cfgs)
for path, cfg in cfgs.items():
fake_labels(cfg)
with open(toml_path, "w") as fp:
toml.dump(cfgs, fp)
class Bunch(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__dict__ = self # 这意味着 Bunch 类的实例将具有与字典相同的行为,可以使用点符号访问和修改其键值对
self._convert_nested_dicts()
def _convert_nested_dicts(self):
for k, v in self.__dict__.items():
if isinstance(v, dict):
self.__dict__[k] = Bunch(**v) # 将字典转换为 Bunch 对象
elif isinstance(v, Bunch):
v._convert_nested_dicts() # 递归处理嵌套的 Bunch 对象
def merge(self, other):
"""提供递归合并功能"""
other = Bunch(other)
for k, v in other.items():
if k not in self:
self[k] = other[k]
else:
if not isinstance(self[k], dict) and not isinstance(v, dict):
self[k] = v
elif isinstance(self[k], dict) and isinstance(v, dict):
self[k].update(v)
else:
raise TypeError(f"{other}不支持合并")
class Window(Tk):
def __init__(self, temp_dir, *args, **kwargs):
super().__init__(*args, **kwargs)
self.temp_buch = Bunch()
self.temp_dir = Path(temp_dir) # 缓存目录
self.cfg_path = self.temp_dir/"data.toml"
self.eyes_dir = self.temp_dir/"eyes"
self.faces_dir = self.temp_dir/"faces"
self.eyes_dir.mkdir(parents=True, exist_ok=True)
self.faces_dir.mkdir(parents=True, exist_ok=True)
self.eyes = []
self.faces = []
self.temp_id = -1
ttk.Style().configure("MyStyle.TButton", padding=1, relief='ridge')
ttk.Style().configure("MyStyle.TRadiobutton", padding=1, relief='ridge')
self.image_id = None
self.im = None
self.var = StringVar()
fig = Figure()
self.ax = fig.add_subplot()
fig.subplots_adjust(bottom=0.2)
frame = ttk.Frame()
self.file_button = ttk.Button(frame, text='加载图片', command=self.open_images, style="MyStyle.TButton")
self.button_next = ttk.Button(frame, text='下一张', command=self.next, style="MyStyle.TButton")
self.button_prev = ttk.Button(frame, text='上一张', command=self.prev, style="MyStyle.TButton")
self.button_eye = ttk.Radiobutton(frame, text='画眼', variable=self.var, value="eye", style="MyStyle.TRadiobutton")
self.button_face = ttk.Radiobutton(frame, text='画皮', variable=self.var, value="face", style="MyStyle.TRadiobutton")
self.button_delete = ttk.Radiobutton(frame, text='删除', variable=self.var, value="delete", style="MyStyle.TRadiobutton")
self.canvas = FigureCanvasTkAgg(fig) # A tk.DrawingArea.
# pack_toolbar=False will make it easier to use a layout manager later on.
self.toolbar = NavigationToolbar2Tk(self.canvas, pack_toolbar=False)
self.toolbar.update()
# pack顺序很重要。部件是按顺序处理的,如果因为窗口太小而没有剩余空间,它们就不会被显示。
# 画布的大小相当灵活,所以我们将其放在最后打包,这样可以确保UI控件在可能的情况下尽可能长时间地显示。
frame.pack(side="top")
self.file_button.pack(side='left')
self.button_eye.pack(side='left')
self.button_face.pack(side='left')
self.button_delete.pack(side='left')
self.button_next.pack(side='right')
self.button_prev.pack(side='right')
self.toolbar.pack(side='bottom', fill="x")
self.canvas.get_tk_widget().pack(side="top", fill="both", expand=True)
self.canvas.mpl_connect("key_press_event", lambda event: self.onkey(event))
self.canvas.mpl_connect("pick_event", lambda event: self.onpick(event))
self.canvas.mpl_connect('button_press_event', self.onclick)
def set_state(self):
num = len(self.image_paths)
if num <= 1:
self.button_prev["state"] = "disabled"
self.button_next["state"] = "disabled"
else:
if self.image_id == 0:
self.button_prev["state"] = "disabled"
self.button_next["state"] = "active"
elif self.image_id < num-1:
self.button_prev["state"] = "active"
self.button_next["state"] = "active"
else:
self.button_prev["state"] = "active"
self.button_next["state"] = "disabled"
def update_image(self, path):
self.im = Image.open(path)
# self.ax.update_from()
self.ax.imshow(self.im)
self.set_state()
self.canvas.draw_idle()
self.canvas.flush_events()
def next(self):
if isinstance(self.image_id, int):
self.image_id += 1
self.update_image(self.image_paths[self.image_id])
self.clear_point()
def prev(self):
if isinstance(self.image_id, int):
self.image_id -= 1
self.update_image(self.image_paths[self.image_id])
self.clear_point()
def open_images(self):
self.image_dir = filedialog.askdirectory()
if self.image_dir:
self.image_paths = [p for p in Path(self.image_dir).iterdir() if p.resolve().suffix.lower() in [".jpg", ".jpeg", ".png"]]
if len(self.image_paths) >= 1:
self.image_id = 0
self.update_image(self.image_paths[self.image_id])
def onpick(self, event):
print('onpick scatter:', event, event.artist)
if isinstance(event.artist, Circle):
event.artist.remove()
self.canvas.draw_idle()
self.canvas.flush_events()
def clear_point(self):
if self.ax.patches:
[p.remove() for p in self.ax.patches]
self.canvas.draw_idle()
self.canvas.flush_events()
def update_point(self, xdata, ydata, alpha=0.5):
R = min(self.im.size)
radius = R//50
label = self.var.get()
if label == "eye":
color = "r"
elif label == "face":
color = 'g'
circle = Circle(
(xdata, ydata),
radius, color=color, fill=True, alpha=alpha,
picker=True,
label=label,
)
pathch = self.ax.add_patch(circle)
pathch.set_picker(True) # 设置可以被选择
self.canvas.draw_idle()
self.canvas.flush_events()
def onclick(self, event):
if event.xdata and event.ydata and self.im:
if event.button == MouseButton.LEFT:
self.var.set("eye")
elif event.button == MouseButton.RIGHT:
self.var.set("face")
elif event.button == MouseButton.MIDDLE:
self.var.set("delete")
return
else:
print(event.button, event.key)
return
self.update_point(event.xdata, event.ydata, alpha=0.5)
print(
f"{'double' if event.dblclick else 'single'} "
f"click: button={event.button:d}, x={event.x:g}, y={event.y:g}, "
f"xdata={event.xdata:g}, ydata={event.ydata:g},"
f"widget: {self.canvas.get_tk_widget()},"
f"width, height: {self.canvas.get_width_height(physical=False)}"
f"=>{self.var.get()}"
)
def onkey(self, event):
if not isinstance(self.image_id, int):
return
print(f"you pressed {event.key}, {self.ax.patches}")
if event.key == "e":
self.temp_id += 1
self.eyes = [patch for patch in self.ax.patches if patch.get_label() == "eye"]
self.faces = [patch for patch in self.ax.patches if patch.get_label() == "face"]
if not (self.eyes or self.faces):
return
path = self.image_paths[self.image_id].as_posix()
bunch = {
path: {
"eyes": {"centers": np.array([node.center for node in self.eyes]).tolist(),},
"faces": {"centers": np.array([node.center for node in self.faces]).tolist(),}
}}
self.temp_buch.update(Bunch(bunch))
print(
f"eyes 点数: {len(self.eyes)},"
f"faces 点数: {len(self.faces)}\n"
f"temp_buch: {self.temp_buch}\n"
)
with open(self.cfg_path, "w") as fp:
toml.dump(self.temp_buch, fp)
elif event.key == "c":
self.clear_point()
win = Window(temp_dir=".temp")
win.wm_title("嵌入 Tk")
# win.mainloop()
''