You can edit the below JavaScript code to customize the image tool.
Apply Changes
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;
}
Apply Changes