拼豆(Perler Beads / Fuse Beads)是一种把彩色塑料豆按图纸排列在钉板上、再加热融合的手工玩法。常见工作流是先在电脑上找图、手动打马赛克、数格子、配颜色,图稍微大一点就要耗尽整个下午。这篇文章要介绍的是一个可以自动完成整条路径的 Python 脚本:输入任意图片,输出一套可用于拼豆的像素化图纸、颜色对照表和用豆数量统计。脚本新增的图片像素化功能,可以直接把 PNG、JPG 甚至手机照片转成钉板网格,不再依赖人工预处理。先给出结果形态:脚本会生成preview.png马赛克预览图、plan.txt文本图纸、plan.html网页图纸和stats.json用豆统计,拿到这四个文件,后面要做的只剩照着图纸往钉板上放豆子。
1. 拼豆图纸脚本的设计思路:图片到钉板的工作流
1.1 手工拼豆流程中的三个耗时环节
手工做拼豆作品,通常要经历三个阶段。
第一个阶段是选图和预处理。找一张喜欢的角色图片,用绘图软件放大、裁切、降分辨率,把它变成一个个方格。方格尺寸必须和钉板数量对应,常见小号钉板是 29×29 颗豆子,大号板可以到 58×58。这个阶段的问题在于,图片缩小之后颜色会糊成一片,边缘也不干净。
第二个阶段是配颜色。拼豆不是任意颜色都有,每个品牌只有几十种标准色。把图片里的每个像素颜色对应到最接近的豆子颜色,靠眼睛逐个看效率很低。人的视觉对颜色差异的判断还会受屏幕色偏影响,经常出现图纸打出来、豆子摆上去才发现颜色不对。
第三个阶段是统计用量。一幅 29×29 的图纸有 841 个格子,手工统计每种颜色需要多少颗豆子非常容易出错,漏算一颗就要跑一趟门店或者等几天快递。
这三个环节正好是脚本能自动化的地方:降采样做像素化,色板匹配做颜色映射,矩阵遍历做数量统计。
1.2 脚本工作流速览
整个脚本的工作流是线性管道,每一步的输入输出都很清楚:
原图 -> [像素化] -> 网格小图 -> [色板映射] -> 代码矩阵 -> [输出] -> 图纸与统计像素化解决“图变成多少乘多少的格子”的问题,色板映射解决“每一格用哪个颜色的豆子”的问题,输出阶段解决“人怎么照着拼”的问题。
选择 Python 而不是 Node.js 或 Go,主要原因是 Pillow 图像库成熟,Image.resize()一行就能完成高质量缩放,不需要引入浏览器环境。脚本本身是纯命令行工具,Windows、macOS、Linux 都能跑,也和标题里提到的“脚本”定位一致。
1.3 为什么色板才是图纸生成的锚点
很多人做拼豆图纸时先关注像素化,但真正决定成果是否还原的关键是色板。
像素化只决定了“分多少格”,它不会让颜色更准。如果一张图有 24 位真彩色,1624 万种颜色,而手头的豆子只有 24 种,最终每一格到底取哪个颜色,完全由色板匹配逻辑决定。
脚本里必须把色板当作第一等公民来处理。理想流程是先定义色板,再把图片颜色映射到色板。反过来如果先缩小图片、再做颜色匹配,会出现一个常见问题:缩放时产生的中间色在色板里根本不存在,匹配结果就会偏向某个不合理的颜色。更稳妥的做法是先把色板匹配函数准备好,再对缩略图的每个像素做一次匹配。
2. 环境准备:Python 3 与 Pillow
2.1 需要准备什么
运行脚本只需要三个条件:
| 环境项 | 要求 | 说明 |
|---|---|---|
| 操作系统 | Windows 10/11、macOS、常见 Linux 发行版 | 脚本使用标准库和 Pillow,不依赖平台特性 |
| Python | 3.9 及以上 | 建议 3.10 或 3.11 |
| Pillow | 9.1 及以上 | 脚本使用Image.ResamplingAPI |
不需要 GPU,不需要 OpenCV,不需要安装大型图像处理框架。学习环境和开发环境用同样的依赖即可,生产环境额外考虑的是色板校准和批量输出,这部分在最后一章展开。
2.2 安装依赖
先确认 Python 已经装好。在终端执行:
python --version输出类似Python 3.11.9就说明可用。如果这条命令在 Windows PowerShell 里报错,先跳到第 7 章第 7.1 节处理 PATH 问题。
接着安装 Pillow:
pip install Pillow如果本机有多个 Python 版本,建议用虚拟环境:
python -m venv venv source venv/bin/activate # macOS / Linux venv\Scripts\activate # Windows PowerShell pip install Pillow安装完成后验证导入:
python -c "from PIL import Image; print(Image.__version__)"能输出版本号就说明依赖就绪。
2.3 项目目录结构
建议在一个独立目录下操作:
beads_maker/ ├── beads_maker.py # 脚本本体 ├── demo.png # 测试图片 └── beads_output/ # 输出目录,自动创建脚本会按用户传入的输出目录自动创建beads_output,不需要手工建目录。图片路径建议用英文文件名,避免个别终端在中文路径上出现编码问题。
3. 第一步:图片像素化,把任意图片变成马赛克
3.1 像素化不是简单打马赛克
图片像素化听起来像美图工具里的马赛克滤镜,但拼豆场景里的像素化有更明确的目标:把原图缩放到一个固定网格,例如 29×29,然后再放大成可视化预览图。
这里的核心不是“模糊”,而是“降采样”。29×29 意味着每颗豆子对应原图里的一块矩形区域,矩形区域内的所有细节都被丢弃,只保留一个代表颜色。
实现时使用两次resize:
第一次把原图缩放到目标网格尺寸,使用LANCZOS重采样算法。这个算法会参考周围像素做高质量加权平均,得到的缩略图每个像素都能代表原图中一小块区域的平均颜色。
第二次把缩略图放大到一个容易被肉眼检查的尺寸,例如每格 20 像素,使用NEAREST算法。它不做平滑,直接把一个像素的颜色铺满整个方块,形成马赛克边缘。
3.2 两次 resize 的代码
def pixelate(img, grid_w, grid_h): downsample = Image.Resampling.LANCZOS harden = Image.Resampling.NEAREST grid_img = img.resize((grid_w, grid_h), downsample) preview = grid_img.resize((grid_w * 20, grid_h * 20), harden) return grid_img, previewgrid_img是后续做颜色匹配用的,preview是给人看的预览图。
这里有一个常见的坑:如果只做一次NEAREST缩放,缩略图每个像素只会取自原图某个点的颜色,容易产生噪点;如果只做一次LANCZOS缩放,缩略图颜色是平均了,可放大的预览图边缘会模糊,看不清格子边界。两次 resize 各司其职,才能让图纸既“颜色平均”又“边界清晰”。
如果安装的 Pillow 版本较老,不提供Image.Resampling,可以退回到Image.LANCZOS和Image.NEAREST,效果一致。
3.3 尺寸策略:钉板宽度和宽高比
拼豆图纸的尺寸必须和钉板对应。脚本通过宽度参数控制网格:
python beads_maker.py demo.png -w 29高度默认按原图宽高比自动计算。如果原图是正方形,结果就是 29×29;如果原图很宽,结果可能是 29×12。
也可以同时指定宽高:
python beads_maker.py demo.png -w 29 -H 29强制指定高度会拉伸图片,导致角色比例变形。除非是有意做拉伸效果,否则建议只指定-w,让高度自动适配。
确定尺寸的代码:
width = args.width if args.height and args.height > 0: height = args.height else: height = max(1, round(img.height / img.width * width))max(1, ...)是为了防止超宽图计算出高度为 0。
3.4 透明图片与超长图处理
脚本在读取图片时统一转成 RGBA 模式:
img = Image.open(args.image).convert("RGBA")RGBA 多了一个 alpha 通道。拼豆没有透明豆,所以透明像素必须替换成实际颜色。脚本默认用米白色(255, 250, 240)填充透明区域,因为白色拼豆在多数作品里是底色常用色。
透明区域处理放在颜色匹配阶段:
r, g, b, a = grid_img.getpixel((x, y)) if a < 128: r, g, b = blank_rgbblank_rgb默认是(255, 250, 240),可以通过--blank参数改成其他 RGB 值。
超长图的问题在高度自动计算时已经解决,但如果原图非常大,例如 8000×6000,LANCZOS缩放会比较慢。可以先在外部把图压缩到长边 2000 像素左右再交给脚本,实际效果几乎没有差别,因为 29×29 网格根本保留不了高分辨率细节。
4. 第二步:色板映射,像素颜色如何变成豆子
4.1 拼豆色板就是颜色的离散集合
拼豆和打印不一样。打印可以把任意颜色渐变换算成 CMYK 网点,拼豆只能从几十种实物颜色里挑一个最接近的。所以色板映射的实质是:在有限颜色集合里,为每个像素找一个距离最近的色板颜色。
如果原图颜色正好落在两个色板颜色的中间,脚本会取距离更小的那个。距离计算方法直接决定像素化后哪个颜色占主导。
4.2 颜色距离:加权 RGB 而不是裸欧氏距离
最直觉的距离计算是 RGB 三维空间里的欧氏距离:
def color_distance(c1, c2): return (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2这个公式简单,但不符合人眼感知。人眼对绿色最敏感,对蓝色相对不敏感。在 RGB 空间里,绿色通道的小幅度变化比蓝色通道更容易被察觉。
推荐使用加权距离:
def color_distance(c1, c2): dr = (c1[0] - c2[0]) ** 2 dg = (c1[1] - c2[1]) ** 2 * 1.6 db = (c1[2] - c2[2]) ** 2 return dr + dg + db绿色通道权重 1.6,意味着两个颜色绿色差异会被放大,匹配结果会更贴近人眼感受。
如果追求更严格的颜色差异度量,可以转换到 CIELAB 或 OKLAB 色彩空间再计算 ΔE。那样需要额外实现色彩空间转换或安装colour-science库,这个脚本为了保持轻量,使用加权 RGB 已经够用。
4.3 示例色板定义
色板是唯一需要用户按实际情况调整的部分。下面这组色板是常见的 24 色集合,RGB 值适合作为起点,但不承诺与任意品牌完全一致:
PALETTE = [ {"code": "A", "name": "白色", "rgb": (255, 250, 240)}, {"code": "B", "name": "浅灰", "rgb": (200, 200, 200)}, {"code": "C", "name": "中灰", "rgb": (128, 128, 128)}, {"code": "D", "name": "深灰", "rgb": (64, 64, 64)}, {"code": "E", "name": "黑色", "rgb": (20, 20, 20)}, {"code": "F", "name": "米黄", "rgb": (245, 230, 180)}, {"code": "G", "name": "柠檬黄", "rgb": (255, 240, 0)}, {"code": "H", "name": "橙色", "rgb": (255, 140, 0)}, {"code": "I", "name": "大红", "rgb": (220, 40, 50)}, {"code": "J", "name": "酒红", "rgb": (140, 20, 30)}, {"code": "K", "name": "粉色", "rgb": (255, 180, 190)}, {"code": "L", "name": "桃红", "rgb": (240, 90, 120)}, {"code": "M", "name": "紫色", "rgb": (140, 60, 160)}, {"code": "N", "name": "深紫", "rgb": (80, 30, 100)}, {"code": "O", "name": "天蓝", "rgb": (120, 190, 240)}, {"code": "P", "name": "蓝色", "rgb": (40, 90, 200)}, {"code": "Q", "name": "深蓝", "rgb": (25, 50, 120)}, {"code": "R", "name": "青色", "rgb": (0, 180, 180)}, {"code": "S", "name": "浅绿", "rgb": (160, 220, 120)}, {"code": "T", "name": "翠绿", "rgb": (60, 170, 70)}, {"code": "U", "name": "深绿", "rgb": (30, 100, 40)}, {"code": "V", "name": "浅棕", "rgb": (190, 150, 100)}, {"code": "W", "name": "棕色", "rgb": (140, 90, 50)}, {"code": "X", "name": "深棕", "rgb": (90, 55, 30)}, ]每个颜色都有一个字母代号,文本图纸就是用这些字母组成的。24 种颜色对应 A 到 X,足够覆盖常见的动漫、像素画和照片类主题。
这里的 RGB 值不一定代表某个真实品牌的官方色卡。实际使用前,建议把手里现有的豆子平铺在自然光下拍照,用取色工具把 RGB 值填进PALETTE,这才是色板校准的正确方式。
4.4 颜色统计:从图纸到采购清单
每个像素匹配到颜色后,脚本边遍历边统计:
def build_grid(grid_img, blank_rgb=(255, 250, 240)): grid = [] stats = {} for y in range(grid_img.height): row = [] for x in range(grid_img.width): r, g, b, a = grid_img.getpixel((x, y)) if a < 128: r, g, b = blank_rgb color = match_color((r, g, b)) row.append(color["code"]) stats.setdefault( color["code"], {"name": color["name"], "rgb": list(color["rgb"]), "count": 0}, ) stats[color["code"]]["count"] += 1 grid.append(row) return grid, statsgrid是二维字符数组,stats是每个颜色代号对应的豆子数。输出stats.json后,可以直接按“颜色->数量”核对库存,形成采购清单。
5. 完整脚本:图片转拼豆图纸生成器
5.1 完整脚本代码
把下面代码保存为beads_maker.py:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """图片转拼豆图纸生成器:像素化 + 色板映射 + 输出图纸与用量统计。""" import argparse import json import math import sys from pathlib import Path try: from PIL import Image except ImportError: sys.exit("缺少 Pillow 依赖,请先执行: pip install Pillow") PALETTE = [ {"code": "A", "name": "白色", "rgb": (255, 250, 240)}, {"code": "B", "name": "浅灰", "rgb": (200, 200, 200)}, {"code": "C", "name": "中灰", "rgb": (128, 128, 128)}, {"code": "D", "name": "深灰", "rgb": (64, 64, 64)}, {"code": "E", "name": "黑色", "rgb": (20, 20, 20)}, {"code": "F", "name": "米黄", "rgb": (245, 230, 180)}, {"code": "G", "name": "柠檬黄", "rgb": (255, 240, 0)}, {"code": "H", "name": "橙色", "rgb": (255, 140, 0)}, {"code": "I", "name": "大红", "rgb": (220, 40, 50)}, {"code": "J", "name": "酒红", "rgb": (140, 20, 30)}, {"code": "K", "name": "粉色", "rgb": (255, 180, 190)}, {"code": "L", "name": "桃红", "rgb": (240, 90, 120)}, {"code": "M", "name": "紫色", "rgb": (140, 60, 160)}, {"code": "N", "name": "深紫", "rgb": (80, 30, 100)}, {"code": "O", "name": "天蓝", "rgb": (120, 190, 240)}, {"code": "P", "name": "蓝色", "rgb": (40, 90, 200)}, {"code": "Q", "name": "深蓝", "rgb": (25, 50, 120)}, {"code": "R", "name": "青色", "rgb": (0, 180, 180)}, {"code": "S", "name": "浅绿", "rgb": (160, 220, 120)}, {"code": "T", "name": "翠绿", "rgb": (60, 170, 70)}, {"code": "U", "name": "深绿", "rgb": (30, 100, 40)}, {"code": "V", "name": "浅棕", "rgb": (190, 150, 100)}, {"code": "W", "name": "棕色", "rgb": (140, 90, 50)}, {"code": "X", "name": "深棕", "rgb": (90, 55, 30)}, ] def color_distance(c1, c2): dr = (c1[0] - c2[0]) ** 2 dg = (c1[1] - c2[1]) ** 2 * 1.6 db = (c1[2] - c2[2]) ** 2 return dr + dg + db def match_color(rgb): best_color = None best_distance = math.inf for item in PALETTE: d = color_distance(rgb, item["rgb"]) if d < best_distance: best_distance = d best_color = item return best_color def pixelate(img, grid_w, grid_h): downsample = Image.Resampling.LANCZOS harden = Image.Resampling.NEAREST grid_img = img.resize((grid_w, grid_h), downsample) preview = grid_img.resize((grid_w * 20, grid_h * 20), harden) return grid_img, preview def build_grid(grid_img, blank_rgb=(255, 250, 240)): grid = [] stats = {} for y in range(grid_img.height): row = [] for x in range(grid_img.width): r, g, b, a = grid_img.getpixel((x, y)) if a < 128: r, g, b = blank_rgb color = match_color((r, g, b)) row.append(color["code"]) stats.setdefault( color["code"], {"name": color["name"], "rgb": list(color["rgb"]), "count": 0}, ) stats[color["code"]]["count"] += 1 grid.append(row) return grid, stats def render_text(grid): lines = [] for row in grid: lines.append("".join(row)) return "\n".join(lines) def render_html(grid, stats, cell_size=20): head = """ <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>拼豆图纸</title> <style> body { font-family: -apple-system, "Microsoft YaHei", sans-serif; padding: 16px; } .board { display: grid; gap: 1px; grid-template-columns: repeat(%d, %dpx); } .cell { width: %dpx; height: %dpx; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; } </style> </head> <body> <h1>拼豆图纸</h1> <div class="board"> """ % (len(grid[0]), cell_size, cell_size, cell_size) code_map = {item["code"]: item for item in PALETTE} cells = [] for row in grid: for code in row: item = code_map[code] r, g, b = item["rgb"] brightness = (r * 299 + g * 587 + b * 114) // 1000 text_color = "#ffffff" if brightness < 140 else "#000000" cells.append( f'<div class="cell" style="background-color:rgb({r},{g},{b});' f'color:{text_color};" title="{item["name"]}">{code}</div>' ) cells_html = "".join(cells) stat_items = [] for code, st in sorted(stats.items(), key=lambda x: -x[1]["count"]): stat_items.append(f"<li>{code} - {st['name']} - {st['count']} 粒</li>") tail = "</div><hr><h2>颜色统计</h2><ul>" + "".join(stat_items) + "</ul></body></html>" return head + cells_html + tail def main(): parser = argparse.ArgumentParser(description="图片转拼豆图纸") parser.add_argument("image", help="输入图片路径") parser.add_argument("-w", "--width", type=int, default=29, help="图纸宽度(豆子个数),默认 29") parser.add_argument("-H", "--height", type=int, default=0, help="图纸高度,默认按原图宽高比计算") parser.add_argument("-o", "--output", default="beads_output", help="输出目录") parser.add_argument("--blank", default="255,250,240", help="透明区域替换颜色,逗号分隔 RGB") args = parser.parse_args() output_dir = Path(args.output) output_dir.mkdir(parents=True, exist_ok=True) img = Image.open(args.image).convert("RGBA") print(f"原图尺寸: {img.width} x {img.height}") width = args.width if args.height and args.height > 0: height = args.height else: height = max(1, round(img.height / img.width * width)) print(f"图纸网格: {width} x {height}") grid_img, preview = pixelate(img, width, height) grid, stats = build_grid(grid_img) preview_path = output_dir / "preview.png" preview.save(preview_path) print(f"像素化预览图: {preview_path}") plan_text = render_text(grid) (output_dir / "plan.txt").write_text(plan_text, encoding="utf-8") print(f"文本图纸: {output_dir / 'plan.txt'}") (output_dir / "stats.json").write_text( json.dumps(stats, ensure_ascii=False, indent=2), encoding="utf-8" ) print(f"颜色统计: {output_dir / 'stats.json'}") plan_html = render_html(grid, stats) (output_dir / "plan.html").write_text(plan_html, encoding="utf-8") print(f"HTML图纸: {output_dir / 'plan.html'}") total = sum(item["count"] for item in stats.values()) print(f"预计用豆: {total} 粒") print("处理完成。") if __name__ == "__main__": main()代码由四个模块组成:色板定义与颜色匹配、图像像素化、图纸渲染、主流程。PALETTE和render_html是后续最可能需要个性化调整的部分。
5.2 命令行参数速查
| 参数 | 默认值 | 作用 |
|---|---|---|
image | 无 | 输入图片路径,必填 |
-w | 29 | 图纸宽度(豆子个数) |
-H | 0 | 图纸高度,0 表示按宽高比自动计算 |
-o | beads_output | 输出目录 |
--blank | 255,250,240 | 透明区域替换色,RGB 逗号分隔 |
实际项目中,-w是最常改的参数。29 对应小号钉板,58 对应大号板。
5.3 运行示例
准备一张测试图片demo.png,然后执行:
python beads_maker.py demo.png -w 29正常输出结果类似:
原图尺寸: 800 x 600 图纸网格: 29 x 22 像素化预览图: beads_output/preview.png 文本图纸: beads_output/plan.txt 颜色统计: beads_output/stats.json HTML图纸: beads_output/plan.html 预计用豆: 638 粒 处理完成。如果原图是 800×600,-w 29时高度自动算成 22,总豆数就是 638。到这一步,脚本的完整链路已经跑通。
6. 输出验证:如何确认图纸可以照着拼
6.1 预览图:先看主体轮廓是否保留
preview.png是第一道检查关。打开它,看主体轮廓是否可辨认。拼豆图纸不是照片,不需要看清五官细节,但角色的整体轮廓、主要色块的位置必须清楚。
如果预览图完全看不出原图内容,优先检查两件事:
- 网格尺寸是否太小。例如一张 1000×800 的多人物图,压到 29×22 后每个人只剩几个格子,自然看不出来。
- 原图是否主体太小、背景太多。建议先裁掉多余背景再跑脚本。
预览图由grid_img.resize((grid_w * 20, grid_h * 20), NEAREST)生成,所以它的尺寸一定是网格的 20 倍,方便肉眼逐格检查。
6.2 文本图纸与 HTML 图纸的用法
plan.txt是纯文本图纸,每一行代表钉板一行,每个字母代表一种颜色。例如:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAA AABBBBBBBBBBBBBBBBBBBBBBBBAA AABCCCCCCCCCCCCCCCCCCCCCCBAA这种格式适合直接打印,也适合用文本对比工具检查两个版本之间的差异。
plan.html更适合在手机或平板上看。浏览器打开后,每个格子都有真实颜色背景和字母代号,鼠标悬停可以看到颜色名称。页面底部还有按用量排序的颜色统计列表,可以直接对着列表分配豆子盒。
实际使用中,手机看plan.html比看plan.txt舒服得多,因为颜色和字母是一体的,不用来回对照表头。
6.3 颜色统计 JSON 的正确使用
stats.json的结构如下:
{ "A": { "name": "白色", "rgb": [255, 250, 240], "count": 320 }, "E": { "name": "黑色", "rgb": [20, 20, 20], "count": 180 } }字段含义:
name:颜色名称rgb:色板中的 RGB 值,用于核对颜色count:这种颜色的豆子数量
采购时建议每种颜色多买 5% 到 10% 作为损耗,因为拼豆容易在熔融过程中移位或掉落。
这一节的信息用于说明如何验证脚本输出。如果stats.json中某个颜色数量异常高,例如黑色占了 70%,很可能是原图背景过暗或透明区域被替换成了深色,需要回去看预览图。
7. 常见问题排查:从导入图片到生成图纸
7.1 PowerShell 提示无法将 python 识别为 cmdlet
在 Windows PowerShell 中,如果运行python时出现:
python : 无法将“python”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。常见原因是 Python 安装时没有勾选“Add Python to PATH”。排查路径:
- 打开 PowerShell,执行
where.exe python,看是否能找到路径。 - 如果找不到,说明 PATH 中确实没有 Python。
- 在开始菜单搜索“Edit the system environment variables”,进入“环境变量”。
- 在“Path”中加入 Python 安装目录,例如
C:\Users\用户名\AppData\Local\Programs\Python\Python311\,同时加入它的Scripts子目录。 - 重新打开 PowerShell,执行
python --version验证。
也可以使用 Microsoft Store 安装 Python,这种方式通常会自动配置 PATH。
7.2 ModuleNotFoundError: No module named 'PIL'
脚本开头已经捕获了缺少 Pillow 的情况,但如果你在另一个环境里手动导入,可能仍然会遇到:
ModuleNotFoundError: No module named 'PIL'这个报错表示 Pillow 没有安装到当前 Python 环境。最可能的情况是机器上有多个 Python,pip install Pillow装进了 A 环境,而python beads_maker.py用的是 B 环境。
解决办法是统一使用带-m的安装方式:
python -m pip install Pillow同样的 Python 解释器执行安装和执行脚本,就不会出现环境错位。
7.3 网格宽度越大越清晰吗
不一定。-w 29已经能把大多数单主体图片的主轮廓表达清楚。继续调大到 58,豆子数量从 841 涨到 3364,手工拼豆时间会成倍增加。
真正决定清晰度的不是网格数量,而是色板与图片颜色的匹配程度。如果色板的棕色系太少,即使网格从 29 调到 58,肤色或者木色区域仍然会大面积映射成错误的灰色系。
遇到图片细节确实很丰富、29 不够用的情况,优先考虑把图片主体裁切居中,让重要部分占更多格子,而不是盲目调大网格。
7.4 图纸颜色发灰、发脏怎么办
颜色发脏通常有两个原因。
第一个原因是色板里面缺少足够的高饱和颜色。原图的亮黄、亮蓝迁到灰色系。
第二个原因是透明背景被替换成了灰色。如果原图是黑底图,透明区域被默认的米白色替换后,匹配出来就是浅灰。
处理方式:
- 检查
PALETTE中颜色是否覆盖常见高饱和色。 - 调整
--blank,将透明背景换成与原图底色接近的颜色。 - 在图片预处理阶段,直接把背景填成纯白色或纯黑色,再交给脚本。
7.5 透明区域变成黑色
如果 PNG 有透明通道,但输出图纸里透明区域全部变成黑色,说明脚本执行时读取的像素顺序有偏差。检查代码里的解包语句:
r, g, b, a = grid_img.getpixel((x, y))必须确保图片是真 RGBA 模式。如果用户在外部把图片转成了 RGB 模式,getpixel只返回三个通道,这句解包就会直接抛错或者错位。在脚本入口统一执行convert("RGBA")正是为了防止这种情况。
如果仍然出现黑色区域,可以把--blank显式指定为255,255,255,并确认执行时参数已经传入build_grid。
8. 从能跑到好用:实际拼豆项目的最佳实践
8.1 用真实豆子校准色板
任何在博客、网络分享中获得的色板 RGB 都只能作为起点,因为生产批次、拍摄环境、屏幕色偏都会让实物颜色和 RGB 值有出入。
校准方法:把手里所有颜色的豆子每样取一颗,平铺在白色 A4 纸上,在自然光下拍照。用取色工具逐个读取 RGB,替换PALETTE中对应项。
这一步做完,脚本的意义才会真正体现。色板越贴近实物,生成图纸的可用度越高。
8.2 支持大图与多钉板拼接
一幅 29×29 的图纸大约相当于一个小挂件,如果要做海报级作品,需要多块 29×29 钉板拼成 58×58 甚至更大。脚本本身不限制网格尺寸,可以直接用-w 58生成整图。
多钉板拼接的实际问题是分块。拼豆玩家通常希望输出每个 29×29 钉板对应的独立图纸。可以基于现有脚本扩展一个分块参数,按 29 步长把grid切成多个子网格,分别调用渲染函数输出独立 HTML。切块时要保证颜色代码不重新计算,直接用同一个色板映射结果,这样拼起来颜色才连续。
对于新手,不要一上来就挑战 58×58。先完成一个 29×29 的单色块作品,跑通“脚本生成图纸到手工拼豆”的完整闭环,再考虑大图。
8.3 批量生成与自定义输出
如果社团活动或工作室需要一次性生成多张图纸,可以用系统脚本做批量调度。在 Windows 下是 bat 文件,在 Linux 和 macOS 下是 shell 脚本。
一个 shell 批量示例,遍历目录下所有 PNG 并生成 29 宽图纸:
#!/usr/bin/env bash for image in images/*.png; do python beads_maker.py "$image" -w 29 -o "out/$(basename "$image" .png)" done每条命令都会生成独立的输出目录,不影响彼此的stats.json。
生产环境使用前,还要考虑三点:
- 原图目录是否只有目标图片,避免误处理。
- 输出目录是否存在同名旧文件,是否需要先清空。
- 批量跑完是否有汇总表,需要把多个
stats.json合并成 Excel 或 CSV 方便采购。
8.4 后续扩展方向
这个脚本的架构足够简单,扩展点也很清晰:
- 色板外置:把
PALETTE抽成palette.json,不同品牌豆子切换配置文件,不需要改代码。 - 限制颜色数量:当某个作品只需要 12 种豆子时,可以在匹配前做一轮颜色聚类,只保留用量最大的 12 种颜色再映射。
- 更精确的颜色距离:把像素从 RGB 转换到 CIELAB 空间,计算真正的感知色差,解决深色区域匹配不准的问题。
- 输出 PDF 图纸:把
plan.txt或 HTML 转成适合 A4 打印的 PDF,方便打印后按格子手工标记进度。 - 反向导入:允许读取现有拼豆作品照片,识别格子颜色后输出数字化图纸,方便整理和分享。
代码本身并不长,读一遍、改一版,比直接复制使用更能理解每一步为什么这么设计。第一次跑通后,建议手动改一下PALETTE的颜色数值,观察preview.png和stats.json的变化,这是理解颜色映射逻辑最直观的练习。