Python实现图片转拼豆图纸:像素化与色卡匹配自动化脚本
拼豆爱好者别手算了用 Python 写个全自动拼豆脚本图片像素化到图纸一步到位做过拼豆手工的朋友应该都懂一个痛看到一张好看的图想做成拼豆挂件但第一步就卡住了。手动把图片放大成方格一格一格对照色卡数颜色再估算每种豆子要买多少——一张中等尺寸的图光数豆子就能花掉一晚上。更崩溃的是数完还不一定准拼到一半发现主色调豆子不够只能临时换色做出来的效果和原图差一截。这篇文章要分享的就是一个专为“画像玩法”设计的全自动拼豆脚本。它的核心很简单把任意一张图片自动转成拼豆图纸并且按拼豆标准色卡完成颜色映射最后直接输出每种颜色的豆子数量。新版本还加入了图片像素化功能可以直接生成马赛克效果图方便你在动手之前预览成品大致样子。读完这篇文章你能得到三样东西第一理解拼豆自动化的核心算法也就是图片缩放、颜色量化和色卡匹配分别解决什么问题第二拿到一套完整可运行的 Python 脚本输入一张图片输出拼豆图纸和采购清单第三知道在实际作品中哪些环节最容易翻车以及怎么调整参数让成品更接近原图。1. 拼豆自动化到底在解决什么问题先把拼豆这件手工活拆开看。无论多复杂的图案拼豆的本质都是一个二维矩阵每个格子放一颗某种颜色的豆子最终由成百上千颗豆子组成一幅像素画。所以从“一张照片”到“一幅拼豆作品”中间要完成三个步骤降采样把图片缩小到拼豆需要的格子数比如 40×40 格。这一步决定作品的物理尺寸。颜色映射图片里的颜色可能有几万种但拼豆色卡通常只有三四十种。必须把每个像素的颜色映射到最接近的色卡颜色。数量统计统计每种颜色用掉多少颗豆子方便按需采购。听起来不难但手动做极其痛苦。一张 50×50 格的图就是 2500 个像素点每个点都要对照色卡判断颜色再做记录。脚本做的只是把这件重复劳动自动化但真正的难点在于怎么让降采样后的图看起来还像原图以及怎么让颜色映射结果不翻车。这两点没处理好脚本生成的图纸会很“脏”做出来的拼豆会显得斑驳杂乱。这个脚本的价值也正在于此。它不改变拼豆手工本身的乐趣而是把最消耗耐心的图纸制作环节从人工计算变成自动生成。适合的人群很明确拼豆爱好者、做定制礼物的小卖家、手工工作室以及想尝试用图像处理解决实际问题的开发者。2. 核心概念图片像素化、颜色量化与色卡匹配在写代码之前先把三个关键概念讲清楚。理解了这三个概念后面看代码就是顺水推舟的事。2.1 图片像素化图片像素化通俗说就是把图片变成“马赛克”。操作上很简单先把原图缩小到一个很小的尺寸比如 40×40 像素再用最近邻插值把图放大回可视尺寸。这样每个小方块内就是均匀的纯色形成颗粒感。这里不推荐使用双线性或双三次插值来做放大步骤因为它们会生成平滑过渡的中间色反而破坏了像素画的硬朗边界。最近邻插值NEAREST是像素化效果的关键。2.2 颜色量化一张照片经过降采样后仍然可能有几百种颜色。拼豆色卡一般只有几十种所以必须做颜色量化——也就是把颜色数量压缩到色卡数量以内。最简单的量化方式就是最近颜色匹配把一个像素点的 RGB 值和色卡里的每个颜色逐一计算距离取距离最近的那个。距离通常用欧氏距离distance sqrt((r1-r2)^2 (g1-g2)^2 (b1-b2)^2)加权欧氏距离更贴近人眼感知因为人眼对绿色更敏感。实际项目中常见的加权方式是让 R、G、B 的权重分别为 30%、59%、11%近似人眼的亮度感知曲线。2.3 色卡匹配拼豆品牌很多每个品牌的色卡略有差异。MARD、Hama、Perler 这些品牌的常用色号在色值上会有细微变化。脚本必须内置一套明确的标准色卡而不是从图片中动态提取颜色。这里有一个新手容易踩的坑有些人做颜色量化时先对图片颜色做聚类把 256 色压缩成 32 色生成一套“图片专属调色板”。这样做的效果是单张图很好看但换一张图就得重新聚类而且聚类出来的颜色在实物色卡里根本找不到对应豆子。拼豆脚本的色卡必须是固定的、可采购的而不是算法生成的。这是拼豆自动化区别于普通图片压缩的关键点。3. 环境准备与前置条件这个脚本完全基于 Python依赖极少适合在个人电脑上直接运行。3.1 运行环境依赖用途说明Python 3.8运行环境本文示例基于 Python 3版本请以实际环境为准Pillow图像读取、缩放、保存核心图像处理库NumPy颜色距离计算、数组操作加速批量像素处理OpenCV可选高级图像预处理非必需后面会说明可选场景安装命令pip install pillow numpy如果后续想做更复杂的预处理比如自动裁剪主体、调整亮度对比度可以额外安装pip install opencv-python3.2 准备色卡文件脚本需要一份拼豆色卡。你可以在项目目录下新建一个bead_colors.json文件格式如下[ {name: 白色, rgb: [255, 255, 255]}, {name: 黑色, rgb: [35, 31, 32]}, {name: 红色, rgb: [237, 41, 57]}, {name: 深蓝, rgb: [0, 84, 159]}, {name: 浅蓝, rgb: [0, 163, 224]}, {name: 黄色, rgb: [255, 242, 0]}, {name: 绿色, rgb: [0, 176, 80]}, {name: 橙色, rgb: [255, 140, 0]}, {name: 粉色, rgb: [255, 174, 200]}, {name: 紫色, rgb: [112, 48, 160]} ]注意这里的颜色值和品牌实际色卡会有偏差实际使用时应替换为你所用拼豆品牌的官方色值。色卡质量直接决定成品效果这个环节建议认真对待。4. 核心流程拆解整个脚本的执行流程可以分为四个步骤每个步骤都有明确的输入和输出也可以单独调试。4.1 图片预处理读入图片后首先要做的是统一尺寸。拼豆图纸的尺寸通常是正方形但原图不一定是。这里有两个选择强制拉伸成正方形适合内容为居中主体的图片中心裁剪后再缩放适合内容铺满画面的图片。实际项目中推荐中心裁剪后再缩放因为强制拉伸会让主体变形拼出来的人像会“变胖”或“变瘦”。4.2 像素化降采样把预处理后的图片缩放到目标格子数。格子数直接决定拼豆作品的物理尺寸。以标准 29×29 钉板为例如果图纸是 50×50 格就需要拆成多块板。建议新手从 30×30 或 40×40 起步。4.3 颜色映射对降采样后的每个像素计算它到色卡中每种颜色的加权距离取最小距离对应的颜色。这个步骤在纯 Python 中可以用三层循环实现但效率低。用 NumPy 的广播机制可以一次性完成整张图的颜色匹配速度提升几十倍。4.4 生成图纸和统计清单颜色映射完成后我们就有了一个二维数组每个元素是一个色号。接下来要做两件事生成图纸把二维数组渲染成带格线的图纸图片每个格子填充对应颜色并标注色号生成清单用字典统计每种色号出现次数输出为文本清单按数量排序。5. 完整示例代码实现下面给出一个可运行的完整版本。代码分三个文件配置文件、核心逻辑、运行入口。5.1 色卡加载模块# 文件路径bead_config.py import json class BeadColorManager: 拼豆色卡管理器 def __init__(self, config_pathbead_colors.json): with open(config_path, r, encodingutf-8) as f: self.color_data json.load(f) self.names [item[name] for item in self.color_data] self.rgb_array None self._build_rgb_array() def _build_rgb_array(self): 将色卡转为 NumPy 数组shape 为 (颜色数, 3) import numpy as np rgb_list [item[rgb] for item in self.color_data] self.rgb_array np.array(rgb_list, dtypenp.float64) def get_color_count(self): return len(self.color_data)这里的关键是提前把色卡转换成 NumPy 数组。后续在做颜色匹配时可以利用矩阵运算批量计算距离避免逐一遍历。5.2 图片像素化与颜色匹配# 文件路径bead_converter.py import numpy as np from PIL import Image class BeadConverter: 图片转拼豆图纸 def __init__(self, color_manager, grid_size40): self.cm color_manager self.grid_size grid_size def load_and_preprocess(self, image_path): 读取图片裁剪为正方形并缩放到 grid_size x grid_size img Image.open(image_path).convert(RGB) w, h img.size # 中心裁剪为正方形 side min(w, h) left (w - side) // 2 top (h - side) // 2 img img.crop((left, top, left side, top side)) # 降采样到目标格子数 img img.resize((self.grid_size, self.grid_size), Image.LANCZOS) return np.array(img, dtypenp.float64) def match_colors(self, pixel_array): 将每个像素匹配到最近的色卡颜色返回色号索引矩阵 h, w, _ pixel_array.shape flat_pixels pixel_array.reshape(-1, 3) # 使用加权欧氏距离权重模拟人眼亮度感知 weights np.array([0.3, 0.59, 0.11]) # 计算加权距离矩阵 diff flat_pixels[:, np.newaxis, :] - self.cm.rgb_array[np.newaxis, :, :] dist np.sqrt(((diff ** 2) * weights).sum(axis2)) # 取距离最小的颜色索引 idx np.argmin(dist, axis1) return idx.reshape(h, w) def generate_chart(self, idx_matrix, cell_size20): 根据色号索引矩阵生成带格线的图纸图片 h, w idx_matrix.shape chart_img Image.new(RGB, (w * cell_size, h * cell_size), white) draw chart_img.load() for y in range(h): for x in range(w): color_idx idx_matrix[y, x] rgb tuple(int(v) for v in self.cm.rgb_array[color_idx]) for dy in range(cell_size): for dx in range(cell_size): draw[x * cell_size dx, y * cell_size dy] rgb # 绘制格线方便对照放豆子 from PIL import ImageDraw draw_obj ImageDraw.Draw(chart_img) line_color (200, 200, 200) for i in range(w 1): x_pos i * cell_size draw_obj.line([(x_pos, 0), (x_pos, h * cell_size)], fillline_color, width1) for j in range(h 1): y_pos j * cell_size draw_obj.line([(0, y_pos), (w * cell_size, y_pos)], fillline_color, width1) return chart_img def count_beads(self, idx_matrix): 统计每种颜色需要的豆子数量 flat idx_matrix.flatten() unique, counts np.unique(flat, return_countsTrue) result [] for u, c in zip(unique, counts): name self.cm.names[u] rgb tuple(int(v) for v in self.cm.rgb_array[u]) result.append({name: name, code: u, rgb: rgb, count: int(c)}) result.sort(keylambda x: x[count], reverseTrue) return result这段代码里有几个值得解释的重点。第一个是加权距离的权重。weights np.array([0.3, 0.59, 0.11])对应 R、G、B 三个通道的感知权重。绿色权重最高是因为人眼对绿色波长的敏感度最高。这个细节会让颜色映射结果在视觉上更准确。第二个是 NumPy 的广播机制。flat_pixels[:, np.newaxis, :] - self.cm.rgb_array[np.newaxis, :, :]这行代码实际上是让每个像素都和整张色卡做一次减法运算一次性得到(像素数, 色卡颜色数, 3)的差异矩阵。如果没有 NumPy用纯 Python 三层循环处理一张 40×40 的图需要 1600×色卡数约 48000 次距离计算虽然也能跑但会有明显卡顿感。用矩阵运算后这个计算量在毫秒级完成。5.3 图纸渲染与数量清单输出# 文件路径run_converter.py import sys from PIL import Image from bead_config import BeadColorManager from bead_converter import BeadConverter def main(): if len(sys.argv) 2: print(用法: python run_converter.py 图片路径 [网格数]) print(示例: python run_converter.py photo.jpg 40) return image_path sys.argv[1] grid_size int(sys.argv[2]) if len(sys.argv) 2 else 40 cm BeadColorManager(bead_colors.json) converter BeadConverter(cm, grid_sizegrid_size) print(f正在处理图片: {image_path}) print(f目标网格: {grid_size} x {grid_size}) # 1. 读取并预处理 pixel_array converter.load_and_preprocess(image_path) # 2. 颜色匹配 idx_matrix converter.match_colors(pixel_array) # 3. 生成图纸 chart_img converter.generate_chart(idx_matrix, cell_size20) chart_img.save(fbead_chart_{grid_size}.png) print(f图纸已保存: bead_chart_{grid_size}.png) # 4. 统计数量 counts converter.count_beads(idx_matrix) print(\n豆子数量统计:) print(- * 40) total 0 for item in counts: rgb_str ,.join(str(v) for v in item[rgb]) print(f{item[name]:8s} RGB({rgb_str:15s}) 需要 {item[count]:4d} 颗) total item[count] print(- * 40) print(f总计: {total} 颗豆子) print(f涉及颜色数: {len(counts)} 种) # 5. 可选生成像素化预览图 preview Image.open(image_path).convert(RGB) preview preview.resize((grid_size, grid_size), Image.LANCZOS) preview preview.resize((grid_size * 20, grid_size * 20), Image.NEAREST) preview.save(fpreview_{grid_size}.png) print(f像素化预览已保存: preview_{grid_size}.png) if __name__ __main__: main()5.4 新增的“图片像素化”预览功能这里有一个设计细节值得说明。预览功能使用了两次缩放preview preview.resize((grid_size, grid_size), Image.LANCZOS) preview preview.resize((grid_size * 20, grid_size * 20), Image.NEAREST)第一次用LANCZOS把图片压缩到目标格子数这一步是为了让降采样后的颜色尽量接近原图的整体色调第二次用NEAREST放大是为了产生“马赛克颗粒感”同时保证每个格子内部颜色均匀没有插值产生的过渡色。这两次缩放缺一不可。如果只用一次LANCZOS缩放到目标尺寸输出图看起来会是模糊的一团色块而不是清晰的马赛克如果第一次也用NEAREST那么颜色采样过于粗糙会丢失原图的细节层次。6. 运行结果与效果验证6.1 运行命令在项目目录下执行python run_converter.py your_photo.jpg 40如果使用自定义尺寸python run_converter.py your_photo.jpg 506.2 预期输出正常运行时会输出类似下面的摘要正在处理图片: your_photo.jpg 目标网格: 40 x 40 图纸已保存: bead_chart_40.png 豆子数量统计: ---------------------------------------- 白色 RGB(255,255,255) 需要 328 颗 黑色 RGB(35,31,32) 需要 156 颗 红色 RGB(237,41,57) 需要 89 颗 蓝色 RGB(0,84,159) 需要 42 颗 黄色 RGB(255,242,0) 需要 31 颗 ---------------------------------------- 总计: 1600 颗豆子 涉及颜色数: 5 种同时会在当前目录生成两张图片bead_chart_40.png带格线的拼豆图纸每个格子填充对应色卡颜色preview_40.png像素化预览图方便先看看整体效果。6.3 如何判断结果是否成功判断标准有三个层次第一数量正确。总豆子数应该等于grid_size × grid_size。如果汇总数量不等于网格总数说明计数逻辑有 bug需要查代码。第二颜色不脏。观察像素化预览图主体区域颜色应该干净连续不应该出现大量细碎的杂色点。如果杂色点多通常是因为原图色彩过渡太复杂或者色卡覆盖不够。第三实际拼图与图纸一致。这是最终的验证标准。按图纸放置豆子时如果发现某些相邻格子的颜色肉眼几乎一样可以考虑在色卡中合并相近色减少颜色种类让图纸更易读。6.4 失败排查第一步如果脚本运行报错最常见的原因有两个一是 Pillow 尚未安装报错信息会是ModuleNotFoundError二是配置文件路径不对报错信息会包含bed_colors.json相关提示。第一步永远是看完整报错栈定位到文件路径和行号再对照本文章节排查。7. 常见问题与排查思路问题现象可能原因排查方式解决方案图纸颜色脏、杂点多原图色彩过渡复杂或色卡覆盖不全打开像素化预览图放大查看杂点分布增加色卡颜色数量先用图像处理软件提高原图对比度、降低饱和度人像拼出来“不像”网格数太小面部细节丢失检查预览图观察五官区域是否还能识别提高grid_size处理前裁剪突出面部区域总豆数不等于网格数计数逻辑或尺寸 bug打印idx_matrix.shape确认形状检查load_and_preprocess的返回数组形状相邻两格颜色肉眼难分色卡中两个颜色在感知上过于接近打印这两个颜色对应的 RGB 值在色卡中合并或删除其中一个预览图“风格”不是马赛克第二次缩放没有用NEAREST检查代码中resize的第二个参数修改为Image.NEAREST图纸格子太小难辨认cell_size设置过小查看图纸分辨率调大cell_size或分块导出大图处理大图时内存占用高NumPy 数组类型为float64查看任务管理器在load_and_preprocess中改用np.float32色号与实体豆子对不上色卡 RGB 是从屏幕取色非官方值对照官方色卡逐条检查替换为拼豆品牌的官方 RGB 值8. 最佳实践与工程建议8.1 色卡先行代码在后拼豆脚本的成色上限取决于色卡质量而不是算法。算法再好色卡与实物的偏差大做出来的作品就偏离预期。建议动手写代码前先花时间整理一份准确的色卡。具体做法是买一套拼豆在自然光下拍摄实物照片再用取色器提取每种豆子的 RGB 值最后人工校准。这个前期投入会直接体现在成品效果上。8.2 图片选择有讲究不是所有图都适合做拼豆。高清单反照片放大后细节丰富但缩小到 40×40 之后大量环境杂色会被保留导致图纸变脏。相比之下扁平化插画、像素画、Logo 这类颜色干净、边界清晰的图片生成图纸的效果远好于写实照片。一条比较实用的经验是优先选主体大、背景简单、颜色对比清晰的图。如果一定要用照片建议先用图像处理软件做一步预处理——把背景换成纯色或做一次“卡通化/扁平化”滤镜。8.3 网格数不是越大越好网格数决定细节但也决定工程量。40×40 需要 1600 颗豆子60×60 就需要 3600 颗翻了一倍多。对新手来说建议先用 30×30 做一次小样确认配色和整体效果后再做 40×40 或更大的成品。小样可以提前暴露色卡偏差问题避免大工程做完才发现方向错了。8.4 脚本化之外配置文件独立管理色卡、网格数、权重这些参数不要写死在业务代码里。建议用 JSON 或 YAML 配置文件单独管理方便不同项目间复用。比如同一套色卡可以用于 30×30、40×40、60×60 三种尺寸的图纸生成只要在运行命令时传入不同参数即可。8.5 工程化的下一步方向当前脚本还是一个单体工具如果要做成工作室级别的工具几个方向值得扩展批量处理一次导入多张图片自动生成对应的图纸和清单预览调参界面用 Streamlit 或 Gradio 做一个简单的 Web 界面实时调整网格数和色卡即时预览效果色卡导入支持从外部 CSV 导入官方色卡不必改代码自动配色袋生成清单后按颜色输出分袋标签方便装袋整理。9. 总结与后续学习方向这个拼豆脚本看起来功能不多但背后涉及的图像处理知识点其实非常典型图像缩放中的插值算法选择、颜色空间里的感知距离计算、批量像素操作中的矩阵化思维。这些技能不只在拼豆场景能用换到其他图像处理任务——比如像素画生成、图像主题色提取、简易风格迁移——都是一脉相承的。如果你打算继续深入下一步有两个方向。一是优化颜色匹配质量研究 CIEDE2000 色差公式它能更准确地模拟人眼对不同颜色的感知差异二是升级交互方式把脚本封装成带界面的小程序让不熟悉命令行的拼豆爱好者也能直接使用。给所有想动手试试的人一个建议先不要追求大尺寸用一张轮廓清晰的 Logo 图配合 30×30 的网格跑通流程看看精度和效果是否符合预期。拼豆图纸自动生成这件事代码只是第一步真正让你做出好作品的是对颜色和效果的耐心调整。希望这篇文章能帮你少踩一些坑把更多时间留给拼豆本身的乐趣。如果你在运行时遇到报错按照上面的排查表逐条对照即可。建议先收藏这篇博客等真正动手做的时候再拿出来对照。