Please bookmark this page to avoid losing your image tool!

Image To Paint By Number Converter

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg, numColors = 16, blurRadius = 3, minRegionSize = 50, outputMode = "outline") {
    // Parse arguments
    const k = parseInt(numColors) || 16;
    const blur = parseInt(blurRadius) || 3;
    const minSize = parseInt(minRegionSize) || 50;
    const outMode = (outputMode === "color") ? "color" : "outline";

    // Setup the internal rendering canvas, scale down if too large to ensure performance
    const maxDimension = 800;
    let width = originalImg.width;
    let height = originalImg.height;
    
    if (width > maxDimension || height > maxDimension) {
        let scale = maxDimension / Math.max(width, height);
        width = Math.floor(width * scale);
        height = Math.floor(height * scale);
    }

    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d', {willReadFrequently: true});

    // Apply pre-blur to reduce noise
    ctx.filter = `blur(${blur}px)`;
    ctx.drawImage(originalImg, 0, 0, width, height);
    ctx.filter = 'none';

    const imgData = ctx.getImageData(0, 0, width, height);
    const data = imgData.data;
    const totalPixels = width * height;

    const yieldThread = () => new Promise(resolve => setTimeout(resolve, 0));

    // Initialize Centroids (pick colors evenly spaced functionally)
    let centroids = [];
    const step = Math.max(4, Math.floor((data.length / 4) / k) * 4);
    for (let i = 0; i < k; i++) {
        let idx = (i * step) % data.length;
        centroids.push([data[idx], data[idx + 1], data[idx + 2]]);
    }

    // K-Means Clustering
    let assignments = new Int32Array(totalPixels);
    const maxKMeansIters = 8;
    
    for (let iter = 0; iter < maxKMeansIters; iter++) {
        let newCentroids = Array.from({ length: k }, () => [0, 0, 0, 0]); // r, g, b, count
        
        for (let i = 0; i < totalPixels; i++) {
            let r = data[i * 4];
            let g = data[i * 4 + 1];
            let b = data[i * 4 + 2];
            
            let minDist = Infinity;
            let bestCluster = 0;
            
            for (let c = 0; c < k; c++) {
                let dr = r - centroids[c][0];
                let dg = g - centroids[c][1];
                let db = b - centroids[c][2];
                let distSq = dr * dr + dg * dg + db * db;
                
                if (distSq < minDist) {
                    minDist = distSq;
                    bestCluster = c;
                }
            }
            
            assignments[i] = bestCluster;
            newCentroids[bestCluster][0] += r;
            newCentroids[bestCluster][1] += g;
            newCentroids[bestCluster][2] += b;
            newCentroids[bestCluster][3]++;
        }
        
        for (let c = 0; c < k; c++) {
            if (newCentroids[c][3] > 0) {
                centroids[c][0] = Math.round(newCentroids[c][0] / newCentroids[c][3]);
                centroids[c][1] = Math.round(newCentroids[c][1] / newCentroids[c][3]);
                centroids[c][2] = Math.round(newCentroids[c][2] / newCentroids[c][3]);
            }
        }
        await yieldThread();
    }

    // Helper: Find connected component regions
    function getRegions(assigns) {
        let visited = new Uint8Array(totalPixels);
        let foundRegions = [];
        
        for (let i = 0; i < totalPixels; i++) {
            if (!visited[i]) {
                let colorIdx = assigns[i];
                let queue = [i];
                visited[i] = 1;
                let regionPixels = [];
                let head = 0;
                
                while (head < queue.length) {
                    let p = queue[head++];
                    regionPixels.push(p);
                    
                    let px = p % width;
                    let py = Math.floor(p / width);
                    
                    if (px > 0 && !visited[p - 1] && assigns[p - 1] === colorIdx) { visited[p - 1] = 1; queue.push(p - 1); }
                    if (px < width - 1 && !visited[p + 1] && assigns[p + 1] === colorIdx) { visited[p + 1] = 1; queue.push(p + 1); }
                    if (py > 0 && !visited[p - width] && assigns[p - width] === colorIdx) { visited[p - width] = 1; queue.push(p - width); }
                    if (py < height - 1 && !visited[p + width] && assigns[p + width] === colorIdx) { visited[p + width] = 1; queue.push(p + width); }
                }
                
                foundRegions.push({
                    colorIdx: colorIdx,
                    pixels: regionPixels
                });
            }
        }
        return foundRegions;
    }

    let regions = getRegions(assignments);
    await yieldThread();

    // Smoothen: Absorb small regions (noise) into surrounding colors
    let changed = false;
    for (let r of regions) {
        if (r.pixels.length < minSize) {
            let newColor = r.colorIdx;
            
            // Find an adjacent color
            for (let p of r.pixels) {
                let px = p % width;
                let py = Math.floor(p / width);
                let neighbors = [];
                if (px > 0) neighbors.push(p - 1);
                if (px < width - 1) neighbors.push(p + 1);
                if (py > 0) neighbors.push(p - width);
                if (py < height - 1) neighbors.push(p + width);
                
                let found = false;
                for (let n of neighbors) {
                    if (assignments[n] !== r.colorIdx) {
                        newColor = assignments[n];
                        found = true;
                        break;
                    }
                }
                if (found) break;
            }
            
            for (let p of r.pixels) {
                assignments[p] = newColor;
            }
            changed = true;
        }
    }

    if (changed) {
        // Re-calculate robust regions after merging small ones
        regions = getRegions(assignments).filter(r => r.pixels.length >= minSize);
    }
    await yieldThread();

    // Calculate centroid for drawing numbers in each valid region
    for (let r of regions) {
        let sumX = 0, sumY = 0;
        for (let p of r.pixels) {
            sumX += p % width;
            sumY += Math.floor(p / width);
        }
        let cx = Math.floor(sumX / r.pixels.length);
        let cy = Math.floor(sumY / r.pixels.length);
        
        let bestP = r.pixels[0];
        let minD = Infinity;
        for (let p of r.pixels) {
            let px = p % width;
            let py = Math.floor(p / width);
            let d = (px - cx) * (px - cx) + (py - cy) * (py - cy);
            if (d < minD) {
                minD = d;
                bestP = p;
            }
        }
        r.centerX = bestP % width;
        r.centerY = Math.floor(bestP / width);
    }

    // Build the Outlines & Colors Output
    const outCanvas = document.createElement('canvas');
    outCanvas.width = width;
    outCanvas.height = height;
    const outCtx = outCanvas.getContext('2d');
    const outImgData = outCtx.createImageData(width, height);
    const outData = outImgData.data;

    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            let p = y * width + x;
            let idx = p * 4;
            let currentRegionColor = assignments[p];
            
            // Boundary detection
            let isBorder = false;
            if (x < width - 1 && assignments[p + 1] !== currentRegionColor) isBorder = true;
            if (y < height - 1 && assignments[p + width] !== currentRegionColor) isBorder = true;
            
            if (isBorder) {
                outData[idx] = 0;
                outData[idx + 1] = 0;
                outData[idx + 2] = 0;
                outData[idx + 3] = 255;
            } else {
                if (outMode === "color") {
                    let rgb = centroids[currentRegionColor];
                    outData[idx] = rgb[0];
                    outData[idx + 1] = rgb[1];
                    outData[idx + 2] = rgb[2];
                    outData[idx + 3] = 255;
                } else {
                    outData[idx] = 255;
                    outData[idx + 1] = 255;
                    outData[idx + 2] = 255;
                    outData[idx + 3] = 255;
                }
            }
        }
    }
    outCtx.putImageData(outImgData, 0, 0);

    // Draw the Numbers
    outCtx.fillStyle = (outMode === "color") ? 'rgba(0, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.9)';
    outCtx.font = "12px Arial, sans-serif";
    outCtx.textAlign = "center";
    outCtx.textBaseline = "middle";
    
    for (let r of regions) {
        outCtx.fillText((r.colorIdx + 1).toString(), r.centerX, r.centerY);
    }

    // Build DOM structure (Image + Palette)
    const container = document.createElement('div');
    container.style.display = 'flex';
    container.style.flexDirection = 'column';
    container.style.alignItems = 'center';
    container.style.fontFamily = 'Arial, sans-serif';
    container.style.width = '100%';
    
    outCanvas.style.maxWidth = '100%';
    outCanvas.style.height = 'auto';
    outCanvas.style.border = '1px solid #ccc';
    outCanvas.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
    container.appendChild(outCanvas);

    // Palette Section
    const paletteDiv = document.createElement('div');
    paletteDiv.style.display = 'flex';
    paletteDiv.style.flexWrap = 'wrap';
    paletteDiv.style.marginTop = '20px';
    paletteDiv.style.gap = '15px';
    paletteDiv.style.justifyContent = 'center';
    paletteDiv.style.maxWidth = `${width}px`;

    for (let i = 0; i < k; i++) {
        const item = document.createElement('div');
        item.style.display = 'flex';
        item.style.alignItems = 'center';
        item.style.gap = '8px';
        item.style.background = '#f9f9f9';
        item.style.padding = '5px 10px';
        item.style.borderRadius = '5px';
        item.style.border = '1px solid #ddd';

        const numText = document.createElement('span');
        numText.textContent = (i + 1).toString();
        numText.style.fontWeight = 'bold';
        numText.style.width = '20px';
        numText.style.textAlign = 'right';

        const swatch = document.createElement('div');
        swatch.style.width = '24px';
        swatch.style.height = '24px';
        swatch.style.borderRadius = '3px';
        swatch.style.backgroundColor = `rgb(${centroids[i][0]}, ${centroids[i][1]}, ${centroids[i][2]})`;
        swatch.style.border = '1px solid #333';

        item.appendChild(numText);
        item.appendChild(swatch);
        paletteDiv.appendChild(item);
    }

    container.appendChild(paletteDiv);
    return container;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

This tool converts any uploaded image into a custom paint-by-numbers template. It uses color clustering to simplify the image into a specific number of colors and identifies distinct regions to create a guided pattern. Users can choose between a color-filled version or a clean outline version, both of which include numbered regions and a corresponding color palette. This tool is ideal for artists, hobbyists, or educators looking to create personalized coloring activities or DIY art projects from their own photos.

Leave a Reply

Your email address will not be published. Required fields are marked *