Skip to content

About

拼豆图纸生成器(开源版):上传图片 → 像素化降采样 → 色卡匹配(CIEDE2000 + k-d 树),两步出图。纯前端 Vue 3 + Vite,图片不上传。Perler / Hama / fuse bead pattern generator: photo → pixelate → bead color codes, 100% in-browser.

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

pindou-bead-pattern

English | 中文

CI License: MIT

把一张照片变成拼豆(Perler / Hama bead)图纸的两步网页工具:

  1. 像素化处理(降采样)——选图片、选像素大小、选色卡,实时预览像素化效果
  2. 色号匹配——把像素化结果落到真实色卡上,得到带色号的拼豆图纸

只有这两步:没有编辑器、没有导出、没有 AI 生成、没有付费墙。

第一步:像素化处理 第二步:色号匹配
像素化处理 色号匹配

(截图用内置示例场景 + Mard 291 色卡、80 格;方板之外的非等比区域留白,不拉伸。)

快速开始

git clone https://github.com/857god/pindou-bead-pattern.git
cd pindou-bead-pattern

npm install
npm run dev        # http://127.0.0.1:5310
npm run build      # 产出 dist/
npm run preview    # 预览构建结果

要求 Node ≥ 20.19(或 ≥ 22.12,跟 Vite 8 的要求一致)。没有后端,所有计算都在浏览器里完成,图片不会上传到任何地方。

两步在做什么

第一步:像素化处理(结构层)

图片 → 降采样到「像素大小 × 像素大小」的方板 → 区域合并 → 色彩简化 → 采样增强 → 空间平滑。 产出的是一张还没落到色卡上的像素网格(每个格子是任意 HEX 颜色)。

  • 尺寸模型是方板:格数恒为 像素大小²,源图按比例缩放后居中,非等比区域留空(渲染成白色)。 所以这一步的预览是正方形。
  • 「标准」= 智能推荐档位(按尺寸档位自动取值);「自定义」= 在标准基线上叠加 5 个滑块: 背景简化、颜色简化、色块清晰度、线条保留、柔和降噪。

第二步:色号匹配(匹配层)

在第一步的网格上:灰度检测 → 精确最近色(CIEDE2000 + k-d 树)→ 块面统一 → 匹配层平滑 → 杂色清理(去杂色)。产出每个格子的色号(如 C29)与用量统计。

  • 「标准」= 智能推荐档位;「自定义」= 手动调去杂色、匹配层平滑、块面统一、鲜艳度、明暗层次。

改动第二步的档位只重跑第二步;改动第一步的参数会重跑第一步(若当前在第二步,再重跑第二步)。

「标准」到底取什么值

「标准」不是一套固定参数,而是按像素大小分档自动取值。档位边界是 < 64 → small、< 104 → medium、< 150 → large、≥ 150 → xlarge:

像素大小 结构层:背景简化 / 颜色简化 / 色块清晰度 / 线条保留 / 柔和降噪 匹配层:去杂色 / 匹配层平滑 / 块面统一
20-52 2 / 2 / 2 / 2 / 1 1 / 1 / 2
53-63 2 / 0 / 2 / 2 / 1 1 / 1 / 2
64-103 1 / 0 / 1 / 1 / 1 1 / 2 / 3
104-149 0 / 0 / 1 / 1 / 0 3 / 3 / 4
≥ 150 0 / 0 / 0 / 0 / 0 2 / 2 / 3

小尺寸需要更多简化(格子少,噪点相对更明显),大尺寸基本交还给算法本身。 鲜艳度与明暗层次在标准模式下恒为 1.0,肤色保护恒为关闭。

色卡数据

本仓库不含任何品牌色卡数据,只带 4 档内置合成色卡(17 / 40 / 152 / 300 色), 所以 clone 下来就能直接跑(此时默认选中色最全的 152 色那张)。

这个页面只开放 Mard 291 一张品牌色卡:外部数据里能找到它就只提供它, 其他品牌和内置合成色卡都不会再进入选择列表(只剩一个选项时界面写成一行文字, 不会摆一个只有单项的下拉)。想换或想加品牌色卡见 public/palettes/README.md:把符合 pindou-color-data 约定的目录放好, 用 PINDOU_COLOR_DATA_ROOT 指过去即可,开发服务器会把它挂到 /palettes/* 下。

色卡数据的来源与正确性由使用者自行负责——不同厂商、不同批次的色号与色值会有差异。

目录结构

src/
├── algorithm/
│   ├── chain.mjs              # 成像主链(从 App 页面机械抽取,后删除两步用不到的代码)
│   ├── stages.mjs             # 两步入口:runStructureStage / runMatchStage
│   ├── palette-source.mjs     # 色卡门面(内置合成 + 外部品牌)
│   ├── palettes.mjs           # 内置合成色卡
│   ├── color-data.node.mjs    # 外部色卡的 Node 侧读取(给 dev server 用)
│   ├── image.mjs              # 图片解码
│   └── sample-scene.mjs       # 内置示例场景
├── components/                # 两步的界面组件
└── render.mjs                 # 画布绘制
scripts/smoke.mjs              # 端到端冒烟(不开浏览器就能跑完两步)
test/                          # node:test 测试

算法来源与验证

src/algorithm/chain.mjs 是从「趣拼豆」App 的 smart-pixel-generator 页面 机械抽取出来的:只把页面方法里的 this. 替换成模块级上下文的 ctx.,不改算法逻辑。 抽取时做过这些校验:

  • 主链函数的函数体与源文件逐行比对,完全一致(抽取脚本自带回比);
  • 页面里没有的运行时(uni.*、canvas 2D、页面 data/computed)用平台 stub 补齐, canvas stub 是纯 JS 实现(含双线性重采样),所以这套算法在 Node 里也能跑。

抽取之后,为了只给仓库留下两步流程真正会走到的代码,又删掉了 735 行(抽取结果的 21.5%)。 原则只有一条:只删「在这两步流程里证明不可达」或「功能已整体移除」的代码, 不动任何会影响结果的算法分支。

删除内容 行数 理由
经典降采样管线 pixelizeImageData + 选择它的开关 141 开关被硬编码成「始终走新管线」,那条分支永远进不去
一次性编排 generatePixelArt、regenerateFromPixelization 155 本仓库按两步拆分调用,不再需要
预设系统 applyPresetConfig / getPresetDescription / updateDescription 118 在源页面里也没有调用点
绘制 drawResult 与本层 doDrawResult 桩 63 本仓库由 src/render.mjs 直接画到 DOM canvas
抖动 applyDithering / Floyd–Steinberg / Atkinson / calculateGradients / addError 147 连同界面入口一起移除,理由见下
高档位专用的 applyTestColorQuantizationRgb 28 只在区域合并档位 ≥10 时触发,属测试页残留
特征分析薄封装 analyzeImage / analyzePalette / shouldSkipFeatureAnalysis、isSkinTonePixelCached、finishPerformanceTrace 59 依赖的 image-analyzer.js 未搬运;运行时实测调用次数为 0
以上各项残留的调用点与分支整理 24

怎么确认「删了但结果没变」:删除前后各跑一遍同一组场景(标准模式 52/96/150/200 格、 自定义结构档位、自定义匹配档位、同色系优先、灰度图、8 色小色卡),逐项比对 结构网格指纹、匹配色号指纹、色数、平均色差、孤立豆数 —— 除下面这一处外逐项完全一致, 匹配色号逐格 0 差异。确认「确实没人调用」的方式是把主链上下文的每个方法与取值器都包一层 计数器跑完整流程,看调用次数。

唯一变化的一处:把结构滑块拉满(背景简化 10 → 区域合并档位 12)时,原本会额外走一次 测试页用的颜色归一化;删掉后同一张图上从 23 色 / 平均色差 6.04 / 孤立 3 格变成 20 色 / 5.68 / 0 格(按本仓库用的两个代理指标是变好的)。其余所有档位逐格一致。

stages.mjs 把主链按源页面的两步拆开调用。拆分不改变结果:与「一次性跑完」对比, 在 48 格与 96 格下匹配结果的 RGBA 与色号逐格 0 差异。

node --test                    # 算法两阶段测试
node scripts/smoke.mjs 96      # 用真实色卡跑完两步并打印实测数据

浏览器里自检

打开 ?autotest=1(例如 http://127.0.0.1:5310/?autotest=1):页面会自动加载内置示例场景、 跑完两步,并把实测数据写进 <pre id="autotest" data-status="ok">。不用打开控制台就能确认链路是否正常:

structure=52x52  occupied=1872  empty=832  structureElapsedMs=155
match=52x52  colors=21  mean=6.62  median=6.02  max=19.19  isolated=8  matchElapsedMs=50

(色数、色差、孤立豆是确定值,换机器也一样;*ElapsedMs 随机器与浏览器波动,仅供参考。)

data-status 为 error 时,节点里会带上错误信息;正常交互路径下的未捕获错误也会显示成页面顶部的横幅。

与原页面的差异

这是从 App 页面里只抽出两步(上传 → 降采样 → 下一步 → 色卡匹配)的版本, 其余功能一律没搬。为了不让看过原页面的人产生误解,差异列在这里:

去掉了:VIP 门禁(标准/自定义两种模式在这里都可以直接用)、像素编辑器、 导出(PNG / PDF 图纸)、AI 生成、云端接口、作品集与历史记录、各类缓存层,以及 抖动(Floyd–Steinberg / Atkinson)——实测它在拼豆成品上只会增加高频噪点 (96 格下孤立豆从 11 格涨到 350 格)、耗时还多 3.5 倍,而且原页面界面上本来也没有入口。

新增了这些原页面没有的东西:

  • ?autotest=1 自检节点:见上一节,方便在没有测试框架的情况下确认链路没坏。
  • ?testdrive=1 交互自检:逐个点控件并断言「结果真的重算了」,用来抓「假控件」, 只在带这个参数时运行,正常使用零开销。

保持一致的部分:算法本身、两步的先后与可回退、标准档位随像素大小分级的取值表、 界面文案(含「孤色清理」「肤色自然」这类原页面用词)、方板留白(非等比区域留白而不是拉伸)、 以及第一步滑块的 350ms 防抖语义。

已知取舍

  • 结构层里的「柔和降噪」(noiseSmooth)、匹配层的「匹配层平滑」与「块面统一」这三个档位, 实测开到最大也几乎不改变结果——它们判断「邻域主色是否占优」,而在此之前区域合并 已经把平面区域抹平了,主色就是格子自己。界面保留了这些控件(与原页面一致), 但不要期待明显效果。真正有效的是孤色清理(去杂色):它把孤立豆(四邻都不同色的格子)从 约 1.4% 降到约 0.2%,代价是被强改颜色的格子色差会变大。
  • 结构层的空间平滑在不改动聚类/区域合并档位时基本是空转(实测只影响 4 格)。

参与开发

见 CONTRIBUTING.md。一句话:不要提交品牌色卡数据,改算法要给前后对比。

许可证

MIT。仓库内不含任何品牌色卡数据。

About

拼豆图纸生成器(开源版):上传图片 → 像素化降采样 → 色卡匹配(CIEDE2000 + k-d 树),两步出图。纯前端 Vue 3 + Vite,图片不上传。Perler / Hama / fuse bead pattern generator: photo → pixelate → bead color codes, 100% in-browser.

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages