做拼豆这几年,一个很深的体会是:“图好找,图纸难画。”尤其当你想把一张心仪的照片、插画甚至游戏截图变成能照着拼的拼豆图纸时,手工一个一个像素点去描色号,基本是体力活。后来我花了不少时间把整条链路做成了一套能落地的技术流程,核心是“像素化生成 + 色板匹配”,既保留了原图的辨识度,又把颜色数量控制在拼豆色板可表达的范围内。这篇文章就把我踩过的坑、试过的方法和可以直接抄作业的代码都整理出来,给同样想把任意图像转为拼豆图纸的朋友参考。
这套方法不只是服务拼豆爱好者,做十字绣图纸、像素画、乐高马赛克、甚至串珠图设计,底层逻辑都是相通的。核心解决三个问题:怎么把连续图像离散成格子、怎么把成百上千种颜色压缩成色板能覆盖的色号、怎么让压缩之后的结果仍然好看且可拼。如果你是第一次接触拼豆图纸生成,或者已经在手动画图但效率很低,这篇内容应该能帮你把工期从几天压缩到几分钟。
1. 整体设计与思路拆解
1.1 为什么偏像素化而非矢量描摹
很多人第一次想到“把图像变成拼豆图纸”,第一反应是用Photoshop或Illustrator的“图像描摹”功能转成矢量,再输出色块。这个思路不是不行,但实际试过就知道,矢量描摹追求的是平滑边缘和色块连续,而拼豆图纸恰恰需要离散格子和可数色号。
拼豆的物理单位是一颗颗豆子,它天然就是像素化的。矢量图里的弧形边缘、串色渐变,到了拼豆层面,要么需要很细的排线去模拟,要么就直接变成不可拼的抽象图形。所以我最终采用的是“先降采样成像素网格,再做颜色量化”这条路径。它的好处是:第一步就把问题和像素的操作绑定,后续所有环节都可以用图像处理的常规手段去解决,比如用 OpenCV 或 Pillow 就能完成大部分工作,不需要引入复杂的轮廓追踪和区域填充算法。
另外,像素化天然保留“信息密度均匀”的特征。对于拼豆图,我们希望在画面的每个区域豆数大致均衡,而不是在细节多的地方塞一堆豆、在背景大色块的地方又空一大片。矢量描摹的结果通常是细节多的区域形状密集,背景区域形状巨大,这种不均匀到了拼豆尺寸环节会很麻烦:要么缩小后细节糊掉,要么放大后背景变成一张巨毯。像素化降采样则是一刀切,固定格子数,每个格子的信息量天然平均。
1.2 色板匹配:从“眼睛挑色”到“算法算色”
色板匹配是整个流程里最有“技术含量”也最容易翻车的环节。市面上的拼豆品牌色号少则二三十个,多则上百个,但相对于图像里可能出现的上千万种颜色来说,还是太少了。直接把每个格子取平均色,然后复制到拼豆图纸上,会出现两种情况:要么平均色在色板里找不到近似色,要么找了一个看着差不多的,但密集拼出来后整体色差大得离谱。
我的做法分两步:先对像素化后的图像做颜色量化,把每个格子的颜色归并到有限数量的聚类中心;再做色板匹配,用距离公式计算每个聚类中心和真实豆色之间的距离,取最近色号。这两步缺一不可。如果不做量化直接匹配,每个格子独立找最近色,结果就是颜色数量超多、色号跳变剧烈,图纸里全是孤立的不同颜色豆子,拼出来视觉效果非常花。
更关键的是,颜色距离不能简单用 RGB 空间里的欧氏距离。人眼对绿色敏感、对红色次之、对蓝色敏感度低,且在暗色区域对亮度差异更敏感,所以直接算 RGB 距离经常会出现“算法觉得像,人眼觉得不像”的翻车。实际推荐的做法,是先把 RGB 转到 Lab 色彩空间或至少做感知加权,再计算距离。Lab 空间里欧氏距离与人对色差的感知高度相关,效果明显更好。
2. 像素化生成的核心参数与原理
2.1 格网尺寸:决定图纸分辨率和豆量
像素化第一步是决定输出多少行、多少列格子。这个参数直接由拼豆底板的尺寸决定。常见的方形拼豆底板有 29×29 格,也有 58×58 格的大块,还有各种定制尺寸。选择逻辑很简单:先定目标长宽比和总豆量上限,再反推格网大小。
如果原图是 3:4 的竖构图,想在 29×29 的方形底板上拼,就必须先裁切。我通常先以短边为基准做中心裁切,再缩放到目标格子数。比如目标 29×29,就把原图裁成正方形,然后缩放到 29×29 像素。这里有个细节:Pillow 默认的重采样算法是双线性插值(BILINEAR),直接缩到 29×29 容易丢细节、边缘发虚。我会改成 LANCZOS 重采样,然后在缩放后顺手做一次锐化,这样得到的格子边缘更清晰。
格网尺寸并不是越大越好。格子多意味着豆子多,耗时和成本也翻了倍。大多数人第一次玩拼豆,28×28 或者 32×32 的图纸已经足够表达一个头像或一只小动物。只有做大幅作品时,才会把格网设到 100×100 以上。经验值供参考:豆数控制在 2500 以内,中等难度;5000 以上就得做好拼两三个晚上的准备。
2.2 颜色量化:把百万色压到几十个色号
图像缩到几十上百个格子之后,每个格子其实还是一个包含很多颜色的区域,得取一个代表色。最简单的是直接取区域内所有像素的平均色。但平均色有一个明显缺陷:如果格子里一边是纯白、一边是纯黑,平均出来是中性灰,而中性灰在拼豆图纸里往往最不讨喜,拼出来显得脏。
更稳的做法,是使用中位数颜色,或者用聚类的方法。聚类的逻辑是:把图里所有像素颜色当作一群点,用 K-Means 聚成 N 个中心,每个格子像素的颜色离哪个中心近,就用哪个中心的颜色。这样比单纯平均更能还原“这个格子给人的直观颜色感受”。
K-Means 在实现上有两个坑。第一,K 值怎么定。我一般把 K 设为色板数量的 1.5 到 2 倍,比如色板有 40 个色号,K 设 60 到 80,聚类后再合并到最近的色号。这样能避免一个色号对应的区域内部颜色差异太大。第二,K-Means 对初始化敏感,同一张图跑两次结果可能不同。我在代码里会固定随机种子,并在 OpenCV 里使用 KMEANS_PP_CENTERS 这个初始化策略,结果更稳定。
另外,量化前必须做一步重要处理:去除抗锯齿边缘。图像缩放后边缘像素大多是半透明的中间过渡色,如果不先处理,这些杂色会影响聚类的纯度。具体做法是把图像先缩放到一个低分辨率(比如目标格数的 2 倍),再用最近邻算法强制去除边缘中间色,或者先转成 “P 模式”做颜色索引。这些细节看着小,但对最终效果影响非常大。
2.3 色彩空间的选择:RGB直配还是做色彩感知校正
色板匹配这一步,我强烈建议不要直接在 RGB 里做。RGB 的颜色距离和人的感知差异完全不匹配。举个例子,RGB 里 (50,50,50) 和 (60,60,60) 的欧氏距离约为 17,而 (180,0,0) 和 (190,0,0) 的欧氏距离是 10,但人眼会觉得前者的亮度差异更明显、后者几乎是同一个红。因为在暗区,人对亮度差异的分辨率更高。
把颜色转换到 Lab 色彩空间后,L 代表亮度,a、b 代表两个色度对立轴。在这个空间里,欧氏距离和人的色差感知高度一致。具体计算时,我会先把 RGB 转成 Lab,再计算每个量化后的颜色和所有拼豆色号的 Lab 距离开方,取最小值对应的色号。实测下来,在 Lab 空间匹配的准确率,比 RGB 直接匹配高出不少,尤其在紫色、深蓝色、暗红色这几个容易翻车的色系上。
Lab 转换在 Python 里可以用 OpenCV 的cv2.cvtColor(pixel, cv2.COLOR_RGB2LAB),也可以用skimage.color.rgb2lab。需要注意 Lab 空间有一定色域限制,RGB 里某些极高饱和的颜色无法精确表达,所以转换后超出范围的边界值要手动钳制,避免出现 NaN 或无穷值。这个小坑我一开始没注意,结果一张高饱和红色图片匹配出来的色号全是暗红。
3. 实操流程与核心环节实现
3.1 图像预处理:裁切、缩放、去噪
进入正式代码之前,先把整条流程串一遍。完整链路是:
- 读取并裁切图像,统一目标长宽比
- 缩放图像,生成像素网格的“原始格子图”
- 对格子图做边缘去抗锯齿处理
- 用颜色量化把每个格子归并到聚类中心
- 用 Lab 距离匹配拼豆色板色号
- 输出带色号和符号的图纸图片,以及一份用料清单
第一步裁切很关键。很多人跳过去直接缩放,结果比例不对,图被压扁,拼出来的人脸像被横向拉伸过。我习惯先把长边缩放至 2000 像素以内,然后以中心为基准裁切到目标长宽比。比如目标是 3:4 的竖版,就在原图上裁出一个 3:4 的矩形区域。这个区域可以在代码里用中心裁切,也可以做成坐标参数,让使用者自己框选。
裁切后的图像一般会有压缩痕迹和噪点,这时做一个轻微的高斯模糊(半径 1-2)能有效减少后续聚类的噪声。模糊不是让图像变糊,而是让细小纹理的颜色过渡更平滑,聚类中心更稳定。我常用高斯核(5,5),sigma 设为 1。噪声严重的手机照片,sigma 可以调到 1.5。但不要过分,否则边缘信息也跟着糊掉。
3.2 像素化核心代码实操
下面这段 Python 代码,实现了裁切、缩放和初步像素化。我用的依赖是 Pillow、OpenCV、NumPy 和 scikit-image,都是常用库,直接 pip 安装即可。
import cv2 import numpy as np from PIL import Image def center_crop_resize(image_path, grid_w, grid_h, out_path): img = Image.open(image_path) # 转 RGB,去掉 alpha 通道 img = img.convert("RGB") # 中心裁切到目标比例 w, h = img.size target_ratio = grid_w / grid_h if w / h > target_ratio: new_w = int(h * target_ratio) x0 = (w - new_w) // 2 img = img.crop((x0, 0, x0 + new_w, h)) else: new_h = int(w / target_ratio) y0 = (h - new_h) // 2 img = img.crop((0, y0, w, y0 + new_h)) # 用 LANCZOS 缩放到网格尺寸 img = img.resize((grid_w, grid_h), Image.LANCZOS) img.save(out_path) return img这段代码的工作逻辑很清晰,按目标宽高比先裁掉多余部分,然后用 LANCZOS 缩放到指定格数。得到的结果就是一张grid_w × grid_h的小图,每个像素对应一格。我开发时会在这一层输出一张放大预览图,用最近邻插值把 29×29 的小图放大 10 倍,方便肉眼先检查构图轮廓是否OK,再继续往下走。
像素化之后的图片,不要直接在原图上取值做颜色量化,而要做到 2 倍精度。这一步的思路是,先放大到grid_w*2 × grid_h*2,用更小粒度去聚合,最后再降回网格尺寸。实践效果是,小图的边缘更干净,颜色更接近直觉。
3.3 色板匹配实现:量化之后算距离
先准备好色板数据。每种拼豆有一个名字和 RGB 值,我用一个字典存起来。当然实际品牌的色号很多,这里只给两个示例色,便于跑通逻辑。真实项目里一般是几十个色号的完整列表。
palette_rgb = { 1: (255, 255, 255), # 白 2: (30, 30, 30), # 黑 3: (200, 30, 30), # 正红 4: (250, 180, 50), # 杏黄 # ... 更多色号 }接下来把上一步生成的网格图读成 NumPy 数组,先做 K-Means 聚类,再逐格匹配最近色号。中间最长的耗时其实是聚类过程。如果格数只有 50×50,数据点也只有 2500 个,聚类非常快。但如果想做大幅 150×150,就有 2.25 万个点,对每个点做几十维颜色的距离计算,还是有点压力。所以我一般先对网格图做一次颜色扁平化:把每个像素的颜色当作三维坐标,用 K-Means 聚成 K 个类,K 设置为色板数量的 2 倍左右;然后每个像素用聚类中心替代,再做 Lab 匹配。
from sklearn.cluster import KMeans from skimage import color def extract_palette(img_array, n_clusters): # img_array shape: (h, w, 3) h, w, _ = img_array.shape pixels = img_array.reshape(-1, 3).astype(np.float32) kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10) kmeans.fit(pixels) centers = kmeans.cluster_centers_.astype(np.uint8) labels = kmeans.labels_.reshape(h, w) return centers, labels def nearest_color_lab(color_rgb, palette_lab): color_lab = color.rgb2lab(color_rgb / 255.0) dist = np.linalg.norm(palette_lab - color_lab, axis=1) return int(np.argmin(dist) + 1)这里有个实操细节要注意:skimage.color.rgb2lab接受的是[0,1]浮点输入,而 OpenCV 读出来的是[0,255]整数,所以要先除以 255。然后色板里的 RGB 也都要先转成 Lab 并缓存一次,避免每一格都重复转换。整个匹配过程本质上是找最近点,每次循环只需要算一个向量对一组向量的距离,用 NumPy 广播写很方便,也不用写复杂循环。
3.4 图纸输出与用料清单
匹配完成后,输出环节同样重要。单纯输出一张“标了色号”的大图还不够,拼豆用户真正需要的是能快速照着拼的图纸,以及去买对应颜色豆子的数量参考。我生成的图纸通常包含三部分:
- 带颜色填充的格子图,每个格子填上匹配后的拼豆色
- 每个颜色对应的符号、编号,标在格子角落,便于区分相近色
- 一份颜色统计表:每个色号用了多少颗豆
颜色统计很简单,就是遍历所有格子的色号值,用字典计数。但我一般会做一个小优化:先做连通域分析,把零星的单颗色号提示出来。大面积背景中出现一颗孤立的另一种颜色,大概率是噪音色号,在图纸里我会标注“可替换为背景色”,这样拼的时候不用反复换豆,效果也不受影响。
图纸的美观度也很重要。拼豆图纸的底色最好是浅色,格子线要清晰。我常用纯白底,格子线用浅灰色(RGB 180, 180, 180),线宽 1 像素,放大后边缘不会太刺眼。每个格子中央或左上角标一个很小的数字色号,有人喜欢中央标号,有人喜欢角落,这个因人而异。建议编号字体大小根据格子尺寸调整,原则是:放大到 200% 后仍然能看清楚,但没放大时不影响看颜色。
4. 常见问题与排查技巧实录
4.1 匹配出来的颜色总是偏灰、偏暗
这是我把 RGB 直接匹配改成 Lab 匹配之前遇到的最大问题。原因是很多像素经过缩放和模糊后,颜色值被推向了中间调,尤其暗部颜色被平均后更灰。后来排查发现两个叠加因素:一是缩放时用了双线性插值,暗部的边缘像素和亮部像素互相污染;二是匹配时在 RGB 空间算距离,算法倾向于选择中间亮度的色号。
解决方式分两层。第一层在预处理阶段,用 LANCZOS 替代双线性,并在量化前增加一步“暗部强化”或者叫“对比度增强”。我常用的是在缩放之后用PIL.ImageEnhance.Contrast增强 1.1 到 1.2 倍,同时把亮度微调 0.95 倍,让暗部更沉、亮部更亮。第二层在匹配阶段,切换到 Lab 并给亮度分量 L 设置一个权重,我一般取weights = [1.0, 0.8, 0.8]。降低 a、b 的权重,能减少因色彩饱和差异导致的误匹配,效果更稳定。
判断是否偏灰有一个快速方法:把匹配后的色号颜色还原成一张图,和原像素化图做平均色差对比,如果总色差小于某个阈值但视觉观感却明显“脏”,那就是亮度分布出了问题,需要回到预处理阶段增强对比度而不是继续调匹配逻辑。
4.2 大面积同色区域出现零散杂色豆
如果你拼出来的成品背景应当是纯色,结果零零星星蹦出好几个别的颜色,问题通常出在颜色量化阶段。K-Means 是全局聚类,它在颜色丰富的区域和颜色单一的区域会平均用力。当图像中存在大面积纯色背景时,聚类中心仍然会为背景分出两个甚至多个相近的颜色,匹配到不同的色号,导致背景不纯净。
解决方案是对聚类结果做一次后合并。具体做法是:算出所有聚类中心两两之间的 Lab 距离,如果距离小于某个阈值(通常 8 到 10 Lab 单位),就认为这两个颜色人眼无法区分,合并为同一类,像素标签也一起替换。合并后再做色板匹配,背景杂色会明显减少。如果用这个方法后还有零星杂点,那就是噪声像素,可以用形态学开运算去掉。
import cv2 # labels 是二维标签图 kernel = np.ones((3, 3), np.uint8) labels_clean = cv2.morphologyEx(labels.astype(np.uint8), cv2.MORPH_OPEN, kernel)开运算会把小于 3×3 的孤立色块消除,替换为周边的主流色。这个操作对拼豆图效果非常好,因为它本质上做的事和拼豆颗粒的视觉整合逻辑一致。
4.3 渐变小图变成马赛克灾难
照片里的渐变天空、柔和的皮肤阴影,在低分辨率像素化后很容易出现颜色跳变。比如天空从左到右由浅蓝渐变到深蓝,如果用 K-Means 聚类,它会切出好几条颜色带,拼出来的效果像一根根彩带,很怪。这个问题在很多自动生成工具里都有,属于开放性问题,没有完美解,但有实用的缓解方案。
我的经验是引入一个“颜色平滑度约束”。在匹配色号之前,先对量化后的网格图做一次中值滤波。中值滤波在去噪的同时可以保留大块结构,但会把细碎的过渡色“拉平”。参数取 3×3 即可,过大会破坏边缘。另外,针对渐变区域,我还会在生成用料清单时额外标注“可用双色混豆过渡”,比如相邻两颗豆使用深浅两个蓝交替排布,这样人眼看到的是渐变效果,而图纸层面仍然只有两个色号,拼起来也不麻烦。
渐变问题是拼豆图纸中少数无法完全靠算法解决的问题,因为拼豆本身是离散的,物理上的平滑渐变必然需要混色技巧。算法能做的,只是让色号分布有序,尽量不出现单颗跳变。
4.4 图纸符号混乱、对不上号
图纸生成了,打印出来却看不懂哪里是哪里,这是一种很常见的失败。原因一般是色号和格子线对比度不足,或者符号标注位置不一致,导致拼到一半就要返工检查。
我总结了三条实操习惯。第一,图纸底色不要用白色以外的深色,否则深色豆在黑底上看不清;第二,相近色号要避免放在相邻格里,虽然算法是按最近色号匹配的,但人工排图时可以做一个微调:如果两个相邻格子的色号,它们对应的 Lab 距离很小,且其中一个在该图里只出现很少次数,就把那个少数色替换为相邻的主流色。第三,打印纸质图纸时,要确保色号数字字号统一,行间距一致,最好每 10 格就加一个加粗的坐标标尺。
另外,我发现把图纸按“分区”输出比一整张更实用。比如一整张 58×58 的图纸,打印成 A4 会非常小。我会按 10×10 切块输出,生成 36 张小图,每张拼完后拼起来,不会拼乱。这个方法在手工圈子里叫“分区图纸”,实际用起来比一整张大图友好太多。
5. 踩坑实录与进阶扩展思路
5.1 时间成本估算与预判
像素化和匹配本身只需几秒,但真正耗时的是“看图微调”。我通常会在生成第一版图纸后,手动检查三个地方:脸的肤色是否统一、背景是否有孤立杂点、深色区域是否糊成一团。这三个问题如果出现,我并不会重新跑全套流程,而是通过微调参数局部解决。
时间成本有一个比较稳定的规律:网格越大,检查时间越长。30×30 的图,检查加微调大概 10 到 15 分钟。100×100 的图,每处细节都要看,可能要 1 到 2 小时。所以我建议普通作品控制在 60×60 以内。想做大幅,也要先跑小样试验配色方案,确定没有颜色灾难再放大,这个小样步骤每个人都需要走,能省不少返工时间。
还有一个细节是豆子亮度差异的问题。不同品牌、不同批次的拼豆,同色号之间也会有微小色差,尤其是珠光豆和透明豆,反光会改变观感。我用算法匹配时,会把比较常见的“闪光色”排除在色板之外,因为这些颜色在照片和屏幕上很难准确呈现,盲拼容易翻车。
5.2 从拼豆延伸到更大范围的像素艺术工具
这套技术流程修正后,完全可以复用到一个更大的工具集合里。我把它做成了一种“可插拔”管线:前端接收任意图像,中间做像素化和量化,后端对接不同的输出格式。拼豆图纸只是其中一个输出端,另外我还接了:
- 十字绣图纸(需要额外把像素格对应到绣布格数,并处理 Aida 布的针法密度)
- 串珠图(把颜色替换成珠子的色号,同时输出珠子用量)
- 乐高马赛克(输出每个乐高颗粒的颜色,以及对应的零件编号)
- 像素画练习图(输出带网格的线稿)
每次适配一种新输出,核心算法几乎不用动,只需要更换色板和输出模板。这个思路让我在后续几个项目里省了大量重复开发的时间。如果你也在做类似工具,我建议把“图像处理核心”和“输出适配层”严格解耦,这样以后想加新玩法非常方便。
5.3 一些个人建议和后续可以做的事
从图像到拼豆图纸,本质上是一个颜色降维的过程,这个过程没有标准答案,只有最适合你要表达的内容的方案。我个人在做了几十张图之后的体会是:技术上要做到“又快又稳”,但视觉上一定要保留“人肉微调”这一步。算法负责把 1600 万色压缩到 40 个色号,而你要负责告诉算法,这款暖黄色是不是比旁边的杏黄更接近你记忆里那个温柔的下午。参数调得再熟练,最后敲定的那个色号,永远该由你来选。
最后分享一个后续可以自己动手扩展的小功能:把生成的结果倒推回原图,做一次“误差热力图”。每一步匹配都可以得到当前格子颜色与原始对应区域颜色的色差,把这个色差按冷暖色渲染成一张热力图,能非常直观地看出图纸在哪些区域失真最大。我做这个功能之后,发现多数失真集中在毛发、草地这类纹理密集区域,属于拼豆物理极限,无法避免;但在皮肤和天空区域,优化空间很大,微调几个色号就能让成品提升一个档次。这个思路值得你去试试。