ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

书法字体风格识别实战:Python+Tkinter 图像分类完整流程解析

书法字体风格识别实战:Python+Tkinter 图像分类完整流程解析 简介基于Python实现的书法字体风格识别器源码包面向对图像分类、桌面应用开发以及书法文化数字化感兴趣的开发者。项目通过输入图片即可预测作品对应的书法字体风格采用Tkinter搭建图形界面完整覆盖了从配置管理、图像预处理与数据集生成到多模型自动评估与图形界面预测展示的实践链路。共16个文件包含5个Python脚本、配置文件、示例图像以及说明文档压缩包仅716KB方便下载与本地复现。其中配置文件指定了字体风格列表和图片目标尺寸预处理脚本负责生成训练集与测试集评估环节借助自动化分类器工具比较多个分类模型并保存F1分数最高的模型最终在图形界面中选择图片即可看到预测结果适合学习小型人工智能应用端到端开发流程。目前已有182人学习下载可满足对书法字体风格识别感兴趣的人群用于练手、改造或作为课程设计参考。1. 书法字体风格识别从图片到分类结果这套 PythonTkinter 源码做了什么把一张书法作品图片丢进界面几秒钟后 Tkinter 窗口弹出一行字体风格预测——这就是这套 Python 写成的书法字体风格识别器在做的事。解压源码包里面是完整的 calligraphy-style-recognition-main 工程0_setting.yaml 管配置1_Xy.py 负责把图片预处理成数据集2_fit.py 用 LazyClassifier 批量比较多个分类模型并按 F1 保存最优结果3_predict.py 则用 Tkinter 搭了个图形界面选图即出预测。对刚开始接触图像分类、又不想只看 MNIST 的人来说它的价值是给了你一条完整可跑的流水线还留出了足够多可以改的接口。适合人群熟悉基础 Python、想用真实书法数据集练手的从业者或学生以及想快速把图片分类做成桌面小工具的人。2. 数据准备链路0_setting.yaml 与 1_Xy.py 如何把图片目录变成 npz 训练集2.1 配置文件先讲清楚风格列表、目标尺寸与路径约定拿到解压后的目录建议先打开的还不是 README而是 0_setting.yaml。这份 yaml 是整个流水线的总开关里面至少有两类信息直接决定后面脚本能不能跑出合理结果一是书法字体风格列表常见的是楷书、行书、草书、隶书、篆书这类五大类二是图片统一缩放的目标尺寸比如 224×224。风格列表的顺序就是标签编码的顺序1_Xy.py 在遍历目录时会按列表顺序给每张图生成 0、1、2 这样的类别号目标尺寸则影响特征空间大小。224×224×3 拉平后是 150528 维特征很多 sklearn 分类器在这个维度上已经不算轻松如果机器内存紧张把 224 改成 128 能明显缩短训练时间但代价是笔画细节的损失需要多验证几轮再定。书法图片还有一个需要提前想清楚的问题要不要做灰度化。白底黑字的书法作品本身彩色信息很少风格区分主要靠笔画结构所以有些实现会把图片先转灰度再喂模型但灰度化之后如果再叠加直方图均衡化或者反色成黑底白字模型学到的特征分布会变。我的建议是 RGB 和灰度二选一关键约束是训练端和预测端保持一致不要在 1_Xy.py 里做灰度化到了 3_predict.py 却忘了去掉convert(RGB)那种前后不一致最容易让结果变成玄学。下面是一份常见的 yaml 配置结构实际字段名以你解压出的源码为准# 0_setting.yaml 结构示例实际字段名以解压源码为准 styles: - 楷书 - 行书 - 草书 - 隶书 - 篆书 image_size: [224, 224] data_dir: ./data train_ratio: 0.8 random_state: 42如果解压出来之后字段名对不上别急着猜。常见做法是临时写一段调试脚本把 yaml 内容打印成字典看清真实键名再动手# 临时检查脚本把 yaml 里真实字段名打印出来 import yaml with open(0_setting.yaml, r, encodingutf-8) as f: cfg yaml.safe_load(f) print(cfg) print(type(cfg), list(cfg.keys()))输出里如果看到键名是 style_list、target_size 之类的变体就把后面脚本里对应变量名统一替换避免运行到一半抛出 KeyError。这也是我拆这类小工程时的第一个习惯先摸清配置长什么样再改代码逻辑。2.2 图像预处理与数据集生成1_Xy.py 的执行链路1_Xy.py 是全局第二个要跑的脚本名字里的 Xy 指的就是特征矩阵 X 和标签向量 y。它的输入是 data 目录下按风格分好的子文件夹输出是一个 data.npz 文件。util.py 和 image_process.py 在这个阶段会被调用util.py 通常负责路径拼接、numpy 保存加载这类胶水功能image_process.py 则更偏向图像读取、尺寸调整、通道转换和可选的增强逻辑。如果你运行 1_Xy.py 时遇到 import error多半不是缺库而是 util.py 里的路径基准没配对先看它 import 了哪些相对路径。整个预处理链路可以拆成六步读取 yaml 配置遍历数据目录逐张图片 resize 到目标尺寸统一转换成 RGB 三通道然后按风格列表生成标签最后用 train_test_split 划分出训练集和测试集并保存成 npz。下面这段代码还原了核心步骤具体实现以解压源码为准# 1_Xy.py 核心步骤按项目常见实现整理实际以解压源码为准 from PIL import Image from pathlib import Path import numpy as np from sklearn.model_selection import train_test_split def load_and_preprocess(styles, data_dir, image_size): X, y [], [] for label, style in enumerate(styles): folder Path(data_dir) / style for img_path in sorted(folder.iterdir()): # convert(RGB) 强制三通道避免 PNG 的 RGBA 变成四通道 img Image.open(img_path).convert(RGB).resize(image_size) X.append(np.asarray(img)) y.append(label) return np.stack(X), np.asarray(y) X, y load_and_preprocess(styles, data_dir, image_size) # stratify 保持训练集和测试集里各类别比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) np.savez( data.npz, X_trainX_train, X_testX_test, y_trainy_train, y_testy_test, ) print(X_train:, X_train.shape, y_train:, y_train.shape)几个关键点说一下。.convert(RGB)是必须的书法图片很多是扫描件或 PNG 导出图不带转换很容易混进 RGBA 四通道数据后面拉平特征维度时直接翻车。.resize(image_size)在旧版本 Pillow 里要传Image.ANTIALIAS新版本直接传目标尺寸即可这个插值参数的坑后面避坑章还会展开。stratifyy是很容易被忽略的一行它保证划分出来的训练集和测试集里每种书法风格所占比例与原始数据一致。如果某个风格样本特别少不做分层抽样测试集里可能完全抽不到那一类F1 分数看起来不错但没有实际意义。跑完 1_Xy.py 之后包里会生成 data.npz。这时候一定要看终端打印的 shape养成习惯X_train 应该是 (N, 224, 224, 3)N 是图片总数最后一位是 RGB 三通道。如果看到 (N, 224, 224, 4)说明哪里漏了 convert如果看到 (N, 3, 224, 224)说明某个环节做了 transpose后面 2_fit.py 里的 reshape 维度会完全对不上。包里带的 1_Xy.png 运行截图就是这一步跑通后的可视化和 shape 输出可以拿来做对照。3. 模型自动选型2_fit.py 用 LazyClassifier 横向比较多分类器并留下最优模型3.1 LazyClassifier 的核心思路与适用边界数据准备好之后进入训练环节2_fit.py 做的事情不是直接训练一个指定的分类器而是先用 LazyClassifier 把 sklearn 里一批常见分类器在同一份数据集上过一遍然后根据指标选一个最终模型保存。这样做的好处很直接书法风格识别在数据规模上属于典型的小样本图像分类每个风格几十到几百张图特征又已经拉平成了向量手动逐个试 LogisticRegression、SVC、RandomForest、ExtraTrees 这些模型再各自调参时间成本太高。LazyClassifier 把这些模型统一封装输入 X_train、X_test输出一张包含 Accuracy、F1、ROC AUC、训练耗时等指标的表格排行一目了然。需要说清楚它的边界LazyClassifier 适合做快速筛选不适合当最终模型。它内部用同一组默认参数跑完所有模型得到的指标反映的是默认参数下的表现不是调参后的上限。我的常见用法是先拿它筛出两三个候选再单独对候选模型做网格搜索。另外它的输入要是二维特征矩阵这也是为什么 2_fit.py 里一定要做 reshape 拉平。如果你把原始的四维图像数组直接丢进去它会先报维度错误再告诉你 reshape。还有一个需要理解的点是LazyClassifier 跑分不能替代交叉验证。它默认只做一次训练测试集划分结果会受随机划分影响。常规做法是在 yaml 里固定 random_state保证每次跑出来的排名可复现。下面这张表总结了选型时的参考维度对比项说明建议数据规模几百到几千张LazyClassifier 直接可用特征维度拉平后约十五万维卡顿就降采样到 128×128类别均衡各风格样本数不均衡优先看宏平均 F1部署环境模型跨机器加载固定 sklearn 小版本3.2 训练脚本参数细节与模型保存2_fit.py 的核心逻辑大致是加载 data.npz把训练测试集的 shape 从 (N,224,224,3) 拉平成 (N,-1)做像素归一化然后调用 LazyClassifier 得到模型排行按 F1 选第一行对应的模型名最后重新训练并保存。下面这段代码按常见实现还原# 2_fit.py 核心逻辑按项目常见实现整理实际以解压源码为准 from lazypredict.Supervised import LazyClassifier from sklearn.ensemble import RandomForestClassifier import numpy as np import pickle data np.load(data.npz, allow_pickleTrue) # reshape 成二维sklearn 分类器不接收 (N,H,W,3) 的图像输入 X_train data[X_train].reshape(len(data[X_train]), -1) / 255.0 X_test data[X_test].reshape(len(data[X_test]), -1) / 255.0 y_train, y_test data[y_train], data[y_test] clf LazyClassifier(verbose1, ignore_warningsTrue) models_df, predictions clf.fit(X_train, X_test, y_train, y_test) # F1 排序列的第一行就是当前数据上的最优模型 print(models_df.head(10)) best_model_name models_df.index[0] print(best model:, best_model_name) # 拿到模型名之后用 sklearn 同名分类器重新训练再保存 best_clf RandomForestClassifier() best_clf.fit(X_train, y_train) with open(best_model.pkl, wb) as fout: pickle.dump(best_clf, fout)这段代码里有几个参数值得单独说。/ 255.0把像素从 0 到 255 缩放到 0 到 1对 SVC、LogisticRegression 这类对特征量纲敏感的模型很关键不做这一步预测结果会偏向数值更大的通道。reshape(-1)的 -1 是让 numpy 自动推断等价于 224×224×3150528。verbose1会在终端实时打印每个模型正在跑方便判断脚本是不是卡住了如果你不想看满屏刷输出改成 verbose0。ignore_warningsTrue用于忽略部分模型在这种高维稀疏特征下的收敛警告但不建议无脑忽略第一次跑还是先看看警告内容。保存模型这一步最容易出现理解偏差。LazyClassifier 的 fit 返回的是评估表不是可直接用于预测的模型对象。也就是说从 models_df 里看到第一名是 RandomForestClassifier并不代表 clf 这个变量里躺着一个训练好的 RandomForest 实例。更稳妥的做法是像上面代码那样拿到模型名之后在 sklearn 里找到同名分类器重新 fit 一次再 pickle。如果解压出来的 2_fit.py 里直接保存了 clf后面 3_predict.py 加载的时候极大概率会报 predict 属性不存在的错。4. 实操避坑数据准备、模型保存与 GUI 预测阶段的五条踩坑记录4.1 预处理与数据集生成阶段两条典型报错坑 1风格列表与目录名对不上生成空数据集现象运行 1_Xy.py 后终端打印 X_train 的 shape 是 (0, 224, 224, 3)或者遍历目录时直接报 No such file or directory但用文件管理器看 data 目录里明明有图。原因0_setting.yaml 里的 styles 写的是楷书、行书而 data 下面的子目录名是楷书(1)、行书 2这种带括号或空格的变体。从网上下载的书法素材批量解压后经常带这类后缀代码用精确匹配自然匹配不到遍历结果为空。解决在预处理函数里加一段目录检查先把 data_dir 下真实存在的子目录名打印出来再回填到 styles 列表。我一般会在 load 循环里加一句if not list(folder.iterdir()): print(空目录, style)让空数据集的问题在预处理阶段就暴露出来而不是等到训练时突然报维度错误才回去排查。坑 2PNG 带 RGBA 通道shape 变成 (N, 224, 224, 4)现象1_Xy.py 跑完打印 X_train.shape 出现 4 通道进入 2_fit.py 拉平后特征数变成 200704比预期多了五万维训练速度明显变慢某些基于距离的模型还会报数值警告。原因书法图片很多是白底黑字的 PNG 导出图PIL 的Image.open()不会自动丢弃 Alpha 透明通道直接 resize 保留原有通道数四通道就这么混了进来。解决统一在读取后加.convert(RGB)这一步会把 RGBA 丢弃并强制转成三通道。注意这个转换要在 resize 之前做顺序反了同样会产生通道错乱。4.2 训练与 GUI 部署阶段三条最容易翻车的点坑 3LazyClassifier 的模型保存存了个空壳现象2_fit.py 跑完生成了 best_model.pkl但 3_predict.py 加载后调用 predict 报AttributeError: LazyClassifier object has no attribute predict或者加载时报找不到 lazypredict 模块。原因LazyClassifier 的 fit 返回的是评估表不包含可直接复用的模型参数。部分实现会把整个 LazyClassifier 实例 pickle 下来但实例内部并没有保存每个模型的训练权重加载自然没法预测。解决取到 models_df 第一行的模型名之后在 sklearn 里找到同名分类器按默认参数重新 fit 一遍再 pickle。如果源码本身已经是这么写的保持原逻辑如果发现直接保存了 clf 对象改成重训后再保存。另外在部署环境里把 sklearn 版本固定住用 requirements.txt 写死小版本能绕开很多跨环境加载的兼容问题。坑 4训练端和预测端预处理不一致GUI 全猜同一类现象3_predict.py 里无论选哪张图片预测结果都固定在同一个风格上比如全是楷书回看批量验证准确率也接近随机水平。原因2_fit.py 训练时做了/ 255.0归一化而 3_predict.py 的预测函数只做了 resize 和 reshape忘了归一化。特征分布从 0 到 1 的区间变成 0 到 255SVC、LogisticRegression 这类模型对特征量纲极其敏感距离计算全偏输出自然集中到某个类别上。解决把预处理逻辑抽成 util.py 里的公共函数比如preprocess_for_model(path, image_size)训练脚本和 GUI 都调用同一个函数保证两端永远一致。这是我拆这个项目时最强调的一点预处理一致性比调模型参数对结果的影响大得多。坑 5Tkinter 图片预览区域空白PhotoImage 被垃圾回收现象GUI 窗口能正常弹出按钮也能点但图片预览区域空白控制台偶尔报TclError: image pyimage1 doesnt exist。原因Tkinter 的 PhotoImage 对象如果没有被全局变量或对象属性持有函数作用域结束后资源会被回收界面上的图片就消失了。这在把图片预览放到函数内部的写法里很常见。解决把 PhotoImage 绑定到全局变量或者窗口对象的属性上比如写成window.current_img ImageTk.PhotoImage(...)而不是用一个函数内局部变量。拿到源码时如果发现 3_predict.py 只显示预测文字、没有图片预览大概率就是这个原因。提示五条坑里前两条在预处理阶段后三条在训练部署阶段。跑这类小项目最费时间的往往不是算法调参而是数据格式、环境版本和前后端预处理不一致这些看着不起眼的小问题。5. 跑通 GUI 之后验证方法、置信度显示与批量识别改造5.1 GUI 预测入口与关键细节3_predict.py 是整个工程的最后一环启动 Tkinter 窗口、加载模型、让用户选一张图片、输出预测风格。代码不长但有两个细节决定能不能一次跑顺。模型要在窗口初始化时就加载好不要在每次点击预测时反复读盘预处理路径要跟训练时完全一致否则模型表现就像随机猜测。核心流程的常见写法是这样# 3_predict.py 核心流程按项目常见实现整理实际以解压源码为准 import tkinter as tk from tkinter import filedialog from PIL import Image import numpy as np import pickle window tk.Tk() window.title(书法字体风格识别) model pickle.load(open(best_model.pkl, rb)) styles [楷书, 行书, 草书, 隶书, 篆书] def predict(): path filedialog.askopenfilename(filetypes[(Images, *.png *.jpg *.jpeg)]) if not path: return img Image.open(path).convert(RGB).resize((224, 224)) feat np.asarray(img).reshape(1, -1) / 255.0 label.config(text预测结果 styles[int(model.predict(feat)[0])]) label tk.Label(window, text点击下方按钮选择图片) label.pack() tk.Button(window, text选择图片, commandpredict).pack() window.mainloop()styles 列表的顺序必须与 0_setting.yaml 保持一致。如果训练时风格顺序是楷书、行书、草书、隶书、篆书这里就不能改成篆书、隶书之类的排列预测索引会错位。稳妥的做法是把它从一个裸列表改成字典映射比如idx_to_style {0: 楷书, 1: 行书, ...}输出时按字典取值避免靠位置索引记错。5.2 验证方法与两个小改造模型部署到 GUI 之前先做一次整体验证用留出来的 X_test 跑一遍预测计算宏平均 F1 和每一类的召回率然后把预测错的图片路径打印出来按风格归类看规律。如果某个风格几乎全错多半是它的训练样本太少或者预处理把它的关键笔画特征抹掉了如果错分集中在两个风格之间说明这两个风格的样本在视觉上确实接近需要考虑增加训练样本或做数据增强。两个小改造值得做。第一是加上置信度显示如果模型支持 predict_proba在 GUI 里把最大概率一起显示出来比如楷书78.3%这样能直观看出模型是笃定还是在勉强猜。第二是批量识别把askopenfilename换成目录选择遍历目录内所有图片逐个调用同一个预处理函数把结果写入 CSV对整理几十上百张样本的图库特别实用。我的习惯是每次拿到这类小工程先按 1_Xy.py、2_fit.py、3_predict.py 的顺序原样跑一遍对照包里带的运行截图确认每个环节正常再动代码。上次改动时我在 GUI 里加了预览图结果忘了把预处理函数统一预测结果全偏到一类排查了一个下午才发现只是训练端除以 255、预测端忘了除。从那以后我每次都会把预处理代码集中到 util.py 里训练、验证、GUI 三处强制走同一个函数。希望帮到你。本文还有配套的精品资源点击获取
返回列表