

本工具将普通图片自动转换为"在格子纸上用铅笔绘画"的艺术效果。支持田字格、横线格、方格、点阵四种格纸风格,配合多种笔触样式,生成类似手绘格子画的图片。
适用场景:
原始图片 → 灰度转换 → 网格划分 → 灰度采样 → 格纸底纹绘制 → 笔触填充 → 输出详细步骤:
pip install Pillow numpycd grid-art-converterpython grid_art.py启动后显示 1100×720 像素的GUI窗口。
┌─────────────────────────────────────────────────────────┐│ 📐 图片转格子画工具 田字格|横线格|方格|点阵 │├──────────┬───────────────────┬───────────────────────────┤│ 控制面板 │ 原图预览 │ 格子画效果预览 ││ │ │ ││ 导入图片 │ │ ││ ──────── │ │ ││ 格子类型 │ [原始图片] │ [转换结果] ││ ○田字格 │ │ ││ ○横线格 │ │ ││ ○方格 │ │ ││ ○点阵 │ │ ││ ──────── │ │ ││ 笔触风格 │ │ ││ ○方块填充 │ │ ││ ○交叉线 │ │ ││ ○斜线 │ │ ││ ○圆形 │ │ ││ ──────── │ │ ││ 格子大小 │ │ ││ [===●==] │ │ ││ 对比度 │ │ ││ [===●==] │ │ ││ ──────── │ │ ││ 颜色 │ filename.jpg │ 田字格|800×600|12px ││ 笔触 [■] │ 1200×900 │ ││ 格线 [■] │ │ ││ 背景 [□] │ │ ││ ──────── │ │ ││ [生成格子画]│ │ ││ [保存结果] │ │ │└──────────┴───────────────────┴───────────────────────────┘点击色块可打开取色器选择任意颜色。
人像照片:
风景照:
简笔画/线稿:
抽象/像素风:
输出图片尺寸 = (原图宽 ÷ 格子大小) × 格子大小 × (原图高 ÷ 格子大小) × 格子大小
例如:原图1200×900,格子12px → 输出 1200×900(100×75格)
grid-art-converter/├── grid_art.py # 主程序(转换引擎 + GUI界面)├── README.md # 快速说明└── DOCUMENTATION.md # 本文档Q: 运行报错 No module named 'numpy'
pip install numpyQ: 生成速度慢
Q: 效果太暗/太亮
Q: 格子线太明显/不明显
Q: 想要彩色效果而非灰度
当前版本为灰度笔触。如需彩色效果可修改 _draw_stroke 方法,从原图采样颜色值作为笔触颜色。
在 _draw_grid 方法中添加新的 elif 分支:
elif self.grid_type == "your_type":# 你的格子绘制逻辑pass在 _draw_stroke 方法中添加新的 elif 分支:
elif self.stroke_style == "your_style":# 你的笔触绘制逻辑pass修改 convert 方法,保留原图RGB信息,在 _draw_stroke 中使用原图对应区域的主色调作为笔触颜色。
"""图片转格子画工具 - Python GUI将普通图片转换为铅笔田字格/横线格风格的手绘效果。原理:将图片划分为网格(每格对应一个像素块)根据该区域的灰度值,在格子里用不同"笔触"填充叠加田字格/横线格底纹,模拟在格子纸上绘画的效果支持模式:田字格模式:方格子+对角虚线,像小学写字本横线格模式:横向线条,像信纸/笔记本方格模式:纯正方格子点阵模式:格子交叉点用点标记依赖: pip install Pillow numpy"""import tkinter as tkfrom tkinter import ttk, filedialog, messagebox, colorchooserfrom PIL import Image, ImageDraw, ImageFont, ImageTk, ImageFilterimport numpy as npimport osclass GridArtConverter:"""图片转格子画核心转换器"""def __init__(self):self.grid_size = 12 # 每格像素大小self.grid_type = "tian" # tian/hline/square/dotself.line_color = "#CCCCCC" # 格子线颜色self.stroke_color = "#333333" # 笔触颜色self.bg_color = "#FFFFFF" # 背景色self.contrast = 1.2 # 对比度增强self.threshold_levels = 5 # 灰度分级数self.stroke_style = "fill" # fill/cross/diagonal/circledef convert(self, img, output_size=None):"""将图片转换为格子画风格img: PIL Image对象output_size: 输出尺寸 (w, h),None则自动计算"""# 转灰度gray = img.convert('L')# 计算网格数量w, h = gray.sizecols = w // self.grid_sizerows = h // self.grid_sizeif cols < 5 or rows < 5:cols = max(cols, 20)rows = max(rows, 20)# 缩小图片到网格数量大小(每像素=一格)small = gray.resize((cols, rows), Image.LANCZOS)pixels = np.array(small, dtype=np.float32)# 对比度增强mean_val = pixels.mean()pixels = (pixels - mean_val) * self.contrast + mean_valpixels = np.clip(pixels, 0, 255)# 输出画布out_w = cols * self.grid_sizeout_h = rows * self.grid_sizecanvas = Image.new('RGB', (out_w, out_h), self.bg_color)draw = ImageDraw.Draw(canvas)# 绘制格子底纹self._draw_grid(draw, out_w, out_h, cols, rows)# 根据灰度值在每格填充笔触for row in range(rows):for col in range(cols):val = pixels[row, col]# 反转:越暗的地方笔触越重intensity = 1.0 - (val / 255.0)self._draw_stroke(draw, col, row, intensity)# 缩放到目标尺寸if output_size:canvas = canvas.resize(output_size, Image.LANCZOS)return canvasdef _draw_grid(self, draw, w, h, cols, rows):"""绘制格子底纹"""gs = self.grid_sizelc = self.line_colorif self.grid_type == "tian":# 田字格:外框+中间十字虚线for r in range(rows + 1):y = r * gsdraw.line([0, y, w, y], fill=lc, width=1)for c in range(cols + 1):x = c * gsdraw.line([x, 0, x, h], fill=lc, width=1)# 中间虚线(每格内中心十字)dash_color = self._lighten_color(lc, 0.5)for r in range(rows):y_mid = r * gs + gs // 2# 横向虚线for x in range(0, w, 6):draw.line([x, y_mid, min(x+3, w), y_mid], fill=dash_color, width=1)for c in range(cols):x_mid = c * gs + gs // 2for y in range(0, h, 6):draw.line([x_mid, y, x_mid, min(y+3, h)], fill=dash_color, width=1)elif self.grid_type == "hline":# 横线格for r in range(rows + 1):y = r * gsdraw.line([0, y, w, y], fill=lc, width=1)elif self.grid_type == "square":# 纯方格for r in range(rows + 1):y = r * gsdraw.line([0, y, w, y], fill=lc, width=1)for c in range(cols + 1):x = c * gsdraw.line([x, 0, x, h], fill=lc, width=1)elif self.grid_type == "dot":# 点阵格for r in range(rows + 1):for c in range(cols + 1):x = c * gsy = r * gsdraw.ellipse([x-1, y-1, x+1, y+1], fill=lc)def _draw_stroke(self, draw, col, row, intensity):"""在格子里画笔触"""if intensity < 0.05:return # 太浅跳过gs = self.grid_sizex1 = col * gs + 1y1 = row * gs + 1x2 = x1 + gs - 2y2 = y1 + gs - 2cx = (x1 + x2) // 2cy = (y1 + y2) // 2# 根据强度调整颜色深浅alpha = min(intensity * 1.3, 1.0)color = self._color_with_alpha(self.stroke_color, alpha)if self.stroke_style == "fill":# 填充式:根据强度填充不同大小的方块margin = int((1 - intensity) * gs // 2)margin = max(margin, 1)draw.rectangle([x1 + margin, y1 + margin, x2 - margin, y2 - margin], fill=color)elif self.stroke_style == "cross":# 交叉线:根据强度画1~3条线lw = max(1, int(intensity * 3))if intensity > 0.2:draw.line([x1, y1, x2, y2], fill=color, width=lw)if intensity > 0.5:draw.line([x2, y1, x1, y2], fill=color, width=lw)if intensity > 0.8:draw.line([cx, y1, cx, y2], fill=color, width=lw)elif self.stroke_style == "diagonal":# 斜线排列:根据强度增加斜线密度lw = 1count = int(intensity * 4) + 1step = gs // (count + 1)for i in range(1, count + 1):offset = i * stepdraw.line([x1, y1 + offset, x1 + offset, y1], fill=color, width=lw)draw.line([x2 - offset, y2, x2, y2 - offset], fill=color, width=lw)elif self.stroke_style == "circle":# 圆形:根据强度画不同大小的圆r = int(intensity * (gs // 2 - 1))r = max(r, 1)draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=color)def _color_with_alpha(self, hex_color, alpha):"""根据alpha混合颜色和背景"""r = int(hex_color[1:3], 16)g = int(hex_color[3:5], 16)b = int(hex_color[5:7], 16)bg_r = int(self.bg_color[1:3], 16)bg_g = int(self.bg_color[3:5], 16)bg_b = int(self.bg_color[5:7], 16)out_r = int(r * alpha + bg_r * (1 - alpha))out_g = int(g * alpha + bg_g * (1 - alpha))out_b = int(b * alpha + bg_b * (1 - alpha))return f"#{out_r:02x}{out_g:02x}{out_b:02x}"def _lighten_color(self, hex_color, factor):"""颜色变浅"""r = int(hex_color[1:3], 16)g = int(hex_color[3:5], 16)b = int(hex_color[5:7], 16)r = int(r + (255 - r) * factor)g = int(g + (255 - g) * factor)b = int(b + (255 - b) * factor)return f"#{r:02x}{g:02x}{b:02x}"============ GUI 主界面 ============class GridArtApp:def init(self, root):self.root = rootself.root.title("图片转格子画工具 - 田字格/横线格绘画风格")self.root.geometry("1100x720")self.root.configure(bg="#f5f5f5")self.converter = GridArtConverter()self.source_img = Noneself.result_img = Noneself.tk_source = Noneself.tk_result = Noneself._build_ui()def _build_ui(self):# 顶部标题栏header = tk.Frame(self.root, bg="#34495e", height=50)header.pack(fill=tk.X)header.pack_propagate(False)tk.Label(header, text="📐 图片转格子画工具", font=("Microsoft YaHei", 14, "bold"),bg="#34495e", fg="white").pack(side=tk.LEFT, padx=20, pady=12)tk.Label(header, text="田字格 | 横线格 | 方格 | 点阵", font=("Microsoft YaHei", 9),bg="#34495e", fg="#95a5a6").pack(side=tk.RIGHT, padx=20)# 主体:三栏布局body = tk.Frame(self.root, bg="#f5f5f5")body.pack(fill=tk.BOTH, expand=True, padx=16, pady=12)# 左栏:控制面板ctrl_panel = tk.Frame(body, bg="#ffffff", width=280, relief=tk.FLAT)ctrl_panel.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 12))ctrl_panel.pack_propagate(False)self._build_controls(ctrl_panel)# 中栏:原图预览mid_panel = tk.Frame(body, bg="#ffffff")mid_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 12))self._build_source_preview(mid_panel)# 右栏:结果预览right_panel = tk.Frame(body, bg="#ffffff")right_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)self._build_result_preview(right_panel)def _build_controls(self, parent):"""控制面板"""pad = {"padx": 14, "pady": 4}# 导入图片tk.Label(parent, text="操作", font=("Microsoft YaHei", 10, "bold"),bg="#ffffff").pack(anchor="w", padx=14, pady=(16, 8))tk.Button(parent, text="📂 导入图片", font=("Microsoft YaHei", 10),bg="#3498db", fg="white", relief=tk.FLAT, cursor="hand2",command=self._load_image).pack(fill=tk.X, **pad)ttk.Separator(parent).pack(fill=tk.X, padx=14, pady=12)# 格子类型tk.Label(parent, text="格子类型", font=("Microsoft YaHei", 10, "bold"),bg="#ffffff").pack(anchor="w", **pad)self.grid_type_var = tk.StringVar(value="tian")types = [("田字格", "tian"), ("横线格", "hline"), ("方格", "square"), ("点阵", "dot")]type_frame = tk.Frame(parent, bg="#ffffff")type_frame.pack(fill=tk.X, **pad)for i, (label, val) in enumerate(types):rb = tk.Radiobutton(type_frame, text=label, variable=self.grid_type_var,value=val, bg="#ffffff", font=("Microsoft YaHei", 9),command=self._on_param_change)rb.grid(row=i // 2, column=i % 2, sticky="w", padx=4, pady=2)# 笔触风格tk.Label(parent, text="笔触风格", font=("Microsoft YaHei", 10, "bold"),bg="#ffffff").pack(anchor="w", padx=14, pady=(12, 4))self.stroke_var = tk.StringVar(value="fill")strokes = [("方块填充", "fill"), ("交叉线", "cross"), ("斜线", "diagonal"), ("圆形", "circle")]stroke_frame = tk.Frame(parent, bg="#ffffff")stroke_frame.pack(fill=tk.X, **pad)for i, (label, val) in enumerate(strokes):rb = tk.Radiobutton(stroke_frame, text=label, variable=self.stroke_var,value=val, bg="#ffffff", font=("Microsoft YaHei", 9),command=self._on_param_change)rb.grid(row=i // 2, column=i % 2, sticky="w", padx=4, pady=2)# 格子大小tk.Label(parent, text="格子大小", font=("Microsoft YaHei", 10, "bold"),bg="#ffffff").pack(anchor="w", padx=14, pady=(12, 4))self.grid_size_var = tk.IntVar(value=12)size_frame = tk.Frame(parent, bg="#ffffff")size_frame.pack(fill=tk.X, **pad)self.size_label = tk.Label(size_frame, text="12px", font=("Microsoft YaHei", 9),bg="#ffffff", fg="#666", width=5)self.size_label.pack(side=tk.RIGHT)ttk.Scale(size_frame, from_=6, to=30, variable=self.grid_size_var,orient=tk.HORIZONTAL, command=self._on_size_change).pack(side=tk.LEFT, fill=tk.X, expand=True)# 对比度tk.Label(parent, text="对比度", font=("Microsoft YaHei", 10, "bold"),bg="#ffffff").pack(anchor="w", padx=14, pady=(12, 4))self.contrast_var = tk.DoubleVar(value=1.2)contrast_frame = tk.Frame(parent, bg="#ffffff")contrast_frame.pack(fill=tk.X, **pad)self.contrast_label = tk.Label(contrast_frame, text="1.2", font=("Microsoft YaHei", 9),bg="#ffffff", fg="#666", width=5)self.contrast_label.pack(side=tk.RIGHT)ttk.Scale(contrast_frame, from_=0.5, to=3.0, variable=self.contrast_var,orient=tk.HORIZONTAL, command=self._on_contrast_change).pack(side=tk.LEFT, fill=tk.X, expand=True)# 颜色设置ttk.Separator(parent).pack(fill=tk.X, padx=14, pady=12)tk.Label(parent, text="颜色", font=("Microsoft YaHei", 10, "bold"),bg="#ffffff").pack(anchor="w", **pad)color_frame = tk.Frame(parent, bg="#ffffff")color_frame.pack(fill=tk.X, **pad)tk.Label(color_frame, text="笔触:", bg="#ffffff", font=("Microsoft YaHei", 9)).grid(row=0, column=0, sticky="w")self.stroke_color_btn = tk.Button(color_frame, bg="#333333", width=4, height=1,relief=tk.FLAT, cursor="hand2", command=self._pick_stroke_color)self.stroke_color_btn.grid(row=0, column=1, padx=8, pady=4)tk.Label(color_frame, text="格线:", bg="#ffffff", font=("Microsoft YaHei", 9)).grid(row=1, column=0, sticky="w")self.line_color_btn = tk.Button(color_frame, bg="#CCCCCC", width=4, height=1,relief=tk.FLAT, cursor="hand2", command=self._pick_line_color)self.line_color_btn.grid(row=1, column=1, padx=8, pady=4)tk.Label(color_frame, text="背景:", bg="#ffffff", font=("Microsoft YaHei", 9)).grid(row=2, column=0, sticky="w")self.bg_color_btn = tk.Button(color_frame, bg="#FFFFFF", width=4, height=1,relief=tk.GROOVE, cursor="hand2", command=self._pick_bg_color)self.bg_color_btn.grid(row=2, column=1, padx=8, pady=4)# 转换 & 保存按钮ttk.Separator(parent).pack(fill=tk.X, padx=14, pady=12)tk.Button(parent, text="🔄 生成格子画", font=("Microsoft YaHei", 11, "bold"),bg="#27ae60", fg="white", relief=tk.FLAT, cursor="hand2",command=self._convert).pack(fill=tk.X, padx=14, pady=4)tk.Button(parent, text="💾 保存结果", font=("Microsoft YaHei", 10),bg="#8e44ad", fg="white", relief=tk.FLAT, cursor="hand2",command=self._save_result).pack(fill=tk.X, padx=14, pady=4)def _build_source_preview(self, parent):"""原图预览"""tk.Label(parent, text="原图", font=("Microsoft YaHei", 10, "bold"),bg="#ffffff").pack(anchor="w", padx=12, pady=(12, 6))self.source_canvas = tk.Canvas(parent, width=350, height=350, bg="#f0f0f0",highlightthickness=1, highlightbackground="#ddd")self.source_canvas.pack(padx=12, pady=8, fill=tk.BOTH, expand=True)self.source_info = tk.Label(parent, text="请导入图片", font=("Microsoft YaHei", 9),bg="#ffffff", fg="#999")self.source_info.pack(pady=(0, 8))def _build_result_preview(self, parent):"""结果预览"""tk.Label(parent, text="格子画效果", font=("Microsoft YaHei", 10, "bold"),bg="#ffffff").pack(anchor="w", padx=12, pady=(12, 6))self.result_canvas = tk.Canvas(parent, width=350, height=350, bg="#f0f0f0",highlightthickness=1, highlightbackground="#ddd")self.result_canvas.pack(padx=12, pady=8, fill=tk.BOTH, expand=True)self.result_info = tk.Label(parent, text="点击「生成格子画」查看效果",font=("Microsoft YaHei", 9), bg="#ffffff", fg="#999")self.result_info.pack(pady=(0, 8))# ============ 事件处理 ============def _load_image(self):"""导入图片"""filepath = filedialog.askopenfilename(filetypes=[("图片文件", "*.png *.jpg *.jpeg *.bmp *.webp *.gif")])if not filepath:returnself.source_img = Image.open(filepath).convert('RGB')self._show_source_preview()self.source_info.config(text=f"{os.path.basename(filepath)} | "f"{self.source_img.size[0]}×{self.source_img.size[1]}")def _show_source_preview(self):"""显示原图预览"""if not self.source_img:return# 缩放到预览区display = self.source_img.copy()display.thumbnail((350, 350), Image.LANCZOS)self.tk_source = ImageTk.PhotoImage(display)self.source_canvas.delete("all")self.source_canvas.create_image(175, 175, image=self.tk_source)def _on_param_change(self, *args):"""参数变更 → 自动重新生成"""if self.source_img:self._convert()def _on_size_change(self, val):"""格子大小变更"""v = int(float(val))self.size_label.config(text=f"{v}px")self.grid_size_var.set(v)def _on_contrast_change(self, val):"""对比度变更"""v = round(float(val), 1)self.contrast_label.config(text=str(v))self.contrast_var.set(v)def _pick_stroke_color(self):"""选择笔触颜色"""color = colorchooser.askcolor(title="笔触颜色", initialcolor=self.converter.stroke_color)if color[1]:self.converter.stroke_color = color[1]self.stroke_color_btn.config(bg=color[1])if self.source_img:self._convert()def _pick_line_color(self):"""选择格线颜色"""color = colorchooser.askcolor(title="格线颜色", initialcolor=self.converter.line_color)if color[1]:self.converter.line_color = color[1]self.line_color_btn.config(bg=color[1])if self.source_img:self._convert()def _pick_bg_color(self):"""选择背景颜色"""color = colorchooser.askcolor(title="背景颜色", initialcolor=self.converter.bg_color)if color[1]:self.converter.bg_color = color[1]self.bg_color_btn.config(bg=color[1])if self.source_img:self._convert()def _convert(self):"""执行转换"""if not self.source_img:messagebox.showwarning("提示", "请先导入图片")return# 更新转换器参数self.converter.grid_size = self.grid_size_var.get()self.converter.grid_type = self.grid_type_var.get()self.converter.stroke_style = self.stroke_var.get()self.converter.contrast = self.contrast_var.get()# 执行转换self.result_img = self.converter.convert(self.source_img)# 显示结果预览display = self.result_img.copy()display.thumbnail((350, 350), Image.LANCZOS)self.tk_result = ImageTk.PhotoImage(display)self.result_canvas.delete("all")self.result_canvas.create_image(175, 175, image=self.tk_result)w, h = self.result_img.sizegrid_type_names = {"tian": "田字格", "hline": "横线格", "square": "方格", "dot": "点阵"}self.result_info.config(text=f"{grid_type_names.get(self.converter.grid_type)} | "f"{w}×{h}px | 格子{self.converter.grid_size}px")def _save_result(self):"""保存结果"""if not self.result_img:messagebox.showwarning("提示", "请先生成格子画")returnfilepath = filedialog.asksaveasfilename(defaultextension=".png",filetypes=[("PNG图片", "*.png"), ("JPEG图片", "*.jpg")],initialfile="grid_art_output.png",title="保存格子画")if filepath:if filepath.lower().endswith('.jpg'):self.result_img.convert('RGB').save(filepath, "JPEG", quality=95)else:self.result_img.save(filepath, "PNG")messagebox.showinfo("成功", f"已保存到:\n{filepath}")def main():root = tk.Tk()app = GridArtApp(root)root.mainloop()if name == 'main':main()