当前位置: 首页 > news >正文

图片任意切割工具(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, Optionalfrom PIL import Image# ——— For file dialog when no input path ———
def pick_image_by_dialog() -> Optional[str]:try:import tkinter as tkfrom tkinter import filedialogroot = 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 Noneexcept Exception as e:print("无法打开文件选择器:", e)return Nonedef 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, colsdef make_outdir(img_path: str, outdir: Optional[str]) -> str:if outdir:os.makedirs(outdir, exist_ok=True)return outdirbase = 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 outdef 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 = 0for bh in band_heights:boxes.append((0, y, w, y + bh))y += bhreturn boxesdef 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 = 0for cw in col_widths:boxes.append((x, 0, x + cw, h))x += cwreturn boxesdef slice_boxes_grid(w: int, h: int, rows: int, cols: int) -> List[Tuple[int, int, int, int]]:# distribute remainder pixels to front rows/colsrow_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 = 0for i, rh in enumerate(row_heights):x = 0for j, cw in enumerate(col_widths):boxes.append((x, y, x + cw, y + rh))x += cwy += rhreturn 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.0centers = []for i, (l, t, r, b) in enumerate(boxes):cx = (l + r) / 2.0cy = (t + b) / 2.0d = (cx - cx_img) ** 2 + (cy - cy_img) ** 2centers.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 idxselif direction in ("bt", "b2t", "bottom-top", "reverse"):return list(reversed(idxs))elif mode == "vertical":if direction in ("lr", "l2r", "left-right"):return idxselif direction in ("rl", "r2l", "right-left", "reverse"):return list(reversed(idxs))elif mode == "grid":rows, cols = rows_colsdef rc_positions():pos = []k = 0for r in range(rows):for c in range(cols):pos.append((r, c, k))k += 1return pospos = 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 idxsdef 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.sizeout_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 = gridboxes = 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_dirdef 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 pdef main():parser = build_argparser()# 如果命令里带 "?",直接显示帮助import sysif "?" in sys.argv:parser.print_help()returnargs = parser.parse_args()# 如果没有任何输入图片参数,进入 GUI 模式if not args.image and len(sys.argv) == 1:try:import tkinter as tkfrom tkinter import ttk, filedialog, messageboxexcept ImportError:print("缺少 tkinter,无法打开 GUI,请安装 tkinter 或传入参数")returnroot = 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("错误", "未选择图片")returngrid_tuple = Noneif mode_var.get() == "grid":try:grid_tuple = parse_grid(grid_var.get())except:messagebox.showerror("错误", "网格格式错误,应为 3x5")returnoutdir = 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()
http://www.hskmm.com/?act=detail&tid=21201

相关文章:

  • 从零构建能自我优化的AI Agent:Reflection和Reflexion机制对比详解与实现
  • 超精简的小型C编译器
  • Day1 Linux 入门:9 个核心命令(whoami/id/pwd 等)
  • 9.29 闲话
  • MMU的作用
  • 大二学计算机系统基础
  • 20250929 之所思 - 人生如梦
  • 9/29
  • 9.29总结
  • lc1040-移动石子直到连续II
  • 2025年9月29日
  • c++算法学习笔记
  • test5
  • 最高人民法院新劳动争议司法解释一 理解与适用
  • PyPI维护者遭遇钓鱼攻击:假冒登录网站威胁开源供应链安全
  • Tomcat 相关漏洞扫描器(一) - 指南
  • 题解:CF2125E Sets of Complementary Sums
  • 929
  • ManySpeech —— 使用 C# 开发人工智能语音应用
  • 20250929
  • 驱动基础知识速览(迅为RK3568文档)
  • 学习笔记-析合树
  • CSPJ2025模拟赛
  • java代码审计-Shiro认证授权
  • CF868F题解
  • ThinkPHP反序列化分析
  • AT_iroha2019_day4_l 题解
  • AT_abc290_f 题解
  • 一张图读懂绿电直连系统架构 - 智慧园区
  • P5469 [NOI2019] 机器人 题解