ARTICLE DETAIL

资讯详情

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

Python书法字体风格识别器:HOG+GLCM特征提取与Tkinter GUI实战

Python书法字体风格识别器:HOG+GLCM特征提取与Tkinter GUI实战 简介这是一份面向Python初学者与图像识别爱好者的书法字体风格识别器源码通过输入书法图片即可自动判别其所属字体风格并借助Tkinter搭建了可视化操作界面适合作为机器学习分类入门练手项目或课程设计参考。压缩包共16个文件约716KB包含5个Python脚本、1个yaml配置文件、1个md说明文档及8张png示例截图脚本分别负责图像预处理与数据集生成、多模型评估选优、GUI预测交互以及通用工具函数封装配置文件则集中管理字体风格列表与图片缩放尺寸等参数。目前已有182人学习下载。读者可获得一套结构清晰、可直接运行的完整项目代码理解从数据准备、模型训练到界面预测的全流程并参考示例截图快速验证识别效果适合用于二次开发或教学演示。1. 从一张字帖照片到风格标签书法字体风格识别器到底在识别什么很多人第一次听到「书法字体风格识别器」脑子里浮现的是 OCR——把字认出来。但真正做过就知道这两件事的目标完全不同OCR 关心「写的是什么字」风格识别关心「这个字是谁的笔法、属于哪一路风格」。你拍一张颜体楷书和一张柳体楷书OCR 可能都输出同一个汉字但风格识别器要给出「颜体」「柳体」这样的判断。这个项目用 Python 把整条链路串起来前端用 Tkinter 做 GUI用户点按钮选图片后台跑特征提取加分类模型最后把风格标签显示在窗口里。它适合两类人一类是想找一个完整可跑的 Python GUI 图像分类练手项目的新手另一类是想把书法数字化、做字帖归档或教学辅助工具的从业者。核心难点不在 GUI而在「风格」这个抽象概念怎么变成模型能吃的数值——这决定了整个方案能不能落地。2. 风格识别的技术选型为什么不用 OCR 那套思路2.1 风格识别与字符识别的本质差异字符识别做的是「归一化到语义」不管你是手写还是印刷最后都映射到有限的字符集。风格识别做的是「保留书写形态差异」同一汉字在不同书体下的笔画粗细、起收笔角度、结构松紧、墨色浓淡恰恰是要放大的信号。如果你直接拿一个训练好的 OCR 模型做特征提取它学到的特征是为了「忽略风格、锁定字符」的对风格分类反而有害。常见做法是两条路一条是手工特征加传统分类器适合小样本、可解释另一条是 CNN 迁移学习适合数据量稍大、追求精度。这个项目标题里没有指定模型我一般会先用手工特征跑通全流程再决定要不要上深度学习。2.2 手工特征方案HOG 灰度共生矩阵 SVM手工特征的核心思路是把「风格」拆成可量化的纹理和形状描述。HOG方向梯度直方图捕捉笔画边缘的方向分布对起收笔的锋芒和转折的方圆周正敏感GLCM灰度共生矩阵捕捉墨色在空间上的粗细变化能区分「丰腴」和「瘦硬」。两者拼接后送进 SVM在小样本下往往比直接 fine-tune CNN 更稳。下面是一个可复现的特征提取函数import cv2 import numpy as np from skimage.feature import hog, graycomatrix, graycoprops def extract_style_features(image_path, resize(256, 256)): # 读取并统一尺寸书法图片长宽比差异大先缩放到固定尺寸 img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(f无法读取图片: {image_path}) img cv2.resize(img, resize, interpolationcv2.INTER_AREA) # 自适应二值化应对不同拍摄光照 binary cv2.adaptiveThreshold( img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 15, 8 ) # HOG 特征orientations 取 9 是经验值pixels_per_cell 取 16x16 兼顾细节与速度 hog_feat hog( binary, orientations9, pixels_per_cell(16, 16), cells_per_block(2, 2), block_normL2-Hys, visualizeFalse, feature_vectorTrue ) # GLCM 特征在 0/45/90/135 四个方向上统计纹理 glcm graycomatrix(img, distances[1, 3], angles[0, np.pi/4, np.pi/2, 3*np.pi/4], levels256, symmetricTrue, normedTrue) contrast graycoprops(glcm, contrast).mean() homogeneity graycoprops(glcm, homogeneity).mean() energy graycoprops(glcm, energy).mean() correlation graycoprops(glcm, correlation).mean() glcm_feat np.array([contrast, homogeneity, energy, correlation]) return np.concatenate([hog_feat, glcm_feat])这段代码里几个参数值得说清楚。adaptiveThreshold的 block size 取 15 是奇数太小会把笔画内部也二值化出噪点太大则对细笔画不敏感THRESH_BINARY_INV让笔画变成白色前景符合 HOG 对边缘的期望。HOG 的pixels_per_cell(16,16)意味着每个 cell 覆盖 16 像素对 256×256 的图来说一个笔画宽度通常在 8 到 20 像素之间这个粒度刚好能捕捉到笔画边缘的方向变化。GLCM 的distances[1,3]同时看相邻像素和隔三个像素的共生关系前者反映边缘锐利度后者反映墨色扩散范围。如果你发现分类器对「楷书 vs 行书」混淆严重优先调orientations到 12 或把pixels_per_cell降到 8让方向分辨率更细。2.3 迁移学习方案用预训练 CNN 做特征抽取当你有几百张以上标注图片时手工特征的上限就出现了。常见做法是拿一个在 ImageNet 上预训练的 ResNet18 或 MobileNetV3去掉最后的全连接层把倒数第二层的输出当特征向量再训练一个线性分类器。这样做的好处是不需要从头训练小样本下也能收敛。关键操作是输入图片要转成三通道、按 ImageNet 的均值和方差归一化并且冻结前面的卷积层只训练最后的分类头。如果你直接 fine-tune 全部层在几百张图上几乎必然过拟合验证集准确率会剧烈震荡。import torch import torchvision.models as models import torchvision.transforms as transforms from PIL import Image # 加载预训练模型去掉分类头 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc torch.nn.Identity() # 输出 512 维特征 model.eval() preprocess transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def cnn_feature(image_path): img Image.open(image_path).convert(RGB) tensor preprocess(img).unsqueeze(0) with torch.no_grad(): feat model(tensor) return feat.squeeze().numpy()model.fc torch.nn.Identity()是把 ResNet 最后的全连接层替换成恒等映射这样输出就是 512 维特征向量而不是 1000 类 logits。Normalize的三个均值和方差是 ImageNet 统计出来的不按这个归一化预训练权重的分布就对不上特征质量会明显下降。torch.no_grad()在推理时省显存也提速。拿到特征后用 sklearn 的LinearSVC或LogisticRegression训练分类器即可通常 20 到 50 张每类就能看到可用的区分度。3. Tkinter GUI 集成把识别器包成能双击运行的工具3.1 界面布局与事件绑定Tkinter 的定位是「够用就好」不要指望它做出花哨的界面。这个工具需要的最小控件是一个按钮选图片、一个 Label 显示缩略图、一个按钮触发识别、一个 Label 显示结果。布局用grid比pack更可控因为你要把图片和文字对齐。下面是一个可直接跑的 GUI 骨架import tkinter as tk from tkinter import filedialog, messagebox from PIL import Image, ImageTk import numpy as np class CalligraphyApp: def __init__(self, root): self.root root self.root.title(书法字体风格识别器) self.root.geometry(520x480) self.image_path None self.classifier None # 训练好的模型启动时加载 # 选图按钮 self.btn_open tk.Button(root, text选择书法图片, commandself.open_image) self.btn_open.grid(row0, column0, padx10, pady10) # 识别按钮 self.btn_predict tk.Button(root, text识别风格, commandself.predict, statetk.DISABLED) self.btn_predict.grid(row0, column1, padx10, pady10) # 图片显示区 self.img_label tk.Label(root, text尚未选择图片, bg#f0f0f0, width50, height15) self.img_label.grid(row1, column0, columnspan2, padx10, pady10) # 结果显示 self.result_var tk.StringVar(value等待识别...) self.result_label tk.Label(root, textvariableself.result_var, font(Microsoft YaHei, 14)) self.result_label.grid(row2, column0, columnspan2, pady10) def open_image(self): path filedialog.askopenfilename( filetypes[(图片文件, *.jpg *.jpeg *.png *.bmp)] ) if not path: return self.image_path path img Image.open(path) img.thumbnail((400, 300)) self.photo ImageTk.PhotoImage(img) self.img_label.config(imageself.photo, text) self.btn_predict.config(statetk.NORMAL) self.result_var.set(图片已加载点击识别) def predict(self): if self.image_path is None: messagebox.showwarning(提示, 请先选择图片) return # 这里调用第 2 章的特征提取和分类器 feat extract_style_features(self.image_path).reshape(1, -1) pred self.classifier.predict(feat)[0] self.result_var.set(f识别结果{pred}) if __name__ __main__: root tk.Tk() app CalligraphyApp(root) root.mainloop()ImageTk.PhotoImage必须保持引用否则图片会被垃圾回收导致显示空白——这是 Tkinter 最经典的坑之一我把它存成self.photo就是为了这个。btn_predict初始设为DISABLED选图后才启用避免用户没选图就点识别。filedialog的filetypes限制常见图片格式减少用户选错文件的情况。3.2 把模型加载和预测接进 GUI 线程Tkinter 是单线程事件循环如果你在按钮回调里做耗时的特征提取或模型推理界面会卡死。常见做法是用threading.Thread把预测放到后台线程再用root.after把结果传回主线程更新 UI。不要直接在子线程里操作 Tkinter 控件否则会随机崩溃。下面是一个安全的异步预测封装import threading def predict_async(self): self.result_var.set(识别中...) self.btn_predict.config(statetk.DISABLED) def worker(): try: feat extract_style_features(self.image_path).reshape(1, -1) pred self.classifier.predict(feat)[0] # 用 after 回到主线程更新 UI self.root.after(0, lambda: self._update_result(pred)) except Exception as e: self.root.after(0, lambda: self._update_result(f出错{e})) threading.Thread(targetworker, daemonTrue).start() def _update_result(self, text): self.result_var.set(f识别结果{text}) self.btn_predict.config(statetk.NORMAL)daemonTrue保证主窗口关闭时后台线程不会阻止进程退出。root.after(0, ...)是 Tkinter 提供的线程安全调度方式它把回调排进主事件循环等价于「尽快在主线程执行」。如果你不做这一步直接在 worker 里改result_var在 Windows 上可能看起来正常在 macOS 或 Linux 上就会报RuntimeError: main thread is not in main loop。4. 避坑与排查书法风格识别落地时最容易翻车的五件事4.1 图片背景干扰导致特征完全跑偏现象同一幅字换个背景拍识别结果就变了。原因HOG 和 GLCM 都在整张图上算背景的纹理和边缘被当成笔画特征。解决在特征提取前加一步前景分割用 Otsu 阈值或cv2.morphologyEx做闭运算填掉笔画内部空洞再把背景置白。如果背景复杂先手动裁剪到字帖区域再送模型。4.2 类别不平衡让分类器偏向多数类现象颜体样本有 200 张瘦金体只有 30 张结果所有输入都被判成颜体。原因SVM 和逻辑回归默认按样本数加权多数类主导决策边界。解决SVC(class_weightbalanced)或对少数类做数据增强旋转 ±5 度、轻微弹性形变但不要用翻转书法翻转后笔画方向就错了。4.3 训练集和测试集按图片随机划分导致虚高准确率现象验证集准确率 95%实际用新拍的照片测试只有 60%。原因同一幅字帖的不同裁剪块被分到了训练和测试两边模型记住了这张纸的纹理而不是风格。解决按「字帖来源」划分同一本帖的所有图片只能出现在训练集或测试集之一。这个坑我踩过血泪经验是先按作者分组再在组内划分。4.4 Tkinter 打包成 exe 后找不到模型文件现象源码运行正常PyInstaller 打包后双击报「模型文件不存在」。原因打包后工作目录变了相对路径失效。解决用sys._MEIPASS判断是否在打包环境把模型文件路径统一成绝对路径。或者更简单把模型序列化成 pickle 后直接嵌进 Python 模块里用import加载。4.5 灰度化丢失墨色浓淡信息现象对「浓墨」和「淡墨」风格区分不出来。原因cv2.imread(..., IMREAD_GRAYSCALE)把三通道压成单通道虽然保留了亮度但如果你后续做了直方图均衡化浓淡差异会被拉平。解决保留原始灰度图做 GLCM不要做全局直方图均衡如果光照不均用 CLAHE 做局部增强clipLimit设 2.0 左右别设太大。5. 让识别器真正好用从单张预测到批量归档的进阶技巧单张识别跑通之后真正有价值的是批量处理。我一般会加一个「选择文件夹」的入口遍历目录下所有图片输出一个 CSV每行是文件名和预测风格再按风格自动分到子文件夹。这样一套字帖扫下来几分钟就能归档完。实现上注意两点一是用concurrent.futures.ThreadPoolExecutor并行提取特征但线程数不要超过 CPU 核数否则 IO 和计算互相抢资源二是预测结果要带置信度SVC用decision_function的 max 值LogisticRegression用predict_proba的 max 值低于阈值的标成「待人工确认」不要硬判。验证方法上我习惯留一个「混淆矩阵 每类 F1」的输出。书法风格之间本身就有过渡比如行楷介于行书和楷书之间混淆矩阵能告诉你哪些类容易混。如果「行书 vs 行楷」的 F1 都低于 0.7说明特征维度不够优先加 HOG 的orientations或换 CNN 特征而不是调 SVM 的 C 值——C 值只影响边界松紧不解决特征不可分的问题。最后一个具体技巧GUI 里加一个「保存特征」按钮把当前图片的特征向量存成 npy 文件。这样你后续想换分类器、加新类别不用重新提取所有图片的特征直接加载 npy 重训即可。这个习惯帮我省过很多次重复计算的时间。做这类工具我的教训是先把特征提取和分类器解耦GUI 只做壳模型和特征单独可测否则一旦界面出问题你连模型对不对都验证不了。希望帮到你。本文还有配套的精品资源点击获取
返回列表