《手写数字识别器》(三)绘图板

前面我们已经学习了 Tkinter Canvas 控件相关的技术,现在我们将会使用前面学习的内容来实现用于手写数字的绘画板。绘画板主要包括四个部分,分别是:

  1. 主窗口界面
  2. 顶部工具栏
  3. 中心绘画区
  4. 底部状态栏

1. 主窗口界面

主界面编写时,包括三个文件:

  1. Config.py 配置文件
  2. MainFrame.py 主窗口类
  3. App.py 入口文件

Config.py 文件内容如下:

# 屏幕大小
SCREEN_W = 500
SCREEN_H = 575

MainFrame.py 文件内容如下:

import tkinter as tk
from Config import *


class MainFrame(tk.Tk):

    def __init__(self):
        super(MainFrame, self).__init__()
        # 设置窗口尺寸位置
        sw = self.winfo_screenwidth()
        sh = self.winfo_screenheight()
        px = int(sw/2 - SCREEN_W/2)
        py = int(sh/2 - SCREEN_H/2)
        self.geometry(f'{SCREEN_W}x{SCREEN_H}+{px}+{py}')
        # 设置其他属性
        self.resizable(False, False)
        self.title('数字手写板')
        # 初始化其他控件
        self.init_widgets()


    def init_widgets(self):
        """初始化窗口控件"""
        pass


App.py 文件内容如下:

from MainFrame import MainFrame


if __name__ == '__main__':
    main_frame = MainFrame()
    main_frame.mainloop()

2. 顶部工具栏

TopBar 的 callbacks 参数为字典类型,根据 key 为顶部工具栏的每个按钮绑定点击事件。

import tkinter as tk


class TopBar(tk.Frame):

    def __init__(self, callbacks={}):
        super(TopBar, self).__init__()
        self.callbacks = callbacks
        self.init_widgets()

    def init_widgets(self):
        # 加载图标素材
        self.load_images(scale=3)
        # 创建按钮对象
        self.buttons = {}
        for name, image in self.images.items():
            self.buttons[name] = tk.Button(self, command=self.callbacks.get(name, None), image=image)
            self.buttons[name].pack(side=tk.LEFT)

    def load_images(self, scale):
        fnames = (('clear',     'source/clear.png'),
                  ('save',      'source/save.png'),
                  ('show',      'source/show.png'),
                  ('open',      'source/open.png'),
                  ('train',     'source/train.png'),
                  ('predict',   'source/predict.png'))
        self.images = {}
        for name, fname in fnames:
            self.images[name] = tk.PhotoImage(file=fname).subsample(scale, scale)


if __name__ == '__main__':

    window = tk.Tk()
    window.geometry('500x575+200+200')
    window.resizable(False, False)

    callbacks = {
        'clear': lambda : print('清屏'),
        'save': lambda : print('保存'),
        'show': lambda : print('展示'),
        'open': lambda : print('打开'),
        'train': lambda : print('训练'),
        'predict': lambda : print('预测'),
    }

    topbar = TopBar(callbacks=callbacks)
    topbar.pack(side=tk.TOP, fill=tk.X)


    window.mainloop()

3. 中心绘画区

这部分的代码比较多,主要包括:

  1. 标记区域
  2. 自由绘制
  3. 清空画布
  4. 打开图像
  5. 保存图像

3.1 标记区域

我们设置画布区域大小为 500×500,并将边框宽度设置为 0,保证存储的图像大小为 500×500。

import tkinter as tk

class CenCanvas(tk.Canvas):
    
    def __init__(self):
        super(CenCanvas, self).__init__()
        self.config(highlightthickness=0, bg='white', width=500, height=500)
        # 显示绘图区域
        self.draw_area()

    def draw_area(self):
        width = 300
        height = 300
        x1, y1 = int(500/2 - width/2), int(500/2 - height/2)
        x2, y2 = x1 + width, y1 + height
        self.area = self.create_rectangle(x1, y1, x2, y2, outline="gray", width=2, dash=(15, 15))

3.2 自由绘制

当鼠标左键按下滑动时,在鼠标经过的位置绘制半径为 6 的圆,从而实现自由绘制图案。这里需要注意的是,当滑动鼠标过快时,上一个绘制的点和当前绘制的点之间会存在较大的空隙,我们这里会使用插值的方式在两个点之间补充绘制多个点。

import tkinter as tk

class CenCanvas(tk.Canvas):
    
    def __init__(self):
        super(CenCanvas, self).__init__()
        self.config(highlightthickness=0, bg='white', width=500, height=500)
        # 绑定各种事件
        self.bind_events()
        # 存储绘制对象
        self.items = []
        ...
        # 存储绘制位置
        self.px = None
        self.py = None

   ....

    def bind_events(self):
        self.bind('<B1-Motion>', self.mouse_motion)
        self.bind('<ButtonRelease-1>', self.mouse_release)
    
    def mouse_release(self, event):
        self.px = None
        self.py = None

    def mouse_motion(self, event):
        cx, cy = event.x, event.y
        # 绘制圆半径
        radius = 6
        draw_points = [(cx, cy)]

        # 计算插值
        if self.px and self.py:
            num_steps = int(max(abs(self.px - cx), abs(self.py-cy)) / 3)
            if num_steps > 1:
                points = self.insert_point((self.px, self.py), (cx, cy), num_steps)
                draw_points.extend(points)
        # 绘制点
        for point in draw_points:
            point = np.array(point)
            point = np.hstack((point - radius, point + radius)).tolist()
            item = self.create_oval(point, fill='#b33939', outline='#b33939')
            self.items.append(item)
        self.px, self.py = cx, cy

    def insert_point(self, point1, point2, num_steps):
        x1, y1 = point1
        x2, y2 = point2
        step_x = (x2 - x1) / num_steps
        step_y = (y2 - y1) / num_steps
        points = []
        for index in range(num_steps):
            ix = x1 + index * step_x
            iy = y1 + index * step_y
            points.append((ix, iy))
        return points


if __name__ == '__main__':
    window = tk.Tk()
    window.geometry('500x575+200+200')
    window.resizable(False, False)

    canvas = CenCanvas()
    canvas.pack()

    frame = tk.Frame()

    window.mainloop()

3.3 清空画布

import tkinter as tk
import numpy as np
from tkinter.filedialog import askopenfilename
from tkinter.filedialog import asksaveasfilename
from PIL import Image, ImageTk, ImageDraw


class CenCanvas(tk.Canvas):
    
    ...

    def clear_canvas(self):
        for item in self.items:
            self.delete(item)
        self.items.clear()
    
    ...


if __name__ == '__main__':
    window = tk.Tk()
    window.geometry('500x575+200+200')
    window.resizable(False, False)

    canvas = CenCanvas()
    canvas.pack()

    frame = tk.Frame()
    clear_button = tk.Button(frame, text='清空', command=lambda : canvas.clear_canvas())
    clear_button.pack(side=tk.LEFT)
    frame.pack(side=tk.BOTTOM)

    window.mainloop()

3.4 打开图像

import tkinter as tk
import numpy as np
from tkinter.filedialog import askopenfilename
from tkinter.filedialog import asksaveasfilename
from PIL import Image, ImageTk, ImageDraw


class CenCanvas(tk.Canvas):
    
    ...

    def open(self):

        # 1. 显示选择图像的对话框(图片路径)
        default_path = os.path.abspath('data/train')
        filename = askopenfilename(initialdir=default_path)
        if not filename:
            return

        # 2. 读取图像数据,并进行转换
        image = Image.open(filename)
        photo = ImageTk.PhotoImage(image)

        # 持有打开的图像,避免销毁
        self.image_hold = photo

        # 3. 将转换后的图像数据绘制到画布上
        if hasattr(self, 'image_item') and self.image_item is not None:
            self.delete(self.image_item)
            if self.image_item in self.items:
                self.items.remove(self.image_item)

        self.image_item = self.create_image(0, 0, image=photo, anchor=tk.NW)
        self.items.append(self.image_item)


if __name__ == '__main__':
    window = tk.Tk()
    window.geometry('500x575+200+200')
    window.resizable(False, False)

    canvas = CenCanvas()
    canvas.pack()

    frame = tk.Frame()
    frame.pack(side=tk.BOTTOM)
    clear_button = tk.Button(frame, text='清空', command=lambda : canvas.clear_canvas())
    clear_button.pack(side=tk.LEFT)

    open_button = tk.Button(frame, text='打开', command=lambda: canvas.open())
    open_button.pack(side=tk.LEFT)

    window.mainloop()

3.5 保存图像

import tkinter as tk
import numpy as np
from tkinter.filedialog import askopenfilename
from tkinter.filedialog import asksaveasfilename
from PIL import Image, ImageTk, ImageDraw


class CenCanvas(tk.Canvas):
    
    ...

    def generate_image(self):
        image = Image.new('RGB', (500, 500), self['bg'])
        draw = ImageDraw.Draw(image)
        for item in self.items:
            if self.type(item) == 'image':
                image.paste(self.image_data, box=self.bbox(item))
            else:
                point = self.coords(item)
                color = self.itemcget(item, 'fill')
                draw.ellipse(point, fill=color)
        return image

    def save(self):
        import os
        data_path = os.path.dirname(os.path.abspath(__file__)) + '\\data\\train'
        print(data_path)
        save_path = asksaveasfilename(initialdir=data_path, title='存储为', defaultextension=".png")
        if not save_path:
            return
        image = self.generate_image()
        image.save(save_path, "png")


if __name__ == '__main__':
    window = tk.Tk()
    window.geometry('500x575+200+200')
    window.resizable(False, False)

    canvas = CenCanvas()
    canvas.pack()

    frame = tk.Frame()
    frame.pack(side=tk.BOTTOM)
    clear_button = tk.Button(frame, text='清空', command=lambda : canvas.clear_canvas())
    clear_button.pack(side=tk.LEFT)

    open_button = tk.Button(frame, text='打开', command=lambda: canvas.open())
    open_button.pack(side=tk.LEFT)

    save_button = tk.Button(frame, text='保存', command=lambda: canvas.save())
    save_button.pack(side=tk.LEFT)

    window.mainloop()

4. 底部状态栏

import tkinter as tk


class StatusBar(tk.Frame):
    
    def __init__(self):
        super(StatusBar, self).__init__()
        self.config(bg='#d2dae2', height=30)

        self.status = tk.Label(self, text='准备就绪')
        self.status.config(bg='#d2dae2')
        self.status.pack(side=tk.LEFT)

    def set_status(self, text):
        self.status['text'] = text


if __name__ == '__main__':
    window = tk.Tk()
    window.geometry('500x575+200+300')
    window.resizable(False, False)

    sbar = StatusBar()
    sbar.pack(side=tk.BOTTOM, fill=tk.X)


    button = tk.Button(text='按钮', command=lambda : sbar.set_status('修改文本'))
    button.pack()


    window.mainloop()

5. 整合界面

修改 MainFrame.py 文件内容如下:

import tkinter as tk
from Config import *
from TopBar import TopBar
from CenCanvas import CenCanvas
from StatusBar import StatusBar



class MainFrame(tk.Tk):

    ....

    def init_widgets(self):
        """初始化窗口控件"""
        callbacks = {
            'clear':    self.clear,
            'save':     self.save,
            'show':     self.show,
            'open':     self.open,
            'train':    self.train,
            'predict':  self.predict,
        }

        self.tbar = TopBar(callbacks=callbacks)
        self.tbar.pack(side=tk.TOP, fill=tk.X)

        self.ccav = CenCanvas()
        self.ccav.pack()

        self.sbar = StatusBar()
        self.sbar.pack(side=tk.BOTTOM, fill=tk.X)

    # 下面为按钮绑定函数
    def clear(self):
        print('清屏')

    def save(self):
        print('保存')

    def show(self):
        print('展开')

    def open(self):
        print('打开')

    def train(self):
        print('训练')

    def predict(self):
        print('推理')
未经允许不得转载:一亩三分地 » 《手写数字识别器》(三)绘图板
评论 (0)

1 + 6 =