マスク画像で切り取るツール
イメージ
マスク画像で切り取るだけでなくアウトラインや輪郭のぼかし バッチ処理ができます

プログラム
import tkinter as tk
from tkinter import filedialog, colorchooser
from PIL import Image, ImageTk
import cv2
import numpy as np
import os
import unicodedata
import re
DEBUG = False
def generate_checker(w, h, block=20):
checker = np.zeros((h, w, 3), dtype=np.uint8)
for y in range(0, h, block):
for x in range(0, w, block):
color = 240 if ((x//block + y//block) % 2 == 0) else 200
checker[y:y+block, x:x+block] = color
return checker
def compose_with_checker(img):
if img is None:
return np.zeros((250, 250, 3), dtype=np.uint8)
if img.shape[2] == 4:
bgr = img[:, :, :3]
alpha = img[:, :, 3] / 255.0
else:
bgr = img
alpha = np.ones(bgr.shape[:2])
h, w = bgr.shape[:2]
checker = generate_checker(w, h)
alpha3 = np.dstack([alpha, alpha, alpha])
composed = (bgr * alpha3 + checker * (1 - alpha3)).astype(np.uint8)
return composed
def long_feather_alpha(alpha, strength):
if strength <= 0:
return alpha
s = np.clip(strength / 100.0, 0.0, 1.0)
blur1 = max(1.0, 6.0 * s)
blur2 = max(1.0, 4.0 * s)
blur3 = max(1.0, 3.0 * s)
erode1 = max(1, int(round(4 * s)))
erode2 = max(1, int(round(2 * s)))
def octagon_kernel(size):
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (size*2+1, size*2+1))
return k
a = alpha.astype(np.uint8).copy()
a = cv2.GaussianBlur(a, (0, 0), blur1)
a = cv2.erode(a, octagon_kernel(erode1))
a = cv2.GaussianBlur(a, (0, 0), blur2)
a = cv2.erode(a, octagon_kernel(erode2))
a = cv2.GaussianBlur(a, (0, 0), blur3)
return a
def apply_glow_outline_to_bgr(bgr, outline_mask, glow_color_bgr, glow_strength):
if glow_strength <= 0:
return bgr
h, w = bgr.shape[:2]
outline = (outline_mask.astype(np.uint8) * 255).astype(np.uint8)
feather_alpha = long_feather_alpha(outline, int(np.clip(glow_strength, 0, 200) * 0.5))
glow_alpha = (feather_alpha.astype(np.float32) / 255.0)[..., None]
color_arr = np.zeros_like(bgr, dtype=np.float32)
color_arr[..., 0] = glow_color_bgr[0]
color_arr[..., 1] = glow_color_bgr[1]
color_arr[..., 2] = glow_color_bgr[2]
glow = (color_arr * glow_alpha).clip(0, 255).astype(np.uint8)
out = cv2.add(bgr, glow)
return out
def process_single_image(image_cv, mask_cv, settings):
h, w = image_cv.shape[:2]
if mask_cv is None:
mask_alpha = np.ones((h, w), dtype=np.uint8) * 255
else:
mask_resized = cv2.resize(mask_cv, (w, h))
if mask_resized.shape[2] == 4:
mask_alpha = mask_resized[:, :, 3]
else:
mask_alpha = cv2.cvtColor(mask_resized, cv2.COLOR_BGR2GRAY)
if image_cv.shape[2] == 4:
image_rgb = image_cv[:, :, :3].astype(np.float32)
image_alpha = image_cv[:, :, 3].astype(np.float32)
alpha_norm = image_alpha / 255.0
alpha_safe = alpha_norm.copy()
alpha_safe[alpha_safe == 0] = 1.0
image_rgb = (image_rgb / alpha_safe[..., None]).clip(0, 255).astype(np.uint8)
else:
image_rgb = image_cv
image_alpha = np.ones((h, w), dtype=np.uint8) * 255
mask_bin = cv2.threshold(mask_alpha, 10, 255, cv2.THRESH_BINARY)[1]
final_alpha = cv2.bitwise_and(image_alpha.astype(np.uint8), mask_bin)
cut_rgb = cv2.bitwise_and(image_rgb, cv2.cvtColor(mask_bin, cv2.COLOR_GRAY2BGR))
if settings.get("enable_feather", False):
feather_strength = int(settings.get("feather_strength", 50))
feather_alpha = long_feather_alpha(final_alpha, feather_strength)
else:
feather_alpha = final_alpha
result = np.dstack([cut_rgb, feather_alpha])
if settings.get("enable_outline", False) or settings.get("enable_glow", False):
outline_size = max(1, int(settings.get("outline_size", 4)))
grad_strength = int(settings.get("grad_strength", 0))
glow_strength = int(settings.get("glow_strength", 80))
oc_b, oc_g, oc_r = settings.get("outline_color_bgr", (0, 170, 255))
glow_color = settings.get("glow_color_bgr", (0, 170, 255))
bgr = result[:, :, :3].copy()
alpha = result[:, :, 3]
_, mask_bin2 = cv2.threshold(alpha, 1, 255, cv2.THRESH_BINARY)
edges = cv2.Canny(mask_bin2, 50, 150)
kernel = np.ones((outline_size, outline_size), np.uint8)
outline = cv2.dilate(edges, kernel)
outline_mask = outline > 0
if settings.get("enable_outline", False):
if settings.get("enable_gradient", False) and grad_strength > 0:
grad = np.linspace(0, grad_strength, w).astype(np.uint8)
grad = np.tile(grad, (h, 1))
grad_norm = (grad.astype(np.float32) / 255.0)[..., None]
color_arr = np.zeros((h, w, 3), dtype=np.float32)
color_arr[..., 0] = oc_b
color_arr[..., 1] = oc_g
color_arr[..., 2] = oc_r
grad_color = (color_arr * grad_norm).clip(0, 255).astype(np.uint8)
bgr[outline_mask] = grad_color[outline_mask]
else:
bgr[outline_mask] = (oc_b, oc_g, oc_r)
if settings.get("enable_glow", False) and glow_strength > 0:
bgr = apply_glow_outline_to_bgr(bgr, outline_mask, glow_color, glow_strength)
result = np.dstack([bgr, alpha])
return result
def sanitize_to_ascii(name):
if not name:
return "mask"
n = unicodedata.normalize('NFKD', name)
n = n.encode('ascii', 'ignore').decode('ascii')
n = n.strip()
n = re.sub(r'\s+', '_', n)
n = re.sub(r'[^A-Za-z0-9_\-]', '', n)
if n == "":
return "mask"
return n
class MaskCutApp:
def __init__(self, root):
self.root = root
self.root.title("Mask Cut - Auto Save by original+mask")
# Left thumbnails
left = tk.Frame(root, width=220)
left.pack(side="left", fill="y")
tk.Label(left, text="Thumbnails").pack(anchor="nw")
self.thumb_canvas = tk.Canvas(left, width=220)
self.thumb_scroll = tk.Scrollbar(left, orient="vertical", command=self.thumb_canvas.yview)
self.thumb_frame = tk.Frame(self.thumb_canvas)
self.thumb_frame.bind("<Configure>", lambda e: self.thumb_canvas.configure(scrollregion=self.thumb_canvas.bbox("all")))
self.thumb_canvas.create_window((0,0), window=self.thumb_frame, anchor="nw")
self.thumb_canvas.configure(yscrollcommand=self.thumb_scroll.set)
self.thumb_canvas.pack(side="left", fill="y", expand=False)
self.thumb_scroll.pack(side="right", fill="y")
self.thumbnail_refs = []
# Center layout: Orig TL / Mask TR small / Result BL
center = tk.Frame(root); center.pack(side="left", fill="both", expand=True)
top_row = tk.Frame(center); top_row.pack(fill="both", expand=True)
orig_frame = tk.Frame(top_row); orig_frame.pack(side="left", fill="both", expand=True, padx=4, pady=4)
tk.Label(orig_frame, text="元画像 (左上)").pack(); self.orig_label = tk.Label(orig_frame); self.orig_label.pack(expand=True)
mask_frame = tk.Frame(top_row, width=220); mask_frame.pack(side="right", fill="y", padx=4, pady=4)
tk.Label(mask_frame, text="マスク (右上・小)").pack(); self.mask_label_small = tk.Label(mask_frame); self.mask_label_small.pack(expand=False)
bottom_row = tk.Frame(center); bottom_row.pack(fill="both", expand=True)
res_frame = tk.Frame(bottom_row); res_frame.pack(side="left", fill="both", expand=True, padx=4, pady=4)
tk.Label(res_frame, text="結果 (左下)").pack(); self.result_label = tk.Label(res_frame); self.result_label.pack(expand=True)
# Right: controls
right = tk.Frame(root); right.pack(side="right", fill="y")
tk.Button(right, text="フォルダを読み込む (サムネイル)", command=self.load_folder).pack(padx=4, pady=4, fill="x")
tk.Button(right, text="マスクを読み込む (single)", command=self.load_mask).pack(padx=4, pady=4, fill="x")
# マスクフォルダボタンは削除(ユーザー要望)
# Cut button + Auto-save under it
self.cut_btn = tk.Button(right, text="切り抜き実行 (single)", command=self.apply_mask)
self.cut_btn.pack(padx=4, pady=(8,4), fill="x")
self.save_under_cut_btn = tk.Button(right, text="保存 (自動: original_mask.png)", command=self.auto_save_image)
self.save_under_cut_btn.pack(padx=4, pady=(0,8), fill="x")
tk.Button(right, text="バッチ処理 (loaded folder)", command=self.batch_process_from_loaded).pack(padx=4, pady=6, fill="x")
# Controls area: checkboxes left, sliders right
controls_frame = tk.Frame(right)
controls_frame.pack(padx=4, pady=(8,4), fill="x")
cb_frame = tk.Frame(controls_frame)
cb_frame.grid(row=0, column=0, sticky="nw", padx=(0,6))
tk.Label(cb_frame, text="効果の選択").pack(anchor="w")
# Default OFF as requested
self.enable_outline_var = tk.IntVar(value=0)
self.enable_gradient_var = tk.IntVar(value=0)
self.enable_feather_var = tk.IntVar(value=0)
self.enable_glow_var = tk.IntVar(value=0)
tk.Checkbutton(cb_frame, text="アウトライン", variable=self.enable_outline_var).pack(anchor="w")
tk.Checkbutton(cb_frame, text="グラデーション", variable=self.enable_gradient_var).pack(anchor="w")
tk.Checkbutton(cb_frame, text="フェザー", variable=self.enable_feather_var).pack(anchor="w")
tk.Checkbutton(cb_frame, text="Glow", variable=self.enable_glow_var).pack(anchor="w")
slider_frame = tk.Frame(controls_frame)
slider_frame.grid(row=0, column=1, sticky="ne")
tk.Label(slider_frame, text="縁取り太さ").pack(anchor="w")
self.outline_scale = tk.Scale(slider_frame, from_=1, to=60, orient="horizontal"); self.outline_scale.set(4); self.outline_scale.pack(fill="x")
tk.Label(slider_frame, text="グラデーション強さ").pack(anchor="w")
self.grad_scale = tk.Scale(slider_frame, from_=0, to=255, orient="horizontal"); self.grad_scale.set(0); self.grad_scale.pack(fill="x")
tk.Label(slider_frame, text="フェザー強さ").pack(anchor="w")
self.feather_scale = tk.Scale(slider_frame, from_=0, to=100, orient="horizontal"); self.feather_scale.set(50); self.feather_scale.pack(fill="x")
tk.Label(slider_frame, text="Glow強さ").pack(anchor="w")
self.glow_scale = tk.Scale(slider_frame, from_=0, to=200, orient="horizontal"); self.glow_scale.set(80); self.glow_scale.pack(fill="x")
tk.Label(right, text="アウトライン色").pack(padx=4, pady=(8,0))
color_frame = tk.Frame(right); color_frame.pack(padx=4, pady=4, fill="x")
self.color_preview = tk.Label(color_frame, text=" ", bg="#ffaa00", relief="sunken"); self.color_preview.pack(side="left", padx=(0,6))
tk.Button(color_frame, text="色を選択", command=self.choose_outline_color).pack(side="left", fill="x")
self.outline_color_bgr = (0x00, 0xaa, 0xff); self.outline_color_hex = "#ffaa00"; self.color_preview.config(bg=self.outline_color_hex)
tk.Label(right, text="Glow色").pack(padx=4, pady=(8,0))
glow_frame = tk.Frame(right); glow_frame.pack(padx=4, pady=4, fill="x")
self.glow_preview = tk.Label(glow_frame, text=" ", bg="#ffaa00", relief="sunken"); self.glow_preview.pack(side="left", padx=(0,6))
tk.Button(glow_frame, text="色を選択", command=self.choose_glow_color).pack(side="left", fill="x")
self.glow_color_bgr = (0x00, 0xaa, 0xff); self.glow_color_hex = "#ffaa00"; self.glow_preview.config(bg=self.glow_color_hex)
tk.Button(right, text="Outline + Gradient + Glow", command=self.apply_outline_gradient).pack(padx=4, pady=(12,4), fill="x")
tk.Button(right, text="保存 (別名で保存)", command=self.save_image_as).pack(padx=4, pady=4, fill="x")
# Internal state
self.image = None
self.image_path = None
self.mask = None
self.mask_path = None
self.current_folder = None
self.result = None
def load_folder(self):
folder = filedialog.askdirectory()
if not folder:
return
self.current_folder = folder
for widget in self.thumb_frame.winfo_children():
widget.destroy()
self.thumbnail_refs.clear()
exts = (".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".webp")
files = [os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith(exts)]
files.sort()
for path in files:
try:
img = Image.open(path).convert("RGBA")
thumb = img.copy()
thumb.thumbnail((180, 180))
tk_img = ImageTk.PhotoImage(thumb)
btn = tk.Button(self.thumb_frame, image=tk_img, text=os.path.basename(path), compound="top",
command=lambda p=path: self.on_thumbnail_click(p))
btn.image = tk_img
btn.pack(padx=4, pady=4)
self.thumbnail_refs.append(tk_img)
except Exception as e:
print("thumb load error:", path, e)
def on_thumbnail_click(self, path):
img_cv = cv2.imread(path, cv2.IMREAD_UNCHANGED)
if img_cv is None:
print("Failed to load:", path)
return
self.image = img_cv
self.image_path = path
self.result = None
self.show_preview(compose_with_checker(self.image), self.orig_label, max_size=420)
def load_mask(self):
path = filedialog.askopenfilename(filetypes=[("Image files","*.png;*.jpg;*.jpeg;*.bmp;*.tiff;*.webp")])
if not path: return
self.mask = cv2.imread(path, cv2.IMREAD_UNCHANGED)
self.mask_path = path
self.show_preview(compose_with_checker(self.mask), self.mask_label_small, max_size=180)
def apply_mask(self):
if self.image is None:
print("image missing")
return
settings = self._gather_settings()
# マスクフォルダは廃止。単一マスクがあればそれを使い、なければ全体切り取り
mask_to_use = self.mask if getattr(self, "mask", None) is not None else None
self.result = process_single_image(self.image, mask_to_use, settings)
self.show_preview(compose_with_checker(self.image), self.orig_label, max_size=420)
if mask_to_use is not None:
self.show_preview(compose_with_checker(mask_to_use), self.mask_label_small, max_size=180)
self.show_preview(compose_with_checker(self.result), self.result_label, max_size=420)
def batch_process_from_loaded(self):
if not self.current_folder:
print("No folder loaded")
return
out_parent = filedialog.askdirectory(title="Select output parent folder (processed_output will be created)")
if not out_parent:
return
out_folder = os.path.join(out_parent, "processed_output")
os.makedirs(out_folder, exist_ok=True)
settings = self._gather_settings()
exts = (".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".webp")
files = [f for f in os.listdir(self.current_folder) if f.lower().endswith(exts)]
files.sort()
total = len(files)
if total == 0:
print("No images found in folder")
return
print(f"Batch start: {total} files from {self.current_folder} -> {out_folder}")
for idx, fname in enumerate(files, 1):
in_path = os.path.join(self.current_folder, fname)
img = cv2.imread(in_path, cv2.IMREAD_UNCHANGED)
if img is None:
print(f"[{idx}/{total}] Failed to load: {fname}")
continue
# マスクフォルダは使わない。単一マスクがあればそれを全画像に適用
mask_cv = self.mask if getattr(self, "mask", None) is not None else None
result = process_single_image(img, mask_cv, settings)
out_path = os.path.join(out_folder, fname)
cv2.imwrite(out_path, result)
print(f"[{idx}/{total}] Done: {fname} -> {out_path}")
print("Batch finished.")
def apply_outline_gradient(self):
if self.result is None:
return
settings = self._gather_settings()
mask_to_use = self.mask if getattr(self, "mask", None) is not None else None
self.result = process_single_image(self.image, mask_to_use, settings)
self.show_preview(compose_with_checker(self.result), self.result_label, max_size=420)
def auto_save_image(self):
if self.result is None:
print("No result to save")
return
if getattr(self, "image_path", None):
base = os.path.splitext(os.path.basename(self.image_path))[0]
else:
base = "image"
mask_name = "nomask"
if getattr(self, "mask_path", None):
mask_name = os.path.splitext(os.path.basename(self.mask_path))[0]
mask_name_ascii = sanitize_to_ascii(mask_name)
out_name = f"{base}_{mask_name_ascii}.png"
if self.current_folder:
out_path = os.path.join(self.current_folder, out_name)
else:
out_path = os.path.join(os.getcwd(), out_name)
cv2.imwrite(out_path, self.result)
print(f"Auto-saved to: {out_path}")
def save_image_as(self):
if self.result is None:
print("No result to save")
return
path = filedialog.asksaveasfilename(defaultextension=".png")
if path:
cv2.imwrite(path, self.result)
print(f"Saved to: {path}")
def choose_outline_color(self):
col = colorchooser.askcolor(title="アウトライン色を選択", initialcolor=getattr(self, "outline_color_hex", "#ffaa00"))
if col and col[0] is not None:
r, g, b = [int(round(v)) for v in col[0]]
self.outline_color_bgr = (b, g, r)
self.outline_color_hex = col[1]
self.color_preview.config(bg=self.outline_color_hex)
def choose_glow_color(self):
col = colorchooser.askcolor(title="Glow色を選択", initialcolor=getattr(self, "glow_color_hex", "#ffaa00"))
if col and col[0] is not None:
r, g, b = [int(round(v)) for v in col[0]]
self.glow_color_bgr = (b, g, r)
self.glow_color_hex = col[1]
self.glow_preview.config(bg=self.glow_color_hex)
def _gather_settings(self):
return {
"enable_outline": bool(self.enable_outline_var.get()),
"enable_gradient": bool(self.enable_gradient_var.get()),
"enable_feather": bool(self.enable_feather_var.get()),
"enable_glow": bool(self.enable_glow_var.get()),
"feather_strength": int(self.feather_scale.get()),
"outline_size": int(self.outline_scale.get()),
"grad_strength": int(self.grad_scale.get()),
"glow_strength": int(self.glow_scale.get()),
"outline_color_bgr": getattr(self, "outline_color_bgr", (0,170,255)),
"glow_color_bgr": getattr(self, "glow_color_bgr", (0,170,255)),
}
def show_preview(self, img, label, max_size=250):
if img is None:
return
if img.shape[2] == 4:
disp = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
else:
disp = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
disp = Image.fromarray(disp)
w, h = disp.size
scale = min(max_size / w, max_size / h, 1.0)
new_size = (int(w * scale), int(h * scale))
disp = disp.resize(new_size)
tk_img = ImageTk.PhotoImage(disp)
label.configure(image=tk_img)
label.image = tk_img
if __name__ == "__main__":
root = tk.Tk()
app = MaskCutApp(root)
root.mainloop()








ディスカッション
コメント一覧
まだ、コメントがありません