Files
recommend/gui/ocr_calorie_gui.py

638 lines
23 KiB
Python

"""
OCR热量识别GUI界面
提供图片上传、OCR识别、结果验证和修正功能
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from PIL import Image, ImageTk
import threading
from typing import Dict, List, Optional, Any
from pathlib import Path
import json
from datetime import datetime
from core.base import UserData, CalorieInfo, FoodRecognitionResult
class OCRCalorieGUI:
"""OCR热量识别GUI界面"""
def __init__(self, parent_window, app_core):
self.parent_window = parent_window
self.app_core = app_core
self.current_image_path = None
self.current_recognition_result = None
self.user_corrections = {}
# 创建主框架
self.main_frame = ttk.Frame(parent_window)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建界面
self._create_widgets()
self._setup_layout()
self._bind_events()
def _create_widgets(self):
"""创建界面组件"""
# 标题
self.title_label = ttk.Label(
self.main_frame,
text="📷 图片OCR热量识别",
font=("Arial", 18, "bold"),
foreground="#2c3e50"
)
# 图片上传区域
self.image_frame = ttk.LabelFrame(
self.main_frame,
text="📸 图片上传",
padding=15,
relief="solid",
borderwidth=1
)
self.upload_button = ttk.Button(
self.image_frame,
text="📁 选择图片",
command=self._select_image,
style="Accent.TButton"
)
self.image_label = ttk.Label(
self.image_frame,
text="请选择包含食物信息的图片",
background="#f8f9fa",
relief="solid",
borderwidth=1,
width=50,
height=15,
anchor="center"
)
# 识别控制区域
self.control_frame = ttk.LabelFrame(
self.main_frame,
text="⚙️ 识别控制",
padding=15,
relief="solid",
borderwidth=1
)
self.recognize_button = ttk.Button(
self.control_frame,
text="🚀 开始识别",
command=self._start_recognition,
state=tk.DISABLED,
style="Accent.TButton"
)
self.progress_bar = ttk.Progressbar(
self.control_frame,
mode='indeterminate',
style="Accent.TProgressbar"
)
self.status_label = ttk.Label(
self.control_frame,
text="✅ 准备就绪",
foreground="#27ae60"
)
# 识别结果区域
self.result_frame = ttk.LabelFrame(self.main_frame, text="识别结果", padding=10)
# 创建结果表格
self.result_tree = ttk.Treeview(
self.result_frame,
columns=('food_name', 'calories', 'confidence', 'source'),
show='headings',
height=8
)
# 设置列标题
self.result_tree.heading('food_name', text='食物名称')
self.result_tree.heading('calories', text='热量(卡路里)')
self.result_tree.heading('confidence', text='置信度')
self.result_tree.heading('source', text='来源')
# 设置列宽
self.result_tree.column('food_name', width=150)
self.result_tree.column('calories', width=100)
self.result_tree.column('confidence', width=80)
self.result_tree.column('source', width=100)
# 结果操作按钮
self.result_button_frame = ttk.Frame(self.result_frame)
self.edit_button = ttk.Button(
self.result_button_frame,
text="编辑结果",
command=self._edit_result,
state=tk.DISABLED
)
self.confirm_button = ttk.Button(
self.result_button_frame,
text="确认结果",
command=self._confirm_result,
state=tk.DISABLED
)
self.clear_button = ttk.Button(
self.result_button_frame,
text="清空结果",
command=self._clear_results
)
# 详细信息区域
self.detail_frame = ttk.LabelFrame(self.main_frame, text="详细信息", padding=10)
self.detail_text = scrolledtext.ScrolledText(
self.detail_frame,
height=8,
width=60
)
# 建议区域
self.suggestion_frame = ttk.LabelFrame(self.main_frame, text="建议", padding=10)
self.suggestion_text = scrolledtext.ScrolledText(
self.suggestion_frame,
height=4,
width=60,
state=tk.DISABLED
)
def _setup_layout(self):
"""设置布局"""
# 标题
self.title_label.pack(pady=(0, 10))
# 图片上传区域
self.image_frame.pack(fill=tk.X, pady=(0, 10))
self.upload_button.pack(side=tk.LEFT, padx=(0, 10))
self.image_label.pack(fill=tk.BOTH, expand=True)
# 识别控制区域
self.control_frame.pack(fill=tk.X, pady=(0, 10))
self.recognize_button.pack(side=tk.LEFT, padx=(0, 10))
self.progress_bar.pack(side=tk.LEFT, padx=(0, 10), fill=tk.X, expand=True)
self.status_label.pack(side=tk.LEFT)
# 识别结果区域
self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 结果表格
result_scrollbar = ttk.Scrollbar(self.result_frame, orient=tk.VERTICAL, command=self.result_tree.yview)
self.result_tree.configure(yscrollcommand=result_scrollbar.set)
self.result_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
result_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 结果操作按钮
self.result_button_frame.pack(fill=tk.X, pady=(10, 0))
self.edit_button.pack(side=tk.LEFT, padx=(0, 10))
self.confirm_button.pack(side=tk.LEFT, padx=(0, 10))
self.clear_button.pack(side=tk.LEFT)
# 详细信息和建议区域
detail_suggestion_frame = ttk.Frame(self.main_frame)
detail_suggestion_frame.pack(fill=tk.BOTH, expand=True)
self.detail_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
self.detail_text.pack(fill=tk.BOTH, expand=True)
self.suggestion_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
self.suggestion_text.pack(fill=tk.BOTH, expand=True)
def _bind_events(self):
"""绑定事件"""
self.result_tree.bind('<<TreeviewSelect>>', self._on_result_select)
self.result_tree.bind('<Double-1>', self._on_result_double_click)
def _select_image(self):
"""选择图片文件"""
file_types = [
("图片文件", "*.jpg *.jpeg *.png *.bmp *.gif"),
("JPEG文件", "*.jpg *.jpeg"),
("PNG文件", "*.png"),
("所有文件", "*.*")
]
file_path = filedialog.askopenfilename(
title="选择包含食物信息的图片",
filetypes=file_types
)
if file_path:
self.current_image_path = file_path
self._display_image(file_path)
self.recognize_button.config(state=tk.NORMAL)
self.status_label.config(text=f"已选择图片: {Path(file_path).name}")
def _display_image(self, image_path: str):
"""显示图片"""
try:
# 加载图片
image = Image.open(image_path)
# 调整图片大小以适应显示区域
display_size = (400, 300)
image.thumbnail(display_size, Image.Resampling.LANCZOS)
# 转换为Tkinter可显示的格式
photo = ImageTk.PhotoImage(image)
# 更新标签
self.image_label.config(image=photo, text="")
self.image_label.image = photo # 保持引用
except Exception as e:
messagebox.showerror("错误", f"无法显示图片: {str(e)}")
self.image_label.config(image="", text="图片显示失败")
def _start_recognition(self):
"""开始OCR识别"""
if not self.current_image_path:
messagebox.showwarning("警告", "请先选择图片")
return
# 禁用按钮,显示进度条
self.recognize_button.config(state=tk.DISABLED)
self.progress_bar.start()
self.status_label.config(text="正在识别...")
# 在新线程中执行识别
thread = threading.Thread(target=self._perform_recognition)
thread.daemon = True
thread.start()
def _perform_recognition(self):
"""执行OCR识别"""
try:
# 准备请求数据
request_data = {
'type': 'recognize_image',
'image_path': self.current_image_path
}
# 获取当前用户数据(这里需要根据实际情况调整)
user_data = UserData(
user_id="current_user",
profile={},
meals=[],
feedback=[],
preferences={}
)
# 调用OCR模块
from modules.ocr_calorie_recognition import OCRCalorieRecognitionModule
ocr_module = OCRCalorieRecognitionModule(self.app_core.config)
if not ocr_module.initialize():
raise Exception("OCR模块初始化失败")
result = ocr_module.process(request_data, user_data)
# 在主线程中更新UI
self.parent_window.after(0, self._on_recognition_complete, result)
except Exception as e:
self.parent_window.after(0, self._on_recognition_error, str(e))
def _on_recognition_complete(self, result):
"""识别完成回调"""
try:
# 停止进度条
self.progress_bar.stop()
self.recognize_button.config(state=tk.NORMAL)
if result.result.get('success', False):
self.current_recognition_result = result.result['result']
self._display_recognition_results()
self.status_label.config(text="识别完成")
else:
error_msg = result.result.get('error', '识别失败')
messagebox.showerror("识别失败", error_msg)
self.status_label.config(text="识别失败")
except Exception as e:
self._on_recognition_error(str(e))
def _on_recognition_error(self, error_msg: str):
"""识别错误回调"""
self.progress_bar.stop()
self.recognize_button.config(state=tk.NORMAL)
self.status_label.config(text="识别失败")
messagebox.showerror("识别错误", f"OCR识别过程中出现错误: {error_msg}")
def _display_recognition_results(self):
"""显示识别结果"""
if not self.current_recognition_result:
return
try:
# 清空现有结果
self._clear_results()
# 显示热量信息
calorie_infos = self.current_recognition_result.calorie_infos
for info in calorie_infos:
self.result_tree.insert('', tk.END, values=(
info.food_name,
f"{info.calories:.1f}" if info.calories else "未知",
f"{info.confidence:.2f}",
info.source
))
# 显示详细信息
self._update_detail_text()
# 显示建议
self._update_suggestion_text()
# 启用操作按钮
self.edit_button.config(state=tk.NORMAL)
self.confirm_button.config(state=tk.NORMAL)
except Exception as e:
messagebox.showerror("错误", f"显示识别结果失败: {str(e)}")
def _update_detail_text(self):
"""更新详细信息文本"""
if not self.current_recognition_result:
return
try:
detail_text = "=== OCR识别详细信息 ===\n\n"
# OCR结果
detail_text += "OCR识别结果:\n"
for ocr_result in self.current_recognition_result.ocr_results:
detail_text += f"- 方法: {ocr_result.method}\n"
detail_text += f" 置信度: {ocr_result.confidence:.2f}\n"
detail_text += f" 识别文本: {ocr_result.text[:100]}...\n\n"
# 处理时间
detail_text += f"处理时间: {self.current_recognition_result.processing_time:.2f}\n"
detail_text += f"整体置信度: {self.current_recognition_result.overall_confidence:.2f}\n"
# 热量信息
detail_text += "\n=== 热量信息 ===\n"
for info in self.current_recognition_result.calorie_infos:
detail_text += f"食物: {info.food_name}\n"
detail_text += f"热量: {info.calories} 卡路里\n"
detail_text += f"置信度: {info.confidence:.2f}\n"
detail_text += f"来源: {info.source}\n"
detail_text += f"原始文本: {info.raw_text}\n\n"
self.detail_text.delete(1.0, tk.END)
self.detail_text.insert(1.0, detail_text)
except Exception as e:
self.detail_text.delete(1.0, tk.END)
self.detail_text.insert(1.0, f"详细信息加载失败: {str(e)}")
def _update_suggestion_text(self):
"""更新建议文本"""
if not self.current_recognition_result:
return
try:
suggestions = self.current_recognition_result.suggestions
suggestion_text = "=== 建议 ===\n\n"
for i, suggestion in enumerate(suggestions, 1):
suggestion_text += f"{i}. {suggestion}\n"
self.suggestion_text.config(state=tk.NORMAL)
self.suggestion_text.delete(1.0, tk.END)
self.suggestion_text.insert(1.0, suggestion_text)
self.suggestion_text.config(state=tk.DISABLED)
except Exception as e:
self.suggestion_text.config(state=tk.NORMAL)
self.suggestion_text.delete(1.0, tk.END)
self.suggestion_text.insert(1.0, f"建议加载失败: {str(e)}")
self.suggestion_text.config(state=tk.DISABLED)
def _on_result_select(self, event):
"""结果选择事件"""
selection = self.result_tree.selection()
if selection:
self.edit_button.config(state=tk.NORMAL)
self.confirm_button.config(state=tk.NORMAL)
else:
self.edit_button.config(state=tk.DISABLED)
self.confirm_button.config(state=tk.DISABLED)
def _on_result_double_click(self, event):
"""结果双击事件"""
self._edit_result()
def _edit_result(self):
"""编辑识别结果"""
selection = self.result_tree.selection()
if not selection:
messagebox.showwarning("警告", "请先选择要编辑的结果")
return
try:
item = selection[0]
values = self.result_tree.item(item, 'values')
food_name = values[0]
calories = values[1]
# 创建编辑对话框
self._create_edit_dialog(food_name, calories, item)
except Exception as e:
messagebox.showerror("错误", f"编辑结果失败: {str(e)}")
def _create_edit_dialog(self, food_name: str, calories: str, item_id: str):
"""创建编辑对话框"""
dialog = tk.Toplevel(self.parent_window)
dialog.title("编辑识别结果")
dialog.geometry("400x300")
dialog.resizable(False, False)
# 居中显示
dialog.transient(self.parent_window)
dialog.grab_set()
# 创建表单
form_frame = ttk.Frame(dialog, padding=20)
form_frame.pack(fill=tk.BOTH, expand=True)
# 食物名称
ttk.Label(form_frame, text="食物名称:").pack(anchor=tk.W, pady=(0, 5))
food_name_var = tk.StringVar(value=food_name)
food_name_entry = ttk.Entry(form_frame, textvariable=food_name_var, width=40)
food_name_entry.pack(fill=tk.X, pady=(0, 15))
# 热量
ttk.Label(form_frame, text="热量(卡路里):").pack(anchor=tk.W, pady=(0, 5))
calories_var = tk.StringVar(value=calories)
calories_entry = ttk.Entry(form_frame, textvariable=calories_var, width=40)
calories_entry.pack(fill=tk.X, pady=(0, 15))
# 置信度
ttk.Label(form_frame, text="置信度:").pack(anchor=tk.W, pady=(0, 5))
confidence_var = tk.StringVar(value="0.95")
confidence_entry = ttk.Entry(form_frame, textvariable=confidence_var, width=40)
confidence_entry.pack(fill=tk.X, pady=(0, 20))
# 按钮
button_frame = ttk.Frame(form_frame)
button_frame.pack(fill=tk.X)
def save_changes():
try:
new_food_name = food_name_var.get().strip()
new_calories = calories_var.get().strip()
new_confidence = float(confidence_var.get())
if not new_food_name:
messagebox.showwarning("警告", "食物名称不能为空")
return
if new_calories and new_calories != "未知":
try:
float(new_calories)
except ValueError:
messagebox.showwarning("警告", "热量必须是数字")
return
# 更新表格
self.result_tree.item(item_id, values=(
new_food_name,
new_calories,
f"{new_confidence:.2f}",
"user_corrected"
))
# 保存用户修正
self.user_corrections[new_food_name] = {
'calories': float(new_calories) if new_calories and new_calories != "未知" else None,
'confidence': new_confidence,
'timestamp': datetime.now().isoformat()
}
dialog.destroy()
messagebox.showinfo("成功", "结果已更新")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def cancel_changes():
dialog.destroy()
ttk.Button(button_frame, text="保存", command=save_changes).pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(button_frame, text="取消", command=cancel_changes).pack(side=tk.LEFT)
def _confirm_result(self):
"""确认识别结果"""
if not self.current_recognition_result:
messagebox.showwarning("警告", "没有可确认的结果")
return
try:
# 获取所有结果
results = []
for item in self.result_tree.get_children():
values = self.result_tree.item(item, 'values')
results.append({
'food_name': values[0],
'calories': float(values[1]) if values[1] != "未知" else None,
'confidence': float(values[2]),
'source': values[3]
})
if not results:
messagebox.showwarning("警告", "没有可确认的结果")
return
# 确认对话框
confirm_msg = "确认以下识别结果:\n\n"
for i, result in enumerate(results, 1):
confirm_msg += f"{i}. {result['food_name']}: {result['calories']} 卡路里\n"
confirm_msg += "\n是否确认这些结果?"
if messagebox.askyesno("确认结果", confirm_msg):
# 保存到餐食记录
self._save_to_meal_record(results)
messagebox.showinfo("成功", "结果已保存到餐食记录")
# 清空结果
self._clear_results()
except Exception as e:
messagebox.showerror("错误", f"确认结果失败: {str(e)}")
def _save_to_meal_record(self, results: List[Dict[str, Any]]):
"""保存到餐食记录"""
try:
# 这里需要调用应用核心的餐食记录功能
# 暂时保存到本地文件
meal_record = {
'timestamp': datetime.now().isoformat(),
'source': 'ocr_recognition',
'foods': results,
'total_calories': sum(r['calories'] for r in results if r['calories'])
}
# 保存到文件
record_file = Path("data/ocr_meal_records.json")
record_file.parent.mkdir(parents=True, exist_ok=True)
records = []
if record_file.exists():
with open(record_file, 'r', encoding='utf-8') as f:
records = json.load(f)
records.append(meal_record)
with open(record_file, 'w', encoding='utf-8') as f:
json.dump(records, f, ensure_ascii=False, indent=2)
except Exception as e:
self.logger.error(f"保存餐食记录失败: {e}")
raise
def _clear_results(self):
"""清空识别结果"""
self.result_tree.delete(*self.result_tree.get_children())
self.detail_text.delete(1.0, tk.END)
self.suggestion_text.config(state=tk.NORMAL)
self.suggestion_text.delete(1.0, tk.END)
self.suggestion_text.config(state=tk.DISABLED)
self.edit_button.config(state=tk.DISABLED)
self.confirm_button.config(state=tk.DISABLED)
self.current_recognition_result = None
if __name__ == "__main__":
# 测试GUI
root = tk.Tk()
root.title("OCR热量识别测试")
root.geometry("800x600")
# 模拟应用核心
class MockAppCore:
def __init__(self):
self.config = type('Config', (), {})()
app_core = MockAppCore()
# 创建GUI
ocr_gui = OCRCalorieGUI(root, app_core)
root.mainloop()