Please bookmark this page to avoid losing your image tool!

Image Repeat Counter

(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, patternImgDataUrl = '', threshold = 0.95) {
    // Create the output canvas to draw the final result on
    const outputCanvas = document.createElement('canvas');
    outputCanvas.width = originalImg.width;
    outputCanvas.height = originalImg.height;
    const ctx = outputCanvas.getContext('2d');
    ctx.drawImage(originalImg, 0, 0);

    // Helper function to display messages on the canvas
    const showMessage = (message) => {
        ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
        ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
        ctx.font = '20px Arial';
        ctx.fillStyle = 'white';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(message, outputCanvas.width / 2, outputCanvas.height / 2);
    };

    if (!patternImgDataUrl) {
        showMessage('Error: A pattern image must be provided.');
        return outputCanvas;
    }

    // Load the pattern image from the provided data URL
    let patternImg;
    try {
        patternImg = await new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => resolve(img);
            img.onerror = () => reject(new Error('Failed to load the pattern image.'));
            img.src = patternImgDataUrl;
        });
    } catch (error) {
        showMessage(error.message);
        return outputCanvas;
    }

    const ow = originalImg.width;
    const oh = originalImg.height;
    const pw = patternImg.width;
    const ph = patternImg.height;

    if (pw === 0 || ph === 0) {
        showMessage('Error: Pattern image has invalid dimensions.');
        return outputCanvas;
    }
    
    // Draw count text helper
    const drawCount = (count) => {
        const text = `Count: ${count}`;
        const fontSize = Math.max(16, Math.round(ow / 40));
        ctx.font = `bold ${fontSize}px Arial`;
        const textMetrics = ctx.measureText(text);
        const padding = fontSize / 2;
        ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
        ctx.fillRect(5, 5, textMetrics.width + padding * 2, fontSize + padding * 2);
        ctx.fillStyle = 'white';
        ctx.textBaseline = 'top';
        ctx.fillText(text, 5 + padding, 5 + padding);
    };

    if (pw > ow || ph > oh) {
        showMessage('Pattern is larger than the original image.');
        drawCount(0);
        return outputCanvas;
    }

    // Use temporary canvases to get pixel data
    const originalCanvas = document.createElement('canvas');
    originalCanvas.width = ow;
    originalCanvas.height = oh;
    const originalCtx = originalCanvas.getContext('2d', { willReadFrequently: true });
    originalCtx.drawImage(originalImg, 0, 0);
    const originalImageData = originalCtx.getImageData(0, 0, ow, oh).data;

    const patternCanvas = document.createElement('canvas');
    patternCanvas.width = pw;
    patternCanvas.height = ph;
    const patternCtx = patternCanvas.getContext('2d', { willReadFrequently: true });
    patternCtx.drawImage(patternImg, 0, 0);
    const patternImageData = patternCtx.getImageData(0, 0, pw, ph).data;

    let candidates = [];

    // Iterate through the original image to find potential matches
    for (let y = 0; y <= oh - ph; y++) {
        for (let x = 0; x <= ow - pw; x++) {
            let totalDifference = 0;
            let pixelsToCompare = 0;

            // Compare the pattern with the current region in the original image
            for (let py = 0; py < ph; py++) {
                for (let px = 0; px < pw; px++) {
                    const patternIndex = (py * pw + px) * 4;
                    // Skip transparent pixels in the pattern for a more robust match
                    if (patternImageData[patternIndex + 3] < 128) {
                        continue;
                    }
                    pixelsToCompare++;

                    const originalIndex = ((y + py) * ow + (x + px)) * 4;

                    // Calculate the Sum of Absolute Differences for R, G, B channels
                    const rDiff = Math.abs(originalImageData[originalIndex] - patternImageData[patternIndex]);
                    const gDiff = Math.abs(originalImageData[originalIndex + 1] - patternImageData[patternIndex + 1]);
                    const bDiff = Math.abs(originalImageData[originalIndex + 2] - patternImageData[patternIndex + 2]);

                    totalDifference += rDiff + gDiff + bDiff;
                }
            }

            if (pixelsToCompare === 0) continue; // Pattern is fully transparent

            const maxDifference = pixelsToCompare * 3 * 255;
            const similarity = 1 - (totalDifference / maxDifference);

            if (similarity >= threshold) {
                candidates.push({ x, y, width: pw, height: ph, similarity });
            }
        }
    }

    // Apply Non-Maximum Suppression to filter overlapping detections
    candidates.sort((a, b) => b.similarity - a.similarity);

    const finalMatches = [];
    const isOverlapping = (rect1, rect2) => {
        return !(rect1.x + rect1.width <= rect2.x ||
                 rect2.x + rect2.width <= rect1.x ||
                 rect1.y + rect1.height <= rect2.y ||
                 rect2.y + rect2.height <= rect1.y);
    };

    while (candidates.length > 0) {
        const bestMatch = candidates.shift();
        if(!bestMatch) continue;
        finalMatches.push(bestMatch);
        
        // Remove all other candidates that overlap with the best (highest similarity) match
        candidates = candidates.filter(candidate => !isOverlapping(bestMatch, candidate));
    }

    // Draw rectangles around the final, non-overlapping matches
    ctx.strokeStyle = 'red';
    ctx.lineWidth = Math.max(2, Math.round(Math.min(pw, ph) / 15));
    finalMatches.forEach(match => {
        ctx.strokeRect(match.x, match.y, match.width, match.height);
    });

    // Draw the final count on the canvas
    drawCount(finalMatches.length);

    return outputCanvas;
}

Free Image Tool Creator

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

Description

The Image Repeat Counter is a tool designed to analyze an image and identify the number of times a specified pattern appears within it. Users can upload an original image and a pattern image, with the tool comparing the two to count occurrences of the pattern. This can be particularly useful for designers and artists looking to detect repetitive elements in their work, such as patterns in textiles or wallpapers, for quality control in manufacturing, or for creating digital artworks that require precise replication of motifs. Additionally, it serves as a resource for image processing tasks involving pattern recognition.

Leave a Reply

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