A technical note for developers, AI agents, and contributors. This document describes the implementation currently present in the Pixel Bead Studio repository. It is intentionally implementation-oriented and does not describe unimplemented machine-learning features.
- Product: https://pixelbeadstudio.com
- Image converter: https://pixelbeadstudio.com/image-to-pixel-bead
- Editable creator: https://pixelbeadstudio.com/pixel-bead-creator
- Pattern library: https://pixelbeadstudio.com/patterns
- Product repository: https://github.com/flashcoder111/beads
Pixel Bead Studio converts a raster image into a structured, editable pixel-bead project. The output is not a decorative approximation of beads and not a single flattened preview. It is a versioned grid whose cells reference a physical bead palette.
The current pipeline is deterministic and browser-based:
image file
-> file validation and browser decode
-> contain/crop framing
-> preview rasterization
-> brightness/contrast/saturation adjustment
-> optional edge-connected background removal
-> resize to target grid
-> active-palette selection
-> CIEDE2000 nearest-color matching
-> optional Floyd–Steinberg-style error diffusion
-> PixelBeadProject
-> editing, material counts, PNG/PDF export
No ML model is required for the conversion itself. The product UI, authentication, persistence, and cloud storage are separate layers around the image-conversion core.
The conversion result is designed to remain useful after the first preview. It can be edited, validated, persisted, transferred to the Creator, counted by material, migrated between project versions, and exported.
The core types are defined in lib/beads-core/types.ts.
type PaletteColor = {
id: string;
name: string;
hex: string;
};
type PixelBeadProject = {
version: 4;
revision: number;
id: string;
title: string;
sourceType: "image" | "manual" | "template";
sourceImage?: string;
templateId?: string;
width: number;
height: number;
cells: number[];
palette: PaletteColor[];
paletteId: string;
paletteVersion: string;
settings?: PatternSettings;
status: "draft" | "ready";
createdAt: string;
updatedAt: string;
};The cell array is row-major:
cellIndex = row * width + column
Each non-negative cell is an index into palette. -1 represents an empty cell, normally produced from transparent pixels. The project invariant is:
cells.length === width * height
Project creation and migration are implemented in lib/beads-core/project.ts. The validator requires a non-empty palette, positive integer dimensions, valid cell indexes, and no more than 60,000 cells.
The browser entry point is generatePatternFromFile.
Supported file types:
image/jpegimage/pngimage/webp
The current maximum input size is 10 MB. The adapter validates MIME type and file size, creates an Object URL, decodes the image through the browser Image API, and reads pixels through Canvas.
The function accepts an AbortSignal. The UI uses this to cancel stale conversions when the user changes grid size, palette, framing, or image settings before the previous conversion completes.
The browser adapter is intentionally separate from the pure grid and color algorithms. A future open-source package can provide Node, browser, or Web Worker decoders without coupling the core to a specific runtime.
The core accepts explicit width and height. It also supports legacy size presets for project compatibility:
| Preset | Width in cells |
|---|---|
small |
29 |
medium |
58 |
large |
87 |
xlarge |
116 |
huge |
145 |
Two framing modes are implemented:
contain: preserve the entire source image, scale it into the grid, and center it. Empty margins may remain;crop: compute a centered source rectangle with the target grid aspect ratio and fill the grid.
The geometry functions are in lib/beads-core/pattern-engine.ts: resolveGridDimensions, letterboxRect, and centerCropRect.
The framing decision is explicit. The converter does not perform semantic subject detection or automatically decide which crop is aesthetically correct.
The converter first creates a preview Canvas for the selected source rectangle. The preview long edge is at least large enough for the target grid and is capped at 1024 pixels. Canvas image smoothing is enabled at high quality.
Adjustments are applied to RGBA pixels before quantization:
gray = 0.299R + 0.587G + 0.114B
satColor = gray + (channel - gray) * saturation
contrast = (satColor - 128) * contrast + 128
result = clamp(contrast + brightness, 0, 255)
The implementation uses brightness, contrast, and saturation values from the project settings. Values are clamped to the byte range after adjustment. The adjusted preview is also retained for comparison in the UI.
Relevant implementation: applyImageAdjustments in lib/beads-core/pattern-engine.ts.
Background removal uses a color tolerance plus edge-connected flood fill. It does not globally remove every pixel close to the selected color.
Algorithm:
- Parse the user-selected six-digit HEX color.
- Seed the queue with matching pixels on the four image borders.
- Compare RGB distance against the normalized tolerance.
- Traverse four-connected neighbors only.
- Set Alpha to zero for every visited pixel.
- Leave disconnected interior regions unchanged.
This behavior avoids removing an interior area merely because it has the same color as the background. When transparentAsEmpty is enabled, pixels with Alpha below the transparency threshold become -1 cells during quantization.
Implementation: applyBackgroundRemoval in lib/beads-core/pattern-engine.ts.
Palette definitions are registered in lib/beads-core/palette.ts. The current registry contains:
- MARD 221
- Perler
- Hama
- Artkal S
- COCO
Each palette definition contains color IDs, display names, HEX values, a version, and bead-pitch metadata. The default palette is MARD 221.
Palette data is a versioned input to conversion. A project stores both paletteId and paletteVersion, so later palette changes do not silently redefine an existing project.
Using every color in a large brand palette for every image usually produces noisy material lists. The implementation therefore selects an active subset before assigning final cells.
The process is:
- For each non-transparent source pixel, find its nearest color in the working palette.
- Count the resulting palette indexes.
- Sort by frequency, with palette index as the deterministic tie-breaker.
- Keep the first
maxColorscolors. - Match each target grid pixel against this active subset.
The default color limits are defined by detail preset and grid size:
| Detail | small | medium | large | xlarge | huge |
|---|---|---|---|---|---|
easy |
12 | 18 | 18 | 24 | 24 |
balanced |
24 | 40 | 61 | 80 | 96 |
high |
32 | 56 | 80 | 112 | 128 |
maxColors can be overridden. If the user enables inventory filtering, allowedColorIds restricts the working palette before active-palette selection and nearest-color matching. A non-empty allowed set is honored; an empty set falls back to the full palette.
The converter does not use RGB Euclidean distance. Each palette HEX is converted to Lab, and the nearest candidate is selected using CIEDE2000.
paletteIndex = argmin(i, CIEDE2000(Lab(source), Lab(palette[i])))
The implementation is in lib/beads-core/color-distance.ts. Palette Lab values are cached so the same palette is not repeatedly converted for every pixel.
This choice improves perceptual ordering for colors that are numerically close in RGB but visibly different, especially around skin tones, muted colors, and hue transitions. It does not model lighting, material gloss, camera response, or the physical appearance of a particular bead batch.
The quantizer supports false, true, and "auto" for dithering.
When enabled, the algorithm scans the grid row by row. After assigning a palette color, it computes the RGB residual and diffuses it to future neighbors using these weights:
7/16
3/16 5/16 1/16
This is Floyd–Steinberg-style error diffusion. It changes the spatial distribution of existing palette colors; it never creates a new palette entry.
"auto" estimates source complexity by counting 4-bit RGB bins. Dithering is enabled when the number of observed bins exceeds 38. The heuristic is deterministic and intentionally simple enough to test.
The quantizer is implemented as quantizeRgba in lib/beads-core/pattern-engine.ts.
The effective order is:
decode
-> select source rectangle
-> resize to preview
-> apply image adjustments
-> capture adjusted preview
-> remove connected background, if enabled
-> resize preview to target grid
-> quantize with neutral adjustments
The last step uses neutral adjustment values because brightness, contrast, and saturation have already been applied to the preview. Reapplying them during quantization would apply the same transform twice.
Once the cells are generated, the UI loads them into the same project model used by manually created and imported patterns.
Core operations include:
- paint, erase, fill, and eyedropper editing;
- revision tracking after valid cell changes;
- material counting with
calculateMaterials; - deterministic suggestions for isolated single-cell cleanup;
- one-shot transfer from the converter to the Creator using
pixel-bead.transfer.v1; - HEX-to-brand palette mapping for imported templates.
The transfer and mapping adapter is lib/pattern-transfer.ts. It uses exact HEX matches first and CIEDE2000 nearest-color matching for non-exact colors.
The same project can be exported as:
- PNG with grid lines, color codes, coordinates, and a material legend;
- PDF with an overview, material list, and board pages.
Large patterns are split into physical 29 × 29 board sections. Actual-size PDF rendering uses the palette's bead-pitch metadata.
Implementations:
The complete file-to-pattern entry point requires a browser Image, Canvas, and File implementation.
import {
DEFAULT_BRAND,
generatePatternFromFile,
getPalette,
} from "./lib/beads-core";
const generated = await generatePatternFromFile(
file,
{
width: 58,
height: 58,
detail: "balanced",
framing: "contain",
adjustments: {
brightness: 0,
contrast: 100,
saturation: 100,
},
maxColors: 40,
dither: false,
transparentAsEmpty: true,
},
{
paletteDefinition: getPalette(DEFAULT_BRAND),
},
);
console.log(generated.width, generated.height);
console.log(generated.cells.length); // width * height
console.log(generated.paletteId, generated.paletteVersion);For a standalone package, the recommended boundary is to keep this browser adapter separate from a pure function accepting RGBA bytes, dimensions, palette data, and conversion settings.
Let P = width × height be the target grid area and K be the number of active palette colors.
- image adjustment: linear in preview pixel count;
- background flood fill:
O(P_preview)in the worst case; - active-palette discovery: linear in sampled pixels multiplied by candidate palette size;
- final quantization:
O(P × K)because each grid cell compares against the active palette; - material accounting:
O(P); - memory for the final project:
O(P + paletteSize).
The current core rejects grids larger than 60,000 cells. The browser adapter rejects unsupported MIME types and files above 10 MB. These limits protect the UI from unbounded Canvas and quantization work; they are implementation safeguards, not claims about the algorithm's theoretical maximum.
The repository tests cover the following implementation contracts:
- CIEDE2000 identity and perceptual ordering;
- registration and lookup of all five palettes;
- background-removal tolerance, connectivity, disabled behavior, and invalid input;
- isolated-cell cleanup suggestions;
- project validation, migration, and HEX-cell import;
- PNG layout and material legend geometry;
- PDF generation and pagination;
- physical
29 × 29board-section splitting; - converter and export browser workflows.
The test suite is indexed in tests/. Focused tests are preferable when extracting the core into a separate repository because they document algorithm invariants more directly than UI snapshots.
The current implementation should not be described as:
- semantic image understanding;
- automatic subject segmentation;
- an image-generation model;
- a guarantee that digital HEX values equal the appearance of physical beads under every light source;
- a copyright, privacy, or licensing solution for user-provided images.
It is a deterministic raster-to-grid quantizer with palette constraints, optional connected background removal, perceptual color matching, and editable project output. Complex images may still require manual cleanup after conversion.
pixel-bead-core/
├── README.md
├── LICENSE
├── packages/
│ ├── core/ # project model, grid validation, palettes, Lab/CIEDE2000, quantizer
│ ├── browser-adapter/ # File, Image, Canvas, preview, and background removal
│ └── exporters/ # PNG and PDF renderers
├── palettes/ # versioned palette data, provenance, and licenses
├── examples/ # input settings, RGBA fixtures, and output projects
└── tests/ # algorithm, migration, adapter, and exporter tests
Recommended API boundaries:
- Keep the quantizer deterministic and side-effect free.
- Keep authentication, database access, storage, and UI outside the core package.
- Accept raw RGBA buffers in the core so Node, browser, and Web Worker adapters can share the algorithm.
- Version palette data and record provenance for every palette release.
- Include fixture images, expected cell arrays, and expected material counts.
- Document invalid-input behavior and size limits as part of the public API.
- Make the README, type definitions, examples, and tests sufficient for an AI agent to understand the system without reading the entire product application.
| Concern | Current implementation |
|---|---|
| Types and project format | lib/beads-core/types.ts |
| File decode and conversion entry | lib/beads-core/browser/image.ts |
| Grid geometry, adjustments, masking, quantization | lib/beads-core/pattern-engine.ts |
| Lab and CIEDE2000 | lib/beads-core/color-distance.ts |
| Palette registry | lib/beads-core/palette.ts |
| Project creation, validation, migration, materials | lib/beads-core/project.ts |
| PNG export | lib/beads-core/browser/png.ts |
| PDF export | lib/beads-core/pdf.ts |
| Converter UI workflow | components/image-converter.tsx |
| Tests | tests/ |
The central engineering decision is to represent the conversion result as a versioned, palette-aware, editable grid rather than a flattened image. That single contract connects image processing, manual editing, inventory counts, persistence, project transfer, and printable export.
Pixel Bead Studio currently implements this contract with browser-local raster processing, explicit framing, optional connected background removal, active-palette reduction, CIEDE2000 matching, optional error diffusion, and validated PixelBeadProject output.