这个图片数字化程序具有以下功能特点:
主要功能:
1. 图片加载与显示
支持多种图片格式(JPG、PNG、BMP等)自动缩放以适应窗口支持滚动查看大图片
2. 坐标系设置
三步设置法:
点击设置原点(红色标记)点击设置X轴正方向(蓝色箭头)点击设置Y轴正方向(绿色箭头)
可自定义实际坐标值支持任意角度的坐标系
3. 交互式数据拾取
点击图片即可拾取数据点实时显示鼠标位置坐标自动转换为实际坐标值数据点用橙色标记并编号
4. 数据管理
表格形式显示所有拾取点支持撤销上一个点支持清除所有点实时更新坐标显示
5. 数据导出
CSV格式:适合Excel处理JSON格式:包含完整坐标系信息TXT格式:纯文本格
6. 实时放大镜窗口
独立的浮动窗口,始终保持在最前面实时显示鼠标周围的放大图像可调节放大倍数(2x-20x)十字准线:虚线十字帮助精确定位中心标记:红色圆圈标记当前像素位置坐标显示:在放大镜中显示当前坐标复选框快速开关放大镜鼠标离开图片时自动隐藏滑块实时调节放大倍数
7. 快捷键支持
M键:快速切换放大镜空格键:切换拾取模式Ctrl+Z:撤销上一个点Delete:清除所有点使用高效的图像裁剪和缩放算法避免不必要的重绘平滑的鼠标跟随效果
使用步骤:
打开图片:点击”打开图片”按钮选择要数字化的图片设置坐标系:
点击”设置原点”,在图片上点击原点位置点击”设置X轴”,点击X轴正方向上的一个点点击”设置Y轴”,点击Y轴正方向上的一个点可在右侧输入实际坐标值并点击”应用坐标值”
拾取数据:
点击”开始拾取点”进入拾取模式在图片上依次点击需要数字化的点点击”停止拾取点”结束拾取
保存数据:点击”保存数据”选择格式并保存
技术特点:
精确的坐标转换:使用线性变换实现像素坐标到实际坐标的精确转换友好的用户界面:清晰的操作提示和状态显示灵活的坐标系:支持任意角度和比例的坐标系完整的数据记录:同时保存像素坐标和实际坐标
这个程序特别适合用于:
科学图表数据提取工程图纸数字化地图坐标采集实验数据记录
程序使用纯Python标准库和PIL库,无需额外安装复杂依赖,运行稳定可靠。
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk, ImageDraw
import json
import csv
import math
from dataclasses import dataclass
from typing import List, Tuple, Optional
@dataclass
class Point:
"""数据点类"""
pixel_x: float
pixel_y: float
real_x: float
real_y: float
name: str = ""
class MagnifierWindow:
"""放大镜窗口类"""
def __init__(self, parent, digitizer):
self.parent = parent
self.digitizer = digitizer
self.window = None
self.canvas = None
self.magnifier_image = None
self.magnifier_photo = None
self.magnification = 5.0 # 放大倍数
self.magnifier_size = 200 # 放大镜窗口大小
self.crosshair_color = 'red'
self.is_active = False
def create_window(self):
"""创建放大镜窗口"""
if self.window is not None:
return
self.window = tk.Toplevel(self.parent)
self.window.title("放大镜")
self.window.geometry(f"{self.magnifier_size}x{self.magnifier_size + 40}")
self.window.resizable(False, False)
# 设置窗口始终在最前面
self.window.attributes('-topmost', True)
# 创建Canvas
self.canvas = tk.Canvas(
self.window,
width=self.magnifier_size,
height=self.magnifier_size,
bg='white',
highlightthickness=1,
highlightbackground='gray'
)
self.canvas.pack()
# 添加控制面板
control_frame = ttk.Frame(self.window)
control_frame.pack(fill=tk.X)
# 放大倍数控制
ttk.Label(control_frame, text="放大倍数:").pack(side=tk.LEFT, padx=5)
self.mag_var = tk.DoubleVar(value=self.magnification)
mag_scale = ttk.Scale(
control_frame,
from_=2,
to=20,
variable=self.mag_var,
orient=tk.HORIZONTAL,
length=100,
command=self.update_magnification
)
mag_scale.pack(side=tk.LEFT, padx=5)
self.mag_label = ttk.Label(control_frame, text=f"{self.magnification:.1f}x")
self.mag_label.pack(side=tk.LEFT, padx=5)
# 关闭按钮
ttk.Button(control_frame, text="关闭", command=self.close).pack(side=tk.RIGHT, padx=5)
# 绑定窗口关闭事件
self.window.protocol("WM_DELETE_WINDOW", self.close)
def update_magnification(self, value):
"""更新放大倍数"""
self.magnification = float(value)
self.mag_label.config(text=f"{self.magnification:.1f}x")
def show(self):
"""显示放大镜"""
if not self.window:
self.create_window()
self.is_active = True
self.window.deiconify()
def hide(self):
"""隐藏放大镜"""
if self.window:
self.window.withdraw()
self.is_active = False
def close(self):
"""关闭放大镜"""
if self.window:
self.window.destroy()
self.window = None
self.canvas = None
self.is_active = False
def update(self, pixel_x, pixel_y):
"""更新放大镜内容"""
if not self.is_active or not self.canvas or not self.digitizer.image:
return
try:
# 计算源图像区域
source_size = self.magnifier_size / self.magnification
half_size = source_size / 2
# 计算裁剪区域,确保坐标有效
x1 = max(0, pixel_x - half_size)
y1 = max(0, pixel_y - half_size)
x2 = min(self.digitizer.image.width, pixel_x + half_size)
y2 = min(self.digitizer.image.height, pixel_y + half_size)
# 确保x1 < x2 和 y1 < y2
if x1 >= x2 or y1 >= y2:
return
# 裁剪图像区域
crop_box = (int(x1), int(y1), int(x2), int(y2))
cropped = self.digitizer.image.crop(crop_box)
# 放大图像
magnified = cropped.resize(
(self.magnifier_size, self.magnifier_size),
Image.Resampling.LANCZOS
)
# 转换为PhotoImage
self.magnifier_photo = ImageTk.PhotoImage(magnified)
# 清除Canvas并显示图像
self.canvas.delete("all")
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.magnifier_photo)
# 计算鼠标在裁剪区域中的相对位置
rel_x = (pixel_x - x1) * self.magnification
rel_y = (pixel_y - y1) * self.magnification
# 绘制十字准线
self.canvas.create_line(
0, rel_y, self.magnifier_size, rel_y,
fill=self.crosshair_color, width=1, dash=(2, 2)
)
self.canvas.create_line(
rel_x, 0, rel_x, self.magnifier_size,
fill=self.crosshair_color, width=1, dash=(2, 2)
)
# 绘制中心点
self.canvas.create_oval(
rel_x - 3, rel_y - 3, rel_x + 3, rel_y + 3,
fill='', outline=self.crosshair_color, width=2
)
# 显示坐标信息
if self.digitizer.is_coordinate_system_ready():
real_x, real_y = self.digitizer.pixel_to_real(pixel_x, pixel_y)
coord_text = f"({real_x:.4f}, {real_y:.4f})"
else:
coord_text = f"({pixel_x:.1f}, {pixel_y:.1f})"
self.canvas.create_text(
5, 5, text=coord_text, fill=self.crosshair_color,
anchor=tk.NW, font=('Arial', 10, 'bold')
)
except Exception as e:
# 静默处理错误,避免频繁报错
pass
class ImageDigitizer:
def __init__(self, root):
self.root = root
self.root.title("图片数字化工具 - 带放大镜功能")
self.root.geometry("1200x800")
# 状态变量
self.image = None
self.photo = None
self.displayed_image = None # 修复:改名避免与方法名冲突
self.scale_factor = 1.0
self.image_offset_x = 0
self.image_offset_y = 0
# 坐标系设置
self.origin_pixel = None # 原点像素坐标
self.x_axis_pixel = None # x轴方向点像素坐标
self.y_axis_pixel = None # y轴方向点像素坐标
# 实际坐标值
self.origin_real = (0, 0)
self.x_axis_real = (1, 0)
self.y_axis_real = (0, 1)
# 数据点列表
self.data_points: List[Point] = []
# 当前模式
self.current_mode = "normal" # normal, set_origin, set_x_axis, set_y_axis, pick_points
# 放大镜
self.magnifier = MagnifierWindow(root, self)
self.magnifier_enabled = tk.BooleanVar(value=False)
# 创建UI
self.create_widgets()
# 绑定放大镜状态变化
self.magnifier_enabled.trace('w', self.on_magnifier_toggle)
def create_widgets(self):
"""创建界面组件"""
# 创建主框架
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 左侧控制面板
control_frame = ttk.LabelFrame(main_frame, text="控制面板", width=320)
control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=5)
control_frame.pack_propagate(False)
# 文件操作
file_frame = ttk.LabelFrame(control_frame, text="文件操作")
file_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Button(file_frame, text="打开图片", command=self.open_image).pack(fill=tk.X, padx=5, pady=2)
ttk.Button(file_frame, text="保存数据", command=self.save_data).pack(fill=tk.X, padx=5, pady=2)
# 放大镜控制
magnifier_frame = ttk.LabelFrame(control_frame, text="放大镜控制")
magnifier_frame.pack(fill=tk.X, padx=5, pady=5)
# 修复:移除command参数,让复选框自动处理状态
self.magnifier_check = ttk.Checkbutton(
magnifier_frame,
text="启用放大镜",
variable=self.magnifier_enabled
)
self.magnifier_check.pack(anchor=tk.W, padx=5, pady=2)
# 添加手动按钮作为备选
ttk.Button(
magnifier_frame,
text="手动切换放大镜",
command=self.toggle_magnifier
).pack(fill=tk.X, padx=5, pady=2)
ttk.Label(magnifier_frame, text="提示:启用放大镜可提高拾取精度",
font=('Arial', 8), foreground='gray').pack(anchor=tk.W, padx=5, pady=2)
# 坐标系设置
coord_frame = ttk.LabelFrame(control_frame, text="坐标系设置")
coord_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(coord_frame, text="步骤1:设置原点").pack(anchor=tk.W, padx=5, pady=2)
ttk.Button(coord_frame, text="设置原点", command=self.set_origin_mode).pack(fill=tk.X, padx=5, pady=2)
ttk.Label(coord_frame, text="步骤2:设置X轴方向").pack(anchor=tk.W, padx=5, pady=2)
ttk.Button(coord_frame, text="设置X轴", command=self.set_x_axis_mode).pack(fill=tk.X, padx=5, pady=2)
ttk.Label(coord_frame, text="步骤3:设置Y轴方向").pack(anchor=tk.W, padx=5, pady=2)
ttk.Button(coord_frame, text="设置Y轴", command=self.set_y_axis_mode).pack(fill=tk.X, padx=5, pady=2)
# 坐标值输入
value_frame = ttk.LabelFrame(control_frame, text="实际坐标值")
value_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(value_frame, text="原点坐标:").grid(row=0, column=0, padx=5, pady=2, sticky=tk.W)
self.origin_x_var = tk.StringVar(value="0")
self.origin_y_var = tk.StringVar(value="0")
ttk.Entry(value_frame, textvariable=self.origin_x_var, width=8).grid(row=0, column=1, padx=2, pady=2)
ttk.Entry(value_frame, textvariable=self.origin_y_var, width=8).grid(row=0, column=2, padx=2, pady=2)
ttk.Label(value_frame, text="X轴点坐标:").grid(row=1, column=0, padx=5, pady=2, sticky=tk.W)
self.x_axis_x_var = tk.StringVar(value="1")
self.x_axis_y_var = tk.StringVar(value="0")
ttk.Entry(value_frame, textvariable=self.x_axis_x_var, width=8).grid(row=1, column=1, padx=2, pady=2)
ttk.Entry(value_frame, textvariable=self.x_axis_y_var, width=8).grid(row=1, column=2, padx=2, pady=2)
ttk.Label(value_frame, text="Y轴点坐标:").grid(row=2, column=0, padx=5, pady=2, sticky=tk.W)
self.y_axis_x_var = tk.StringVar(value="0")
self.y_axis_y_var = tk.StringVar(value="1")
ttk.Entry(value_frame, textvariable=self.y_axis_x_var, width=8).grid(row=2, column=1, padx=2, pady=2)
ttk.Entry(value_frame, textvariable=self.y_axis_y_var, width=8).grid(row=2, column=2, padx=2, pady=2)
ttk.Button(value_frame, text="应用坐标值", command=self.apply_coordinate_values).grid(row=3, column=0, columnspan=3, padx=5, pady=5)
# 数据拾取
pick_frame = ttk.LabelFrame(control_frame, text="数据拾取")
pick_frame.pack(fill=tk.X, padx=5, pady=5)
self.pick_button = ttk.Button(pick_frame, text="开始拾取点", command=self.toggle_pick_mode)
self.pick_button.pack(fill=tk.X, padx=5, pady=2)
ttk.Button(pick_frame, text="清除所有点", command=self.clear_points).pack(fill=tk.X, padx=5, pady=2)
ttk.Button(pick_frame, text="撤销上一个点", command=self.undo_last_point).pack(fill=tk.X, padx=5, pady=2)
# 快捷键提示
shortcut_frame = ttk.LabelFrame(control_frame, text="快捷键")
shortcut_frame.pack(fill=tk.X, padx=5, pady=5)
shortcuts = [
"空格键: 切换拾取模式",
"Ctrl+Z: 撤销上一个点",
"Delete: 清除所有点",
"M: 切换放大镜"
]
for shortcut in shortcuts:
ttk.Label(shortcut_frame, text=shortcut, font=('Arial', 8)).pack(anchor=tk.W, padx=5, pady=1)
# 数据显示
data_frame = ttk.LabelFrame(control_frame, text="拾取的数据点")
data_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 创建Treeview显示数据
columns = ('序号', 'X坐标', 'Y坐标', '像素X', '像素Y')
self.data_tree = ttk.Treeview(data_frame, columns=columns, show='headings', height=10)
for col in columns:
self.data_tree.heading(col, text=col)
if col == '序号':
self.data_tree.column(col, width=40)
else:
self.data_tree.column(col, width=60)
# 添加滚动条
scrollbar = ttk.Scrollbar(data_frame, orient=tk.VERTICAL, command=self.data_tree.yview)
self.data_tree.configure(yscrollcommand=scrollbar.set)
self.data_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 右侧图片显示区域
canvas_frame = ttk.LabelFrame(main_frame, text="图片显示")
canvas_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5)
# 创建Canvas和滚动条
self.canvas = tk.Canvas(canvas_frame, bg='white')
h_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview)
v_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=self.canvas.yview)
self.canvas.configure(xscrollcommand=h_scrollbar.set, yscrollcommand=v_scrollbar.set)
self.canvas.grid(row=0, column=0, sticky='nsew')
h_scrollbar.grid(row=1, column=0, sticky='ew')
v_scrollbar.grid(row=0, column=1, sticky='ns')
canvas_frame.grid_rowconfigure(0, weight=1)
canvas_frame.grid_columnconfigure(0, weight=1)
# 状态栏
self.status_var = tk.StringVar(value="请打开图片开始")
status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# 绑定鼠标事件
self.canvas.bind("<Button-1>", self.on_canvas_click)
self.canvas.bind("<Motion>", self.on_mouse_motion)
self.canvas.bind("<Leave>", self.on_mouse_leave)
# 绑定键盘事件
self.root.bind("<space>", lambda e: self.toggle_pick_mode())
self.root.bind("<Control-z>", lambda e: self.undo_last_point())
self.root.bind("<Delete>", lambda e: self.clear_points())
self.root.bind("<m>", lambda e: self.toggle_magnifier())
self.root.bind("<M>", lambda e: self.toggle_magnifier())
def on_magnifier_toggle(self, *args):
"""处理放大镜状态变化"""
if self.magnifier_enabled.get():
self.magnifier.show()
self.status_var.set("放大镜已启用")
else:
self.magnifier.hide()
self.status_var.set("放大镜已关闭")
def toggle_magnifier(self):
"""切换放大镜"""
current_state = self.magnifier_enabled.get()
self.magnifier_enabled.set(not current_state)
def open_image(self):
"""打开图片文件"""
file_path = filedialog.askopenfilename(
title="选择图片文件",
filetypes=[("图片文件", "*.jpg *.jpeg *.png *.bmp *.gif *.tiff"), ("所有文件", "*.*")]
)
if file_path:
try:
self.image = Image.open(file_path)
self.display_image_on_canvas() # 修复:改名避免与方法名冲突
self.status_var.set(f"已加载图片: {file_path}")
self.reset_coordinate_system()
except Exception as e:
messagebox.showerror("错误", f"无法打开图片: {str(e)}")
def display_image_on_canvas(self):
"""在Canvas上显示图片"""
if not self.image:
return
# 计算缩放比例以适应窗口
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if canvas_width > 1 and canvas_height > 1:
scale_x = canvas_width / self.image.width
scale_y = canvas_height / self.image.height
self.scale_factor = min(scale_x, scale_y, 1.0) # 不放大,只缩小
# 缩放图片
display_width = int(self.image.width * self.scale_factor)
display_height = int(self.image.height * self.scale_factor)
# 修复:使用不同的属性名
self.displayed_image = self.image.resize((display_width, display_height), Image.Resampling.LANCZOS)
self.photo = ImageTk.PhotoImage(self.displayed_image)
# 清除Canvas并显示图片
self.canvas.delete("all")
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo, tags="image")
# 更新Canvas滚动区域
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
# 重绘坐标系和数据点
self.redraw_all()
def on_canvas_click(self, event):
"""处理Canvas点击事件"""
if not self.image:
return
# 获取点击位置的像素坐标
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
# 转换为原始图片坐标
pixel_x = x / self.scale_factor
pixel_y = y / self.scale_factor
if self.current_mode == "set_origin":
self.origin_pixel = (pixel_x, pixel_y)
self.status_var.set(f"原点已设置: ({pixel_x:.1f}, {pixel_y:.1f})")
self.current_mode = "normal"
self.redraw_all()
elif self.current_mode == "set_x_axis":
self.x_axis_pixel = (pixel_x, pixel_y)
self.status_var.set(f"X轴方向已设置: ({pixel_x:.1f}, {pixel_y:.1f})")
self.current_mode = "normal"
self.redraw_all()
elif self.current_mode == "set_y_axis":
self.y_axis_pixel = (pixel_x, pixel_y)
self.status_var.set(f"Y轴方向已设置: ({pixel_x:.1f}, {pixel_y:.1f})")
self.current_mode = "normal"
self.redraw_all()
elif self.current_mode == "pick_points":
if self.is_coordinate_system_ready():
real_x, real_y = self.pixel_to_real(pixel_x, pixel_y)
point = Point(pixel_x, pixel_y, real_x, real_y)
self.data_points.append(point)
self.update_data_display()
self.redraw_all()
self.status_var.set(f"已拾取点: ({real_x:.3f}, {real_y:.3f})")
else:
messagebox.showwarning("警告", "请先设置完整的坐标系")
def on_mouse_motion(self, event):
"""处理鼠标移动事件"""
if not self.image:
return
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
pixel_x = x / self.scale_factor
pixel_y = y / self.scale_factor
# 更新放大镜
if self.magnifier_enabled.get():
self.magnifier.update(pixel_x, pixel_y)
# 更新状态栏
if self.is_coordinate_system_ready():
real_x, real_y = self.pixel_to_real(pixel_x, pixel_y)
self.status_var.set(f"像素: ({pixel_x:.1f}, {pixel_y:.1f}) | 实际: ({real_x:.4f}, {real_y:.4f})")
else:
self.status_var.set(f"像素: ({pixel_x:.1f}, {pixel_y:.1f})")
def on_mouse_leave(self, event):
"""鼠标离开Canvas时隐藏放大镜"""
if self.magnifier_enabled.get():
self.magnifier.hide()
def set_origin_mode(self):
"""进入设置原点模式"""
self.current_mode = "set_origin"
self.status_var.set("请在图片上点击设置原点")
def set_x_axis_mode(self):
"""进入设置X轴模式"""
self.current_mode = "set_x_axis"
self.status_var.set("请在图片上点击设置X轴正方向点")
def set_y_axis_mode(self):
"""进入设置Y轴模式"""
self.current_mode = "set_y_axis"
self.status_var.set("请在图片上点击设置Y轴正方向点")
def toggle_pick_mode(self):
"""切换拾取模式"""
if self.current_mode == "pick_points":
self.current_mode = "normal"
self.pick_button.config(text="开始拾取点")
self.status_var.set("拾取模式已关闭")
else:
if self.is_coordinate_system_ready():
self.current_mode = "pick_points"
self.pick_button.config(text="停止拾取点")
self.status_var.set("拾取模式已开启,点击图片拾取数据点")
else:
messagebox.showwarning("警告", "请先设置完整的坐标系")
def is_coordinate_system_ready(self):
"""检查坐标系是否已设置完整"""
return (self.origin_pixel is not None and
self.x_axis_pixel is not None and
self.y_axis_pixel is not None)
def pixel_to_real(self, pixel_x, pixel_y):
"""将像素坐标转换为实际坐标"""
if not self.is_coordinate_system_ready():
return pixel_x, pixel_y
# 获取实际坐标值
try:
ox = float(self.origin_x_var.get())
oy = float(self.origin_y_var.get())
xx = float(self.x_axis_x_var.get())
xy = float(self.x_axis_y_var.get())
yx = float(self.y_axis_x_var.get())
yy = float(self.y_axis_y_var.get())
except ValueError:
return pixel_x, pixel_y
# 计算基向量
vx = self.x_axis_pixel[0] - self.origin_pixel[0]
vy = self.x_axis_pixel[1] - self.origin_pixel[1]
wx = self.y_axis_pixel[0] - self.origin_pixel[0]
wy = self.y_axis_pixel[1] - self.origin_pixel[1]
# 计算相对于原点的向量
dx = pixel_x - self.origin_pixel[0]
dy = pixel_y - self.origin_pixel[1]
# 解线性方程组
# dx = a * vx + b * wx
# dy = a * vy + b * wy
det = vx * wy - vy * wx
if abs(det) < 1e-10:
return pixel_x, pixel_y
a = (dx * wy - dy * wx) / det
b = (vx * dy - vy * dx) / det
# 计算实际坐标
real_x = ox + a * (xx - ox) + b * (yx - ox)
real_y = oy + a * (xy - oy) + b * (yy - oy)
return real_x, real_y
def apply_coordinate_values(self):
"""应用输入的坐标值"""
try:
self.origin_real = (float(self.origin_x_var.get()), float(self.origin_y_var.get()))
self.x_axis_real = (float(self.x_axis_x_var.get()), float(self.x_axis_y_var.get()))
self.y_axis_real = (float(self.y_axis_x_var.get()), float(self.y_axis_y_var.get()))
# 重新计算所有数据点的实际坐标
for point in self.data_points:
point.real_x, point.real_y = self.pixel_to_real(point.pixel_x, point.pixel_y)
self.update_data_display()
self.status_var.set("坐标值已应用")
except ValueError:
messagebox.showerror("错误", "请输入有效的数字")
def redraw_all(self):
"""重绘所有元素"""
if not self.image:
return
# 清除除图片外的所有元素
self.canvas.delete("all")
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo, tags="image")
# 绘制坐标系
if self.origin_pixel:
x = self.origin_pixel[0] * self.scale_factor
y = self.origin_pixel[1] * self.scale_factor
self.canvas.create_oval(x-5, y-5, x+5, y+5, fill='red', outline='darkred', width=2)
self.canvas.create_text(x+10, y-10, text="原点", fill='red', anchor=tk.W)
if self.origin_pixel and self.x_axis_pixel:
x1 = self.origin_pixel[0] * self.scale_factor
y1 = self.origin_pixel[1] * self.scale_factor
x2 = self.x_axis_pixel[0] * self.scale_factor
y2 = self.x_axis_pixel[1] * self.scale_factor
self.canvas.create_line(x1, y1, x2, y2, fill='blue', width=2, arrow=tk.LAST)
self.canvas.create_text(x2+10, y2, text="X轴", fill='blue', anchor=tk.W)
if self.origin_pixel and self.y_axis_pixel:
x1 = self.origin_pixel[0] * self.scale_factor
y1 = self.origin_pixel[1] * self.scale_factor
x2 = self.y_axis_pixel[0] * self.scale_factor
y2 = self.y_axis_pixel[1] * self.scale_factor
self.canvas.create_line(x1, y1, x2, y2, fill='green', width=2, arrow=tk.LAST)
self.canvas.create_text(x2+10, y2, text="Y轴", fill='green', anchor=tk.W)
# 绘制数据点
for i, point in enumerate(self.data_points):
x = point.pixel_x * self.scale_factor
y = point.pixel_y * self.scale_factor
self.canvas.create_oval(x-3, y-3, x+3, y+3, fill='orange', outline='darkorange')
self.canvas.create_text(x+5, y-5, text=str(i+1), fill='darkorange', anchor=tk.W, font=('Arial', 8))
def update_data_display(self):
"""更新数据显示"""
# 清除现有数据
for item in self.data_tree.get_children():
self.data_tree.delete(item)
# 添加新数据
for i, point in enumerate(self.data_points):
self.data_tree.insert('', 'end', values=(
i+1,
f"{point.real_x:.4f}",
f"{point.real_y:.4f}",
f"{point.pixel_x:.1f}",
f"{point.pixel_y:.1f}"
))
def clear_points(self):
"""清除所有数据点"""
if self.data_points and messagebox.askyesno("确认", "确定要清除所有数据点吗?"):
self.data_points.clear()
self.update_data_display()
self.redraw_all()
self.status_var.set("已清除所有数据点")
def undo_last_point(self):
"""撤销上一个点"""
if self.data_points:
self.data_points.pop()
self.update_data_display()
self.redraw_all()
self.status_var.set("已撤销上一个点")
def reset_coordinate_system(self):
"""重置坐标系"""
self.origin_pixel = None
self.x_axis_pixel = None
self.y_axis_pixel = None
self.data_points.clear()
self.update_data_display()
self.redraw_all()
def save_data(self):
"""保存数据到文件"""
if not self.data_points:
messagebox.showwarning("警告", "没有数据可保存")
return
file_path = filedialog.asksaveasfilename(
title="保存数据",
defaultextension=".csv",
filetypes=[
("CSV文件", "*.csv"),
("JSON文件", "*.json"),
("文本文件", "*.txt"),
("所有文件", "*.*")
]
)
if file_path:
try:
if file_path.endswith('.json'):
self.save_as_json(file_path)
elif file_path.endswith('.csv'):
self.save_as_csv(file_path)
else:
self.save_as_txt(file_path)
self.status_var.set(f"数据已保存到: {file_path}")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
def save_as_json(self, file_path):
"""保存为JSON格式"""
data = {
'coordinate_system': {
'origin': {
'pixel': self.origin_pixel,
'real': self.origin_real
},
'x_axis': {
'pixel': self.x_axis_pixel,
'real': self.x_axis_real
},
'y_axis': {
'pixel': self.y_axis_pixel,
'real': self.y_axis_real
}
},
'points': [
{
'index': i+1,
'pixel': (p.pixel_x, p.pixel_y),
'real': (p.real_x, p.real_y),
'name': p.name
}
for i, p in enumerate(self.data_points)
]
}
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def save_as_csv(self, file_path):
"""保存为CSV格式"""
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['序号', 'X坐标', 'Y坐标', '像素X', '像素Y', '备注'])
for i, point in enumerate(self.data_points):
writer.writerow([
i+1,
f"{point.real_x:.6f}",
f"{point.real_y:.6f}",
f"{point.pixel_x:.2f}",
f"{point.pixel_y:.2f}",
point.name
])
def save_as_txt(self, file_path):
"""保存为文本格式"""
with open(file_path, 'w', encoding='utf-8') as f:
f.write("# 图片数字化数据
")
f.write(f"# 原点: 像素{self.origin_pixel} -> 实际{self.origin_real}
")
f.write(f"# X轴: 像素{self.x_axis_pixel} -> 实际{self.x_axis_real}
")
f.write(f"# Y轴: 像素{self.y_axis_pixel} -> 实际{self.y_axis_real}
")
f.write("
序号 X坐标 Y坐标 像素X 像素Y
")
for i, point in enumerate(self.data_points):
f.write(f"{i+1} {point.real_x:.6f} {point.real_y:.6f} "
f"{point.pixel_x:.2f} {point.pixel_y:.2f}
")
def main():
root = tk.Tk()
app = ImageDigitizer(root)
root.mainloop()
if __name__ == "__main__":
main()



















暂无评论内容