import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.widgets import Slider, TextBox
import struct
import os
import re
import logging
# 配置日志系统
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 设置matplotlib中文字体支持
plt.rcParams["font.family"] = ["SimHei"]
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
class BinaryVisualizer:
def __init__(self):
self.root = None
self.menubar = None # 显式引用菜单栏
self.files = {} # 存储已加载的文件数据
self.current_file_alias = None
self.current_frame = 0
self.width = 512 # 默认宽度
self.height = 341 # 默认高度
self.block_size = self.width * self.height # 每帧的像素数
self.value_offset = 0 # 数据偏移值
self.display_mode = "固定范围" # 默认显示模式
self.custom_min = -1000 # 默认自定义最小值
self.custom_max = 1000 # 默认自定义最大值
self.operations = {} # 存储运算结果
self.current_display_source = "original" # 当前显示源: "original" 或 "operation"
self.selected_operation = None # 当前选择的运算结果
# 允许的公式字符集合
self.allowed_formula_chars = set(['+', '-', '*', '/', '(', ')', 'numpy.', 'np.', '.', '_', ' '])
self.allowed_formula_chars.update([str(i) for i in range(10)])
# 初始化matplotlib设置
plt.rcParams["figure.figsize"] = (10, 7)
plt.rcParams["image.cmap"] = "gray"
def detect_possible_resolutions(self, pixel_count):
"""检测可能的分辨率"""
possible_resolutions = []
max_width = int(np.sqrt(pixel_count)) * 2
for width in range(100, max_width, 2): # 步长为2,确保宽度是偶数
if pixel_count % width == 0:
height = pixel_count // width
possible_resolutions.append((width, height))
return possible_resolutions
def load_data(self, file_path=None, alias=None):
"""加载二进制数据文件"""
if not file_path:
file_path = filedialog.askopenfilename(
title="选择二进制数据文件",
filetypes=[("二进制文件", "*.dat"), ("所有文件", "*.*")]
)
if not file_path:
return False
# 自动生成别称(A, B, C, ...)
if not alias:
used_aliases = set(self.files.keys())
for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
if c not in used_aliases:
alias = c
break
else:
messagebox.showerror("错误", "无法生成文件别称,已达到最大限制")
return False
try:
with open(file_path, 'rb') as bin_file:
data = bin_file.read()
total_bytes = len(data)
# 每像素2字节,计算可能的帧数
pixel_count = total_bytes // 2
possible_frames = pixel_count // self.block_size
if possible_frames == 0:
# 尝试检测可能的分辨率
possible_resolutions = self.detect_possible_resolutions(pixel_count)
if not possible_resolutions:
messagebox.showerror("错误", f"文件太小,无法包含一个完整的{self.width}x{self.height}帧")
return False
# 让用户选择分辨率
resolution_window = tk.Toplevel(self.root)
resolution_window.title("选择分辨率")
tk.Label(resolution_window, text="无法使用默认分辨率,请选择或输入:").pack(padx=10, pady=10)
resolution_frame = tk.Frame(resolution_window)
resolution_frame.pack(padx=10, pady=5)
tk.Label(resolution_frame, text="宽度:").pack(side=tk.LEFT, padx=5)
width_entry = tk.Entry(resolution_frame, width=10)
width_entry.insert(0, str(self.width))
width_entry.pack(side=tk.LEFT, padx=5)
tk.Label(resolution_frame, text="高度:").pack(side=tk.LEFT, padx=5)
height_entry = tk.Entry(resolution_frame, width=10)
height_entry.insert(0, str(self.height))
height_entry.pack(side=tk.LEFT, padx=5)
def apply_resolution():
try:
self.width = int(width_entry.get())
self.height = int(height_entry.get())
self.block_size = self.width * self.height
resolution_window.destroy()
self.load_data(file_path, alias) # 重新加载
except ValueError:
messagebox.showerror("错误", "请输入有效的整数")
tk.Button(resolution_window, text="应用", command=apply_resolution).pack(pady=10)
return False
logger.info(f"文件大小: {total_bytes} 字节")
logger.info(f"可能的帧数: {possible_frames}")
# 解析每一帧数据
frames = []
for frame_idx in range(possible_frames):
start = frame_idx * self.block_size * 2 # 每像素2字节
end = start + self.block_size * 2
frame_data = data[start:end]
# 确保帧大小是偶数
if len(frame_data) % 2 != 0:
logger.warning(f"帧 {frame_idx+1} 字节数为奇数,最后一个字节将被忽略")
frame_data = frame_data[:-1]
# 解析为有符号整数并应用偏移
frame_values = []
for i in range(0, len(frame_data), 2):
low_byte = frame_data[i]
high_byte = frame_data[i+1]
try:
signed_value = struct.unpack('<h', bytes([low_byte, high_byte]))[0]
except struct.error as e:
logger.error(f"解析帧 {frame_idx+1} 时出错: {e}")
signed_value = 0 # 默认值
adjusted_value = signed_value + self.value_offset
frame_values.append((signed_value, adjusted_value))
# 转换为二维数组
try:
signed_array = np.array([x[0] for x in frame_values]).reshape((self.height, self.width))
adjusted_array = np.array([x[1] for x in frame_values]).reshape((self.height, self.width))
frames.append((signed_array, adjusted_array))
except ValueError as e:
logger.error(f"重塑帧 {frame_idx+1} 时出错: {e}")
# 添加空白帧作为替代
frames.append((np.zeros((self.height, self.width)), np.zeros((self.height, self.width))))
# 存储文件数据
self.files[alias] = {
'path': file_path,
'data': data,
'frames': frames
}
# 如果是第一个文件或明确指定了当前文件
if not self.current_file_alias or alias == self.current_file_alias:
self.current_file_alias = alias
self.current_frame = 0
# 更新UI
if self.slider_frame:
self.slider_frame.valmax = len(frames) - 1
self.slider_frame.set_val(0)
self.slider_frame.set_active(True) # 启用滑块
if self.textbox_frame:
self.textbox_frame.set_val("1")
self.update_display_range()
# 更新文件选择菜单
self.update_file_menu()
logger.info(f"成功加载文件 {alias}: {os.path.basename(file_path)}")
return True
except Exception as e:
messagebox.showerror("错误", f"加载文件时出错: {str(e)}")
logger.error(f"加载文件时出错: {e}", exc_info=True)
return False
def load_multiple_files(self):
"""加载多个二进制数据文件"""
file_paths = filedialog.askopenfilenames(
title="选择多个二进制数据文件",
filetypes=[("二进制文件", "*.dat"), ("所有文件", "*.*")]
)
if not file_paths:
return
for file_path in file_paths:
self.load_data(file_path)
def select_file(self, alias):
"""选择当前文件"""
if alias in self.files:
self.current_file_alias = alias
self.current_frame = 0
# 更新UI
if self.slider_frame:
self.slider_frame.valmax = len(self.files[alias]['frames']) - 1
self.slider_frame.set_val(0)
if self.textbox_frame:
self.textbox_frame.set_val("1")
self.update_display_range()
def on_frame_change(self, val):
"""帧滑块值改变时调用"""
frame_idx = int(val)
if self.current_file_alias and self.current_file_alias in self.files:
max_frame = len(self.files[self.current_file_alias]['frames']) - 1
if frame_idx > max_frame:
frame_idx = max_frame
self.slider_frame.set_val(frame_idx)
self.current_frame = frame_idx
self.textbox_frame.set_val(str(frame_idx + 1))
self.update_display_range()
def on_frame_number_submit(self, text):
"""帧号输入提交时调用"""
try:
frame_num = int(text)
if self.current_file_alias and self.current_file_alias in self.files:
max_frame = len(self.files[self.current_file_alias]['frames'])
if frame_num < 1 or frame_num > max_frame:
messagebox.showinfo("提示", f"请输入1到{max_frame}之间的帧号")
frame_num = min(max(frame_num, 1), max_frame)
self.current_frame = frame_num - 1
self.slider_frame.set_val(self.current_frame)
self.update_display_range()
except ValueError:
messagebox.showerror("错误", "请输入有效的整数")
def set_display_mode(self, mode):
"""设置显示模式"""
self.display_mode = mode
self.update_display_range()
def set_custom_range(self):
"""设置自定义显示范围"""
range_window = tk.Toplevel(self.root)
range_window.title("设置自定义范围")
tk.Label(range_window, text="最小值:").pack(padx=10, pady=5)
min_entry = tk.Entry(range_window, width=10)
min_entry.insert(0, str(self.custom_min))
min_entry.pack(padx=10)
tk.Label(range_window, text="最大值:").pack(padx=10, pady=5)
max_entry = tk.Entry(range_window, width=10)
max_entry.insert(0, str(self.custom_max))
max_entry.pack(padx=10)
def apply_range():
try:
self.custom_min = int(min_entry.get())
self.custom_max = int(max_entry.get())
if self.custom_min >= self.custom_max:
messagebox.showerror("错误", "最小值必须小于最大值")
return
self.display_mode = "自定义"
self.update_display_range()
range_window.destroy()
except ValueError:
messagebox.showerror("错误", "请输入有效的整数")
tk.Button(range_window, text="应用", command=apply_range).pack(pady=10)
def update_display_range(self):
"""更新显示范围"""
if not self.current_file_alias or self.current_file_alias not in self.files:
return
frames = self.files[self.current_file_alias]['frames']
if not frames or self.current_frame >= len(frames):
return
current_frame_data = frames[self.current_frame][1] # 使用偏移后的数据
try:
if self.display_mode == "固定范围":
vmin = self.custom_min
vmax = self.custom_max
elif self.display_mode == "帧平均±1000":
frame_mean = np.mean(current_frame_data)
vmin = max(frame_mean - 1000, np.min(current_frame_data))
vmax = min(frame_mean + 1000, np.max(current_frame_data))
elif self.display_mode == "帧平均±200":
frame_mean = np.mean(current_frame_data)
vmin = max(frame_mean - 200, np.min(current_frame_data))
vmax = min(frame_mean + 200, np.max(current_frame_data))
else: # 自定义
vmin = self.custom_min
vmax = self.custom_max
# 更新图像显示
self.current_image.set_data(current_frame_data)
self.current_image.set_clim(vmin, vmax)
# 更新直方图
self.ax_hist.clear()
self.ax_hist.hist(current_frame_data.flatten(), bins=50, range=(vmin, vmax), alpha=0.7)
self.ax_hist.set_title("像素值分布直方图")
self.ax_hist.set_xlabel("像素值")
self.ax_hist.set_ylabel("频次")
self.ax_hist.grid(True)
# 更新标题
frame_mean = np.mean(current_frame_data)
frame_std = np.std(current_frame_data)
# 安全处理文件名
try:
if self.current_file_alias in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
file_name = f"文件 {self.current_file_alias}"
else:
file_name = self.current_file_alias
except Exception as e:
logger.error(f"处理文件名时出错: {e}")
file_name = "未知文件"
self.fig.suptitle(f"{file_name} - 帧 {self.current_frame+1}/{len(frames)} - 均值: {frame_mean:.2f}, 标准差: {frame_std:.2f}")
self.canvas.draw()
except Exception as e:
logger.error(f"更新显示范围时出错: {e}")
messagebox.showerror("显示错误", f"更新显示时出错: {str(e)}")
def export_to_txt(self):
"""导出当前帧为TXT文件"""
if not self.current_file_alias or self.current_file_alias not in self.files:
messagebox.showinfo("提示", "请先加载文件")
return
frames = self.files[self.current_file_alias]['frames']
if not frames or self.current_frame >= len(frames):
messagebox.showinfo("提示", "无效的帧")
return
current_frame_data = frames[self.current_frame][1] # 使用偏移后的数据
file_path = filedialog.asksaveasfilename(
title="保存当前帧为文本文件",
defaultextension=".txt",
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
)
if not file_path:
return
try:
with open(file_path, 'w') as txt_file:
# 写入头部信息
txt_file.write(f"# 数据导出 - 文件: {self.current_file_alias}\n")
txt_file.write(f"# 帧号: {self.current_frame+1}/{len(frames)}\n")
txt_file.write(f"# 分辨率: {self.width}x{self.height}\n")
txt_file.write(f"# 像素值范围: {np.min(current_frame_data)} - {np.max(current_frame_data)}\n")
txt_file.write(f"# 均值: {np.mean(current_frame_data):.2f}, 标准差: {np.std(current_frame_data):.2f}\n")
txt_file.write("#\n")
# 写入数据
for row in current_frame_data:
row_str = ' '.join([f"{float(val):.6f}" for val in row])
txt_file.write(row_str + '\n')
messagebox.showinfo("成功", f"帧数据已保存到 {file_path}")
except Exception as e:
messagebox.showerror("错误", f"保存文件时出错: {str(e)}")
def set_resolution(self):
"""设置图像分辨率"""
resolution_window = tk.Toplevel(self.root)
resolution_window.title("设置分辨率")
tk.Label(resolution_window, text="宽度:").pack(padx=10, pady=5)
width_entry = tk.Entry(resolution_window, width=10)
width_entry.insert(0, str(self.width))
width_entry.pack(padx=10)
tk.Label(resolution_window, text="高度:").pack(padx=10, pady=5)
height_entry = tk.Entry(resolution_window, width=10)
height_entry.insert(0, str(self.height))
height_entry.pack(padx=10)
def apply_resolution():
try:
new_width = int(width_entry.get())
new_height = int(height_entry.get())
if new_width <= 0 or new_height <= 0:
messagebox.showerror("错误", "宽度和高度必须为正整数")
return
self.width = new_width
self.height = new_height
self.block_size = self.width * self.height
# 重新加载当前文件以应用新的分辨率
if self.current_file_alias and self.current_file_alias in self.files:
file_path = self.files[self.current_file_alias]['path']
self.load_data(file_path, self.current_file_alias)
resolution_window.destroy()
except ValueError:
messagebox.showerror("错误", "请输入有效的整数")
tk.Button(resolution_window, text="应用", command=apply_resolution).pack(pady=10)
def set_offset(self):
"""设置数据偏移值"""
offset_window = tk.Toplevel(self.root)
offset_window.title("设置偏移值")
tk.Label(offset_window, text="偏移值:").pack(padx=10, pady=5)
offset_entry = tk.Entry(offset_window, width=10)
offset_entry.insert(0, str(self.value_offset))
offset_entry.pack(padx=10)
def apply_offset():
try:
self.value_offset = int(offset_entry.get())
# 重新加载当前文件以应用新的偏移值
if self.current_file_alias and self.current_file_alias in self.files:
file_path = self.files[self.current_file_alias]['path']
self.load_data(file_path, self.current_file_alias)
offset_window.destroy()
except ValueError:
messagebox.showerror("错误", "请输入有效的整数")
tk.Button(offset_window, text="应用", command=apply_offset).pack(pady=10)
def create_formula(self):
"""创建公式窗口"""
if len(self.files) < 2:
messagebox.showinfo("提示", "请先加载至少两个文件")
return
formula_window = tk.Toplevel(self.root)
formula_window.title("公式计算")
formula_window.geometry("600x400")
# 可用文件列表
tk.Label(formula_window, text="可用文件:").pack(padx=10, pady=5, anchor="w")
files_frame = tk.Frame(formula_window)
files_frame.pack(padx=10, fill="x")
file_listbox = tk.Listbox(files_frame, width=50)
file_listbox.pack(side=tk.LEFT, fill="both", expand=True)
scrollbar = tk.Scrollbar(files_frame, orient="vertical", command=file_listbox.yview)
scrollbar.pack(side=tk.RIGHT, fill="y")
file_listbox.config(yscrollcommand=scrollbar.set)
for alias in sorted(self.files.keys()):
file_listbox.insert(tk.END, f"文件 {alias}: {os.path.basename(self.files[alias]['path'])}")
# 公式输入
tk.Label(formula_window, text="公式输入 (使用文件别称如 A,B,C 进行 +-*/ 运算):").pack(padx=10, pady=5, anchor="w")
formula_frame = tk.Frame(formula_window)
formula_frame.pack(padx=10, fill="x")
formula_entry = tk.Entry(formula_frame, width=50)
formula_entry.pack(side=tk.LEFT, fill="x", expand=True)
# 插入文件按钮
def insert_file_alias():
selection = file_listbox.curselection()
if selection:
alias = sorted(self.files.keys())[selection[0]]
formula_entry.insert(tk.INSERT, alias)
tk.Button(formula_frame, text="插入文件", command=insert_file_alias).pack(side=tk.LEFT, padx=5)
# 帧选择
tk.Label(formula_window, text="选择帧:").pack(padx=10, pady=5, anchor="w")
frame_frame = tk.Frame(formula_window)
frame_frame.pack(padx=10, fill="x")
frame_var = tk.StringVar(value="same")
frame_options = [
("所有文件使用相同帧", "same"),
("为每个文件指定帧", "different")
]
for text, value in frame_options:
tk.Radiobutton(frame_frame, text=text, variable=frame_var, value=value).pack(anchor="w")
# 相同帧选择
same_frame_frame = tk.Frame(formula_window)
same_frame_frame.pack(padx=10, pady=5, fill="x")
tk.Label(same_frame_frame, text="帧号:").pack(side=tk.LEFT)
same_frame_entry = tk.Entry(same_frame_frame, width=10)
same_frame_entry.insert(0, "1")
same_frame_entry.pack(side=tk.LEFT, padx=5)
# 结果名称
tk.Label(formula_window, text="结果名称:").pack(padx=10, pady=5, anchor="w")
result_name_frame = tk.Frame(formula_window)
result_name_frame.pack(padx=10, fill="x")
result_name_entry = tk.Entry(result_name_frame, width=20)
result_name_entry.insert(0, "Result")
result_name_entry.pack(side=tk.LEFT, padx=5)
# 计算按钮
def calculate_formula():
formula = formula_entry.get().strip()
if not formula:
messagebox.showerror("错误", "请输入公式")
return
# 验证公式
valid_aliases = set(self.files.keys())
used_aliases = set(re.findall(r'[A-Z]', formula))
if not used_aliases.issubset(valid_aliases):
invalid_aliases = used_aliases - valid_aliases
messagebox.showerror("错误", f"公式中包含无效的文件别称: {', '.join(invalid_aliases)}")
return
# 额外验证公式安全性
if not self._validate_formula(formula):
return
try:
frame_mode = frame_var.get()
if frame_mode == "same":
try:
frame_num = int(same_frame_entry.get())
except ValueError:
messagebox.showerror("错误", "请输入有效的帧号")
return
# 检查所有文件是否有该帧
for alias in used_aliases:
if frame_num < 1 or frame_num > len(self.files[alias]['frames']):
messagebox.showerror("错误", f"文件 {alias} 没有帧 {frame_num}")
return
# 创建变量字典
variables = {}
for alias in used_aliases:
variables[alias] = self.files[alias]['frames'][frame_num-1][1] # 使用偏移后的数据
# 计算结果
try:
result = self.safe_eval(formula, variables)
if result is None:
raise ValueError("公式计算失败")
except Exception as e:
messagebox.showerror("错误", f"公式计算出错: {str(e)}")
return
# 保存结果
result_name = result_name_entry.get().strip()
if not result_name:
result_name = "Result"
# 检查结果名称是否已存在
if result_name in self.operations:
if not messagebox.askyesno("确认", f"结果名称 '{result_name}' 已存在,是否覆盖?"):
return
# 保存结果
self.operations[result_name] = {
'formula': formula,
'frame_mode': frame_mode,
'frame_num': frame_num if frame_mode == "same" else None,
'result': result
}
messagebox.showinfo("成功", f"公式计算成功,结果已保存为 '{result_name}'")
formula_window.destroy()
# 更新操作菜单
self.update_operations_menu()
except Exception as e:
messagebox.showerror("错误", f"执行计算时出错: {str(e)}")
logger.error(f"执行计算时出错: {e}", exc_info=True)
tk.Button(formula_window, text="计算", command=calculate_formula).pack(pady=10)
def safe_eval(self, formula, variables):
"""安全执行公式计算"""
# 验证公式
for char in formula:
if char not in self.allowed_formula_chars:
logger.warning(f"公式包含不允许的字符: {char}")
return None
# 禁用所有内置函数,只允许numpy
return eval(formula, {'__builtins__': None, 'np': np, 'numpy': np}, variables)
def _validate_formula(self, formula):
"""验证公式安全性"""
for char in formula:
if char not in self.allowed_formula_chars:
messagebox.showerror("错误", f"公式包含不允许的字符: {char}")
logger.error(f"非法公式: {formula}")
return False
return True
def show_operation_result(self, name):
"""显示运算结果"""
if name in self.operations:
self.selected_operation = name
result = self.operations[name]['result']
# 更新图像显示
self.current_image.set_data(result)
# 更新显示范围
vmin = np.min(result)
vmax = np.max(result)
self.current_image.set_clim(vmin, vmax)
# 更新直方图
self.ax_hist.clear()
self.ax_hist.hist(result.flatten(), bins=50, range=(vmin, vmax), alpha=0.7)
self.ax_hist.set_title("运算结果像素值分布")
self.ax_hist.set_xlabel("像素值")
self.ax_hist.set_ylabel("频次")
self.ax_hist.grid(True)
# 更新标题
formula = self.operations[name]['formula']
self.fig.suptitle(f"运算结果 - {name}: {formula}")
self.canvas.draw()
self.current_display_source = "operation"
self.source_var.set("运算结果")
# 更新数据源菜单
self._update_source_menu()
def save_operation_result(self):
"""保存当前运算结果为TXT文件"""
if not self.operations:
messagebox.showinfo("提示", "没有可用的运算结果")
return
# 获取当前显示的运算结果名称
current_name = None
if self.current_display_source == "operation" and self.selected_operation:
current_name = self.selected_operation
# 如果没有当前显示的结果,让用户选择
if not current_name or current_name not in self.operations:
if len(self.operations) == 1:
current_name = list(self.operations.keys())[0]
else:
# 创建选择窗口
select_window = tk.Toplevel(self.root)
select_window.title("选择运算结果")
tk.Label(select_window, text="请选择要保存的运算结果:").pack(padx=10, pady=10)
result_listbox = tk.Listbox(select_window, width=50)
result_listbox.pack(padx=10, fill="both", expand=True)
for name in sorted(self.operations.keys()):
result_listbox.insert(tk.END, f"{name}: {self.operations[name]['formula']}")
def on_select():
selection = result_listbox.curselection()
if selection:
selected_name = sorted(self.operations.keys())[selection[0]]
select_window.destroy()
self._save_result_to_txt(selected_name)
button_frame = tk.Frame(select_window)
button_frame.pack(pady=10)
tk.Button(button_frame, text="确定", command=on_select).pack(side=tk.LEFT, padx=10)
tk.Button(button_frame, text="取消", command=select_window.destroy).pack(side=tk.LEFT, padx=10)
return
else:
self._save_result_to_txt(current_name)
def _save_result_to_txt(self, name):
"""实际保存运算结果到TXT文件,支持多种格式"""
result = self.operations[name]['result']
file_path = filedialog.asksaveasfilename(
title="保存运算结果为文本文件",
defaultextension=".txt",
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
)
if not file_path:
return
# 选择保存格式
format_window = tk.Toplevel(self.root)
format_window.title("选择保存格式")
format_var = tk.StringVar(value="decimal")
format_frame = tk.Frame(format_window)
format_frame.pack(padx=10, pady=10)
format_options = [
("带符号十进制数", "signed"),
("十进制数", "decimal"),
("十六进制", "hex")
]
for text, value in format_options:
tk.Radiobutton(format_frame, text=text, variable=format_var, value=value).pack(anchor="w", pady=2)
def save_with_format():
try:
format_type = format_var.get()
format_window.destroy()
with open(file_path, 'w') as txt_file:
# 写入公式信息
txt_file.write(f"# 运算公式: {self.operations[name]['formula']}\n")
txt_file.write(f"# 保存格式: {dict(format_options).get(format_type, format_type)}\n")
txt_file.write("#\n")
# 写入数据
for row in result:
if format_type == "hex":
# 转换为十六进制格式
row_str = ' '.join([f"{int(val):04X}" for val in row])
elif format_type == "signed":
# 带符号十进制
row_str = ' '.join([f"{int(val):d}" for val in row])
else: # decimal
# 十进制
row_str = ' '.join([f"{int(val):d}" for val in row])
txt_file.write(row_str + '\n')
messagebox.showinfo("成功", f"运算结果已保存到 {file_path}")
except Exception as e:
messagebox.showerror("错误", f"保存文件时出错: {str(e)}")
tk.Button(format_window, text="保存", command=save_with_format).pack(pady=10)
tk.Button(format_window, text="取消", command=format_window.destroy).pack(pady=10)
def export_to_txt(self):
"""导出当前帧为TXT文件,支持多种格式"""
if not self.current_file_alias or self.current_file_alias not in self.files:
messagebox.showinfo("提示", "请先加载文件")
return
frames = self.files[self.current_file_alias]['frames']
if not frames or self.current_frame >= len(frames):
messagebox.showinfo("提示", "无效的帧")
return
current_frame_data = frames[self.current_frame][1] # 使用偏移后的数据
file_path = filedialog.asksaveasfilename(
title="保存当前帧为文本文件",
defaultextension=".txt",
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
)
if not file_path:
return
# 选择保存格式
format_window = tk.Toplevel(self.root)
format_window.title("选择保存格式")
format_var = tk.StringVar(value="decimal")
format_frame = tk.Frame(format_window)
format_frame.pack(padx=10, pady=10)
format_options = [
("带符号十进制数", "signed"),
("十进制数", "decimal"),
("十六进制", "hex")
]
for text, value in format_options:
tk.Radiobutton(format_frame, text=text, variable=format_var, value=value).pack(anchor="w", pady=2)
def save_with_format():
try:
format_type = format_var.get()
format_window.destroy()
with open(file_path, 'w') as txt_file:
# 写入头部信息
txt_file.write(f"# 数据导出 - 文件: {self.current_file_alias}\n")
txt_file.write(f"# 帧号: {self.current_frame+1}/{len(frames)}\n")
txt_file.write(f"# 分辨率: {self.width}x{self.height}\n")
txt_file.write(f"# 像素值范围: {np.min(current_frame_data)} - {np.max(current_frame_data)}\n")
txt_file.write(f"# 均值: {np.mean(current_frame_data):.2f}, 标准差: {np.std(current_frame_data):.2f}\n")
txt_file.write("#\n")
# 写入数据
for row in current_frame_data:
if format_type == "hex":
# 转换为十六进制格式
row_str = ' '.join([f"{int(val):04X}" for val in row])
elif format_type == "signed":
# 带符号十进制
row_str = ' '.join([f"{int(val):d}" for val in row])
else: # decimal
# 十进制
row_str = ' '.join([f"{int(val):d}" for val in row])
txt_file.write(row_str + '\n')
messagebox.showinfo("成功", f"帧数据已保存到 {file_path}")
except Exception as e:
messagebox.showerror("错误", f"保存文件时出错: {str(e)}")
tk.Button(format_window, text="保存", command=save_with_format).pack(pady=10)
tk.Button(format_window, text="取消", command=format_window.destroy).pack(pady=10)
def set_display_source(self, source):
"""设置显示源"""
self.current_display_source = source
if source == "original":
# 显示原始数据
if self.current_file_alias and self.current_file_alias in self.files:
frames = self.files[self.current_file_alias]['frames']
if frames and self.current_frame < len(frames):
current_frame_data = frames[self.current_frame][1] # 使用偏移后的数据
# 更新图像显示
self.current_image.set_data(current_frame_data)
# 更新显示范围
self.update_display_range()
elif source == "operation" and self.selected_operation and self.selected_operation in self.operations:
# 显示运算结果
self.show_operation_result(self.selected_operation)
def on_source_changed(self, event=None):
"""数据源选择变化时调用"""
source = self.source_var.get()
if source == "原始数据":
self.set_display_source("original")
elif source == "运算结果" and self.operations:
if not self.selected_operation or self.selected_operation not in self.operations:
# 如果没有选择运算结果,选择第一个
self.selected_operation = list(self.operations.keys())[0]
self.set_display_source("operation")
def _get_or_create_menu(self, menu_name):
"""获取或创建菜单"""
try:
if not self.menubar:
self.menubar = tk.Menu(self.root)
self.root.config(menu=self.menubar)
# 检查菜单是否已存在
for i in range(self.menubar.index('end') + 1):
menu_label = self.menubar.entrycget(i, 'label')
if menu_label == menu_name:
return self.menubar.nametowidget(self.menubar.entrycget(i, 'menu'))
# 创建新菜单
new_menu = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label=menu_name, menu=new_menu)
return new_menu
except Exception as e:
logger.error(f"获取或创建菜单时出错: {e}")
# 不返回任何内容,让调用者处理None值
return None
def update_display_menu(self):
"""更新显示菜单"""
try:
display_menu = self._get_or_create_menu("显示")
if not display_menu:
return
display_menu.delete(0, 'end')
# 创建显示模式变量
if not hasattr(self, 'display_mode_var'):
self.display_mode_var = tk.StringVar(value=self.display_mode)
# 显示模式
display_mode_menu = tk.Menu(display_menu, tearoff=0)
display_menu.add_cascade(label="显示模式", menu=display_mode_menu)
display_modes = ["固定范围", "帧平均±1000", "帧平均±200", "自定义"]
for mode in display_modes:
display_mode_menu.add_radiobutton(
label=mode,
variable=self.display_mode_var, # 修正为变量对象
value=mode,
command=lambda m=mode: self.set_display_mode(m)
)
file_menu.add_separator()
file_menu.add_command(label="导出当前帧为TXT...", command=self.export_to_txt)
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.root.quit)
except Exception as e:
logger.error(f"更新文件菜单时出错: {e}")
def _update_source_menu(self):
"""更新数据源菜单"""
try:
if not self.menubar:
return
# 查找显示菜单
display_menu = None
for i in range(self.menubar.index('end') + 1):
if self.menubar.entrycget(i, 'label') == "显示":
menu_name = self.menubar.entrycget(i, 'menu')
display_menu = self.menubar.nametowidget(menu_name)
break
if not display_menu:
return
# 查找数据源菜单项
source_menu = None
for i in range(display_menu.index('end') + 1):
if display_menu.entrycget(i, 'label') == "数据源":
menu_name = display_menu.entrycget(i, 'menu')
source_menu = display_menu.nametowidget(menu_name)
break
if not source_menu:
return
# 清除并重新添加菜单项
source_menu.delete(0, 'end')
source_menu.add_radiobutton(
label="原始数据",
variable=self.source_var,
value="原始数据",
command=self.on_source_changed
)
if self.operations:
source_menu.add_radiobutton(
label="运算结果",
variable=self.source_var,
value="运算结果",
command=self.on_source_changed
)
except Exception as e:
logger.error(f"更新数据源菜单时出错: {e}")
def update_formula_menu(self):
"""更新公式菜单"""
try:
formula_menu = self._get_or_create_menu("公式")
formula_menu.delete(0, 'end')
formula_menu.add_command(label="创建公式...", command=self.create_formula)
if self.operations:
formula_menu.add_separator()
operations_menu = tk.Menu(formula_menu, tearoff=0)
formula_menu.add_cascade(label="运算结果", menu=operations_menu)
for name in sorted(self.operations.keys()):
operations_menu.add_command(
label=f"{name}: {self.operations[name]['formula']}",
command=lambda n=name: self.show_operation_result(n)
)
formula_menu.add_separator()
formula_menu.add_command(label="保存当前运算结果", command=self.save_operation_result)
except Exception as e:
logger.error(f"更新公式菜单时出错: {e}")
def update_operations_menu(self):
"""更新运算结果菜单"""
try:
operations_menu = self._get_or_create_menu("运算结果")
operations_menu.delete(0, 'end')
if self.operations:
for name in sorted(self.operations.keys()):
operations_menu.add_command(
label=f"{name}: {self.operations[name]['formula']}",
command=lambda n=name: self.show_operation_result(n)
)
operations_menu.add_separator()
operations_menu.add_command(
label="保存当前运算结果",
command=self.save_operation_result
)
except Exception as e:
logger.error(f"更新运算结果菜单时出错: {e}")
def _update_source_menu(self):
"""更新数据源菜单"""
try:
if not self.menubar:
return
# 查找显示菜单
display_menu_idx = None
for i in range(self.menubar.index('end') + 1):
if self.menubar.entrycget(i, 'label') == "显示":
display_menu_idx = i
break
if display_menu_idx is None:
return
display_menu = self.menubar.entrycget(display_menu_idx, 'menu')
# 查找数据源菜单项
source_menu_idx = None
for i in range(display_menu.index('end') + 1):
if display_menu.entrycget(i, 'label') == "数据源":
source_menu_idx = i
break
if source_menu_idx is None:
return
source_menu = display_menu.entrycget(source_menu_idx, 'menu')
# 清除并重新添加菜单项
source_menu.delete(0, 'end')
source_menu.add_radiobutton(
label="原始数据",
variable=self.source_var,
value="原始数据",
command=self.on_source_changed
)
if self.operations:
source_menu.add_radiobutton(
label="运算结果",
variable=self.source_var,
value="运算结果",
command=self.on_source_changed
)
except Exception as e:
logger.error(f"更新数据源菜单时出错: {e}")
def initialize_ui(self):
"""初始化用户界面"""
self.root = tk.Tk()
self.root.title("二进制数据可视化工具")
self.root.geometry("1200x800")
# 创建主框架
main_frame = tk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True)
# 创建控制框架
control_frame = tk.Frame(main_frame, height=50)
control_frame.pack(fill=tk.X, side=tk.TOP, padx=10, pady=5)
# 数据源切换控件
source_frame = tk.Frame(control_frame)
source_frame.pack(fill=tk.X, side=tk.RIGHT, padx=(10, 0), pady=5)
tk.Label(source_frame, text="显示源:").pack(side=tk.LEFT, padx=5)
self.source_var = tk.StringVar(value="original")
source_combo = ttk.Combobox(source_frame, textvariable=self.source_var, width=15)
source_combo['values'] = ("原始数据", "运算结果")
source_combo.pack(side=tk.LEFT, padx=5)
source_combo.bind("<<ComboboxSelected>>", self.on_source_changed)
# 创建Matplotlib图形
self.fig, (self.ax_image, self.ax_hist) = plt.subplots(2, 1, figsize=(10, 7), gridspec_kw={'height_ratios': [4, 1]})
self.fig.subplots_adjust(hspace=0.3)
# 初始图像
self.current_image = self.ax_image.imshow(np.zeros((self.height, self.width)), cmap='gray')
self.ax_image.axis('off')
# 初始直方图
self.ax_hist.hist(np.zeros(self.height * self.width), bins=50, alpha=0.7)
self.ax_hist.set_title("像素值分布直方图")
self.ax_hist.set_xlabel("像素值")
self.ax_hist.set_ylabel("频次")
self.ax_hist.grid(True)
# 创建Canvas
self.canvas = FigureCanvasTkAgg(self.fig, master=main_frame)
self.canvas.draw()
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# 添加Matplotlib工具栏
toolbar = NavigationToolbar2Tk(self.canvas, main_frame)
toolbar.update()
# 创建帧控制滑块
slider_frame = tk.Frame(main_frame, height=50)
slider_frame.pack(fill=tk.X, side=tk.BOTTOM, padx=10, pady=5)
# 帧号标签和输入框
tk.Label(slider_frame, text="帧号:").pack(side=tk.LEFT, padx=5)
# 创建Matplotlib文本框用于输入帧号
self.textbox_frame = TextBox(
plt.axes([0.15, 0.01, 0.05, 0.04]),
'',
initial="1",
color='white',
hovercolor='lightgoldenrodyellow'
)
self.textbox_frame.on_submit(self.on_frame_number_submit)
# 创建Matplotlib滑块用于控制帧
self.slider_frame = Slider(
plt.axes([0.25, 0.01, 0.65, 0.04]),
'帧',
0,
100, # 初始范围,会在加载文件后更新
valinit=0,
valstep=1
)
self.slider_frame.on_changed(self.on_frame_change)
self.slider_frame.set_active(False) # 初始禁用,直到加载文件
# 初始化菜单
self.initialize_menus()
# 更新菜单状态
self.update_file_menu()
self.update_display_menu()
self.update_formula_menu()
# 设置数据源菜单
self._update_source_menu()
# 显示欢迎信息
self.fig.suptitle("欢迎使用二进制数据可视化工具\n请从文件菜单打开二进制数据文件")
self.canvas.draw()
def initialize_menus(self):
"""初始化菜单栏"""
if not self.menubar:
self.menubar = tk.Menu(self.root)
self.root.config(menu=self.menubar)
def run(self):
"""运行应用程序"""
self.initialize_ui()
self.root.mainloop()
if __name__ == "__main__":
app = BinaryVisualizer()
app.run() 提示Traceback (most recent call last):
File "E:\zlt_work\zlt_work\DATA\读取多文件测试.py", line 1185, in <module>
app.run()
File "E:\zlt_work\zlt_work\DATA\读取多文件测试.py", line 1180, in run
self.initialize_ui()
File "E:\zlt_work\zlt_work\DATA\读取多文件测试.py", line 1161, in initialize_ui
self.update_file_menu()
AttributeError: 'BinaryVisualizer' object has no attribute 'update_file_menu'
最新发布