Please bookmark this page to avoid losing your image tool!

Image Thermal Imaging Filter

(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.
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;
}

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 Thermal Imaging Filter allows users to apply a thermal color mapping effect to images, transforming standard visuals into color-coded representations based on pixel intensity. This utility is useful for visualizing data in scientific fields, enhancing art with thermal aesthetics, or providing clearer thermal insights in engineering and design applications. Users can adjust the intensity range for better visualization of specific areas within their images, making it versatile for various analytical and creative purposes.

Leave a Reply

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