You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, neighborhoodSize = 5, sensitivity = 1.0, overlayOpacity = 0.75) {
// Utility to safely parse numerical inputs, even if provided as strings
const parseNum = (val, defaultVal) => {
const num = parseFloat(val);
return isNaN(num) ? defaultVal : num;
};
// Configuring analysis parameters
const nSize = Math.max(1, Math.round(parseNum(neighborhoodSize, 5)));
const r = Math.floor(nSize / 2); // Neighborhood radius
const sens = Math.max(0.01, parseNum(sensitivity, 1.0));
const opacity = Math.max(0, Math.min(1, parseNum(overlayOpacity, 0.75)));
const width = originalImg.width;
const height = originalImg.height;
// Set up canvas and draw original image
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0);
const imgData = ctx.getImageData(0, 0, width, height);
const data = imgData.data;
// We use integral images (Summed Area Tables) for O(1) local mean & variance calculation.
// Float64Array avoids precision loss over large accumulations of square values.
const S = new Float64Array((width + 1) * (height + 1));
const Sq = new Float64Array((width + 1) * (height + 1));
// Calculate integral images in a single pass
for (let y = 0; y < height; y++) {
const rowOffset = y * width;
const sRowOffset = y * (width + 1);
const sNextRowOffset = (y + 1) * (width + 1);
for (let x = 0; x < width; x++) {
const outIdx = (rowOffset + x) * 4;
// Standard BT.601 Luma
const lumaVal = data[outIdx] * 0.299 + data[outIdx + 1] * 0.587 + data[outIdx + 2] * 0.114;
const sumAbove = S[sRowOffset + (x + 1)];
const sumLeft = S[sNextRowOffset + x];
const sumAboveLeft = S[sRowOffset + x];
S[sNextRowOffset + (x + 1)] = lumaVal + sumAbove + sumLeft - sumAboveLeft;
const lumaSq = lumaVal * lumaVal;
const sqAbove = Sq[sRowOffset + (x + 1)];
const sqLeft = Sq[sNextRowOffset + x];
const sqAboveLeft = Sq[sRowOffset + x];
Sq[sNextRowOffset + (x + 1)] = lumaSq + sqAbove + sqLeft - sqAboveLeft;
}
}
// Precalculate Heatmap Colors (LUT for Extreme Performance)
// Blue (Low Confusion) -> Cyan -> Green -> Yellow -> Red (High Confusion)
const lut = new Uint8Array(256 * 3);
for (let i = 0; i < 256; i++) {
const norm = i / 255;
const h = (1 - norm) * 240;
const c = 1;
const x_col = c * (1 - Math.abs((h / 60) % 2 - 1));
let r_ = 0, g_ = 0, b_ = 0;
if (h >= 0 && h <= 60) { r_ = c; g_ = x_col; b_ = 0; }
else if (h > 60 && h <= 120) { r_ = x_col; g_ = c; b_ = 0; }
else if (h > 120 && h <= 180) { r_ = 0; g_ = c; b_ = x_col; }
else { r_ = 0; g_ = x_col; b_ = c; }
lut[i * 3] = Math.round(r_ * 255);
lut[i * 3 + 1] = Math.round(g_ * 255);
lut[i * 3 + 2] = Math.round(b_ * 255);
}
const outputData = ctx.createImageData(width, height);
const out = outputData.data;
// Apply Heatmap and Blend
for (let y = 0; y < height; y++) {
const y0_ = Math.max(0, y - r);
const y1_ = Math.min(height - 1, y + r);
const rowWidth = width + 1;
const offset_y0 = y0_ * rowWidth;
const offset_y1 = (y1_ + 1) * rowWidth;
for (let x = 0; x < width; x++) {
const outIdx = (y * width + x) * 4;
const a = data[outIdx + 3];
// Skip fully transparent pixels
if (a === 0) continue;
const x0 = Math.max(0, x - r);
const x1 = Math.min(width - 1, x + r);
const x0_ = x0;
const x1_ = x1 + 1;
// Extract window sums from integral images
const S_rect = S[offset_y1 + x1_] - S[offset_y0 + x1_] - S[offset_y1 + x0_] + S[offset_y0 + x0_];
const Sq_rect = Sq[offset_y1 + x1_] - Sq[offset_y0 + x1_] - Sq[offset_y1 + x0_] + Sq[offset_y0 + x0_];
const count = (x1 - x0 + 1) * (y1_ - y0_ + 1);
const mean = S_rect / count;
const variance = Math.max(0, (Sq_rect / count) - (mean * mean));
// Standard Deviation serves as our local "Confusion / Entropy" metric
const stdDev = Math.sqrt(variance);
// Normalize complexity metric, dynamically mapped based on user sensitivity
let norm = stdDev / (48 / sens);
norm = Math.max(0, Math.min(1, norm));
const lutIdx = Math.round(norm * 255) * 3;
const lumaVal = data[outIdx] * 0.299 + data[outIdx + 1] * 0.587 + data[outIdx + 2] * 0.114;
// Blend chosen heatmap color with the underlying grayscale base image
out[outIdx] = lut[lutIdx] * opacity + lumaVal * (1 - opacity);
out[outIdx + 1] = lut[lutIdx + 1] * opacity + lumaVal * (1 - opacity);
out[outIdx + 2] = lut[lutIdx + 2] * opacity + lumaVal * (1 - opacity);
out[outIdx + 3] = a;
}
}
ctx.putImageData(outputData, 0, 0);
// Build the visual container to present the tool nicely
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'center';
container.style.fontFamily = 'system-ui, -apple-system, sans-serif';
container.style.gap = '16px';
container.style.padding = '16px';
container.style.width = '100%';
container.style.boxSizing = 'border-box';
container.style.backgroundColor = '#f9fafb';
container.style.borderRadius = '8px';
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
canvas.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1)';
canvas.style.borderRadius = '6px';
// Construct Legend Info Container
const legendContainer = document.createElement('div');
legendContainer.style.width = '100%';
legendContainer.style.maxWidth = '500px';
legendContainer.style.display = 'flex';
legendContainer.style.flexDirection = 'column';
legendContainer.style.gap = '8px';
const title = document.createElement('div');
title.innerText = 'Confusion Analysis Heatmap (Visual Complexity Filter)';
title.style.fontWeight = '600';
title.style.fontSize = '14px';
title.style.color = '#374151';
title.style.textAlign = 'center';
const bar = document.createElement('div');
bar.style.height = '12px';
bar.style.width = '100%';
bar.style.background = 'linear-gradient(to right, hsl(240, 100%, 50%), hsl(180, 100%, 50%), hsl(120, 100%, 50%), hsl(60, 100%, 50%), hsl(0, 100%, 50%))';
bar.style.borderRadius = '999px';
const labels = document.createElement('div');
labels.style.display = 'flex';
labels.style.justifyContent = 'space-between';
labels.style.fontSize = '12px';
labels.style.fontWeight = '500';
labels.style.color = '#6b7280';
labels.innerHTML = '<span>Low Detail (Smooth)</span><span>Medium</span><span>High Details (Confusion)</span>';
legendContainer.appendChild(title);
legendContainer.appendChild(bar);
legendContainer.appendChild(labels);
container.appendChild(canvas);
container.appendChild(legendContainer);
return container;
}
Apply Changes