You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, threshold = 0.8) {
/**
* Helper function to load an an image from a Base64 string.
* @param {string} base64 - The Base64 encoded image data.
* @returns {Promise<Image>} A promise that resolves with the loaded Image object.
*/
const loadImageFromBase64 = (base64) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = base64;
});
};
/**
* Helper function to convert ImageData to a grayscale representation.
* This simplifies the matching process and makes it color-independent.
* @param {ImageData} imageData - The pixel data from a canvas.
* @returns {{data: Uint8ClampedArray, width: number, height: number}}
*/
const convertToGrayscale = (imageData) => {
const grayData = new Uint8ClampedArray(imageData.width * imageData.height);
for (let i = 0; i < imageData.data.length; i += 4) {
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
// Using the luminosity method for grayscale conversion
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
grayData[i / 4] = gray;
}
return {
data: grayData,
width: imageData.width,
height: imageData.height
};
};
/**
* Performs template matching using Normalized Cross-Correlation (NCC).
* This is computationally intensive and may be slow on large images.
* @param {{data: Uint8ClampedArray, width: number, height: number}} source - The source image (grayscale).
* @param {{data: Uint8ClampedArray, width: number, height: number}} template - The template image (grayscale).
* @returns {{x: number, y: number, score: number}} The best match position and its score.
*/
const findBestMatch = (source, template) => {
let bestMatch = { x: 0, y: 0, score: -1 };
const { data: sourceData, width: sw, height: sh } = source;
const { data: templateData, width: tw, height: th } = template;
// Pre-calculate template mean and standard deviation
let templateSum = 0;
let templateSumSq = 0;
for (let i = 0; i < templateData.length; i++) {
templateSum += templateData[i];
templateSumSq += templateData[i] * templateData[i];
}
const templateMean = templateSum / templateData.length;
const templateStdDev = Math.sqrt(templateSumSq / templateData.length - templateMean * templateMean);
// Iterate over the source image
for (let y = 0; y <= sh - th; y++) {
for (let x = 0; x <= sw - tw; x++) {
let windowSum = 0;
let windowSumSq = 0;
let crossCorrelationSum = 0;
// Iterate over the template/window
for (let j = 0; j < th; j++) {
for (let i = 0; i < tw; i++) {
const sourceIdx = (y + j) * sw + (x + i);
const templateIdx = j * tw + i;
const sourcePixel = sourceData[sourceIdx];
const templatePixel = templateData[templateIdx];
windowSum += sourcePixel;
windowSumSq += sourcePixel * sourcePixel;
crossCorrelationSum += sourcePixel * templatePixel;
}
}
const windowMean = windowSum / templateData.length;
const windowStdDev = Math.sqrt(windowSumSq / templateData.length - windowMean * windowMean);
let score = -1;
if (windowStdDev > 0 && templateStdDev > 0) {
const numerator = (crossCorrelationSum / templateData.length) - (windowMean * templateMean);
const denominator = windowStdDev * templateStdDev;
score = numerator / denominator;
}
if (score > bestMatch.score) {
bestMatch = { x, y, score };
}
}
}
return bestMatch;
};
// --- Main Function Logic ---
// Define logo templates using Base64 to avoid external URLs
const templates = [
{ name: 'Spotify', base64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAARRSURBVFhHzZg/aBRXFMf/O+9eN4wKxoWIKIKdhaAoklARLKyNVNroG/gHsbQJlqxttGlWCiEpaGGsiIZGUBBCRFAqKnYpEYsWgoKIiLh3d3eH5d178e7e3by684Hhzt2d9/1f7t337j15+Pjx/9cQmCQSgZlJJAKzP2kEMn0xI1g/ySAY+F8JAsGQxJ9T0UqgT0rEBAwGg0dY3P7qRQC32z1m45t+A8DT05PRKxT63zQagVwuR6vVkqZptNYYjUaEw+EHyxJFEf1+v8FgENlsljRNQ9M0xGIxTCYTQqEQyWSSbrf7H2+1WqHrOhqNBrLZbM+SgEKhQMViEcvlkul0ir7vY7FYEAwGEM/niyljJpMhFoshHA4jGAzI5/OkadrzJGA0GkGv1xMNh7DZvEVsNpmYTCakUqnPY7PZen5iYgF8Pi+mpKTgfN43pVL5B/f7fSSSiZjb2cGc3BxmpycB1Go1pNNpDAZD8vk8er2eTCbT/TckEgkYDAby+TzS6TSGwyEqlQqJRIJgMKDfS1OT/P/N7u4uZmdn0ev1yOfzxGazUavVUKlUcDgceDye53sS0ev1sNls+Hy+p0mkcW4P523b0Gq1sNlsyGaz5HI5vN5bL7RaLSQSCVKpFO12m8lkQqVSITs7O+mB8ff3B9/3kc1mYTabkcvlaLVa2Gw2hMNhvN7bL6hUKvR6PbxeL4lEAmVlpT0h/x83mQyKxSJpNBqpVApt24amabjdbvh8PhqNJtLpNN1u9x/e7/eZnp4mkUjA6XTC7/ej1+vh9/vx+/0YDAZkMhkyMjKQTqeJSqXCaDRisVhQqVTQarWQy+VQaDRC67V/I5VKUb/fR6fTYTAYNDv3eTwerFarX+x2u4hEIgSDQdTrdZRKJcRiMSQSCfJm/tXjR9jtduRyOQwGA7w5+fPsnQ6Hwz1+eXlpL3BxcQGz2YxCoYBgMIhKpUKlUqFarQaDwQCv14uPj49/8Pl85OXlIRaLIRwOIxqNIhaLhW3L93uMMZLJJDKZDLlcLg/Ue2l3t9uN3W7HZrNB0zSMxWIIBAKEw2EkEglUKhVCoRA+ny+I//8uFovl/1B0XcdutzsQk/l8jlwux+l0wuVykclkcDgceDwebNuGpunHqNVq0Gq1UKvVUKvVUCoVVCqV2b8f+7yVUi2Vyh3b7fajvVwuYzabcbvdUKvVqNfrqNfrqNVq/B+j3Q65nE5kNhtyuRx0XUev10Ov10Ou1mIymZCfn4+7uzupVKqH/4tGo0Enk0Gj0SAWi+HzeRCJRBAOh1Gr1RAOh/l83q/j5uYGfr8fTqeTD38uFApxOBzi9/txOByiKEohzOfzUSqV+P3+p8g9cTgcUavVMBgM4Pf70Wq1NDMzmZ+Xh1KpxGq1wuPx4PV6SSTS3/Ue/9U3IqNfKBCIZPqjRsB/AVKJZCDWSvUAAAAASUVORK5CYII=' },
{ name: 'Apple Music', base64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAANdSURBVFhH7ZfvS1NRGMYfExOzkQyCQqXyD1gkWYmERkUihYpgH6gUFQn6Y0hRkS5FEYIgVqKICIhIlqYy2R8l0eJ+03i+33vu4T33nnPu7u5zL3Qfzof7ed/nvd899x7SNC3/C8MhM0M+N4Mpm/wZ2AgyaAIGwNf2PzZg0s8NWAZz3XG96cM5gR04jXNnOQMbMA3+2/4cBj8N413B6p+Zp/b+uT2f8Boc06lO/Xo1Vj8i/z4/Pz8/bVbrl5lO56Kjo8O+v/r1Qdd1zM/Pw/u1u+0vjUaDRCLx4mKx2M/m5+cRj8cxDAO/34+fnx8UCgXk83kkEgncbjeys7NxcnKiM3d0dODs7Iz0ejQawWq1wmq1otfrMTExgbGxMezbtw+Dg4MYGBjYy+3Zsyc2NjbQ6/VQKBRwOBxobm5mamrKWvLz8+Hz+XBzc4O1tTXa29sxPz+PxMTEfV6Xl5fR3NzM+Pi4VpIkEQ8PD6murqampgb9/f1wuVxISEjA5uYmhoaGEAgEkM/nOX/w/h+Xy8WsLCy8tLurqwurq6sIBAIIhUJoaGjAysqKyuvr6wvj4+OYnp5GKBSCxWJBpVKhoKBANo9JSUlITU1FRkYGpqamUCgUcDgcGBgYwNbWFvr7+/H19YVOp4NarbZ8w+fzITs7m5SUlDsajUbSajUcDgcGBgbUajUajYbExcVheXkZg4ODMDo6qr21pKQkFhYW1Gq1a2RkRN3d3ePj43i9nq/Ly8szMDDArKzsb3d1dTnEx8djcHDQZrPZkZCQAK/XazQa8PT0dHZ2FqWlpUQiEcTjccTjcaxtbcHpdGJgYAAOhwMvv//6nZGRERoaGjA0NITW1lbKysowNzeHalZWVtTX18d8gYGBaGlpQSaTITY2FoVCgVwuR0NDA7Kzs9HpdDAYDDA7O4vS0tJdLS0t8fv9yMvLQ319PZWVlTh9+jQ+/vhjHB0dYXZ2FtXV1bS0tEBVVZXGxka0trZibGws4/P19SVeXl74xRdfEAgElEol7Ha7VqslnU7z9PQkLy8vtra2UFlZiVAoRCAQQDqdJpPJEA6HicViBAIB9Pf3j46OVqvV3N/fhx84uP3g4CCLzc1NNDc3t7e3IxaLEQwGsf/+/aRSKVwu1wMCA+LxuclkMp/Ph1KpNDQ0gcvlwu1243a74XA4VCoVlUoFrVZLMBikUqnk83k0Gi3kUqlUKpRKJeRS+fVCoRAulwuPxwOPx4PP54NarYbb7QbvGvP5PFwuFzqdDpvNBq/XS6VSIReLxWJRKhVyqVQKhVwuuVT+w1wupVJJpVIu5HK5VCq5XO55+vSpnJwc9PX1IRqNYrFYEIlEkEqlcLlcaDQaEokEIpHIof+O2z6hKxQK8fl8cDgc0HUdhmEgm83i8/kwGo1wu93o9XpUKhWZTAbpdJparYbfyVQqRbFYRFVVFYqKiv5n9uDgIGw2G+vr6zExMYGlpSWsra3hH3PjO1RUVODg4AAbGxvkWkNDQ9jY2MD09DS++OILRkdHMSYnJzEyMoKysrJ/sC0rKyvF9PT07u4uLBYL+vr6MDY2JpvNDgaDyM/Ph0QiAV3XsbKyAgMDAxgfHyf+X59bLpdhsVhwdnZGKpW6uzsfU1NTkEql8Pl8+Pj4QCQSQSAQQDAYJC4uDjabDScnJzAYDHTv+r9+wzA1Mib4b2EwBf+8gYnBNhD3HwAAAABJRU5ErkJggg==' },
{ name: 'SoundCloud', base64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHPSURBVFhH7ZhNTsNAEIWn0cQq6GmpCBQ8QpAKeAI4AnxKvgCIBJ8ATxAmSiKgpYgHqIgKRFyBfSExm2l3N6Q80p70VtrWvBwZ2bnPNz/r7J3l5eX/D1F/Rz/343U86/L90oDqM5zQ8O04b5f4y0+7uC7jK6/h3oXoWJ+o82k86qG7nQe9Kveq3p3t2VXu9XpcrcfUe92/z/t9fTq3u/t80qPzYy+O53v6Qn055c/R9f2+h+sCqo/h/vxe1+K8lqf1+qXyut7F9Hh9P6yud2n83J/3+XQ86mH2uJ+m8s411K/R6d73E4X1er2p5f6e8vKk69qX74fX5Wp9H8/ruC+b6hpc12/E/XgYvSrbg9l1cO1k9J1uQe+qg97V6+F8D7f24LqcR/V+3Wl1z/t53C7X3u06j/f33u19t8XqV239oN6nqL9h/o7XW1Wv2vptvj5f8A1Vb3R+r/u6jHuv4a+v+V7G4j2sOqwO98t+Hqv7N3r+D9e16Dpc+uA3oPq3N0j97y+hHl3Iuup+9A3aH+R77o/3Vn9j0v0Vuv/S6P7dK6/h2/f8E6h+jQ2k/lU2UP2cbKD67WQD1efKBqqfKhsoflM2UPyMbKD42dkA8UPKBopHlQ0Uj4gNFAdq+X+mH5R9HwcAAAAASUVORK5CYII=' },
{ name: 'YouTube Music', base64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIBSURBVFhH7ZfNTsMwEIWnJSUq6GmpCBQ8QpAKeAI4AnxKvgCIBJ8AT5AnSiKgpYgHqIgKRBS8QxITaTe3hJQn2pM+hbb178iZnXkmn1l3ZntmZma/Yag+4f772e3a1t5zHjG8g+uG8n4S8nU9D2PwfD0Pw+H5eh6Gw+s+h4PxfkSjX1G4T9f7eR8N/g/9c7i/D8fh9L3H/Xk9H4+n6911W3c/Hw/D/w+h4XWcw/P5l8uH4f45rGv2M2E/2d5/L3rfx4N48Ho9DMP78Xg/D8f7vN+nw/v+08v6fTgcqPsd9v99Ph+u38d9vQyH8bF7nwx++R7P5+N5GA7D43kYjufrbTgc7u/DYTgMh+H34/j7MBw+79M+l1h/x+F8Hw7H4/0w+l2Pw/v1PByH9+t5/z6cP5B78I7f4bS7B/m939Hwf/kM+v4d8v9Qd1sft6O653A/h4+g/3S+P2c4jN+3oO71o77Wv3g8qPtd9/0xGo/3YzgcDMN/03043P/3YTi+P4fD4X4/3I+n4T7cDu+D42X4uP9s/P0/j/9EUPm4xP9E+d1i/J/cZ/rP7zR/S/U5d3fXv3q/g+t5GA7h/g+v52F4vR/fT3j4q/t1w687O00AAAAASUVORK5CYII=' },
];
// Setup the output canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// For performance, cap the image size.
const MAX_WIDTH = 800;
const scale = originalImg.width > MAX_WIDTH ? MAX_WIDTH / originalImg.width : 1;
canvas.width = originalImg.width * scale;
canvas.height = originalImg.height * scale;
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// Get grayscale data of the source image
const sourceImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const sourceGrayscale = convertToGrayscale(sourceImageData);
const foundLogos = [];
// Process each template
for (const template of templates) {
const templateImg = await loadImageFromBase64(template.base64);
const tempCanvas = document.createElement('canvas');
tempCanvas.width = templateImg.width;
tempCanvas.height = templateImg.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(templateImg, 0, 0);
const templateImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const templateGrayscale = convertToGrayscale(templateImageData);
const match = findBestMatch(sourceGrayscale, templateGrayscale);
if (match.score > threshold) {
foundLogos.push({
name: template.name,
x: match.x,
y: match.y,
width: templateImg.width,
height: templateImg.height,
score: match.score
});
}
}
// Draw the results on the canvas
ctx.font = '16px Arial';
ctx.lineWidth = 2;
for (const logo of foundLogos) {
ctx.strokeStyle = '#00FF00'; // Bright green for the box
ctx.strokeRect(logo.x, logo.y, logo.width, logo.height);
const text = `${logo.name} (${(logo.score * 100).toFixed(1)}%)`;
const textMetrics = ctx.measureText(text);
const textX = logo.x;
const textY = logo.y > 20 ? logo.y - 5 : logo.y + logo.height + 15;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(textX - 2, textY - 14, textMetrics.width + 4, 18);
ctx.fillStyle = '#00FF00'; // Bright green for the text
ctx.fillText(text, textX, textY);
}
return canvas;
}
Apply Changes