Browse Source

重构部分代码,添加新功能:自动保存配置、生成md文件等

release/1.2.6
hiroi-sora 3 years ago
parent
commit
9a8dacd161
  1. 633
      main.py

633
main.py

@ -1,6 +1,7 @@
from selectAreaWin import SelectAreaWin # 子窗口
from asset import iconPngBase64, getHelpText # 资源
from asset import IconPngBase64, GetHelpText # 资源
from callingOCR import CallingOCR # OCR调用接口
from config import Config
import os
import time
@ -9,12 +10,12 @@ import threading # 线程
from PIL import Image
import tkinter as tk
import tkinter.filedialog
from tkinter import ttk
from tkinter import Variable, ttk
from windnd import hook_dropfiles # 文件拖拽
from pyperclip import copy as pyperclipCopy # 剪贴板
from webbrowser import open as webOpen # “关于”面板打开项目网址
ProjectVer = "1.1" # 版本号
ProjectVer = "1.2.0" # 版本号
ProjectName = f"Umi-OCR 批量图片转文字 v{ProjectVer}" # 名称
ProjectWeb = "https://github.com/hiroi-sora/Umi-OCR"
@ -22,14 +23,15 @@ ProjectWeb = "https://github.com/hiroi-sora/Umi-OCR"
class Win:
def __init__(self):
self.imgDict = {} # 当前载入的图片信息字典,key为表格组件id。必须为有序字典,python3.6以上默认是。
self.areaInfo = None # 特殊处理区域数据
# self.areaInfo = None # 特殊处理区域数据
self.isRunning = 0 # 0未在运行,1正在运行,2正在停止
def initWin(): # 初始化主窗口
# 1.初始化主窗口
def initWin():
self.win = tk.Tk()
self.win.title(ProjectName)
# 窗口大小与位置
w, h = 360, 520 # 窗口初始大小与最小大小
w, h = 360, 500 # 窗口初始大小与最小大小
ws, hs = self.win.winfo_screenwidth(), self.win.winfo_screenheight()
x, y = round(ws/2 - w/2), round(hs/2 - h/2) # 初始位置,屏幕正中
self.win.minsize(w, h) # 最小大小
@ -37,20 +39,57 @@ class Win:
self.win.protocol("WM_DELETE_WINDOW", self.onClose) # 窗口关闭
# 图标
self.iconImg = tkinter.PhotoImage(
data=iconPngBase64) # 载入图标,base64转
data=IconPngBase64) # 载入图标,base64转
self.win.iconphoto(False, self.iconImg) # 设置窗口图标
initWin()
# 2.初始化变量、配置项
def initVar():
self.cfgVar = { # 设置项tk变量
# 输出文件设置
"isOutputFile": tk.BooleanVar(), # T时输出内容写入本地文件
"outputFilePath": tk.StringVar(), # 输出文件目录
"outputFileName": tk.StringVar(), # 输出文件名称
# 输出格式设置
"isOutputDebug": tk.BooleanVar(), # T时输出调试信息
"isIgnoreNoText": tk.BooleanVar(), # T时忽略(不输出)没有文字的图片信息
"outputStyle": tk.IntVar(), # 1:纯文本,2:Markdown
# 识别器设置
"ocrToolPath": tk.StringVar(), # 识别器路径
"imageSuffix": tk.StringVar(), # 图片后缀
}
Config.initValue(self.cfgVar) # 初始化设置项
# 面板值改变时,更新到配置值,并写入本地
self.saveTimer = None # 计时器,改变面板值一段时间后写入本地
def configSave(): # 保存值的事件
Config.save()
self.saveTimer = None
def valueChange(key): # 值改变的事件
Config.update(key) # 更新配置项
if Config.isSaveItem(key):
if self.saveTimer: # 计时器已存在,则停止已存在的
self.win.after_cancel(self.saveTimer)
self.saveTimer = None
self.saveTimer = self.win.after(200, configSave)
for key in self.cfgVar:
self.cfgVar[key].trace( # 跟踪值改变事件
"w", lambda *e, key=key: valueChange(key))
initVar()
# 3. 初始化组件
def initTop(): # 顶部按钮
tk.Frame(self.win, height=5).pack(side='top')
vFrame1 = tk.Frame(self.win)
vFrame1.pack(side='top', fill="x", padx=5)
fr = tk.Frame(self.win)
fr.pack(side='top', fill="x", padx=5)
# 右侧按钮
self.btnRun = tk.Button(
vFrame1, text='开始任务', width=12, height=2, command=self.run)
fr, text='开始任务', width=12, height=2, command=self.run)
self.btnRun.pack(side='right', padx=5)
# 左侧文本和进度条
vFrame2 = tk.Frame(vFrame1)
vFrame2 = tk.Frame(fr)
vFrame2.pack(side='top', fill='x')
labelUse = tk.Label(vFrame2, text="使用说明",
fg="gray", cursor="hand2")
@ -62,35 +101,33 @@ class Win:
self.labelFractions.pack(side='right')
self.labelTime = tk.Label(vFrame2, text="0s") # 已用时间 12.3s
self.labelTime.pack(side='right', padx=5)
self.progressbar = ttk.Progressbar(vFrame1)
self.progressbar = ttk.Progressbar(fr)
self.progressbar.pack(side='top', padx=5, fill="x")
initTop()
# 初始化选项卡组件
self.notebook = ttk.Notebook(self.win)
self.notebook = ttk.Notebook(self.win) # 初始化选项卡组件
self.notebook.pack(expand=True, fill=tk.BOTH) # 填满父组件
def initTab1(): # 表格卡
tabFrame = tk.Frame(self.notebook) # 选项卡主容器
self.notebook.add(tabFrame, text=f'{"处理列表": ^10s}')
vFrame1 = tk.Frame(tabFrame)
vFrame1.pack(side='top', fill='x', pady=2)
tk.Button(vFrame1, text=' 浏览 ', command=self.openFileWin).pack(
# 顶栏
fr1 = tk.Frame(tabFrame)
fr1.pack(side='top', fill='x', pady=2)
tk.Button(fr1, text=' 浏览 ', command=self.openFileWin).pack(
side='left', padx=5)
tk.Label(vFrame1, text="或直接拖入").pack(side='left')
tk.Button(vFrame1, text='清空表格', width=12, command=self.clearTable).pack(
tk.Label(fr1, text="或直接拖入").pack(side='left')
tk.Button(fr1, text='清空表格', width=12, command=self.clearTable).pack(
side='right')
tk.Button(vFrame1, text='移除选中图片', width=12, command=self.delImgList).pack(
tk.Button(fr1, text='移除选中图片', width=12, command=self.delImgList).pack(
side='right', padx=5)
vFrame2 = tk.Frame(tabFrame)
vFrame2.pack(side='top', fill='both')
columns = ['name', 'time', 'score']
# 表格主体
fr2 = tk.Frame(tabFrame)
fr2.pack(side='top', fill='both')
self.table = ttk.Treeview(
master=vFrame2, # 父容器
master=fr2, # 父容器
height=50, # 表格显示的行数,height行
columns=columns, # 显示的列
columns=['name', 'time', 'score'], # 显示的列
show='headings', # 隐藏首列
)
hook_dropfiles(self.table, func=self.draggedImages) # 注册文件拖入
@ -101,144 +138,180 @@ class Win:
self.table.column('name', minwidth=40)
self.table.column('time', width=20, minwidth=20)
self.table.column('score', width=30, minwidth=30)
scroll = tk.Scrollbar( # 绑定滚动条
vFrame2, orient='vertical', command=self.table.yview)
scroll.pack(side="left", fill='y')
self.table["yscrollcommand"] = scroll.set
vbar = tk.Scrollbar( # 绑定滚动条
fr2, orient='vertical', command=self.table.yview)
vbar.pack(side="left", fill='y')
self.table["yscrollcommand"] = vbar.set
initTab1()
def initTab2(): # 输出卡
self.tabFrameOutput = tk.Frame(self.notebook) # 选项卡主容器
self.notebook.add(self.tabFrameOutput, text=f'{"识别内容": ^10s}')
vFrame1 = tk.Frame(self.tabFrameOutput)
vFrame1.pack(side='top', fill='x', pady=2)
fr1 = tk.Frame(self.tabFrameOutput)
fr1.pack(side='top', fill='x', pady=2)
self.isAutoRoll = tk.IntVar()
self.isAutoRoll.set(1)
tk.Checkbutton(vFrame1, variable=self.isAutoRoll, text="自动滚动到底部").pack(
tk.Checkbutton(fr1, variable=self.isAutoRoll, text="自动滚动到底部").pack(
side='left')
tk.Button(vFrame1, text='清空版面', width=12,
tk.Button(fr1, text='清空版面', width=12,
command=lambda: self.textOutput.delete('1.0', tk.END)).pack(side='right')
tk.Button(vFrame1, text='复制到剪贴板', width=12,
tk.Button(fr1, text='复制到剪贴板', width=12,
command=lambda: pyperclipCopy(self.textOutput.get("1.0", tk.END))).pack(side='right', padx=5)
vFrame2 = tk.Frame(self.tabFrameOutput)
vFrame2.pack(side='top', fill='both')
scroll = tk.Scrollbar( # 滚动条
vFrame2, orient='vertical')
scroll.pack(side="right", fill='y')
self.textOutput = tk.Text(vFrame2, height=500, width=500)
fr2 = tk.Frame(self.tabFrameOutput)
fr2.pack(side='top', fill='both')
vbar = tk.Scrollbar(fr2, orient='vertical') # 滚动条
vbar.pack(side="right", fill='y')
self.textOutput = tk.Text(fr2, height=500, width=500)
self.textOutput.pack(fill='both', side="left")
scroll["command"] = self.textOutput.yview
self.textOutput["yscrollcommand"] = scroll.set
vbar["command"] = self.textOutput.yview
self.textOutput["yscrollcommand"] = vbar.set
initTab2()
def initTab3(): # 设置卡
tabFrame = tk.Frame(self.notebook) # 选项卡主容器
self.notebook.add(tabFrame, text=f'{"设置": ^10s}')
self.labelOptionTips = tk.Label(tabFrame, fg="red") # 提示
self.labelOptionTips.pack()
# 输出文件设置
vFrameOutFile = tk.LabelFrame(tabFrame, text="输出设置")
vFrameOutFile.pack(side='top', fill='x', pady=2, padx=5)
vFrameO1 = tk.Frame(vFrameOutFile)
vFrameO1.pack(side='top', fill='x', pady=2)
self.isOutputFile = tk.IntVar()
self.isOutputFile.set(1)
tk.Checkbutton(vFrameO1, variable=self.isOutputFile, text="将识别内容写入txt文件").pack(
side='left')
self.isOutputDebug = tk.IntVar()
self.isOutputDebug.set(0)
tk.Checkbutton(vFrameO1, variable=self.isOutputDebug, text="输出调试信息").pack(
side='left', padx=23)
tk.Label(vFrameOutFile, fg="gray", text="下面两项为空时,默认输出到第一张图片所在的文件夹").pack(
side='top', padx=5)
vFrameO2 = tk.Frame(vFrameOutFile)
vFrameO2.pack(side='top', fill='x', pady=2)
tk.Label(vFrameO2, text="输出目录: ").pack(
side='left', padx=5)
self.enOutPath = tk.Entry(vFrameO2)
self.enOutPath.pack(side='top', fill="x", padx=5)
vFrameO3 = tk.Frame(vFrameOutFile)
vFrameO3.pack(side='top', fill='x', pady=2)
tk.Label(vFrameO3, text="输出文件名:").pack(side='left', padx=5)
self.enOutName = tk.Entry(vFrameO3)
self.enOutName.pack(side='top', fill="x", padx=5)
# 忽略区域
vFrameArea = tk.LabelFrame(tabFrame, text="忽略图片中某些区域内的文字")
vFrameArea.pack(side='top', fill='x', pady=2, padx=5)
vFrameA1 = tk.Frame(vFrameArea)
vFrameA1.pack(side='top', fill='x', pady=2)
tk.Button(vFrameA1, text='添加忽略区域',
command=self.openSelectArea).pack(side="left", padx=5)
tk.Button(vFrameA1, text='清空所有区域',
command=self.clearArea).pack(side="left")
self.areaLabel = tk.Label(vFrameA1, text="待添加", padx=5)
self.areaLabel.pack(side="right")
self.canvasHeight = 140 # 画板高度不变,宽度根据选区回传数据调整
self.canvas = tk.Canvas(vFrameArea, width=249, height=self.canvasHeight,
bg="black")
self.canvas.pack(side='top')
# 识别器exe与图片后缀设置
vFrameEXE = tk.LabelFrame(tabFrame, text="识别器设置 [切换多国语言和不同格式图片]")
vFrameEXE.pack(side='top', fill='x', pady=2, padx=5)
vFrame5 = tk.Frame(vFrameEXE)
vFrame5.pack(side='top', fill='x', pady=2)
tk.Label(vFrame5, text="识别器路径:").pack(
side='left', padx=5)
self.enEXE = tk.Entry(vFrame5)
self.enEXE.pack(side='top', fill="x", padx=5)
self.enEXE.insert(0, "PaddleOCR-json\PaddleOCR_json.exe")
vFrame4 = tk.Frame(vFrameEXE)
vFrame4.pack(side='top', fill='x', pady=2)
tk.Label(vFrame4, text="图片后缀: ").pack(
side='left', padx=5)
self.enInSuffix = tk.Entry(vFrame4)
self.enInSuffix.pack(side='top', fill="x", padx=5)
self.enInSuffix.insert(
0, ".jpg .jpe .jpeg .jfif .png .webp .bmp .tif .tiff")
# 关于面板,隐藏在设置选项卡默认大小的下方外面
tk.Frame(tabFrame, height=20).pack()
vFrameAbout = tk.LabelFrame(
tabFrame, text="关于")
vFrameAbout.pack(side='top', fill='both', padx=5, ipady=10)
tk.Label(vFrameAbout, image=self.iconImg).pack() # 图标
tk.Label(vFrameAbout, text=ProjectName, fg="gray").pack()
labelWeb = tk.Label(vFrameAbout, text=ProjectWeb, cursor="hand2",
fg="deeppink")
labelWeb.pack() # 文字
labelWeb.bind( # 绑定鼠标左键点击,打开网页
'<Button-1>', self.openProjectWeb)
tk.Frame(tabFrame, height=150).pack()
tk.Label(tabFrame, text="真没有啦!", fg="gray").pack()
def initOptFrame(): # 初始化可滚动画布 及 内嵌框架
optVbar = tk.Scrollbar(
tabFrame, orient="vertical") # 创建滚动条
optVbar.pack(side="right", fill="y")
self.optCanvas = tk.Canvas(
tabFrame, highlightthickness=0) # 创建画布,用于承载框架。highlightthickness取消高亮边框
self.optCanvas.pack(side="left", fill="both",
expand="yes") # 填满父窗口
self.optCanvas["yscrollcommand"] = optVbar.set # 绑定滚动条
optVbar["command"] = self.optCanvas.yview
self.optFrame = tk.Frame(self.optCanvas) # 容纳设置项的框架
self.optFrame.pack()
self.optCanvas.create_window( # 框架塞进画布
(0, 0), window=self.optFrame, anchor="nw")
self.labelOptionTips = tk.Label(self.optFrame, fg="red")
self.labelOptionTips.pack()
initOptFrame()
LabelFramePadY = 3 # 每个区域上下间距
def initArea(): # 忽略区域设置
self.areaLabel = tk.LabelFrame(
self.optFrame, text="忽略图片中某些区域内的文字")
self.areaLabel.pack(side='top', fill='x',
ipady=2, pady=LabelFramePadY, padx=4)
self.areaLabel.grid_columnconfigure(0, minsize=4)
tk.Button(self.areaLabel, text='添加区域',
command=self.openSelectArea).grid(column=1, row=0, sticky="w")
tk.Button(self.areaLabel, text='清空区域',
command=self.clearArea).grid(column=1, row=1, sticky="w")
self.areaLabel.grid_rowconfigure(2, minsize=10)
# tk.Button(self.areaLabel, text='读取预设',
# command=self.showTest).grid(column=1, row=3, sticky="w")
# tk.Button(self.areaLabel, text='保存预设',
# command=self.showTest).grid(column=1, row=4, sticky="w")
self.areaLabel.grid_columnconfigure(2, minsize=4)
self.canvasHeight = 140 # 画板高度不变,宽度根据选区回传数据调整
self.canvas = tk.Canvas(self.areaLabel, width=249, height=self.canvasHeight,
bg="black")
self.canvas.grid(column=3, row=0, rowspan=10)
initArea()
def initOutFile(): # 输出文件设置
frameOutFile = tk.LabelFrame(self.optFrame, text="输出设置")
frameOutFile.pack(side='top', fill='x',
ipady=2, pady=LabelFramePadY, padx=4)
fr1 = tk.Frame(frameOutFile)
fr1.pack(side='top', fill='x', pady=2, padx=5)
tk.Checkbutton(fr1, variable=self.cfgVar["isOutputFile"],
text="将识别内容写入本地文件").grid(column=0, row=0, columnspan=2, sticky="w")
tk.Checkbutton(fr1, text="输出调试信息", variable=self.cfgVar["isOutputDebug"]
).grid(column=0, row=1, sticky="w")
tk.Checkbutton(fr1, text="忽略无文字的图片", variable=self.cfgVar["isIgnoreNoText"],
).grid(column=1, row=1, sticky="w")
tk.Radiobutton(fr1, text='纯文本.txt文件', value=1, variable=self.cfgVar["outputStyle"],
).grid(column=0, row=2, sticky="w")
tk.Radiobutton(fr1, text='Markdown风格.md文件', value=2, variable=self.cfgVar["outputStyle"],
).grid(column=1, row=2, sticky="w")
tk.Label(fr1, fg="gray",
text="下面两项为空时,默认输出到第一张图片所在的文件夹"
).grid(column=0, row=3, columnspan=2, sticky="nsew")
fr2 = tk.Frame(frameOutFile)
fr2.pack(side='top', fill='x', pady=2, padx=5)
tk.Label(fr2, text="输出目录:").grid(column=0, row=3, sticky="w")
enOutPath = tk.Entry(
fr2, textvariable=self.cfgVar["outputFilePath"])
enOutPath.grid(column=1, row=3, sticky="nsew")
fr2.grid_rowconfigure(4, minsize=2) # 第二行拉开间距
tk.Label(fr2, text="输出文件名:").grid(column=0, row=5, sticky="w")
enOutName = tk.Entry(
fr2, textvariable=self.cfgVar["outputFileName"])
enOutName.grid(column=1, row=5, sticky="nsew")
fr2.grid_columnconfigure(1, weight=1) # 第二列自动扩充
initOutFile()
def initOcrUI(): # 识别器exe与图片后缀设置
frameOCR = tk.LabelFrame(
self.optFrame, text="识别器设置 [切换多国语言和不同格式图片]")
frameOCR.pack(side='top', fill='x', ipady=2,
pady=LabelFramePadY, padx=4)
fr1 = tk.Frame(frameOCR)
fr1.pack(side='top', fill='x', pady=2, padx=5)
tk.Label(fr1, text="识别器路径:").grid(column=0, row=0, sticky="w")
enEXE = tk.Entry(fr1, textvariable=self.cfgVar["ocrToolPath"])
enEXE.grid(column=1, row=0, sticky="nsew")
tk.Label(fr1, text="图片后缀:").grid(column=0, row=2, sticky="w")
enInSuffix = tk.Entry(
fr1, textvariable=self.cfgVar["imageSuffix"])
enInSuffix.grid(column=1, row=2, sticky="nsew")
fr1.grid_columnconfigure(1, weight=1)
fr1.grid_rowconfigure(1, minsize=2)
initOcrUI()
def initAbout(): # 关于面板
frameAbout = tk.LabelFrame(
self.optFrame, text="关于")
frameAbout.pack(side='top', fill='x', ipady=2,
pady=LabelFramePadY, padx=4)
tk.Label(frameAbout, image=self.iconImg).pack() # 图标
tk.Label(frameAbout, text=ProjectName, fg="gray").pack()
labelWeb = tk.Label(frameAbout, text=ProjectWeb, cursor="hand2",
fg="deeppink")
labelWeb.pack() # 文字
labelWeb.bind( # 绑定鼠标左键点击,打开网页
'<Button-1>', self.openProjectWeb)
initAbout()
def initOptFrameWH(): # 初始化框架的宽高
self.optFrame.update() # 强制刷新
rH = self.optFrame.winfo_height() # 由组件撑起的 框架高度
self.optCanvas.config(scrollregion=(0, 0, 0, rH)) # 画布内高度为框架高度
self.optFrame.pack_propagate(False) # 禁用框架自动宽高调整
self.optFrame["height"] = rH # 手动还原高度。一次性设置,之后无需再管。
self.optCanvasWidth = 1 # 宽度则是随窗口大小而改变。
def onCanvasResize(event): # 绑定画布大小改变事件
cW = event.width-3 # 当前 画布宽度
if not cW == self.optCanvasWidth: # 若与上次不同:
self.optFrame["width"] = cW # 修改设置页 框架宽度
self.optCanvasWidth = cW
self.optCanvas.bind( # 绑定画布大小改变事件。只有画布组件前台显示时才会触发,减少性能占用
'<Configure>', onCanvasResize)
def onCanvasMouseWheel(event): # 绑定画布中滚轮滚动事件
self.optCanvas.yview_scroll(
1 if event.delta < 0 else -1, "units")
self.optCanvas.bind_all("<MouseWheel>", onCanvasMouseWheel)
initOptFrameWH()
# self.notebook.select(tabFrame)
initTab3()
self.win.mainloop()
def openSelectArea(self): # 打开选择区域
if not self.isRunning == 0:
return
defaultPath = ""
if self.imgDict:
defaultPath = next(iter(self.imgDict.values()))["path"]
self.win.attributes("-disabled", 1) # 禁用父窗口
SelectAreaWin(self.closeSelectArea, defaultPath)
def closeSelectArea(self, info=None): # 关闭选择区域,获取选择区域数据
self.win.attributes("-disabled", 0) # 启用父窗口
if not info:
return
self.areaInfo = info
self.areaLabel["text"] = f"生效分辨率:{info[0][0]}x{info[0][1]}"
self.canvas.delete(tk.ALL) # 清除画布
scale = self.canvasHeight / info[0][1] # 显示缩放比例
width = int(self.canvasHeight * (info[0][0] / info[0][1]))
self.canvas["width"] = width
areaColor = ["red", "green", "gold1"]
for i in range(3):
for a in info[1][i]:
x0, y0 = a[0][0]*scale, a[0][1]*scale,
x1, y1 = a[1][0]*scale, a[1][1]*scale,
self.canvas.create_rectangle(
x0, y0, x1, y1, fill=areaColor[i]) # 绘制新图
# 加载图片 ===============================================
def draggedImages(self, paths): # 拖入图片
if not self.isRunning == 0:
@ -251,51 +324,87 @@ class Win:
def openFileWin(self): # 打开选择文件窗
if not self.isRunning == 0:
return
suf = self.enInSuffix.get() # 许可后缀
suf = Config.get("imageSuffix") # 许可后缀
paths = tk.filedialog.askopenfilenames(
title='选择图片', filetypes=[('图片', suf)])
self.addImagesList(paths)
def addImagesList(self, paths):
suf = self.enInSuffix.get().split() # 许可后缀
def addImagesList(self, paths): # 添加一批图片列表
suf = Config.get("imageSuffix").split() # 许可后缀列表
def addImage(path): # 添加一张图片。传入路径,许可后缀。
path = path.replace("/", "\\") # 浏览是左斜杠,拖入是右斜杠;需要统一
if suf and os.path.splitext(path)[1].lower() not in suf:
return # 需要判别许可后缀 且 文件后缀不在许可内,不添加。
# 检测是否重复
for key, value in self.imgDict.items():
if value["path"] == path:
return
# 检测是否可用
try:
s = Image.open(path).size
except Exception as e:
tk.messagebox.showwarning(
"遇到了一点小问题", f"图片载入失败。图片地址:\n{path}\n\n错误信息:\n{e}")
return
# 计算路径
p = os.path.abspath(os.path.join(path, os.pardir)) # 父文件夹
if not Config.get("outputFilePath"): # 初始化输出路径
Config.set("outputFilePath", p)
if not Config.get("outputFileName"): # 初始化输出文件名
n = f"[转文字]_{os.path.basename(p)}"
Config.set("outputFileName", n)
# 加入待处理列表
name = os.path.basename(path) # 带后缀的文件名
tableInfo = (name, "", "")
id = self.table.insert('', 'end', values=tableInfo) # 添加到表格组件中
dictInfo = {"name": name, "path": path, "size": s}
self.imgDict[id] = (dictInfo) # 添加到字典中
for path in paths: # 遍历拖入的所有路径
if os.path.isdir(path): # 若是目录
subFiles = os.listdir(path) # 遍历子文件
for s in subFiles:
self.addImage(path+"\\"+s, suf) # 添加
addImage(path+"\\"+s) # 添加
elif os.path.isfile(path): # 若是文件:
self.addImage(path, suf) # 直接添加
addImage(path) # 直接添加
def addImage(self, path, okSuf=None): # 添加一张图片。传入路径,许可后缀。
path = path.replace("/", "\\") # 浏览是左斜杠,拖入是右斜杠;需要统一
if okSuf and os.path.splitext(path)[1].lower() not in okSuf:
return # 需要判别许可后缀 且 文件后缀不在许可内,不添加。
# 检测是否重复
for key, value in self.imgDict.items():
if value["path"] == path:
return
# 检测是否可用
try:
s = Image.open(path).size
except Exception as e:
tk.messagebox.showwarning(
"遇到了一点小问题", f"图片载入失败。图片地址:\n{path}\n\n错误信息:\n{e}")
# 忽略区域 ===============================================
def openSelectArea(self): # 打开选择区域
if not self.isRunning == 0:
return
# 计算路径
p = os.path.abspath(os.path.join(path, os.pardir)) # 父文件夹
if not self.enOutPath.get(): # 初始化输出路径
self.enOutPath.delete('0', tk.END)
self.enOutPath.insert(0, p)
if not self.enOutName.get(): # 初始化输出文件名
n = f"[转文字]_{os.path.basename(p)}.txt"
self.enOutName.delete('0', tk.END)
self.enOutName.insert(0, n)
# 加入待处理列表
name = os.path.basename(path) # 带后缀的文件名
tableInfo = (name, "", "")
id = self.table.insert('', 'end', values=tableInfo) # 添加到表格组件中
dictInfo = {"name": name, "path": path, "size": s}
self.imgDict[id] = (dictInfo) # 添加到字典中
defaultPath = ""
if self.imgDict:
defaultPath = next(iter(self.imgDict.values()))["path"]
self.win.attributes("-disabled", 1) # 禁用父窗口
SelectAreaWin(self.closeSelectArea, defaultPath)
def closeSelectArea(self): # 关闭选择区域,获取选择区域数据
self.win.attributes("-disabled", 0) # 启用父窗口
area = Config.get("ignoreArea")
if not area:
return
self.areaLabel["text"] = f"忽略区域 生效分辨率:{area['size'][0]}x{area['size'][1]}"
self.canvas.delete(tk.ALL) # 清除画布
scale = self.canvasHeight / area['size'][1] # 显示缩放比例
width = int(self.canvasHeight * (area['size'][0] / area['size'][1]))
self.canvas["width"] = width
areaColor = ["red", "green", "gold1"]
for i in range(3):
for a in area['area'][i]:
x0, y0 = a[0][0]*scale, a[0][1]*scale,
x1, y1 = a[1][0]*scale, a[1][1]*scale,
self.canvas.create_rectangle(
x0, y0, x1, y1, fill=areaColor[i]) # 绘制新图
def clearArea(self): # 清空忽略区域
Config.set("ignoreArea", None)
self.areaLabel["text"] = "忽略图片中某些区域内的文字"
self.canvas.delete(tk.ALL) # 清除画布
self.canvas["width"] = int(self.canvasHeight * (16/9))
# 表格操作 ===============================================
def clearTable(self): # 清空表格
if not self.isRunning == 0:
@ -304,19 +413,13 @@ class Win:
self.labelPercentage["text"] = "0%"
self.labelFractions["text"] = "0/0"
self.labelTime["text"] = "0s"
self.enOutPath.delete('0', tk.END)
self.enOutName.delete('0', tk.END)
Config.set("outputFilePath", "")
Config.set("outputFileName", "")
self.imgDict = {}
chi = self.table.get_children()
for i in chi:
self.table.delete(i) # 表格组件移除
def clearArea(self): # 清空忽略区域
self.areaInfo = None
self.areaLabel["text"] = "待添加"
self.canvas.delete(tk.ALL) # 清除画布
self.canvas["width"] = int(self.canvasHeight * (16/9))
def delImgList(self): # 图片列表中删除选中
if not self.isRunning == 0:
return
@ -344,14 +447,16 @@ class Win:
if not self.imgDict:
return
# 检测识别器存在
exePath = self.enEXE.get()
if not os.path.exists(exePath):
ocrToolPath = Config.get("ocrToolPath")
if not os.path.exists(ocrToolPath):
tk.messagebox.showerror(
'遇到了一点小问题', f'未在以下地址找到识别器!\n{exePath}')
'遇到了一点小问题', f'未在以下地址找到识别器!\n{ocrToolPath}')
return
# 创建输出文件
if self.isOutputFile.get() == 1:
outPath = self.enOutPath.get() + "\\" + self.enOutName.get()
if Config.get("isOutputFile"):
suffix = ".txt" if Config.get("outputStyle") == 1 else ".md"
outPath = Config.get("outputFilePath") + \
"\\" + Config.get("outputFileName")+suffix
try:
if os.path.exists(outPath): # 文件存在
os.remove(outPath) # 删除文件
@ -381,19 +486,48 @@ class Win:
async def run_(self): # 异步,执行任务
self.labelPercentage["text"] = "初始化"
isOutputFile = self.isOutputFile.get() # 是否输出文件
isOutputDebug = self.isOutputDebug.get() # 是否输出调试
areaInfo = self.areaInfo
isOutputFile = Config.get("isOutputFile") # 是否输出文件
isOutputDebug = Config.get("isOutputDebug") # 是否输出调试
isIgnoreNoText = Config.get("isIgnoreNoText") # 是否忽略无字图片
outputStyle = Config.get("outputStyle") # 输出风格
areaInfo = Config.get("ignoreArea")
if isOutputFile:
outPath = self.enOutPath.get() + "\\" + self.enOutName.get() # 输出文件路径
suffix = ".txt" if outputStyle == 1 else ".md"
outPath = Config.get("outputFilePath") + \
"\\" + Config.get("outputFileName")+suffix
def output(outStr, type_): # 输出字符串
"""
debug
text
name
none
"""
# 写入输出面板,无需格式
self.textOutput.insert(tk.END, f"\n{outStr}\n")
if self.isAutoRoll.get(): # 需要自动滚动
self.textOutput.see(tk.END)
def output(str_): # 输出字符串
# 写入本地文件,按照格式
if isOutputFile:
if outputStyle == 1: # 纯文本风格
if type_ == "debug":
outStr = f"```\n{outStr}```\n"
elif type_ == "name":
outStr = f"\n\n≦ {outStr} ≧\n"
elif outputStyle == 2: # markdown风格
if type_ == "debug":
outStr = f"```\n{outStr}```\n"
elif type_ == "text":
outList = outStr.split("\n")
outStr = ""
for i in outList:
outStr += f"> {i} \n"
elif type_ == "name":
path = outStr.replace(" ", "%20")
outStr = f"---\n![{outStr}]({path})\n[{outStr}]({path})\n"
with open(outPath, "a", encoding='utf-8') as f: # 追加写入本地文件
f.write(str_)
self.textOutput.insert(tk.END, str_) # 1写入输出面板
if self.isAutoRoll.get(): # 需要自动滚动
self.textOutput.see(tk.END)
f.write(outStr)
def close(): # 关闭所有异步相关的东西
del self.ocr # 关闭OCR进程
@ -401,31 +535,35 @@ class Win:
self.setRunning(0)
self.labelPercentage["text"] = "已终止"
def getText(oget, img): # 分析一张图转出的文字
def analyzeText(oget, img): # 分析一张图转出的文字
def isInBox(aPos0, aPos1, bPos0, bPos1): # 检测框左上、右下角,待测者左上、右下角
return bPos0[0] >= aPos0[0] and bPos0[1] >= aPos0[1] and bPos1[0] <= aPos1[0] and bPos1[1] <= aPos1[1]
def isIden(): # 是否识别区域模式
if areaInfo[1][1]: # 需要检测
if areaInfo["area"][1]: # 需要检测
for o in oget: # 遍历每一个文字块
for a in areaInfo[1][1]: # 遍历每一个检测块
for a in areaInfo["area"][1]: # 遍历每一个检测块
if isInBox(a[0], a[1], (o["box"][0], o["box"][1]), (o["box"][4], o["box"][5])):
return True
text = ""
textDebug = "" # 调试信息
score = 0 # 平均置信度
scoreNum = 0
if not areaInfo or not areaInfo[0][0] == img["size"][0] or not areaInfo[0][1] == img["size"][1]:
# 无需忽略区域
if not areaInfo or not areaInfo["size"][0] == img["size"][0] or not areaInfo["size"][1] == img["size"][1]:
for i in oget:
text += i["text"]+"\n"
score += i["score"]
scoreNum += 1
# 判断,是忽略模式2
# 忽略模式2
elif isIden():
fn = 0 # 记录忽略的数量
for o in oget:
flag = True
for a in areaInfo[1][2]: # 遍历每一个检测块
for a in areaInfo["area"][2]: # 遍历每一个检测块
if isInBox(a[0], a[1], (o["box"][0], o["box"][1]), (o["box"][4], o["box"][5])):
flag = False # 踩到任何一个块,GG
break
@ -436,12 +574,14 @@ class Win:
else:
fn += 1
if isOutputDebug:
textDebug = f"〔忽略模式2:忽略{fn}条〕\n"
else: # 否则,忽略模式1
textDebug = f"忽略模式2:忽略{fn}条\n"
# 忽略模式1
else:
fn = 0 # 记录忽略的数量
for o in oget:
flag = True
for a in areaInfo[1][0]: # 遍历每一个检测块
for a in areaInfo["area"][0]: # 遍历每一个检测块
if isInBox(a[0], a[1], (o["box"][0], o["box"][1]), (o["box"][4], o["box"][5])):
flag = False # 踩到任何一个块,GG
break
@ -452,30 +592,29 @@ class Win:
else:
fn += 1
if isOutputDebug:
textDebug = f"忽略模式1:忽略{fn}条\n"
if text and not scoreNum == 0:
text = textDebug+text
textDebug = f"忽略模式1:忽略{fn}条\n"
if text and not scoreNum == 0: # 区域内有文本,计算置信度
score /= scoreNum
score = str(score)
score = str(score) # 转文本
else:
text = textDebug+"所有文字在忽略范围内\n"
score = "全部忽略"
return text, score
score = "1" # 区域内没有文本,置信度为1
return text, textDebug, score
# 开始
startStr = f"\n任务开始时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}\n"
startStr = f"任务开始时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}\n"
output(startStr, "text")
if isOutputDebug:
if areaInfo:
startStr += f"忽略区域:开启\n适用分辨率:{areaInfo[0]}\n"
startStr += f"忽略区域1:{areaInfo[1][0]}\n"
startStr += f"识别区域:{areaInfo[1][1]}\n"
startStr += f"忽略区域2:{areaInfo[1][2]}\n"
startStr = f'忽略区域:开启\n适用分辨率:{areaInfo["size"]}\n'
startStr += f'忽略区域1:{areaInfo["area"][0]}\n'
startStr += f'识别区域:{areaInfo["area"][1]}\n'
startStr += f'忽略区域2:{areaInfo["area"][2]}\n'
else:
startStr += f"忽略区域:关闭\n"
output(startStr)
startStr = f"忽略区域:关闭\n"
output(startStr, "debug")
# 创建OCR进程
exe = self.enEXE.get()
self.ocr = CallingOCR(exe)
self.ocr = CallingOCR(Config.get("ocrToolPath"))
# 初始化UI
for key in self.imgDict.keys(): # 清空表格参数
self.table.set(key, column='time', value="")
@ -512,9 +651,11 @@ class Win:
value=needTimeStr[:4]) # 时间写入表格
# 分析数据
dataStr = ""
textDebug = ""
if oget['code'] == 100: # 成功
numOK += 1
dataStr, score = getText(oget['data'], value) # 获取文字
dataStr, textDebug, score = analyzeText(
oget['data'], value) # 获取文字
elif oget['code'] == 101: # 无文字
numNON += 1
score = "无文字"
@ -523,28 +664,34 @@ class Win:
dataStr = "识别失败" # 不管开不开输出调试,都要输出报错
dataStr += f",错误码:{oget['code']}\n错误信息:{str(oget['data'])}\n"
score = "失败"
writeStr = f'\n\n≦ {value["name"]} ≧\n'
if isOutputDebug: # 输出调试
writeStr += f"〔识别耗时:{needTimeStr}s 置信度:{score}〕\n"
writeStr += dataStr # 输出内容
self.table.set(key, column='score',
value=score[:4]) # 写入表格
output(writeStr)
# 写入表格
self.table.set(key, column='score', value=score[:4])
# 格式化输出
if isIgnoreNoText and not dataStr:
continue # 忽略无字图片
output(value["name"], "name")
if isOutputDebug:
output(
f"识别耗时:{needTimeStr}s 置信度:{score}\n{textDebug}", "debug")
output(dataStr, "text")
except Exception as e:
tk.messagebox.showerror(
'遇到了亿点小问题', f'图片识别异常:\n{value["name"]}\n异常信息:\n{e}')
# print(f'出问题了:{value["name"]}\n{e}')
# 结束
endTime = time.time()
endStr = f"\n\n\n任务结束时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(endTime))}\n"
output("\n\n---\n", "none")
endStr = f"任务结束时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(endTime))}\n"
output(endStr, "text")
if isOutputDebug:
endStr += f"任务耗时(秒): {endTime-startTime}\n"
endStr += f"单张平均耗时: {(endTime-startTime)/allNum}\n"
endStr = f"任务耗时(秒): {endTime-startTime}\n"
if not allNum == 0:
endStr += f"单张平均耗时: {(endTime-startTime)/allNum}\n"
endStr += f"共计图片数量: {numOK+numNON+numERR}\n"
endStr += f"识别正常 的图片数量: {numOK}\n"
endStr += f"未识别到文字 的图片数量:{numNON}\n"
endStr += f"识别失败 的图片数量: {numERR}\n\n"
output(endStr)
endStr += f"识别失败 的图片数量: {numERR}\n"
output(endStr, "debug")
close() # 完成后关闭
self.labelPercentage["text"] = "完成!"
@ -559,7 +706,7 @@ class Win:
if not tkinter.messagebox.askokcancel('提示', '将清空输出面板。要继续吗?'):
return
self.textOutput.delete('1.0', tk.END)
self.textOutput.insert(tk.END, getHelpText(ProjectWeb))
self.textOutput.insert(tk.END, GetHelpText(ProjectWeb))
def openProjectWeb(self, e=None): # 打开项目网页
webOpen(ProjectWeb)
@ -579,6 +726,10 @@ class Win:
else:
self.win.after(50, self.waitClose) # 等待关闭,50ms轮询一次是否已结束子进程
# def showTest(self):
# tk.messagebox.showwarning(
# "警告", "此功能尚未完工。")
if __name__ == "__main__":
Win()

Loading…
Cancel
Save