图片浏览器

图片浏览器#

如果中文显示异常,可以考虑:

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()
---------------------------------------------------------------------------
TclError                                  Traceback (most recent call last)
Cell In[2], line 1
----> 1 win = Window(temp_dir=".temp")
      2 win.wm_title("嵌入 Tk")
      3 # win.mainloop()

Cell In[1], line 58, in Window.__init__(self, temp_dir, *args, **kwargs)
     57 def __init__(self, temp_dir, *args, **kwargs):
---> 58     super().__init__(*args, **kwargs)
     59     self.temp_buch = Bunch()
     60     self.temp_dir = Path(temp_dir) # 缓存目录

File /opt/hostedtoolcache/Python/3.12.7/x64/lib/python3.12/tkinter/__init__.py:2346, in Tk.__init__(self, screenName, baseName, className, useTk, sync, use)
   2344         baseName = baseName + ext
   2345 interactive = False
-> 2346 self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
   2347 if _debug:
   2348     self.tk.settrace(_print_command)

TclError: no display name and no $DISPLAY environment variable