Skip to content

shinelikeamillion/perler-bead-algorithm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 

Repository files navigation

拼豆图纸生成算法:图片转拼豆图纸、色板匹配与平滑优化 / Perler Bead Pattern Generator Algorithm

将位图转换为可制作的拼豆网格:感知色彩匹配、网格采样、杂色合并与连通背景移除。
Convert a bitmap into a buildable fuse-bead grid with perceptual colour matching, cell sampling, speckle cleanup, and connected-background removal.

在线拼豆工具 / Live tool · 图纸广场 / Pattern gallery

关键词 / Keywords: 拼豆图纸生成器、图片转拼豆、拼豆图纸、拼豆色号、拼豆用量表、拼豆算法、Perler bead pattern generator、fuse bead pattern、photo to perler beads、pixel art to beads、Oklab colour quantization。

中文说明

流程

  1. 将 sRGB 转换为 Oklab,在更接近人眼感知的空间中比较颜色。
  2. 按目标列数切分图片;每个单元格提取平均色或主色,并忽略透明像素。
  3. 将单元格代表色映射到最近的色板颜色。
  4. 使用四邻域 BFS 合并相近区域,减少单像素杂色。
  5. 从边界开始 flood fill,移除与白色接近的连通背景;内部孔洞会被保留。

OklabDistance = 100 × √((ΔL)² + (Δa)² + (Δb)²)

时间复杂度约为 O(W × H + R × C × P);其中 W × H 是源像素数,R × C 是输出网格,P 是色板数量。色板较大时可以用 KD-tree 加速最近邻查询。

关键实现(TypeScript)

type RGB = { r: number; g: number; b: number };
type Oklab = { l: number; a: number; b: number };
type Cell = { hex: string; external: boolean };
type PaletteColor = { hex: string; rgb: RGB };

const linear = (v: number) => {
  const n = v / 255;
  return n <= 0.04045 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4;
};

export function rgbToOklab({ r, g, b }: RGB): Oklab {
  r = linear(r); g = linear(g); b = linear(b);
  const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
  const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
  const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
  const lr = Math.cbrt(l), mr = Math.cbrt(m), sr = Math.cbrt(s);
  return {
    l: 0.2104542553 * lr + 0.7936177850 * mr - 0.0040720468 * sr,
    a: 1.9779984951 * lr - 2.4285922050 * mr + 0.4505937099 * sr,
    b: 0.0259040371 * lr + 0.7827717662 * mr - 0.8086757660 * sr,
  };
}

export function colourDistance(a: RGB, b: RGB): number {
  const x = rgbToOklab(a), y = rgbToOklab(b);
  return 100 * Math.hypot(x.l - y.l, x.a - y.a, x.b - y.b);
}

export function nearestPaletteColour(target: RGB, palette: PaletteColor): PaletteColor;
export function nearestPaletteColour(target: RGB, palette: PaletteColor[]): PaletteColor {
  let best = palette[0], distance = Infinity;
  for (const candidate of palette) {
    const next = colourDistance(target, candidate.rgb);
    if (next < distance) { best = candidate; distance = next; }
    if (next === 0) break;
  }
  return best;
}

function hexToRgb(hex: string): RGB {
  const n = Number.parseInt(hex.slice(1), 16);
  return { r: n >> 16, g: (n >> 8) & 255, b: n & 255 };
}

// Merge four-neighbour cells that are perceptually close. The most frequent
// colour becomes the output colour for the whole connected region.
export function mergeRegions(grid: Cell[][], threshold: number): Cell[][] {
  const out = grid.map(row => row.map(cell => ({ ...cell })));
  const seen = out.map(row => row.map(() => false));
  const rows = out.length, cols = out[0]?.length ?? 0;
  for (let y = 0; y < rows; y++) for (let x = 0; x < cols; x++) {
    if (seen[y][x] || out[y][x].external) continue;
    const seed = hexToRgb(out[y][x].hex);
    const queue: [number, number][] = [[y, x]], region: [number, number][] = [];
    const counts = new Map<string, number>();
    while (queue.length) {
      const [cy, cx] = queue.pop()!;
      if (cy < 0 || cx < 0 || cy >= rows || cx >= cols || seen[cy][cx]) continue;
      const cell = out[cy][cx];
      if (cell.external || colourDistance(seed, hexToRgb(cell.hex)) > threshold) continue;
      seen[cy][cx] = true; region.push([cy, cx]);
      counts.set(cell.hex, (counts.get(cell.hex) ?? 0) + 1);
      queue.push([cy - 1, cx], [cy + 1, cx], [cy, cx - 1], [cy, cx + 1]);
    }
    const colour = [...counts].sort((a, b) => b[1] - a[1])[0]?.[0];
    if (colour) region.forEach(([cy, cx]) => { out[cy][cx].hex = colour; });
  }
  return out;
}

// Remove only border-connected near-white cells. Interior holes remain intact.
export function removeConnectedBackground(grid: Cell[][], tolerance: number): Cell[][] {
  const out = grid.map(row => row.map(cell => ({ ...cell })));
  const rows = out.length, cols = out[0]?.length ?? 0;
  const queue: [number, number][] = [];
  for (let x = 0; x < cols; x++) queue.push([0, x], [rows - 1, x]);
  for (let y = 0; y < rows; y++) queue.push([y, 0], [y, cols - 1]);
  while (queue.length) {
    const [y, x] = queue.pop()!;
    if (y < 0 || x < 0 || y >= rows || x >= cols || out[y][x].external) continue;
    if (colourDistance(hexToRgb(out[y][x].hex), { r: 255, g: 255, b: 255 }) > tolerance) continue;
    out[y][x].external = true;
    queue.push([y - 1, x], [y + 1, x], [y, x - 1], [y, x + 1]);
  }
  return out;
}

网格采样建议:对每个 cell 只统计 alpha ≥ 128 的像素;平均模式计算加权平均色,主色模式对量化 RGB 计数。边缘平滑可使用 alpha 作为采样权重。

更顺滑的拼豆结果 / Smoother pattern roadmap

当前基线适合快速、可解释的图纸生成。若目标是让照片类图像在低分辨率拼豆网格中更连贯,应优先增加空间一致性,而不是只提高区域合并阈值。

优先级 方法 效果 取舍
1 Linear-light downsampling 在线性光空间缩小图片,减少暗边与脏色 计算成本很低,建议默认启用
2 Bilateral / guided pre-filter 平滑同色区域,同时保留主体边缘 适合照片;参数过强会损失细节
3 Adaptive palette subset 先从完整色板选出 16–48 个最需要的颜色,再做映射 色号更少、BOM 更干净;可能牺牲少量色彩准确度
4 Edge-aware MRF / ICM 联合优化全图颜色;相邻格倾向一致,边缘处保持分界 最能减少散点;比逐格映射更耗时
5 Optional dithering 用误差扩散改善渐变层次 不应默认开启:会增加颗粒感和色号数量

推荐默认流水线:

linear-light resize
  → edge-preserving pre-filter
  → adaptive palette selection
  → Oklab nearest-colour assignment
  → edge-aware ICM smoothing
  → optional region merge / background flood fill

可将全局优化写为以下能量函数,其中 p 是网格单元、cₚ 是其最终色号:

E(c) = Σₚ colourDistance(sourceₚ, cₚ)
     + λ Σ₍ₚ,q₎ wₚq × [cₚ ≠ c_q]

wₚq 在平坦区域较大,鼓励邻格统一;在源图高梯度边缘较小,保留轮廓。ICM(iterated conditional modes)可从最近色匹配结果开始,反复尝试替换每个单元为候选色,以降低局部总能量。

常见问题 / FAQ

如何把图片转成拼豆图纸? 上传图片到 SharingHub 拼豆图纸生成器,选择图纸宽度和色号体系,即可生成可编辑的拼豆网格、PNG、SVG 与 CSV 用量表。

为什么不用 RGB 欧氏距离直接匹配拼豆颜色? RGB 距离与人眼感知不一致;Oklab 更适合判断两颗拼豆颜色看起来是否接近,因此能减少明显的色相误差。

怎样让拼豆图纸更顺滑? 优先使用线性光缩放、边缘保留滤波、紧凑色板和边缘感知 ICM;不要默认开启抖动,因为它会让拼豆图案更颗粒化并增加色号数量。

可以用于 Perler、Hama、MARD 或其他拼豆色板吗? 可以。核心算法只需要每个色板颜色的 HEX/RGB 值;品牌编码和用量清单属于色板适配层。

English overview

Pipeline

  1. Convert sRGB into Oklab for perceptually meaningful colour distance.
  2. Divide the image into target cells; extract an average or dominant opaque colour per cell.
  3. Map each representative colour to the closest bead-palette colour.
  4. Run four-neighbour BFS region merging to eliminate isolated speckles.
  5. Flood fill from the border to remove connected, near-white background while preserving interior holes.

The embedded TypeScript covers the colour transform, nearest-palette matching, region cleanup, and connected-background removal. In production, cache Oklab palette values and avoid reading canvas pixels repeatedly.

Smoother pattern roadmap

The baseline is fast and explainable. For smoother low-resolution bead patterns, add spatial consistency instead of only increasing a region-merge threshold:

  1. Linear-light downsampling reduces dark fringes and muddy colours with minimal cost.
  2. Bilateral or guided filtering smooths flat regions while retaining subject boundaries.
  3. Adaptive palette selection chooses a compact subset (for example, 16–48 colours) before final matching, reducing colour flicker and BOM noise.
  4. Edge-aware MRF/ICM optimisation minimises colour error plus a neighbour-disagreement penalty; weights drop near strong source-image edges.
  5. Dithering is optional, not the default: it can improve gradients but creates visual grain and increases colour variety.

Recommended production pipeline: linear-light resize → edge-preserving pre-filter → adaptive palette subset → Oklab matching → edge-aware ICM smoothing → optional cleanup/background removal.

FAQ

How do I convert a photo to a Perler bead pattern? Upload it to the SharingHub Perler Bead Pattern Generator, set a grid width and palette, then export an editable bead grid, PNG, SVG, or CSV parts list.

Why use Oklab instead of RGB distance? Oklab distance better approximates perceived colour difference, so the selected fuse-bead colours are less likely to look visibly wrong.

Which bead brands can use this algorithm? The algorithm accepts any palette expressed as HEX/RGB values, including Perler, Hama, MARD, Artkal, and custom fuse-bead palettes.

License and attribution / 许可证与署名

The production implementation in SharingHub was refactored from the pixelation and flood-fill approach in Zippland/perler-beads, which is licensed under AGPL-3.0. Any redistribution or network deployment of derivative algorithm code must comply with AGPL-3.0.

Palette codes and brand mappings are data assets with their own provenance and are intentionally not bundled in this algorithm note.

Try it

Use the complete browser tool, palette mapping, exports, and community gallery at:

https://www.sharinghub.org/tools/perler-beads

About

拼豆图纸,拼豆图纸广场,拼豆作品,拼豆下载,拼豆色号,Perler Beads

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors