You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, minIntensityStr = "0.0", maxIntensityStr = "1.0") {
// Thermal colormap: Black (cold) -> Indigo -> Red -> Orange -> Yellow -> White (hot)
const thermalColorMap = [
{ pos: 0.0, r: 0, g: 0, b: 0 }, // Black
{ pos: 0.20, r: 75, g: 0, b: 130 }, // Indigo (approx. {r:75, g:0, b:130})
{ pos: 0.40, r: 255, g: 0, b: 0 }, // Red
{ pos: 0.60, r: 255, g: 165, b: 0 }, // Orange
{ pos: 0.80, r: 255, g: 255, b: 0 }, // Yellow
{ pos: 1.0, r: 255, g: 255, b: 255 } // White
];
/**
* Interpolates a color from a colormap based on an intensity value.
* @param {number} intensity - The intensity value, expected to be in [0, 1].
* @param {Array<object>} colorMap - The colormap array, where each object is {pos, r, g, b}.
* @returns {object} An object {r, g, b} representing the interpolated color.
*/
function _interpolateColor(intensity, colorMap) {
if (isNaN(intensity)) {
// Default to black or first color in map if intensity is NaN
return { r: colorMap[0].r, g: colorMap[0].g, b: colorMap[0].b };
}
// Ensure intensity is within the [0, 1] range for reliable mapping.
const saneIntensity = Math.max(0, Math.min(1, intensity));
if (saneIntensity <= colorMap[0].pos) {
return { r: colorMap[0].r, g: colorMap[0].g, b: colorMap[0].b };
}
if (saneIntensity >= colorMap[colorMap.length - 1].pos) {
const last = colorMap[colorMap.length - 1];
return { r: last.r, g: last.g, b: last.b };
}
for (let i = 0; i < colorMap.length - 1; i++) {
const c1 = colorMap[i];
const c2 = colorMap[i+1];
// Check if intensity falls between c1.pos and c2.pos
if (saneIntensity >= c1.pos && saneIntensity < c2.pos) {
// Avoid division by zero if positions are identical (ill-formed map)
if (c2.pos === c1.pos) {
return { r: c1.r, g: c1.g, b: c1.b };
}
// Linear interpolation factor
const t = (saneIntensity - c1.pos) / (c2.pos - c1.pos);
const r = Math.round(c1.r * (1 - t) + c2.r * t);
const g = Math.round(c1.g * (1 - t) + c2.g * t);
const b = Math.round(c1.b * (1 - t) + c2.b * t);
return { r, g, b };
}
}
// Fallback for any unexpected scenario (e.g. malformed map or intensity value)
// This typically should not be reached if map covers [0,1] and intensity is clamped.
const fallbackColor = colorMap[colorMap.length - 1];
return { r: fallbackColor.r, g: fallbackColor.g, b: fallbackColor.b };
}
const canvas = document.createElement('canvas');
// Use naturalWidth/Height for <img> elements, fallback to width/height for other Image types or if natural* not set.
canvas.width = originalImg.naturalWidth || originalImg.width || 0;
canvas.height = originalImg.naturalHeight || originalImg.height || 0;
// Handle cases where image dimensions might be zero or invalid
if (canvas.width === 0 || canvas.height === 0) {
console.error("processImage: Invalid image dimensions (0x0).");
// Return a small placeholder canvas with an error message if possible
canvas.width = canvas.width === 0 ? 200 : canvas.width;
canvas.height = canvas.height === 0 ? 100 : canvas.height;
const errorCtx = canvas.getContext('2d');
if (errorCtx) {
errorCtx.fillStyle = 'lightgray';
errorCtx.fillRect(0, 0, canvas.width, canvas.height);
errorCtx.fillStyle = 'red';
errorCtx.font = '16px Arial';
errorCtx.textAlign = 'center';
errorCtx.textBaseline = 'middle';
errorCtx.fillText('Error: Invalid image dimensions.', canvas.width / 2, canvas.height / 2);
}
return canvas;
}
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!ctx) {
console.error("processImage: Failed to get 2D context.");
// Cannot draw an error message if context is null. The empty canvas (sized above) will be returned.
return canvas;
}
try {
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
} catch (e) {
console.error("processImage: Error drawing original image to canvas:", e);
ctx.fillStyle = 'lightgray';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'red';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Error: Could not draw original image.', canvas.width / 2, canvas.height / 2);
return canvas;
}
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
// This often happens with cross-origin images without CORS approval
console.error("processImage: Error getting image data (cross-origin issue?):", e);
// Clear the canvas (it might be tainted and show original image) and draw an error.
ctx.fillStyle = 'lightgray';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'red';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Error: Could not access image data.', canvas.width / 2, canvas.height / 2);
return canvas;
}
const data = imageData.data;
// Parse and validate min/max intensity parameters
let pMin = parseFloat(minIntensityStr);
let pMax = parseFloat(maxIntensityStr);
if (isNaN(pMin) || !isFinite(pMin)) pMin = 0.0;
if (isNaN(pMax) || !isFinite(pMax)) pMax = 1.0;
// Ensure pMin <= pMax; if not, swap them.
if (pMin > pMax) {
[pMin, pMax] = [pMax, pMin];
}
// Clamp the user-defined intensity window to the valid [0,1] range.
pMin = Math.max(0, Math.min(1, pMin));
pMax = Math.max(0, Math.min(1, pMax));
const intensityRange = pMax - pMin;
for (let i = 0; i < data.length; i += 4) {
const rPx = data[i];
const gPx = data[i+1];
const bPx = data[i+2];
// Alpha channel (data[i+3]) is preserved.
// Calculate grayscale intensity (luminance using NTSC coefficients: 0.299R + 0.587G + 0.114B)
const rawGrayIntensity = (0.299 * rPx + 0.587 * gPx + 0.114 * bPx) / 255.0; // Normalized to [0,1]
let mappedIntensity;
if (intensityRange === 0) {
// If min and max intensity are the same, create a threshold effect.
// Pixels at or above this intensity map to the "hot" end, else to "cold" end.
mappedIntensity = (rawGrayIntensity >= pMin) ? 1.0 : 0.0;
} else {
// Map the raw intensity to the [0,1] range based on pMin and pMax window
mappedIntensity = (rawGrayIntensity - pMin) / intensityRange;
}
// Clamp result to [0,1] before passing to color interpolation.
// _interpolateColor also has a clamp, but being explicit maintains clarity.
mappedIntensity = Math.max(0, Math.min(1, mappedIntensity));
const thermalColor = _interpolateColor(mappedIntensity, thermalColorMap);
data[i] = thermalColor.r; // Red channel
data[i+1] = thermalColor.g; // Green channel
data[i+2] = thermalColor.b; // Blue channel
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes