---
# System prepended metadata

title: 图片转文字格式 X|Y|FFFFFF 可选取像素
tags: [Python系統設計]

---

---
tags: Python系統設計
---

# 图片转文字格式 X|Y|FFFFFF 可选取像素
## image_pixel_picker
```python
import tkinter as tk
from tkinter import filedialog, messagebox
from PIL import Image, ImageTk, ImageColor
import os
import csv
from openpyxl import Workbook
from tkinter import simpledialog

# 初始化变量
selected_pixels = set()
zoom_level = 1.0 # 默认的缩放级别
MIN_ZOOM = 1.0
MAX_ZOOM = 32.0
tk_img = None  # 存放缩放后的图像
img = None
image_path = ""
pixel_size = 1  # 像素格子大小
show_rgb = True  # 默认显示RGB
selection_box = None  # 用于存储框选区域的坐标

# 打开图片函数
def open_image():
    global img, tk_img, image_path

    try:
        image_path = filedialog.askopenfilename(filetypes=[("Image files", "*.png;*.jpg;*.jpeg;*.bmp")])
        if not image_path:
            return

        reset_selection(full=True)

        # 转为 RGB 后，再转为 BMP 格式避免内存错误
        from io import BytesIO
        buffer = BytesIO()
        Image.open(image_path).convert('RGB').save(buffer, format='BMP')
        buffer.seek(0)
        img = Image.open(buffer)

        width, height = img.size
        display_width = int(width * pixel_size * zoom_level)
        display_height = int(height * pixel_size * zoom_level)
        canvas.config(scrollregion=(0, 0, display_width, display_height))

        tk_img = ImageTk.PhotoImage(img.resize((display_width, display_height), Image.NEAREST))
        canvas.create_image(0, 0, anchor='nw', image=tk_img)
        draw_grid(width, height)

    except Exception as e:
        messagebox.showerror("错误", f"无法打开图片：{e}")

# 绘制网格函数
def draw_grid(width, height):
    for y in range(height):
        for x in range(width):
            canvas.create_rectangle(
                x * pixel_size * zoom_level, y * pixel_size * zoom_level,
                (x + 1) * pixel_size * zoom_level, (y + 1) * pixel_size * zoom_level,
                outline='gray'
            )

# 计算选取的最左上角
def get_origin():
    """找到选取中最左上角的点"""
    if not selected_pixels:
        return None
    return min(selected_pixels, key=lambda p: (p[1], p[0]))  # y优先

# 重绘选取
def redraw_selection():
    global tk_img
    if img is None:
        return

    # 获取图像的原始宽度和高度
    width, height = img.size

    # 计算缩放后的宽度和高度
    display_width = int(width * pixel_size * zoom_level)
    display_height = int(height * pixel_size * zoom_level)

    # 清除 Canvas 上的所有内容（包括网格和图片）
    canvas.delete("all")

    # 缩放图像
    zoomed_img = img.resize((display_width, display_height), Image.NEAREST)
    tk_img = ImageTk.PhotoImage(zoomed_img)

    # 设置新的滚动区域，确保图像的完整显示
    canvas.config(scrollregion=(0, 0, display_width, display_height))

    # 在 Canvas 上显示缩放后的图像
    canvas.create_image(0, 0, anchor='nw', image=tk_img)

    # 绘制网格
    draw_grid(width, height)

    # 获取最左上角的坐标
    origin = get_origin()
    display_lines = []

    # 按照Y排序，再按X排序
    sorted_pixels = sorted(selected_pixels, key=lambda p: (p[1], p[0]))

    for x, y in sorted_pixels:
        rel_x = x - origin[0]
        rel_y = y - origin[1]
        r, g, b = img.getpixel((x, y))
        color = f'{b:02X}{g:02X}{r:02X}'

        # 转换为HSV
        hsv = rgb_to_hsv(r, g, b)

        # 获取显示格式
        if show_rgb:
            color_display = f"{b:02X}{g:02X}{r:02X}"
        else:
            color_display = f"{hsv[0]},{hsv[1]},{hsv[2]}"

        # 绘制红框
        canvas.create_rectangle(
            x * pixel_size * zoom_level, y * pixel_size * zoom_level,
            (x + 1) * pixel_size * zoom_level, (y + 1) * pixel_size * zoom_level,
            outline='red', width=2
        )

        display_lines.append(f"{rel_x}|{rel_y}|{color_display}")

    # 确保 (0, 0) 坐标显示
    if selected_pixels:
        text_output.delete("1.0", tk.END)
        text_output.insert(tk.END, "\n".join(display_lines))


# 处理点击事件
def on_click(event):
    if img is None:
        return

    x = event.x // pixel_size
    y = event.y // pixel_size
    coord = (x, y)

    if coord in selected_pixels:
        selected_pixels.remove(coord)  # 取消选取
    else:
        selected_pixels.add(coord)     # 新增选取

    redraw_selection()

# RGB转HSV
def rgb_to_hsv(r, g, b):
    r /= 255.0
    g /= 255.0
    b /= 255.0
    mx = max(r, g, b)
    mn = min(r, g, b)
    h = (mx + mn) / 2
    s = h
    v = mx
    return (h, s, v)

# 切换显示RGB/HSV
def toggle_rgb_hsv():
    global show_rgb
    show_rgb = not show_rgb
    redraw_selection()

# 选择保存格式并保存
def save_selection():
    if not selected_pixels or not img or not image_path:
        return

    output_path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt"), ("CSV files", "*.csv"), ("Excel files", "*.xlsx")])
    if not output_path:
        return

    # 导出为txt
    if output_path.endswith(".txt"):
        save_as_txt(output_path)

    # 导出为CSV
    elif output_path.endswith(".csv"):
        save_as_csv(output_path)

    # 导出为Excel
    elif output_path.endswith(".xlsx"):
        save_as_excel(output_path)

# 保存为TXT
def save_as_txt(output_path):
    content = text_output.get("1.0", tk.END).strip()
    if not content:
        messagebox.showinfo("提示", "没有内容可以储存。")
        return
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(content)

# 保存为CSV
def save_as_csv(output_path):
    content = text_output.get("1.0", tk.END).strip()
    if not content:
        messagebox.showinfo("提示", "没有内容可以储存。")
        return

    rows = [line.split('|') for line in content.splitlines()]
    with open(output_path, 'w', newline='', encoding='utf-8') as f:
        writer = csv.writer(f)
        writer.writerow(["X", "Y", "Color"])
        writer.writerows(rows)

# 保存为Excel
def save_as_excel(output_path):
    content = text_output.get("1.0", tk.END).strip()
    if not content:
        messagebox.showinfo("提示", "没有内容可以储存。")
        return

    wb = Workbook()
    ws = wb.active
    ws.append(["X", "Y", "Color"])

    for line in content.splitlines():
        parts = line.split('|')
        if len(parts) == 3:
            ws.append(parts)

    wb.save(output_path)

# 重置选取
def reset_selection(full=False):
    """清除选取（不重新载入图档，除非 full=True）"""
    selected_pixels.clear()
    text_output.delete("1.0", tk.END)
    canvas.delete("all")
    if img and not full:
        width, height = img.size
        canvas.create_image(0, 0, anchor='nw', image=tk_img)
        draw_grid(width, height)

# 框选功能的鼠标事件
def on_select_start(event):
    global selection_box
    # 使用 canvasx 和 canvasy 方法来获取放大后的鼠标位置，并考虑缩放比例
    x = int(canvas.canvasx(event.x) // (pixel_size * zoom_level))
    y = int(canvas.canvasy(event.y) // (pixel_size * zoom_level))
    selection_box = [x, y]

def on_select_end(event):
    global selection_box
    if selection_box:
        # 获取框选的起始和结束坐标，考虑缩放比例
        x1, y1 = selection_box
        x2, y2 = int(canvas.canvasx(event.x) // (pixel_size * zoom_level)), int(canvas.canvasy(event.y) // (pixel_size * zoom_level))

        # 如果是点击而非拖曳（小于一个格子），当作单点选择
        if abs(x2 - x1) < 1 and abs(y2 - y1) < 1:
            coord = (x1, y1)
            if coord in selected_pixels:
                selected_pixels.remove(coord)  # 取消选取
            else:
                selected_pixels.add(coord)     # 新增选取
        else:
            # 框选逻辑
            left, right = sorted([x1, x2])
            top, bottom = sorted([y1, y2])
            for x in range(left, right + 1):
                for y in range(top, bottom + 1):
                    selected_pixels.add((x, y))

        redraw_selection()
    selection_box = None
    
MAX_PIXELS = 100000000  # 最大安全像素数，可根据测试调整

def zoom(factor):
    global zoom_level
    new_zoom = zoom_level * factor

    # 计算放大后是否超过最大像素数
    if img:
        width, height = img.size
        expected_pixels = (width * new_zoom) * (height * new_zoom)
        if expected_pixels > MAX_PIXELS:
            messagebox.showwarning("警告", f"放大后图像像素数超过 {MAX_PIXELS:,}，可能导致程序崩溃，已阻止操作。")
            return

    if MIN_ZOOM <= new_zoom <= MAX_ZOOM:
        old_x = canvas.canvasx(0)
        old_y = canvas.canvasy(0)
        w = canvas.winfo_width()
        h = canvas.winfo_height()

        center_x = old_x + w / 2
        center_y = old_y + h / 2

        zoom_level = new_zoom
        redraw_selection()

        new_center_x = center_x * factor
        new_center_y = center_y * factor

        canvas.xview_moveto((new_center_x - w / 2) / (img.width * pixel_size * zoom_level))
        canvas.yview_moveto((new_center_y - h / 2) / (img.height * pixel_size * zoom_level))

       
# 放大功能
def zoom_in():
    global zoom_level
    zoom_level = min(zoom_level * 1.2, MAX_ZOOM)  # 最大放大5倍
    redraw_selection()

# 缩小功能
def zoom_out():
    global zoom_level
    zoom_level = max(zoom_level / 1.2, MIN_ZOOM)  # 最小缩小0.2倍
    redraw_selection()

# 创建主窗口
root = tk.Tk()
root.title("图像像素选择器")

# 建立可滚动的框架
canvas_frame = tk.Frame(root)
canvas_frame.pack(fill="both", expand=True)

# 水平与垂直滚动条
x_scroll = tk.Scrollbar(canvas_frame, orient="horizontal")
x_scroll.pack(side="bottom", fill="x")

y_scroll = tk.Scrollbar(canvas_frame, orient="vertical")
y_scroll.pack(side="right", fill="y")

# Canvas 本体
canvas = tk.Canvas(canvas_frame, xscrollcommand=x_scroll.set, yscrollcommand=y_scroll.set)
canvas.pack(side="left", fill="both", expand=True)

x_scroll.config(command=canvas.xview)
y_scroll.config(command=canvas.yview)

# 按钮区
btn_frame = tk.Frame(root)
btn_frame.pack()

tk.Button(btn_frame, text="打开图片", command=open_image).pack(side='left', padx=5)
tk.Button(btn_frame, text="储存选择", command=save_selection).pack(side='left', padx=5)
tk.Button(btn_frame, text="切换显示 RGB/HSV", command=toggle_rgb_hsv).pack(side='left', padx=5)
tk.Button(btn_frame, text="清除选择", command=lambda: reset_selection(full=False)).pack(side='left', padx=5)
tk.Button(btn_frame, text="放大 +", command=lambda: zoom(2)).pack(side='left', padx=5)
tk.Button(btn_frame, text="缩小 -", command=lambda: zoom(0.5)).pack(side='left', padx=5)


# 显示区域
text_output = tk.Text(root, height=10, width=50)
text_output.pack(pady=5)

# 绑定鼠标点击和框选事件
# canvas.bind("<Button-1>", on_click)
canvas.bind("<ButtonPress-1>", on_select_start)
canvas.bind("<ButtonRelease-1>", on_select_end)

root.mainloop()

```