『欠片幻彩』は、指定したフォルダ内の画像を読み込み、縮小して並べることで、モザイクアート風の1枚画像を作成するツールです。
>>GUI外観 >>使い方以下は『欠片幻彩』最新版 のソースコード全文です:
import os
import random
from datetime import datetime
import tkinter as tk
from tkinter import filedialog, colorchooser, messagebox
from PIL import Image, ImageTk, ImageOps
class ToolTip:
def __init__(self, widget, text=""):
self.widget = widget
self.text = text
self.tip_window = None
widget.bind("<Enter>", self.show_tip)
widget.bind("<Leave>", self.hide_tip)
def show_tip(self, event=None):
if self.tip_window or not self.text:
return
x = self.widget.winfo_rootx() + 20
y = self.widget.winfo_rooty() + 20
self.tip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry(f"+{x}+{y}")
label = tk.Label(
tw,
text=self.text,
justify="left",
background="#ffffe0",
relief="solid",
borderwidth=1,
font=("Noto Sans JP", 9)
)
label.pack(ipadx=4, ipady=2)
def hide_tip(self, event=None):
if self.tip_window:
self.tip_window.destroy()
self.tip_window = None
class MosaicApp:
def __init__(self, root):
self.root = root
self.root.title("フォトモザイクメーカー - 欠片幻彩")
self.width_var = tk.IntVar(value=1920)
self.height_var = tk.IntVar(value=1080)
self.scale_var = tk.DoubleVar(value=0.1)
self.bg_color = "#ffffff"
self.transparent_bg_var = tk.BooleanVar(value=False)
self.folder_path = ""
self.generated_img = None
self.format_var = tk.StringVar(value="PNG")
self.arrangement_var = tk.StringVar(value="random")
self.sort_order_var = tk.StringVar(value="asc")
self.same_size_crop_var = tk.BooleanVar(value=False)
self.crop_basis_var = tk.StringVar(value="first")
self.crop_width_var = tk.IntVar(value=200)
self.crop_height_var = tk.IntVar(value=200)
main_frame = tk.Frame(root, padx=10, pady=10)
main_frame.pack(fill="both", expand=True)
left_frame = tk.LabelFrame(main_frame, text="設定", padx=10, pady=10)
left_frame.pack(side="left", fill="y")
tk.Label(left_frame, text="画像サイズ:").grid(row=0, column=0, sticky="w")
tk.Entry(left_frame, textvariable=self.width_var, width=6).grid(row=0, column=1)
tk.Label(left_frame, text="x").grid(row=0, column=2)
tk.Entry(left_frame, textvariable=self.height_var, width=6).grid(row=0, column=3)
self.bg_label = tk.Label(left_frame, text="背景色:")
self.bg_label.grid(row=1, column=0, sticky="w")
self.bg_button = tk.Button(left_frame, text="選択", command=self.choose_color)
self.bg_button.grid(row=1, column=1, sticky="w")
self.color_preview = tk.Label(left_frame, bg=self.bg_color, width=4, relief="sunken")
self.color_preview.grid(row=1, column=2, columnspan=2, sticky="w")
self.transparent_check = tk.Checkbutton(
left_frame,
text="背景を透明に",
variable=self.transparent_bg_var,
command=self.update_bg_controls
)
self.transparent_check.grid(row=2, column=0, columnspan=4, sticky="w", pady=(2, 5))
tk.Button(left_frame, text="画像フォルダ選択", command=self.choose_folder).grid(
row=3, column=0, columnspan=4, pady=(5, 0), sticky="w"
)
self.folder_label = tk.Label(
left_frame, text="未選択", wraplength=220, fg="gray", anchor="w", justify="left"
)
self.folder_label.grid(row=4, column=0, columnspan=4, sticky="w", pady=(2, 5))
self.folder_tooltip = ToolTip(self.folder_label, "")
tk.Label(left_frame, text="縮小率:").grid(row=5, column=0, sticky="w")
tk.Entry(left_frame, textvariable=self.scale_var, width=6).grid(row=5, column=1, sticky="w")
tk.Label(left_frame, text="画像群の配列:").grid(row=6, column=0, sticky="w")
self.arrangement_random_radio = tk.Radiobutton(
left_frame,
text="ランダム",
variable=self.arrangement_var,
value="random",
command=self.update_arrangement_controls
)
self.arrangement_random_radio.grid(row=6, column=1, sticky="w")
self.arrangement_aligned_radio = tk.Radiobutton(
left_frame,
text="整列",
variable=self.arrangement_var,
value="aligned",
command=self.update_arrangement_controls
)
self.arrangement_aligned_radio.grid(row=6, column=2, sticky="w")
self.sort_order_label = tk.Label(left_frame, text="整列順:")
self.sort_order_label.grid(row=7, column=0, sticky="w")
self.sort_asc_radio = tk.Radiobutton(
left_frame,
text="ファイル名昇順",
variable=self.sort_order_var,
value="asc"
)
self.sort_asc_radio.grid(row=7, column=1, sticky="w")
self.sort_desc_radio = tk.Radiobutton(
left_frame,
text="ファイル名降順",
variable=self.sort_order_var,
value="desc"
)
self.sort_desc_radio.grid(row=7, column=2, sticky="w")
self.sort_random_radio = tk.Radiobutton(
left_frame,
text="ランダム",
variable=self.sort_order_var,
value="random"
)
self.sort_random_radio.grid(row=7, column=3, sticky="w")
self.same_size_crop_check = tk.Checkbutton(
left_frame,
text="同一サイズでトリミング",
variable=self.same_size_crop_var,
command=self.update_arrangement_controls
)
self.same_size_crop_check.grid(row=8, column=0, columnspan=4, sticky="w", pady=(2, 2))
self.crop_basis_label = tk.Label(left_frame, text="トリミング基準:")
self.crop_basis_label.grid(row=9, column=0, sticky="w")
self.crop_basis_first_radio = tk.Radiobutton(
left_frame,
text="先頭画像基準",
variable=self.crop_basis_var,
value="first",
command=self.update_arrangement_controls
)
self.crop_basis_first_radio.grid(row=9, column=1, sticky="w")
self.crop_basis_min_radio = tk.Radiobutton(
left_frame,
text="最小画像基準",
variable=self.crop_basis_var,
value="min",
command=self.update_arrangement_controls
)
self.crop_basis_min_radio.grid(row=9, column=2, sticky="w")
self.crop_basis_max_radio = tk.Radiobutton(
left_frame,
text="最大画像基準",
variable=self.crop_basis_var,
value="max",
command=self.update_arrangement_controls
)
self.crop_basis_max_radio.grid(row=9, column=3, sticky="w")
self.crop_basis_custom_radio = tk.Radiobutton(
left_frame,
text="指定サイズ",
variable=self.crop_basis_var,
value="custom",
command=self.update_arrangement_controls
)
self.crop_basis_custom_radio.grid(row=10, column=1, sticky="w")
self.crop_size_label = tk.Label(left_frame, text="指定サイズ:")
self.crop_size_label.grid(row=10, column=2, sticky="e")
self.crop_width_entry = tk.Entry(left_frame, textvariable=self.crop_width_var, width=6)
self.crop_width_entry.grid(row=10, column=3, sticky="w")
self.crop_size_x_label = tk.Label(left_frame, text="x")
self.crop_size_x_label.grid(row=10, column=3, padx=(48, 0), sticky="w")
self.crop_height_entry = tk.Entry(left_frame, textvariable=self.crop_height_var, width=6)
self.crop_height_entry.grid(row=10, column=3, padx=(68, 0), sticky="w")
tk.Label(left_frame, text="保存形式:").grid(row=11, column=0, sticky="w")
format_menu = tk.OptionMenu(left_frame, self.format_var, "PNG", "JPG")
format_menu.config(width=8)
format_menu.grid(row=11, column=1, sticky="w")
tk.Button(left_frame, text="生成", command=self.generate, width=10).grid(
row=12, column=0, columnspan=2, pady=5
)
tk.Button(left_frame, text="保存", command=self.save, width=10).grid(
row=12, column=2, columnspan=2, pady=5
)
right_frame = tk.LabelFrame(main_frame, text="プレビュー", padx=10, pady=10)
right_frame.pack(side="right", fill="both", expand=True)
self.preview_label = tk.Label(right_frame)
self.preview_label.pack(expand=True)
bottom_frame = tk.Frame(root)
bottom_frame.pack(side="bottom", pady=(0, 6))
version_label = tk.Label(bottom_frame, text="ver.3.0.4", fg="grey", font=("Noto Sans JP", 8))
version_label.pack(side="left")
help_label = tk.Label(
bottom_frame,
text=" ヘルプ",
fg="blue",
cursor="hand2",
font=("Noto Sans JP", 8, "underline")
)
help_label.pack(side="left")
help_label.bind("<Button-1>", self.show_help)
self.update_bg_controls()
self.update_arrangement_controls()
def choose_color(self):
if self.transparent_bg_var.get():
return
color = colorchooser.askcolor(title="背景色を選択")
if color[1]:
self.bg_color = color[1]
self.color_preview.config(bg=self.bg_color)
def update_bg_controls(self):
if self.transparent_bg_var.get():
self.bg_label.config(fg="gray")
self.bg_button.config(state="disabled")
self.color_preview.config(bg="#d9d9d9")
else:
self.bg_label.config(fg="black")
self.bg_button.config(state="normal")
self.color_preview.config(bg=self.bg_color)
def update_arrangement_controls(self):
is_aligned = self.arrangement_var.get() == "aligned"
state_aligned = "normal" if is_aligned else "disabled"
fg_aligned = "black" if is_aligned else "gray"
self.sort_order_label.config(fg=fg_aligned)
self.sort_asc_radio.config(state=state_aligned)
self.sort_desc_radio.config(state=state_aligned)
self.sort_random_radio.config(state=state_aligned)
self.same_size_crop_check.config(state=state_aligned)
is_crop_enabled = is_aligned and self.same_size_crop_var.get()
state_crop = "normal" if is_crop_enabled else "disabled"
fg_crop = "black" if is_crop_enabled else "gray"
self.crop_basis_label.config(fg=fg_crop)
self.crop_basis_first_radio.config(state=state_crop)
self.crop_basis_min_radio.config(state=state_crop)
self.crop_basis_max_radio.config(state=state_crop)
self.crop_basis_custom_radio.config(state=state_crop)
is_custom = is_crop_enabled and self.crop_basis_var.get() == "custom"
state_custom = "normal" if is_custom else "disabled"
fg_custom = "black" if is_custom else "gray"
self.crop_size_label.config(fg=fg_custom)
self.crop_size_x_label.config(fg=fg_custom)
self.crop_width_entry.config(state=state_custom)
self.crop_height_entry.config(state=state_custom)
def choose_folder(self):
self.folder_path = filedialog.askdirectory(title="画像フォルダを選択")
if self.folder_path:
self.folder_label.config(text=self.folder_path, fg="black")
self.folder_tooltip.text = self.folder_path
else:
self.folder_label.config(text="未選択", fg="gray")
self.folder_tooltip.text = ""
def get_image_files(self):
return [
f for f in os.listdir(self.folder_path)
if f.lower().endswith((".png", ".jpg", ".jpeg", ".bmp"))
]
def open_and_scale_image(self, img_path, scale):
img = Image.open(img_path)
img = ImageOps.exif_transpose(img).convert("RGBA")
new_w = max(1, int(img.width * scale))
new_h = max(1, int(img.height * scale))
return img.resize((new_w, new_h))
def fit_crop_center(self, img, target_w, target_h):
src_w, src_h = img.size
if src_w <= 0 or src_h <= 0 or target_w <= 0 or target_h <= 0:
return img
scale = max(target_w / src_w, target_h / src_h)
resized_w = max(1, int(src_w * scale))
resized_h = max(1, int(src_h * scale))
img = img.resize((resized_w, resized_h))
left = max(0, (resized_w - target_w) // 2)
top = max(0, (resized_h - target_h) // 2)
right = left + target_w
bottom = top + target_h
return img.crop((left, top, right, bottom))
def get_scaled_sizes(self, files, scale):
sizes = []
for file in files:
img_path = os.path.join(self.folder_path, file)
try:
with Image.open(img_path) as img:
img = ImageOps.exif_transpose(img)
new_w = max(1, int(img.width * scale))
new_h = max(1, int(img.height * scale))
sizes.append((file, new_w, new_h))
except Exception:
continue
return sizes
def determine_crop_target_size(self, files, scale):
basis = self.crop_basis_var.get()
if basis == "custom":
custom_w = self.crop_width_var.get()
custom_h = self.crop_height_var.get()
if custom_w <= 0 or custom_h <= 0:
return None
return (custom_w, custom_h)
sizes = self.get_scaled_sizes(files, scale)
if not sizes:
return None
if basis == "first":
_, w, h = sizes[0]
return (w, h)
elif basis == "min":
min_w = min(w for _, w, _ in sizes)
min_h = min(h for _, _, h in sizes)
return (min_w, min_h)
elif basis == "max":
max_w = max(w for _, w, _ in sizes)
max_h = max(h for _, _, h in sizes)
return (max_w, max_h)
return None
def get_image_for_jpeg(self):
if self.generated_img.mode == "RGB":
return self.generated_img
if self.transparent_bg_var.get():
white_bg = Image.new("RGB", self.generated_img.size, (255, 255, 255))
white_bg.paste(self.generated_img, mask=self.generated_img.getchannel("A"))
return white_bg
return self.generated_img.convert("RGB")
def generate(self):
if not self.folder_path:
messagebox.showwarning("警告", "画像フォルダを選択してください。")
return
W, H = self.width_var.get(), self.height_var.get()
scale = self.scale_var.get()
if W <= 0 or H <= 0:
messagebox.showwarning("警告", "画像サイズは1以上を指定してください。")
return
if scale <= 0:
messagebox.showwarning("警告", "縮小率は0より大きい値を指定してください。")
return
if self.arrangement_var.get() == "aligned" and self.same_size_crop_var.get():
if self.crop_basis_var.get() == "custom":
if self.crop_width_var.get() <= 0 or self.crop_height_var.get() <= 0:
messagebox.showwarning("警告", "指定サイズは1以上を指定してください。")
return
if self.transparent_bg_var.get():
mosaic = Image.new("RGBA", (W, H), (0, 0, 0, 0))
else:
mosaic = Image.new("RGBA", (W, H), self.bg_color)
files = self.get_image_files()
if not files:
messagebox.showwarning("警告", "フォルダに画像がありません。")
return
arrangement = self.arrangement_var.get()
if arrangement == "random":
random.shuffle(files)
for file in files:
img_path = os.path.join(self.folder_path, file)
try:
img = self.open_and_scale_image(img_path, scale)
except Exception:
continue
x = random.randint(0, max(0, W - img.width))
y = random.randint(0, max(0, H - img.height))
mosaic.alpha_composite(img, (x, y))
else:
sort_mode = self.sort_order_var.get()
if sort_mode == "asc":
files = sorted(files, key=lambda x: x.lower())
elif sort_mode == "desc":
files = sorted(files, key=lambda x: x.lower(), reverse=True)
else:
random.shuffle(files)
target_size = None
if self.same_size_crop_var.get():
target_size = self.determine_crop_target_size(files, scale)
if target_size is None:
messagebox.showwarning("警告", "トリミング基準サイズを決定できませんでした。")
return
x, y = 0, 0
row_height = 0
for file in files:
img_path = os.path.join(self.folder_path, file)
try:
img = self.open_and_scale_image(img_path, scale)
if self.same_size_crop_var.get():
img = self.fit_crop_center(img, target_size[0], target_size[1])
except Exception:
continue
if x + img.width > W:
x = 0
y += row_height
row_height = 0
if y + img.height > H:
break
mosaic.alpha_composite(img, (x, y))
x += img.width
row_height = max(row_height, img.height)
self.generated_img = mosaic
preview = mosaic.copy()
preview_bg = Image.new("RGBA", preview.size, (255, 255, 255, 255))
preview = Image.alpha_composite(preview_bg, preview).convert("RGB")
preview.thumbnail((500, 500))
self.tk_img = ImageTk.PhotoImage(preview)
self.preview_label.config(image=self.tk_img)
def save(self):
if self.generated_img is None:
messagebox.showwarning("警告", "まず画像を生成してください。")
return
selected_format = self.format_var.get().upper()
if self.transparent_bg_var.get() and selected_format == "JPG":
result = messagebox.askyesno(
"確認",
"JPEGは透明背景を保存できません。\n"
"透明部分は白背景で保存されます。\n\n"
"このままJPEGで保存しますか?"
)
if not result:
return
extension = ".png" if selected_format == "PNG" else ".jpg"
default_name = datetime.now().strftime("%Y%m%d_%H%M%S") + extension
filetypes = [
("PNG画像", "*.png"),
("JPEG画像", "*.jpg"),
("すべてのファイル", "*.*")
]
path = filedialog.asksaveasfilename(
title="保存先とファイル名を指定",
defaultextension=extension,
initialfile=default_name,
filetypes=filetypes
)
if not path:
return
base, ext = os.path.splitext(path)
ext = ext.lower()
expected_ext = ".png" if selected_format == "PNG" else ".jpg"
valid_exts = (".png",) if selected_format == "PNG" else (".jpg", ".jpeg")
if ext not in valid_exts:
path = base + expected_ext
try:
if selected_format == "PNG":
self.generated_img.save(path, format="PNG")
else:
rgb_img = self.get_image_for_jpeg()
rgb_img.save(path, format="JPEG", quality=95)
messagebox.showinfo("保存完了", f"保存しました:\n{path}")
except Exception as e:
messagebox.showerror("保存エラー", f"保存に失敗しました。\n{e}")
def show_help(self, event=None):
help_window = tk.Toplevel(self.root)
help_window.title("ヘルプ")
help_window.resizable(False, False)
help_window.transient(self.root)
help_window.grab_set()
message = (
"【使い方】\n"
"1. 画像サイズを指定します。\n"
"2. 背景色を選ぶか、「背景を透明に」をチェックします。\n"
"3. 画像フォルダを選択します。\n"
"4. 縮小率を指定し、配列方法を選んで「生成」を押します。\n"
"5. 保存形式を選び、「保存」を押して保存先とファイル名を指定します。\n\n"
"【補足】\n"
"・PNGは透明背景に対応しています。\n"
"・JPEGは透明背景に対応していないため、透明部分は白で保存されます。\n"
"・「ランダム」はランダム座標に配置します。\n"
"・「整列」は左上から順に並べます。\n"
"・整列順は「ファイル名昇順」「ファイル名降順」「ランダム」から選べます。\n"
"・「同一サイズでトリミング」をONにすると、各画像を同じサイズに中央トリミングして整列します。\n"
"・トリミング基準は「先頭画像基準」「最小画像基準」「最大画像基準」「指定サイズ」から選べます。"
)
tk.Label(
help_window,
text=message,
justify="left",
anchor="w",
padx=12,
pady=12,
wraplength=420
).pack()
tk.Button(help_window, text="閉じる", command=help_window.destroy, width=10).pack(pady=(0, 12))
if __name__ == "__main__":
root = tk.Tk()
# タイトルバーアイコン表示用
data = '''R0lGODlhEAAQAIYAAAAAAEJYafpzWVJVVvNqTkW36j5UY/8AAP9/AKpVVVWq/+5pU///APtxWD2q2fFxUgB/f0Gq20hhc/ZuVENVZUBWZv9/f/9VVeRrUOdFRfxpTMFmWNpkTft1Wn//////f3ubsX9///9yTknB9vtyV0W26UKx40ey4P98YUCr2/WTSEeo01Wqqv+qVVVVqv+yM0JbbPi5O0FWZv/EOkFSYv/KPi2y6j+////KPjpYai5SZj1VZDxUZP//Mz9VVT9OXSRISD8/fz8/SwD//3VbXwZdhhxKYgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAQAi0AAEIHEgQgAeCAwAYCMAwAAUOGlAIEEDggAUAQQBQkCDDB4AIBUZEAHCjYEIjGyY+wFHjBYIDBQEkBMBCoAMFBYE0DPDjRAEbAhMgBFBBAo8GAh4saEAAwMWCEAoyiClwpsAQVAXSMCDQBQAHJVIMwVmVKEMYQkwUKLACAdmEC3cWAUGABAYACMruYKhDxIQFEwUsAHBBYNQBOYhMmKgiBoAWMYUS6JChx4ysAxNcOMDgQ8GAADs='''
root.tk.call('wm', 'iconphoto', root._w, tk.PhotoImage(data=data))
app = MosaicApp(root)
root.mainloop()