图片任意切割工具(Python 3.8 实现)
图片任意切割工具(Python 3.8 实现)
在日常工作或个人创作中,我们经常会遇到需要把一张图片按比例切割的情况,比如:
- 将长截图拆分成若干小段,方便排版展示
- 把一张大图切割成网格,用于拼接、打印或艺术化处理
- 测试图像处理算法时,快速得到分块数据
为此,我开发了一个 图片任意切割工具(基于 Python 3.8 + Pillow + Tkinter),支持命令行和图形界面两种模式,简单易用。
功能特点
-
多种切割模式
- 横向切割(按行)
- 纵向切割(按列)
- 网格切割(行 × 列)
-
灵活的保存顺序
- 从上到下(tb)、从下到上(bt)
- 从左到右(lr)、从右到左(rl)
- 网格顺序:
lr-tb / rl-tb / lr-bt / rl-bt / snake(蛇形) / reverse - 中心向外:按照与图片中心的距离由近到远保存
-
双模式支持
- 命令行模式:适合批量处理和自动化脚本
- GUI 图形界面模式:如果不输入参数,自动弹出 Windows 界面,选择图片、份数、方向等信息即可
-
自动输出目录
- 默认在原图目录下生成
<图片名>_slices文件夹,自动保存切割结果
- 默认在原图目录下生成
使用方法
命令行模式
# 显示帮助(支持 ? 或 --help)
python image_slicer.py --help
python image_slicer.py ?
# 例1:水平切成 4 份(自上而下)
python image_slicer.py input.jpg --mode horizontal --parts 4 --direction tb
# 例2:垂直切成 3 份(右→左)
python image_slicer.py input.jpg --mode vertical --parts 3 --direction rl
# 例3:网格切割 3x5,蛇形顺序保存
python image_slicer.py input.jpg --mode grid --grid 3x5 --direction snake
# 例4:网格切割 4x4,按“中心→外围”距离排序保存
python image_slicer.py input.jpg --mode grid --grid 4x4 --center-out
GUI 模式
如果你直接运行而不带参数:
python image_slicer.py
会自动弹出一个 Windows 图形界面,在界面中可以:
- 选择切割模式(水平 / 垂直 / 网格)
- 输入份数或行列数
- 设置切割顺序
- 勾选是否“中心向外”
- 点击“选择图片并执行切割”即可
无需记命令行参数,更适合普通用户。
代码:
# -*- coding: utf-8 -*-
"""
Image Slicer Tool (Python 3.8)
Author: ChatGPT
"""
import os
import math
import argparse
from typing import List, Tuple, Optional
from PIL import Image
# ——— For file dialog when no input path ———
def pick_image_by_dialog() -> Optional[str]:
try:
import tkinter as tk
from tkinter import filedialog
root = tk.Tk()
root.withdraw()
path = filedialog.askopenfilename(
title="选择要切割的图片",
filetypes=[
("Image files", "*.png;*.jpg;*.jpeg;*.bmp;*.tiff;*.webp"),
("All files", "*.*"),
],
)
root.update()
root.destroy()
return path or None
except Exception as e:
print("无法打开文件选择器:", e)
return None
def parse_grid(s: str) -> Tuple[int, int]:
"""
Parse 'rowsxcols' (e.g. 3x5 or 3×5) into (rows, cols)
"""
s = s.lower().replace("×", "x")
if "x" not in s:
raise argparse.ArgumentTypeError("网格格式应为 rowsxcols,例如 3x5 或 3×5")
r, c = s.split("x", 1)
rows = int(r.strip())
cols = int(c.strip())
if rows <= 0 or cols <= 0:
raise argparse.ArgumentTypeError("rows/cols 必须为正整数")
return rows, cols
def make_outdir(img_path: str, outdir: Optional[str]) -> str:
if outdir:
os.makedirs(outdir, exist_ok=True)
return outdir
base = os.path.splitext(os.path.basename(img_path))[0]
parent = os.path.dirname(img_path) or os.getcwd()
out = os.path.join(parent, f"{base}_slices")
os.makedirs(out, exist_ok=True)
return out
def slice_boxes_horizontal(w: int, h: int, parts: int) -> List[Tuple[int, int, int, int]]:
"""
Horizontal cutting → split along horizontal lines (top->bottom bands).
Each box: (left, top, right, bottom)
"""
band_heights = []
for i in range(parts):
bh = (h // parts) + (1 if i < (h % parts) else 0)
band_heights.append(bh)
boxes = []
y = 0
for bh in band_heights:
boxes.append((0, y, w, y + bh))
y += bh
return boxes
def slice_boxes_vertical(w: int, h: int, parts: int) -> List[Tuple[int, int, int, int]]:
"""
Vertical cutting → split along vertical lines (left->right slices).
"""
col_widths = []
for i in range(parts):
cw = (w // parts) + (1 if i < (w % parts) else 0)
col_widths.append(cw)
boxes = []
x = 0
for cw in col_widths:
boxes.append((x, 0, x + cw, h))
x += cw
return boxes
def slice_boxes_grid(w: int, h: int, rows: int, cols: int) -> List[Tuple[int, int, int, int]]:
# distribute remainder pixels to front rows/cols
row_heights = [(h // rows) + (1 if i < (h % rows) else 0) for i in range(rows)]
col_widths = [(w // cols) + (1 if j < (w % cols) else 0) for j in range(cols)]
boxes = []
y = 0
for i, rh in enumerate(row_heights):
x = 0
for j, cw in enumerate(col_widths):
boxes.append((x, y, x + cw, y + rh))
x += cw
y += rh
return boxes # row-major order (top→bottom, left→right)
def sort_indices_for_direction(
boxes: List[Tuple[int, int, int, int]],
w: int,
h: int,
mode: str,
direction: str,
center_out: bool,
rows_cols: Tuple[int, int] = (0, 0),
) -> List[int]:
idxs = list(range(len(boxes)))
if center_out:
cx_img, cy_img = w / 2.0, h / 2.0
centers = []
for i, (l, t, r, b) in enumerate(boxes):
cx = (l + r) / 2.0
cy = (t + b) / 2.0
d = (cx - cx_img) ** 2 + (cy - cy_img) ** 2
centers.append((d, i))
centers.sort(key=lambda x: x[0])
return [i for _, i in centers]
if mode == "horizontal":
if direction in ("tb", "t2b", "top-bottom"):
return idxs
elif direction in ("bt", "b2t", "bottom-top", "reverse"):
return list(reversed(idxs))
elif mode == "vertical":
if direction in ("lr", "l2r", "left-right"):
return idxs
elif direction in ("rl", "r2l", "right-left", "reverse"):
return list(reversed(idxs))
elif mode == "grid":
rows, cols = rows_cols
def rc_positions():
pos = []
k = 0
for r in range(rows):
for c in range(cols):
pos.append((r, c, k))
k += 1
return pos
pos = rc_positions()
if direction == "lr-tb":
order = sorted(pos, key=lambda x: (x[0], x[1]))
elif direction == "rl-tb":
order = sorted(pos, key=lambda x: (x[0], -x[1]))
elif direction == "lr-bt":
order = sorted(pos, key=lambda x: (-x[0], x[1]))
elif direction == "rl-bt":
order = sorted(pos, key=lambda x: (-x[0], -x[1]))
elif direction == "snake":
order = []
for r in range(rows):
row_items = [p for p in pos if p[0] == r]
row_items = sorted(row_items, key=lambda x: x[1])
if r % 2 == 1:
row_items = list(reversed(row_items))
order.extend(row_items)
elif direction in ("reverse", "bt-lr", "rb-lt"):
order = list(reversed(pos))
else:
order = sorted(pos, key=lambda x: (x[0], x[1]))
return [k for _, __, k in order]
return idxs
def save_tiles(
img: Image.Image,
boxes: List[Tuple[int, int, int, int]],
outdir: str,
prefix: str,
order: List[int],
) -> None:
digits = max(2, int(math.ceil(math.log10(len(boxes) + 1))))
for out_idx, i in enumerate(order, 1):
box = boxes[i]
tile = img.crop(box)
filename = f"{prefix}_{str(out_idx).zfill(digits)}.png"
tile.save(os.path.join(outdir, filename))
def run(
image_path: Optional[str],
outdir: Optional[str],
mode: str,
parts: Optional[int],
grid: Optional[Tuple[int, int]],
direction: str,
center_out: bool,
) -> str:
if not image_path:
image_path = pick_image_by_dialog()
if not image_path:
raise SystemExit("未选择图片,已退出。")
img = Image.open(image_path)
w, h = img.size
out_dir = make_outdir(image_path, outdir)
base = os.path.splitext(os.path.basename(image_path))[0]
prefix = f"{base}_{mode}"
if mode == "horizontal":
if not parts or parts < 1:
raise SystemExit("--parts 必须是正整数(水平切割需要指定份数)")
boxes = slice_boxes_horizontal(w, h, parts)
order = sort_indices_for_direction(boxes, w, h, mode, direction, center_out)
elif mode == "vertical":
if not parts or parts < 1:
raise SystemExit("--parts 必须是正整数(垂直切割需要指定份数)")
boxes = slice_boxes_vertical(w, h, parts)
order = sort_indices_for_direction(boxes, w, h, mode, direction, center_out)
elif mode == "grid":
if grid is None:
raise SystemExit("--grid 需要指定,格式 rowsxcols 例如 3x5")
rows, cols = grid
boxes = slice_boxes_grid(w, h, rows, cols)
order = sort_indices_for_direction(boxes, w, h, mode, direction, center_out, (rows, cols))
else:
raise SystemExit("未知的 mode")
save_tiles(img, boxes, out_dir, prefix, order)
return out_dir
def build_argparser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
description="图片任意切割工具:支持水平/垂直/网格切割、方向控制、中心向外排序。Python 3.8",
epilog=(
"示例:\n"
" 1) 水平切成 4 份,自上而下保存:\n"
" python image_slicer.py input.jpg --mode horizontal --parts 4 --direction tb\n"
" 2) 垂直切成 3 份,自右向左保存:\n"
" python image_slicer.py input.jpg --mode vertical --parts 3 --direction rl\n"
" 3) 网格切割 3x5,按蛇形顺序保存:\n"
" python image_slicer.py input.jpg --mode grid --grid 3x5 --direction snake\n"
" 4) 网格切割 4×4,按“中心 → 外围”距离排序保存:\n"
" python image_slicer.py input.jpg --mode grid --grid 4x4 --center-out\n"
" 5) 不带任何参数运行,将弹出文件选择器:\n"
" python image_slicer.py"
),
formatter_class=argparse.RawTextHelpFormatter,
)
p.add_argument("image", nargs="?", help="输入图片路径;省略则弹出文件选择器")
p.add_argument("--outdir", help="输出文件夹;默认为与图片同目录的 <name>_slices")
p.add_argument(
"--mode",
choices=["horizontal", "vertical", "grid"],
default="grid",
help="切割模式:horizontal(横向/按行)、vertical(纵向/按列)、grid(网格);默认 grid",
)
p.add_argument("--parts", type=int, help="水平/垂直模式下切成几份(正整数)")
p.add_argument("--grid", type=parse_grid, help="网格模式下的行×列,例如 3x5 或 3×5")
p.add_argument(
"--direction",
default="lr-tb",
help=(
"保存顺序(非中心模式):\n"
" horizontal: tb(上→下) | bt(下→上)\n"
" vertical : lr(左→右) | rl(右→左)\n"
" grid : lr-tb | rl-tb | lr-bt | rl-bt | snake | reverse\n"
"默认 lr-tb"
),
)
p.add_argument(
"--center-out",
action="store_true",
help="按与图像中心的距离由近到远排序保存(适用于任意模式)",
)
return p
def main():
parser = build_argparser()
# 如果命令里带 "?",直接显示帮助
import sys
if "?" in sys.argv:
parser.print_help()
return
args = parser.parse_args()
# 如果没有任何输入图片参数,进入 GUI 模式
if not args.image and len(sys.argv) == 1:
try:
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
except ImportError:
print("缺少 tkinter,无法打开 GUI,请安装 tkinter 或传入参数")
return
root = tk.Tk()
root.title("图片切割工具 - GUI模式")
# ========== 界面元素 ==========
tk.Label(root, text="切割模式:").grid(row=0, column=0, sticky="w")
mode_var = tk.StringVar(value="grid")
ttk.Combobox(root, textvariable=mode_var, values=["horizontal", "vertical", "grid"]).grid(row=0, column=1)
tk.Label(root, text="份数 (水平/垂直):").grid(row=1, column=0, sticky="w")
parts_var = tk.IntVar(value=2)
tk.Entry(root, textvariable=parts_var).grid(row=1, column=1)
tk.Label(root, text="网格 (行x列):").grid(row=2, column=0, sticky="w")
grid_var = tk.StringVar(value="2x2")
tk.Entry(root, textvariable=grid_var).grid(row=2, column=1)
tk.Label(root, text="方向:").grid(row=3, column=0, sticky="w")
dir_var = tk.StringVar(value="lr-tb")
ttk.Combobox(root, textvariable=dir_var,
values=["tb", "bt", "lr", "rl", "lr-tb", "rl-tb", "lr-bt", "rl-bt", "snake", "reverse"]).grid(
row=3, column=1)
center_out_var = tk.BooleanVar(value=False)
tk.Checkbutton(root, text="中心向外排序", variable=center_out_var).grid(row=4, columnspan=2)
def run_gui():
img_path = filedialog.askopenfilename(title="选择要切割的图片")
if not img_path:
messagebox.showerror("错误", "未选择图片")
return
grid_tuple = None
if mode_var.get() == "grid":
try:
grid_tuple = parse_grid(grid_var.get())
except:
messagebox.showerror("错误", "网格格式错误,应为 3x5")
return
outdir = run(
image_path=img_path,
outdir=None,
mode=mode_var.get(),
parts=parts_var.get() if mode_var.get() != "grid" else None,
grid=grid_tuple,
direction=dir_var.get(),
center_out=center_out_var.get()
)
messagebox.showinfo("完成", f"切割完成,输出目录:{outdir}")
tk.Button(root, text="选择图片并执行切割", command=run_gui).grid(row=5, columnspan=2, pady=10)
root.mainloop()
return
# 命令行模式
out_dir = run(
image_path=args.image,
outdir=args.outdir,
mode=args.mode,
parts=args.parts,
grid=args.grid,
direction=args.direction.lower() if args.direction else "lr-tb",
center_out=args.center_out,
)
print("切割完成,输出目录:", out_dir)
if __name__ == "__main__":
main()
浙公网安备 33010602011771号