Python图片转拼豆图纸:像素化与色板映射自动化教程
拼豆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 与 Pillow2.1 需要准备什么运行脚本只需要三个条件环境项要求说明操作系统Windows 10/11、macOS、常见 Linux 发行版脚本使用标准库和 Pillow不依赖平台特性Python3.9 及以上建议 3.10 或 3.11Pillow9.1 及以上脚本使用Image.ResamplingAPI不需要 GPU不需要 OpenCV不需要安装大型图像处理框架。学习环境和开发环境用同样的依赖即可生产环境额外考虑的是色板校准和批量输出这部分在最后一章展开。2.2 安装依赖先确认 Python 已经装好。在终端执行python --version输出类似Python 3.11.9就说明可用。如果这条命令在 Windows PowerShell 里报错先跳到第 7 章第 7.1 节处理 PATH 问题。接着安装 Pillowpip 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×6000LANCZOS缩放会比较慢。可以先在外部把图压缩到长边 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_size20): head !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.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 classboard % (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( fdiv classcell stylebackground-color:rgb({r},{g},{b}); fcolor:{text_color}; title{item[name]}{code}/div ) cells_html .join(cells) stat_items [] for code, st in sorted(stats.items(), keylambda x: -x[1][count]): stat_items.append(fli{code} - {st[name]} - {st[count]} 粒/li) tail /divhrh2颜色统计/h2ul .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, typeint, default29, help图纸宽度豆子个数默认 29) parser.add_argument(-H, --height, typeint, default0, help图纸高度默认按原图宽高比计算) parser.add_argument(-o, --output, defaultbeads_output, help输出目录) parser.add_argument(--blank, default255,250,240, help透明区域替换颜色逗号分隔 RGB) args parser.parse_args() output_dir Path(args.output) output_dir.mkdir(parentsTrue, exist_okTrue) 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, encodingutf-8) print(f文本图纸: {output_dir / plan.txt}) (output_dir / stats.json).write_text( json.dumps(stats, ensure_asciiFalse, indent2), encodingutf-8 ) print(f颜色统计: {output_dir / stats.json}) plan_html render_html(grid, stats) (output_dir / plan.html).write_text(plan_html, encodingutf-8) print(fHTML图纸: {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无输入图片路径必填-w29图纸宽度豆子个数-H0图纸高度0 表示按宽高比自动计算-obeads_output输出目录--blank255,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 环境。最可能的情况是机器上有多个 Pythonpip 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的变化这是理解颜色映射逻辑最直观的练习。