import os
import json
from tkinter import *
from tkinter import filedialog
from PIL import Image, ImageTk
class ImageSplitterGUI:
def __init__(self, root):
self.root = root
self.root.title("画像分割ツール")
self.folder = ""
self.image_path = ""
self.image = None
self.zoomed_image = None
self.tk_image = None
self.split_data = {"vertical": [], "horizontal": []}
self.mode_var = StringVar(value="vertical")
self.dragging = False
self.drag_line_type = None
self.drag_index = None
self.temp_results = []
# ★ ズーム倍率(初期値100%)
self.zoom_percent = 100
self.setup_ui()
# ---------------- UI ----------------
def setup_ui(self):
self.root.configure(bg="#f0f0f0")
main = Frame(self.root, bg="#f0f0f0")
main.pack(fill=BOTH, expand=True)
# 左:サムネイル
left = Frame(main, bg="#e0e0e0", bd=2, relief=GROOVE)
left.pack(side=LEFT, fill=Y)
Button(left, text="フォルダ選択", command=self.select_folder,
font=("Meiryo", 11), bg="#ffffff").pack(fill=X, padx=5, pady=5)
thumb_container = Frame(left, bg="#e0e0e0")
thumb_container.pack(fill=BOTH, expand=True, padx=5, pady=5)
self.thumb_canvas = Canvas(thumb_container, bg="#d0d0d0", width=150, bd=2, relief=SOLID)
self.thumb_canvas.pack(side=LEFT, fill=BOTH, expand=True)
thumb_scroll = Scrollbar(thumb_container, orient=VERTICAL, command=self.thumb_canvas.yview)
thumb_scroll.pack(side=RIGHT, fill=Y)
self.thumb_canvas.configure(yscrollcommand=thumb_scroll.set)
self.thumb_inner = Frame(self.thumb_canvas, bg="#d0d0d0")
self.thumb_canvas.create_window((0, 0), window=self.thumb_inner, anchor="nw")
self.thumb_inner.bind("<Configure>", lambda e: self.thumb_canvas.configure(
scrollregion=self.thumb_canvas.bbox("all")
))
# 中央:プレビュー
center = Frame(main, bg="#f0f0f0")
center.pack(side=LEFT, fill=BOTH, expand=True)
# ★ 情報表示(1行)
self.info_label = Label(
center,
text="Size: - x - Click: X=- Y=- Zoom: 100%",
bg="#f0f0f0",
font=("Meiryo", 12)
)
self.info_label.pack(pady=5)
preview_container = Frame(center, bg="#f0f0f0")
preview_container.pack(fill=BOTH, expand=True)
self.preview_canvas = Canvas(preview_container, bg="gray", bd=2, relief=SOLID)
self.preview_canvas.pack(side=LEFT, fill=BOTH, expand=True)
v_scroll = Scrollbar(preview_container, orient=VERTICAL, command=self.preview_canvas.yview)
v_scroll.pack(side=RIGHT, fill=Y)
h_scroll = Scrollbar(center, orient=HORIZONTAL, command=self.preview_canvas.xview)
h_scroll.pack(fill=X)
self.preview_canvas.configure(
yscrollcommand=v_scroll.set,
xscrollcommand=h_scroll.set
)
# 内部キャンバス
self.canvas = Canvas(self.preview_canvas, bg="gray")
self.canvas_id = self.preview_canvas.create_window((0, 0), window=self.canvas, anchor="nw")
self.canvas.bind("<Configure>", self.update_scrollregion)
# イベント
self.canvas.bind("<Button-1>", self.on_click)
self.canvas.bind("<ButtonPress-3>", self.on_drag_start)
self.canvas.bind("<B3-Motion>", self.on_drag_move)
self.canvas.bind("<ButtonRelease-3>", self.on_drag_end)
# ボタン類
btn_frame = Frame(center, bg="#f0f0f0")
btn_frame.pack(pady=5)
btn_style = {"font": ("Meiryo", 13), "width": 12, "height": 2}
Radiobutton(btn_frame, text="縦線モード", variable=self.mode_var,
value="vertical", bg="#f0f0f0", font=("Meiryo", 12)).grid(row=0, column=0, padx=4)
Radiobutton(btn_frame, text="横線モード", variable=self.mode_var,
value="horizontal", bg="#f0f0f0", font=("Meiryo", 12)).grid(row=0, column=1, padx=4)
Radiobutton(btn_frame, text="縦横モード", variable=self.mode_var,
value="both", bg="#f0f0f0", font=("Meiryo", 12)).grid(row=0, column=2, padx=4)
Button(btn_frame, text="キャンセル(最後)", command=self.undo_last, **btn_style).grid(row=0, column=3, padx=4)
Button(btn_frame, text="全てクリア", command=self.reset_lines, **btn_style).grid(row=0, column=4, padx=4)
Button(btn_frame, text="縦に分割", command=self.split_vertical, **btn_style).grid(row=1, column=0, padx=4)
Button(btn_frame, text="横に分割", command=self.split_horizontal, **btn_style).grid(row=1, column=1, padx=4)
Button(btn_frame, text="縦横に分割", command=self.split_both, **btn_style).grid(row=1, column=2, padx=4)
Button(btn_frame, text="分割結果保存", command=self.save_split_results, **btn_style).grid(row=1, column=3, padx=4)
Button(btn_frame, text="線情報保存", command=self.save_split_data, **btn_style).grid(row=1, column=4, padx=4)
Button(btn_frame, text="線情報読み込み", command=self.load_split_data, **btn_style).grid(row=2, column=0, padx=4)
# ★ バッチ処理ボタン(フォルダー内の全画像を自動分割)
Button(btn_frame, text="バッチ処理", command=self.batch_process, **btn_style).grid(row=2, column=1, padx=4)
# ★ ズームスライダー
# ★ ZOOM + 微調整(1行にまとめる)
zoom_adjust_frame = Frame(center, bg="#f0f0f0")
zoom_adjust_frame.pack(pady=10)
# ZOOM ラベル
Label(zoom_adjust_frame, text="Zoom", bg="#f0f0f0", font=("Meiryo", 12)).grid(row=0, column=0, padx=5)
# ZOOM スライダー
self.zoom_slider = Scale(
zoom_adjust_frame,
from_=10, to=300,
orient=HORIZONTAL,
length=200,
command=self.on_zoom_slider
)
self.zoom_slider.set(100)
self.zoom_slider.grid(row=0, column=1, padx=10)
# 微調整ボタン(← → ↑ ↓)
Button(zoom_adjust_frame, text="←", width=4, command=lambda: self.adjust_last_line(-1)).grid(row=0, column=2)
Button(zoom_adjust_frame, text="→", width=4, command=lambda: self.adjust_last_line(1)).grid(row=0, column=3)
Button(zoom_adjust_frame, text="↑", width=4, command=lambda: self.adjust_last_line(-1, vertical=False)).grid(row=0, column=4)
Button(zoom_adjust_frame, text="↓", width=4, command=lambda: self.adjust_last_line(1, vertical=False)).grid(row=0, column=5)
# 右:分割結果
right = Frame(main, bg="#e0e0e0", bd=2, relief=GROOVE)
right.pack(side=LEFT, fill=Y)
Label(right, text="分割結果プレビュー", bg="#e0e0e0").pack(pady=5)
result_container = Frame(right, bg="#e0e0e0")
result_container.pack(fill=BOTH, expand=True)
self.result_canvas = Canvas(result_container, bg="#d0d0d0", width=200, bd=2, relief=SOLID)
self.result_canvas.pack(side=LEFT, fill=BOTH, expand=True)
result_scroll = Scrollbar(result_container, orient=VERTICAL, command=self.result_canvas.yview)
result_scroll.pack(side=RIGHT, fill=Y)
self.result_canvas.configure(yscrollcommand=result_scroll.set)
self.result_inner = Frame(self.result_canvas, bg="#d0d0d0")
self.result_canvas.create_window((0, 0), window=self.result_inner, anchor="nw")
self.result_inner.bind("<Configure>", lambda e: self.result_canvas.configure(
scrollregion=self.result_canvas.bbox("all")
))
# ---------------- スクロール領域更新 ----------------
def update_scrollregion(self, event=None):
self.preview_canvas.configure(
scrollregion=self.preview_canvas.bbox("all")
)
# ---------------- フォルダ選択 ----------------
def select_folder(self):
self.folder = filedialog.askdirectory()
if self.folder:
self.load_thumbnails()
# ---------------- サムネイル読み込み ----------------
def load_thumbnails(self):
for w in self.thumb_inner.winfo_children():
w.destroy()
for file in sorted(os.listdir(self.folder)):
if file.lower().endswith((".png", ".jpg", ".jpeg")):
path = os.path.join(self.folder, file)
try:
img = Image.open(path)
img.thumbnail((120, 120))
tk_img = ImageTk.PhotoImage(img)
btn = Button(self.thumb_inner, image=tk_img,
command=lambda p=path: self.load_image_from_thumb(p),
bg="#d0d0d0", relief=RIDGE, bd=2)
btn.image = tk_img
btn.pack(pady=4)
except:
pass
# ---------------- サムネイルクリックで画像読み込み ----------------
def load_image_from_thumb(self, path):
self.image_path = path
self.load_image()
self.load_split_data()
self.update_zoomed_image()
self.draw_image_and_lines()
self.clear_result_thumbs()
self.temp_results = []
self.update_info_label()
# ---------------- 画像読み込み ----------------
def load_image(self):
self.image = Image.open(self.image_path)
self.update_zoomed_image()
self.update_info_label()
# ---------------- ズーム画像生成 ----------------
def update_zoomed_image(self):
if not self.image:
return
scale = self.zoom_percent / 100.0
new_w = int(self.image.width * scale)
new_h = int(self.image.height * scale)
self.zoomed_image = self.image.resize((new_w, new_h), Image.LANCZOS)
self.tk_image = ImageTk.PhotoImage(self.zoomed_image)
self.canvas.config(width=new_w, height=new_h)
self.preview_canvas.configure(scrollregion=(0, 0, new_w, new_h))
# ---------------- 情報ラベル更新(1行) ----------------
def update_info_label(self, click_x=None, click_y=None):
if self.image:
size_text = f"Size: {self.image.width}x{self.image.height}"
else:
size_text = "Size: - x -"
if click_x is None or click_y is None:
click_text = "Click: X=- Y=-"
else:
click_text = f"Click: X={click_x} Y={click_y}"
zoom_text = f"Zoom: {self.zoom_percent}%"
self.info_label.config(text=f"{size_text} {click_text} {zoom_text}")
# ---------------- ズームスライダー変更 ----------------
def on_zoom_slider(self, value):
self.zoom_percent = int(float(value))
self.update_zoomed_image()
self.draw_image_and_lines()
self.update_info_label()
# ---------------- 線描画(ズーム対応) ----------------
def draw_image_and_lines(self):
if not self.zoomed_image:
return
self.canvas.delete("all")
self.canvas.create_image(0, 0, anchor=NW, image=self.tk_image)
scale = self.zoom_percent / 100.0
w, h = self.zoomed_image.width, self.zoomed_image.height
# ★ ズーム後の位置に変換して線を描画
for x in self.split_data["vertical"]:
zx = int(x * scale)
self.canvas.create_line(zx, 0, zx, h, fill="red", width=2)
for y in self.split_data["horizontal"]:
zy = int(y * scale)
self.canvas.create_line(0, zy, w, zy, fill="red", width=2)
# ---------------- 左クリックで線追加(ズーム対応) ----------------
def on_click(self, event):
if not self.image:
return
scale = self.zoom_percent / 100.0
orig_x = int(event.x / scale)
orig_y = int(event.y / scale)
self.update_info_label(orig_x, orig_y)
mode = self.mode_var.get()
if mode == "vertical":
self.split_data["vertical"].append(orig_x)
elif mode == "horizontal":
self.split_data["horizontal"].append(orig_y)
elif mode == "both":
self.split_data["vertical"].append(orig_x)
self.split_data["horizontal"].append(orig_y)
self.draw_image_and_lines()
# ---------------- 最後の線微調整 ----------------
def adjust_last_line(self, delta, vertical=True):
if vertical:
if self.split_data["vertical"]:
self.split_data["vertical"][-1] += delta
else:
if self.split_data["horizontal"]:
self.split_data["horizontal"][-1] += delta
self.draw_image_and_lines()
# ---------------- 右ドラッグ開始 ----------------
def on_drag_start(self, event):
if not self.image:
return
scale = self.zoom_percent / 100.0
x = int(event.x / scale)
y = int(event.y / scale)
threshold = 5
for i, vx in enumerate(self.split_data["vertical"]):
if abs(vx - x) <= threshold:
self.dragging = True
self.drag_line_type = "vertical"
self.drag_index = i
return
for i, hy in enumerate(self.split_data["horizontal"]):
if abs(hy - y) <= threshold:
self.dragging = True
self.drag_line_type = "horizontal"
self.drag_index = i
return
# ---------------- 右ドラッグ中 ----------------
def on_drag_move(self, event):
if not self.dragging:
return
scale = self.zoom_percent / 100.0
x = int(event.x / scale)
y = int(event.y / scale)
if self.drag_line_type == "vertical":
self.split_data["vertical"][self.drag_index] = x
else:
self.split_data["horizontal"][self.drag_index] = y
self.draw_image_and_lines()
# ---------------- 右ドラッグ終了 ----------------
def on_drag_end(self, event):
self.dragging = False
self.drag_line_type = None
self.drag_index = None
# ---------------- 最後の線削除 ----------------
def undo_last(self):
if self.mode_var.get() == "vertical" and self.split_data["vertical"]:
self.split_data["vertical"].pop()
elif self.mode_var.get() == "horizontal" and self.split_data["horizontal"]:
self.split_data["horizontal"].pop()
elif self.mode_var.get() == "both":
if self.split_data["vertical"]:
self.split_data["vertical"].pop()
if self.split_data["horizontal"]:
self.split_data["horizontal"].pop()
self.draw_image_and_lines()
# ---------------- 全線削除 ----------------
def reset_lines(self):
self.split_data = {"vertical": [], "horizontal": []}
self.draw_image_and_lines()
# ---------------- 分割ボタン ----------------
def split_vertical(self):
self.do_split(vertical=False, horizontal=True)
def split_horizontal(self):
self.do_split(vertical=True, horizontal=False)
def split_both(self):
self.do_split(vertical=True, horizontal=True)
# ---------------- 分割処理(オフセット削除済み) ----------------
def do_split(self, vertical, horizontal):
if not self.image:
return
v = sorted(self.split_data["vertical"]) if vertical else []
h = sorted(self.split_data["horizontal"]) if horizontal else []
v = [0] + v + [self.image.width]
h = [0] + h + [self.image.height]
self.clear_result_thumbs()
self.temp_results = []
for i in range(len(v)-1):
for j in range(len(h)-1):
crop = self.image.crop((v[i], h[j], v[i+1], h[j+1]))
self.temp_results.append(crop)
self.add_result_thumb(crop)
# ---------------- 分割結果プレビュー ----------------
def clear_result_thumbs(self):
for w in self.result_inner.winfo_children():
w.destroy()
def add_result_thumb(self, img):
preview = img.copy()
preview.thumbnail((140, 140))
tk_img = ImageTk.PhotoImage(preview)
lbl = Label(self.result_inner, image=tk_img, bg="#d0d0d0", relief=RIDGE, bd=2)
lbl.image = tk_img
lbl.pack(pady=4)
lbl.bind("<Button-1>", lambda e, im=img: self.open_preview_window(im))
# ---------------- 拡大表示 ----------------
def open_preview_window(self, img):
win = Toplevel(self.root)
win.title("拡大プレビュー")
tk_img = ImageTk.PhotoImage(img)
lbl = Label(win, image=tk_img)
lbl.image = tk_img
lbl.pack(padx=10, pady=10)
Button(win, text="閉じる", command=win.destroy).pack(pady=5)
# ---------------- 分割結果保存 ----------------
def save_split_results(self):
if not self.temp_results:
return
base = os.path.splitext(os.path.basename(self.image_path))[0]
out_dir = os.path.join(self.folder, "split_output")
os.makedirs(out_dir, exist_ok=True)
for idx, img in enumerate(self.temp_results):
img.save(os.path.join(out_dir, f"{base}_{idx:04d}.png"))
# ---------------- 線情報読み込み ----------------
def load_split_data(self):
program_name = os.path.splitext(os.path.basename(__file__))[0]
json_path = program_name + ".json"
if os.path.exists(json_path):
with open(json_path, "r", encoding="utf-8") as f:
self.split_data = json.load(f)
else:
self.split_data = {"vertical": [], "horizontal": []}
if self.image:
self.draw_image_and_lines()
self.update_info_label()
# ---------------- 線情報保存 ----------------
def save_split_data(self):
program_name = os.path.splitext(os.path.basename(__file__))[0]
json_path = program_name + ".json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(self.split_data, f, indent=2, ensure_ascii=False)
# ---------------- バッチ処理(フォルダー内の全画像を自動分割) ----------------
def batch_process(self):
if not self.folder:
return
# 対象ファイル一覧
files = [f for f in os.listdir(self.folder)
if f.lower().endswith((".png", ".jpg", ".jpeg"))]
if not files:
return
out_dir = os.path.join(self.folder, "batch_output")
os.makedirs(out_dir, exist_ok=True)
for file in files:
path = os.path.join(self.folder, file)
img = Image.open(path)
# 分割線が無い場合はスキップ
if not self.split_data["vertical"] and not self.split_data["horizontal"]:
continue
v = sorted(self.split_data["vertical"])
h = sorted(self.split_data["horizontal"])
v = [0] + v + [img.width]
h = [0] + h + [img.height]
base = os.path.splitext(file)[0]
# 分割して保存
idx = 0
for i in range(len(v)-1):
for j in range(len(h)-1):
crop = img.crop((v[i], h[j], v[i+1], h[j+1]))
crop.save(os.path.join(out_dir, f"{base}_{idx:04d}.png"))
idx += 1
# ---------------- 起動 ----------------
if __name__ == "__main__":
root = Tk()
root.geometry("1200x800")
app = ImageSplitterGUI(root)
root.mainloop()
ディスカッション
コメント一覧
まだ、コメントがありません