Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import tkinter as tk
- from tkinter import filedialog, messagebox
- from PIL import Image, ImageTk, ImageDraw, ImageFont
- import os
- from collections import defaultdict
- import math
- class PixelArtGenerator:
- """
- 一個使用 Tkinter 的 GUI 應用程式,可將圖片轉換為使用限定調色盤的 Pixel Art。
- """
- def __init__(self, master):
- self.master = master
- self.master.title("Pixel Art 產生器")
- self.master.geometry("900x900")
- self.FREE_PALETTE_HEX = [
- "#000000",
- "#3c3c3c",
- "#787878",
- "#d2d2d2",
- "#ffffff",
- "#600018",
- "#ed1c24",
- "#ff7f27",
- "#f6aa09",
- "#f9dd3b",
- "#fffabc",
- "#0eb968",
- "#13e67b",
- "#87ff5e",
- "#0c816e",
- "#10aea6",
- "#13e1be",
- "#28509e",
- "#4093e4",
- "#60f7f2",
- "#6b50f6",
- "#99b1fb",
- "#780c99",
- "#aa38b9",
- "#e09ff9",
- "#cb007a",
- "#ec1f80",
- "#f38da9",
- "#684634",
- "#95682a",
- "#f8b277",
- ]
- self.NONFREE_PALETTE_HEX = [
- "#cdc59e",
- "#b3b9d1",
- "#aaaaaa",
- "#a50e1e",
- "#fa8072",
- "#e45c1a",
- "#9c8431",
- "#c5ad31",
- "#e8d45f",
- "#4a6b3a",
- "#5a944a",
- "#84c573",
- "#0f799f",
- "#bbfaf2",
- "#7dc7ff",
- "#4d31b8",
- "#4a4284",
- "#7a71c4",
- "#b5aef1",
- "#9b5249",
- "#d18078",
- "#fab6a4",
- "#dba463",
- "#7b6352",
- "#9c846b",
- "#d6b594",
- "#d18051",
- "#ffc5a5",
- "#6d643f",
- "#948c6b",
- "#333941",
- "#6d758d",
- ]
- self.free_palette_rgb = [self.hex_to_rgb(c) for c in self.FREE_PALETTE_HEX]
- self.nonfree_palette_rgb = [
- self.hex_to_rgb(c) for c in self.NONFREE_PALETTE_HEX
- ]
- self.PALETTE_HEX = []
- self.palette_rgb = []
- self.image_path = None
- self.original_image = None
- self.aspect_ratio = 1.0
- control_frame = tk.Frame(master, padx=10, pady=10)
- control_frame.pack(fill=tk.X)
- self.select_button = tk.Button(
- control_frame, text="1. 選擇圖片", command=self.select_image
- )
- self.select_button.pack(pady=5, fill=tk.X)
- self.file_label = tk.Label(
- control_frame, text="尚未選擇任何圖片", wraplength=400, justify=tk.LEFT
- )
- self.file_label.pack(pady=5)
- size_frame = tk.Frame(control_frame)
- size_frame.pack(pady=10)
- tk.Label(size_frame, text="2. 設定新尺寸 (寬 x 高):").pack(side=tk.LEFT, padx=5)
- self.width_var = tk.StringVar()
- self.height_var = tk.StringVar()
- self.width_entry = tk.Entry(size_frame, textvariable=self.width_var, width=7)
- self.width_entry.pack(side=tk.LEFT)
- self.width_entry.bind("<KeyRelease>", self.update_height)
- tk.Label(size_frame, text="x").pack(side=tk.LEFT, padx=5)
- self.height_entry = tk.Entry(size_frame, textvariable=self.height_var, width=7)
- self.height_entry.pack(side=tk.LEFT)
- self.height_entry.bind("<KeyRelease>", self.update_width)
- unlock_frame = tk.Frame(control_frame)
- unlock_frame.pack(pady=5)
- tk.Label(unlock_frame, text="解鎖顏色數量 (0-3):").pack(side=tk.LEFT, padx=5)
- self.unlock_count_var = tk.IntVar(value=0)
- self.unlock_scale = tk.Scale(
- unlock_frame,
- from_=0,
- to=3,
- orient=tk.HORIZONTAL,
- variable=self.unlock_count_var,
- length=150,
- )
- self.unlock_scale.pack(side=tk.LEFT)
- weights_frame = tk.LabelFrame(
- control_frame, text="區域權重調整", padx=10, pady=10
- )
- weights_frame.pack(pady=10, fill=tk.X)
- self.w1_var = tk.DoubleVar(value=1.0)
- self.w2_var = tk.DoubleVar(value=0.6)
- self.w3_var = tk.DoubleVar(value=0.0)
- tk.Label(weights_frame, text="1x1 (細節):").grid(row=0, column=0, sticky="w")
- tk.Scale(
- weights_frame,
- from_=0,
- to=2.0,
- resolution=0.1,
- orient=tk.HORIZONTAL,
- variable=self.w1_var,
- length=200,
- ).grid(row=0, column=1)
- tk.Label(weights_frame, text="2x2 (區域):").grid(row=1, column=0, sticky="w")
- tk.Scale(
- weights_frame,
- from_=0,
- to=2.0,
- resolution=0.1,
- orient=tk.HORIZONTAL,
- variable=self.w2_var,
- length=200,
- ).grid(row=1, column=1)
- tk.Label(weights_frame, text="3x3 (整體):").grid(row=2, column=0, sticky="w")
- tk.Scale(
- weights_frame,
- from_=0,
- to=2.0,
- resolution=0.1,
- orient=tk.HORIZONTAL,
- variable=self.w3_var,
- length=200,
- ).grid(row=2, column=1)
- advanced_frame = tk.LabelFrame(
- control_frame, text="進階色彩選項", padx=10, pady=6
- )
- advanced_frame.pack(pady=5, fill=tk.X)
- tk.Label(advanced_frame, text="色差公式:").grid(row=0, column=0, sticky="w")
- self.color_distance_mode_var = tk.StringVar(value="Lab (CIEDE2000)")
- distance_menu = tk.OptionMenu(
- advanced_frame,
- self.color_distance_mode_var,
- "Euclidean",
- "Lab (CIE76)",
- "Lab (CIEDE2000)",
- )
- distance_menu.grid(row=0, column=1, sticky="w")
- self.gamma_linearize_var = tk.BooleanVar(value=False)
- gamma_check = tk.Checkbutton(
- advanced_frame,
- text="使用線性空間進行區域平均 (Gamma 校正)",
- variable=self.gamma_linearize_var,
- )
- gamma_check.grid(row=1, column=0, columnspan=2, sticky="w", pady=2)
- tk.Label(advanced_frame, text="抖色模式:").grid(row=2, column=0, sticky="w")
- self.dithering_mode_var = tk.StringVar(value="Ordered")
- dither_menu = tk.OptionMenu(
- advanced_frame,
- self.dithering_mode_var,
- "None",
- "Ordered",
- "Floyd-Steinberg",
- )
- dither_menu.grid(row=2, column=1, sticky="w")
- self.generate_button = tk.Button(
- control_frame, text="3. 產生 Pixel Art", command=self.generate_pixel_art
- )
- self.generate_button.pack(pady=10, fill=tk.X)
- image_frame = tk.Frame(master, padx=10, pady=10)
- image_frame.pack(fill=tk.BOTH, expand=True)
- image_frame.columnconfigure(0, weight=1)
- image_frame.columnconfigure(1, weight=1)
- image_frame.rowconfigure(1, weight=1)
- tk.Label(image_frame, text="原始圖片預覽", font=("Arial", 12)).grid(
- row=0, column=0, pady=5
- )
- tk.Label(image_frame, text="Pixel Art 結果", font=("Arial", 12)).grid(
- row=0, column=1, pady=5
- )
- self.original_label = tk.Label(image_frame, bg="lightgrey")
- self.original_label.grid(row=1, column=0, sticky="nsew", padx=5)
- self.result_label = tk.Label(image_frame, bg="lightgrey")
- self.result_label.grid(row=1, column=1, sticky="nsew", padx=5)
- self.dither_matrix = [
- [1 / 10, 8 / 10, 4 / 10],
- [6 / 10, 10 / 10, 2 / 10],
- [5 / 10, 3 / 10, 7 / 10],
- ]
- self._palette_lab = None
- def hex_to_rgb(self, hex_color: str) -> tuple:
- hex_color = hex_color.lstrip("#")
- return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
- def get_text_color(self, background_rgb: tuple) -> tuple:
- r, g, b = background_rgb
- luminance = 0.299 * r + 0.587 * g + 0.114 * b
- return (255, 255, 255) if luminance < 128 else (0, 0, 0)
- def _find_best_unlocks(self, small_image: Image, k: int) -> list:
- if k == 0 or not self.nonfree_palette_rgb:
- return []
- rgb_image = small_image.convert("RGB")
- unique_pixels = rgb_image.getcolors(rgb_image.size[0] * rgb_image.size[1])
- if not unique_pixels:
- return []
- improvement_scores = defaultdict(float)
- nonfree_rgb_to_hex = {
- rgb: hex_val
- for hex_val, rgb in zip(self.NONFREE_PALETTE_HEX, self.nonfree_palette_rgb)
- }
- def dist_sq(c1, c2):
- return (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2
- for count, pixel_rgb in unique_pixels:
- min_dist_free_sq = min(
- dist_sq(pixel_rgb, free_c) for free_c in self.free_palette_rgb
- )
- best_nonfree_c = min(
- self.nonfree_palette_rgb, key=lambda c: dist_sq(pixel_rgb, c)
- )
- min_dist_nonfree_sq = dist_sq(pixel_rgb, best_nonfree_c)
- if min_dist_nonfree_sq < min_dist_free_sq:
- improvement = (min_dist_free_sq - min_dist_nonfree_sq) * count
- best_nonfree_hex = nonfree_rgb_to_hex[best_nonfree_c]
- improvement_scores[best_nonfree_hex] += improvement
- sorted_colors = sorted(
- improvement_scores.items(), key=lambda item: item[1], reverse=True
- )
- return [color_hex for color_hex, score in sorted_colors[:k]]
- def update_height(self, event=None):
- try:
- w = int(self.width_var.get())
- h = int(w / self.aspect_ratio)
- self.height_var.set(str(h))
- except (ValueError, ZeroDivisionError):
- self.height_var.set("")
- def update_width(self, event=None):
- try:
- h = int(self.height_var.get())
- w = int(h * self.aspect_ratio)
- self.width_var.set(str(w))
- except (ValueError, ZeroDivisionError):
- self.width_var.set("")
- def _create_integral_image(self, image: Image):
- """建立 R/G/B 的積分影像 (可選線性化)。
- 若啟用 gamma 線性化,則在建立積分時將 sRGB 轉線性 (0..1) 再累積,回傳 float 陣列。"""
- w, h = image.size
- pixels = list(image.getdata())
- linearize = self.gamma_linearize_var.get()
- integral_r = [[0.0] * (w + 1) for _ in range(h + 1)]
- integral_g = [[0.0] * (w + 1) for _ in range(h + 1)]
- integral_b = [[0.0] * (w + 1) for _ in range(h + 1)]
- srgb_to_lin = self._srgb_to_linear_component
- for y in range(h):
- for x in range(w):
- r, g, b, a = pixels[y * w + x]
- if linearize:
- r_lin = srgb_to_lin(r / 255.0)
- g_lin = srgb_to_lin(g / 255.0)
- b_lin = srgb_to_lin(b / 255.0)
- rv, gv, bv = r_lin, g_lin, b_lin
- else:
- rv, gv, bv = r, g, b
- integral_r[y + 1][x + 1] = (
- rv + integral_r[y][x + 1] + integral_r[y + 1][x] - integral_r[y][x]
- )
- integral_g[y + 1][x + 1] = (
- gv + integral_g[y][x + 1] + integral_g[y + 1][x] - integral_g[y][x]
- )
- integral_b[y + 1][x + 1] = (
- bv + integral_b[y][x + 1] + integral_b[y + 1][x] - integral_b[y][x]
- )
- return integral_r, integral_g, integral_b
- def _get_block_average(self, integrals, x, y, size, w, h):
- ir, ig, ib = integrals
- if size % 2 != 0:
- half = size // 2
- x1, y1 = max(0, x - half), max(0, y - half)
- x2, y2 = min(w - 1, x + half), min(h - 1, y + half)
- else:
- half = size // 2
- x1, y1 = max(0, x - half + 1), max(0, y - half + 1)
- x2, y2 = min(w - 1, x + half), min(h - 1, y + half)
- area = (x2 - x1 + 1) * (y2 - y1 + 1)
- if area == 0:
- return 0, 0, 0
- x1, y1, x2, y2 = x1 + 1, y1 + 1, x2 + 1, y2 + 1
- sum_r = ir[y2][x2] - ir[y1 - 1][x2] - ir[y2][x1 - 1] + ir[y1 - 1][x1 - 1]
- sum_g = ig[y2][x2] - ig[y1 - 1][x2] - ig[y2][x1 - 1] + ig[y1 - 1][x1 - 1]
- sum_b = ib[y2][x2] - ib[y1 - 1][x2] - ib[y2][x1 - 1] + ib[y1 - 1][x1 - 1]
- if self.gamma_linearize_var.get():
- return sum_r / area, sum_g / area, sum_b / area
- return int(sum_r / area), int(sum_g / area), int(sum_b / area)
- def _srgb_to_linear_component(self, c: float) -> float:
- if c <= 0.04045:
- return c / 12.92
- return ((c + 0.055) / 1.055) ** 2.4
- def _linear_to_srgb_component(self, c: float) -> float:
- if c <= 0.0031308:
- return 12.92 * c
- return 1.055 * (c ** (1 / 2.4)) - 0.055
- def _rgb_to_lab(self, r: float, g: float, b: float, already_linear=False):
- """將 RGB 轉 Lab (D65);r,g,b 可為 0..255 (sRGB) 或線性 0..1 (already_linear=True)。"""
- if not already_linear:
- r_lin = self._srgb_to_linear_component(r / 255.0)
- g_lin = self._srgb_to_linear_component(g / 255.0)
- b_lin = self._srgb_to_linear_component(b / 255.0)
- else:
- r_lin, g_lin, b_lin = r, g, b
- X = r_lin * 0.4124564 + g_lin * 0.3575761 + b_lin * 0.1804375
- Y = r_lin * 0.2126729 + g_lin * 0.7151522 + b_lin * 0.0721750
- Z = r_lin * 0.0193339 + g_lin * 0.1191920 + b_lin * 0.9503041
- Xn, Yn, Zn = 0.95047, 1.0, 1.08883
- x, y, z = X / Xn, Y / Yn, Z / Zn
- def f(t):
- return t ** (1 / 3) if t > 0.008856 else (7.787 * t + 16 / 116)
- fx, fy, fz = f(x), f(y), f(z)
- L = 116 * fy - 16
- a = 500 * (fx - fy)
- b = 200 * (fy - fz)
- return L, a, b
- def _prepare_palette(self):
- """根據目前設定預先計算 palette 對應的 Lab / 線性值。"""
- mode = self.color_distance_mode_var.get()
- self._palette_lab = None
- if mode.startswith("Lab"):
- self._palette_lab = [
- self._rgb_to_lab(r, g, b) for (r, g, b) in self.palette_rgb
- ]
- def _color_distance_sq(self, c1, c2):
- """根據選擇的色差公式回傳平方距離 (避免多次開根)。"""
- mode = self.color_distance_mode_var.get()
- if mode == "Euclidean":
- return (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2
- if mode == "Lab (CIE76)":
- return (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2
- return self._lab_ciede2000(c1, c2) ** 2
- def _convert_sample_to_space(self, rgb_tuple, linear_source=False):
- """將 (r,g,b) 轉為距離計算所需的空間表示。
- 若為 Lab 模式則輸出 (L,a,b),否則維持原值 (可能是 sRGB int 或線性 float)。"""
- mode = self.color_distance_mode_var.get()
- r, g, b = rgb_tuple
- if mode.startswith("Lab"):
- if linear_source:
- return self._rgb_to_lab(r, g, b, already_linear=True)
- return self._rgb_to_lab(r, g, b)
- return rgb_tuple
- def _lab_ciede2000(self, lab1, lab2):
- """計算 CIEDE2000 ΔE (簡化實作,資料來源: 標準公式)。"""
- L1, a1, b1 = lab1
- L2, a2, b2 = lab2
- avg_L = (L1 + L2) / 2.0
- C1 = math.sqrt(a1 * a1 + b1 * b1)
- C2 = math.sqrt(a2 * a2 + b2 * b2)
- avg_C = (C1 + C2) / 2.0
- G = 0.5 * (1 - math.sqrt((avg_C**7) / (avg_C**7 + 25**7))) if avg_C != 0 else 0
- a1p = (1 + G) * a1
- a2p = (1 + G) * a2
- C1p = math.sqrt(a1p * a1p + b1 * b1)
- C2p = math.sqrt(a2p * a2p + b2 * b2)
- avg_Cp = (C1p + C2p) / 2.0
- def _hp(ap, b):
- if ap == 0 and b == 0:
- return 0
- h = math.degrees(math.atan2(b, ap))
- return h + 360 if h < 0 else h
- h1p = _hp(a1p, b1)
- h2p = _hp(a2p, b2)
- if abs(h1p - h2p) > 180:
- avg_hp = (h1p + h2p + 360) / 2.0
- else:
- avg_hp = (h1p + h2p) / 2.0
- T = (
- 1
- - 0.17 * math.cos(math.radians(avg_hp - 30))
- + 0.24 * math.cos(math.radians(2 * avg_hp))
- + 0.32 * math.cos(math.radians(3 * avg_hp + 6))
- - 0.20 * math.cos(math.radians(4 * avg_hp - 63))
- )
- dhp = h2p - h1p
- if abs(dhp) > 180:
- dhp += 360 if h2p <= h1p else -360
- dLp = L2 - L1
- dCp = C2p - C1p
- dHp = (
- 2 * math.sqrt(C1p * C2p) * math.sin(math.radians(dhp / 2.0))
- if C1p * C2p != 0
- else 0
- )
- SL = 1 + (0.015 * (avg_L - 50) ** 2) / math.sqrt(20 + (avg_L - 50) ** 2)
- SC = 1 + 0.045 * avg_Cp
- SH = 1 + 0.015 * avg_Cp * T
- delta_ro = 30 * math.exp(-(((avg_hp - 275) / 25) ** 2))
- RC = 2 * math.sqrt((avg_Cp**7) / (avg_Cp**7 + 25**7)) if avg_Cp != 0 else 0
- RT = -math.sin(math.radians(2 * delta_ro)) * RC
- dE = math.sqrt(
- (dLp / SL) ** 2
- + (dCp / SC) ** 2
- + (dHp / SH) ** 2
- + RT * (dCp / SC) * (dHp / SH)
- )
- return dE
- def select_image(self):
- path = filedialog.askopenfilename(
- filetypes=[("Image Files", "*.png *.jpg *.jpeg *.bmp *.gif")]
- )
- if not path:
- return
- self.image_path = path
- self.original_image = Image.open(self.image_path).convert("RGBA")
- self.file_label.config(text=f"已選擇: {os.path.basename(self.image_path)}")
- w, h = self.original_image.size
- self.aspect_ratio = w / h if h > 0 else 1.0
- self.width_var.set(str(w))
- self.height_var.set(str(h))
- self.display_image(self.original_image, self.original_label)
- self.result_label.config(image="")
- self.result_label.image = None
- def generate_pixel_art(self):
- if not self.original_image:
- messagebox.showerror("錯誤", "請先選擇一張圖片!")
- return
- try:
- target_w = int(self.width_var.get())
- target_h = int(self.height_var.get())
- if target_w <= 0 or target_h <= 0:
- raise ValueError
- except ValueError:
- messagebox.showerror("錯誤", "請輸入有效的正整數作為寬度和高度!")
- return
- small_image = self.original_image.resize(
- (target_w, target_h), Image.Resampling.NEAREST
- )
- unlock_k = self.unlock_count_var.get()
- best_unlocks_hex = self._find_best_unlocks(small_image, unlock_k)
- self.PALETTE_HEX = self.FREE_PALETTE_HEX + best_unlocks_hex
- self.palette_rgb = [self.hex_to_rgb(c) for c in self.PALETTE_HEX]
- if best_unlocks_hex:
- unlocked_str = "\n".join(best_unlocks_hex)
- messagebox.showinfo(
- "顏色已解鎖",
- f"已為您自動選擇並解鎖 {len(best_unlocks_hex)} 種顏色:\n{unlocked_str}",
- )
- w1, w2, w3 = self.w1_var.get(), self.w2_var.get(), self.w3_var.get()
- dithering_mode = self.dithering_mode_var.get()
- self._prepare_palette()
- integrals = self._create_integral_image(small_image)
- final_small_image = Image.new("RGBA", (target_w, target_h))
- use_fs = dithering_mode == "Floyd-Steinberg"
- gamma_lin = self.gamma_linearize_var.get()
- if use_fs:
- work = []
- for y in range(target_h):
- row = []
- for x in range(target_w):
- r, g, b, a = small_image.getpixel((x, y))
- if a < 128:
- row.append([0.0, 0.0, 0.0, 0.0])
- else:
- r_lin = self._srgb_to_linear_component(r / 255.0)
- g_lin = self._srgb_to_linear_component(g / 255.0)
- b_lin = self._srgb_to_linear_component(b / 255.0)
- row.append([r_lin, g_lin, b_lin, 1.0])
- work.append(row)
- def palette_color_space():
- mode = self.color_distance_mode_var.get()
- if mode.startswith("Lab"):
- return self._palette_lab
- return self.palette_rgb
- palette_space_values = palette_color_space()
- def to_space(rgb_tuple, is_linear=False):
- mode = self.color_distance_mode_var.get()
- r, g, b = rgb_tuple
- if mode.startswith("Lab"):
- if is_linear:
- return self._rgb_to_lab(r, g, b, already_linear=True)
- else:
- return self._rgb_to_lab(r, g, b, already_linear=False)
- else:
- if is_linear:
- return (r * 255.0, g * 255.0, b * 255.0)
- else:
- return rgb_tuple
- if not use_fs:
- for y in range(target_h):
- for x in range(target_w):
- r, g, b, a = small_image.getpixel((x, y))
- if a < 128:
- final_small_image.putpixel((x, y), (0, 0, 0, 0))
- continue
- avg_2x2 = self._get_block_average(
- integrals, x, y, 2, target_w, target_h
- )
- avg_3x3 = self._get_block_average(
- integrals, x, y, 3, target_w, target_h
- )
- if gamma_lin:
- r_lin = self._srgb_to_linear_component(r / 255.0)
- g_lin = self._srgb_to_linear_component(g / 255.0)
- b_lin = self._srgb_to_linear_component(b / 255.0)
- sample_1x1 = to_space((r_lin, g_lin, b_lin), is_linear=True)
- sample_2x2 = to_space(avg_2x2, is_linear=True)
- sample_3x3 = to_space(avg_3x3, is_linear=True)
- else:
- sample_1x1 = to_space((r, g, b), is_linear=False)
- sample_2x2 = to_space(avg_2x2, is_linear=False)
- sample_3x3 = to_space(avg_3x3, is_linear=False)
- color_losses = []
- for idx, color in enumerate(self.palette_rgb):
- palette_sample = palette_space_values[idx]
- loss_1 = self._color_distance_sq(sample_1x1, palette_sample)
- loss_2 = self._color_distance_sq(sample_2x2, palette_sample)
- loss_3 = self._color_distance_sq(sample_3x3, palette_sample)
- total_loss = w1 * loss_1 + w2 * loss_2 + w3 * loss_3
- color_losses.append((total_loss, color))
- color_losses.sort(key=lambda x: x[0])
- best_color = color_losses[0][1]
- final_color = best_color
- if dithering_mode == "Ordered" and len(color_losses) > 1:
- second_color = color_losses[1][1]
- def dist_sq_rgb(c1, c2):
- return (
- (c1[0] - c2[0]) ** 2
- + (c1[1] - c2[1]) ** 2
- + (c1[2] - c2[2]) ** 2
- )
- total_dist = math.sqrt(dist_sq_rgb(best_color, second_color))
- if total_dist > 0:
- dist_to_best = math.sqrt(dist_sq_rgb((r, g, b), best_color))
- ratio = dist_to_best / total_dist
- threshold = self.dither_matrix[y % 3][x % 3]
- if ratio > threshold:
- final_color = second_color
- final_small_image.putpixel((x, y), final_color + (255,))
- else:
- for y in range(target_h):
- for x in range(target_w):
- r_lin, g_lin, b_lin, a_lin = work[y][x]
- if a_lin == 0.0:
- final_small_image.putpixel((x, y), (0, 0, 0, 0))
- continue
- sample_current = to_space((r_lin, g_lin, b_lin), is_linear=True)
- color_losses = []
- for idx, color in enumerate(self.palette_rgb):
- palette_sample = palette_space_values[idx]
- loss = self._color_distance_sq(sample_current, palette_sample)
- color_losses.append((loss, color))
- color_losses.sort(key=lambda x: x[0])
- best_color = color_losses[0][1]
- final_small_image.putpixel((x, y), best_color + (255,))
- q_r_lin = self._srgb_to_linear_component(best_color[0] / 255.0)
- q_g_lin = self._srgb_to_linear_component(best_color[1] / 255.0)
- q_b_lin = self._srgb_to_linear_component(best_color[2] / 255.0)
- err_r = r_lin - q_r_lin
- err_g = g_lin - q_g_lin
- err_b = b_lin - q_b_lin
- def add_err(xx, yy, fr):
- if 0 <= xx < target_w and 0 <= yy < target_h:
- if work[yy][xx][3] > 0:
- work[yy][xx][0] += err_r * fr
- work[yy][xx][1] += err_g * fr
- work[yy][xx][2] += err_b * fr
- work[yy][xx][0] = max(0.0, min(1.0, work[yy][xx][0]))
- work[yy][xx][1] = max(0.0, min(1.0, work[yy][xx][1]))
- work[yy][xx][2] = max(0.0, min(1.0, work[yy][xx][2]))
- add_err(x + 1, y, 7 / 16)
- add_err(x - 1, y + 1, 3 / 16)
- add_err(x, y + 1, 5 / 16)
- add_err(x + 1, y + 1, 1 / 16)
- scale = 12
- large_w, large_h = target_w * scale, target_h * scale
- annotated_image = Image.new("RGBA", (large_w, large_h))
- draw = ImageDraw.Draw(annotated_image)
- preview_image = final_small_image.resize(
- (large_w, large_h), Image.Resampling.NEAREST
- )
- try:
- font = ImageFont.truetype("arial.ttf", size=max(8, scale - 4))
- except IOError:
- font = ImageFont.load_default()
- for x in range(target_w):
- for y in range(target_h):
- r, g, b, a = final_small_image.getpixel((x, y))
- if a > 128:
- color_index = self.palette_rgb.index((r, g, b))
- text_color = self.get_text_color((r, g, b))
- label_text = str(color_index + 1)
- bbox = draw.textbbox((0, 0), label_text, font=font)
- text_width = bbox[2] - bbox[0]
- text_height = bbox[3] - bbox[1]
- top_left = (x * scale, y * scale)
- text_x = top_left[0] + (scale - text_width) / 2
- text_y = top_left[1] + (scale - text_height) / 2
- draw.rectangle(
- [top_left, (top_left[0] + scale, top_left[1] + scale)],
- fill=(r, g, b, 255),
- )
- draw.text((text_x, text_y), label_text, font=font, fill=text_color)
- grid_line_color = self.hex_to_rgb("#787878") + (255,)
- for x_ in range(1, target_w):
- draw.line(
- [(x_ * scale, 0), (x_ * scale, large_h)], fill=grid_line_color, width=1
- )
- for y_ in range(1, target_h):
- draw.line(
- [(0, y_ * scale), (large_w, y_ * scale)], fill=grid_line_color, width=1
- )
- try:
- base, _ = os.path.splitext(self.image_path)
- preview_path = f"{base}_preview.png"
- preview_image.save(preview_path, "PNG")
- output_path = f"{base}_pixel.png"
- annotated_image.save(output_path, "PNG")
- messagebox.showinfo(
- "成功",
- f"圖片已成功儲存至:\n預覽圖: {preview_path}\n像素圖: {output_path}",
- )
- except Exception as e:
- messagebox.showerror("儲存失敗", f"無法儲存檔案:\n{e}")
- self.display_image(preview_image, self.result_label)
- def display_image(self, img: Image, label: tk.Label):
- label_w, label_h = 400, 400
- display_img = img.copy()
- display_img.thumbnail((label_w, label_h), Image.Resampling.NEAREST)
- tk_image = ImageTk.PhotoImage(display_img)
- label.config(image=tk_image)
- label.image = tk_image
- if __name__ == "__main__":
- root = tk.Tk()
- app = PixelArtGenerator(root)
- root.mainloop()
Add Comment
Please, Sign In to add comment