Please bookmark this page to avoid losing your image tool!

Image HSI Color Display Tool

(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.
async function processImage(originalImg) {
    const width = originalImg.width;
    const height = originalImg.height;

    // Handle cases where the image might be empty or invalid
    if (width === 0 || height === 0) {
        console.warn("Original image has zero width or height.");
        const placeholderCanvas = document.createElement('canvas');
        // Create a small placeholder rather than a 0x0 or excessively large canvas
        placeholderCanvas.width = (width > 0 ? width : 100) * 3;
        placeholderCanvas.height = (height > 0 ? height : 100);
        const pCtx = placeholderCanvas.getContext('2d');
        pCtx.fillStyle = '#eee';
        pCtx.fillRect(0,0,placeholderCanvas.width, placeholderCanvas.height);
        pCtx.fillStyle = 'black';
        pCtx.font = "16px Arial";
        pCtx.textAlign = "center";
        pCtx.fillText("Empty or invalid image", placeholderCanvas.width / 2, placeholderCanvas.height / 2);
        return placeholderCanvas;
    }

    // Create a temporary canvas to get image data
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = width;
    tempCanvas.height = height;
    const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
    
    try {
        tempCtx.drawImage(originalImg, 0, 0, width, height);
    } catch (e) {
        console.error("Error drawing original image to temporary canvas:", e);
        const errorCanvas = document.createElement('canvas');
        errorCanvas.width = width * 3 > 0 ? width * 3 : 300;
        errorCanvas.height = height > 0 ? height : 100;
        const errCtx = errorCanvas.getContext('2d');
        errCtx.font = "16px Arial"; errCtx.fillStyle = "red"; errCtx.textAlign = "center";
        errCtx.fillText("Error: Could not draw image.", errorCanvas.width/2, errorCanvas.height/2);
        return errorCanvas;
    }
    
    let imageData;
    try {
        imageData = tempCtx.getImageData(0, 0, width, height);
    } catch (e) {
        console.error("Error getting image data (possibly tainted canvas):", e);
        const errorCanvas = document.createElement('canvas');
        errorCanvas.width = width * 3 > 0 ? width * 3 : 450; // Adjusted for potentially longer message
        errorCanvas.height = height > 0 ? height : 100;
        const errCtx = errorCanvas.getContext('2d');
        errCtx.font = "16px Arial"; errCtx.fillStyle = "red"; errCtx.textAlign = "center";
        let msg1 = "Error: Could not read pixel data.";
        let msg2 = "Image might be cross-origin or format not supported for reading.";
        errCtx.fillText(msg1, errorCanvas.width/2, errorCanvas.height/2 - 10);
        errCtx.fillText(msg2, errorCanvas.width/2, errorCanvas.height/2 + 10);
        return errorCanvas;
    }
    
    const data = imageData.data;

    // Create the output canvas
    const outputCanvas = document.createElement('canvas');
    
    // Check for excessively large canvas dimensions
    // Max typical browser canvas dimension limit is 32767, some use 16384 or less.
    const MAX_CANVAS_SIDE = 16384; 
    if (width * 3 > MAX_CANVAS_SIDE || height > MAX_CANVAS_SIDE || width > MAX_CANVAS_SIDE) {
        console.warn(`Output canvas dimensions (${width*3}x${height}) may exceed limits or perform poorly.`);
        // We'll proceed, browser might cap it or throw error.
    }

    try {
        outputCanvas.width = width * 3;
        outputCanvas.height = height;
    } catch (e) {
        console.error("Error setting output canvas dimensions (possibly too large):", e);
        const errorCanvas = document.createElement('canvas');
        errorCanvas.width = 300; errorCanvas.height = 100;
        const errCtx = errorCanvas.getContext('2d');
        errCtx.font = "16px Arial"; errCtx.fillStyle = "red"; errCtx.textAlign = "center";
        errCtx.fillText("Error: Output canvas too large.", errorCanvas.width/2, errorCanvas.height/2);
        return errorCanvas;
    }
    
    const outCtx = outputCanvas.getContext('2d');

    const hueImageData = outCtx.createImageData(width, height);
    const satImageData = outCtx.createImageData(width, height);
    const intImageData = outCtx.createImageData(width, height);

    const hueData = hueImageData.data;
    const satData = satImageData.data;
    const intData = intImageData.data;

    const PI = Math.PI;
    const EPSILON_INTENSITY = 1e-9; // Threshold for intensity to be considered zero (black)
    const EPSILON_SATURATION = 1e-5; // Threshold for saturation to be considered zero (achromatic)
    const EPSILON_DENOMINATOR = 1e-9; // Threshold for H denominator squared

    for (let i = 0; i < data.length; i += 4) {
        const r = data[i];
        const g = data[i+1];
        const b = data[i+2];
        const alpha = data[i+3];

        const r_norm = r / 255;
        const g_norm = g / 255;
        const b_norm = b / 255;

        const intensity = (r_norm + g_norm + b_norm) / 3;

        let saturation;
        if (intensity < EPSILON_INTENSITY) { 
            saturation = 0; // Black or near-black
        } else {
            const min_rgb = Math.min(r_norm, g_norm, b_norm);
            saturation = 1 - (min_rgb / intensity);
        }
        // Clamp saturation due to potential floating point inaccuracies for achromatic colors
        if (saturation < EPSILON_SATURATION) {
             saturation = 0;
        }

        let hue = 0; // Default hue for achromatic colors (saturation is 0)
        if (saturation > EPSILON_SATURATION) { // Only calculate hue for chromatic colors
            // Using R-G, R-B, G-B for intermediate calculations
            const val_R_G = r_norm - g_norm;
            const val_R_B = r_norm - b_norm;
            const val_G_B = g_norm - b_norm; // Not explicitly used in numerator in this form

            // Numerator: 0.5 * ((R-G) + (R-B)) = R - 0.5*(G+B)
            const numerator_H = 0.5 * (val_R_G + val_R_B);
            // Denominator_sq: (R-G)^2 + (R-B)*(G-B)
            const denominator_H_sq = (val_R_G * val_R_G) + (val_R_B * val_G_B);

            if (denominator_H_sq > EPSILON_DENOMINATOR) {
                const denominator_H = Math.sqrt(denominator_H_sq);
                let acos_arg = numerator_H / denominator_H;
                
                // Clamp argument of acos to [-1, 1] to avoid NaN from precision errors
                acos_arg = Math.max(-1, Math.min(1, acos_arg));
                
                let angle_rad = Math.acos(acos_arg); // Result in [0, PI]

                if (b_norm > g_norm) {
                    hue = 2 * PI - angle_rad; // Adjust to [0, 2*PI]
                } else {
                    hue = angle_rad;
                }
            }
            // If denominator_H_sq is effectively zero, hue remains 0.
            // This case (R=G=B) should be caught by saturation being zero.
        }

        // Scale H, S, I to [0, 255] for display
        // Hue is in [0, 2*PI] radians, scale to [0, 255]
        const h_disp = Math.round((hue / (2 * PI)) * 255);
        // Saturation is in [0, 1], scale to [0, 255]
        const s_disp = Math.round(saturation * 255);
        // Intensity is in [0, 1], scale to [0, 255]
        const i_disp = Math.round(intensity * 255);

        // Set pixel data for each component image
        // Index `i` is the start of RGBA components for the current pixel
        hueData[i]   = h_disp; hueData[i+1] = h_disp; hueData[i+2] = h_disp; hueData[i+3] = alpha;
        satData[i]   = s_disp; satData[i+1] = s_disp; satData[i+2] = s_disp; satData[i+3] = alpha;
        intData[i]   = i_disp; intData[i+1] = i_disp; intData[i+2] = i_disp; intData[i+3] = alpha;
    }

    // Put the H, S, I ImageData onto the output canvas
    outCtx.putImageData(hueImageData, 0, 0);
    outCtx.putImageData(satImageData, width, 0);
    outCtx.putImageData(intImageData, width * 2, 0);
    
    // Add labels (H, S, I) to each panel
    let fontSize = 16;
    if (width > 0) { // make font size somewhat responsive to image width
        fontSize = Math.min(24, Math.max(10, Math.floor(width / 25)));
    }
    
    const labelBoxWidth = fontSize + 14; 
    const labelBoxHeight = fontSize + 8; 
    const margin = Math.max(5, Math.floor(fontSize/3)); // Margin from panel edge to label box

    outCtx.font = `bold ${fontSize}px Arial`;
    outCtx.textAlign = 'center';
    outCtx.textBaseline = 'middle';

    const panelPositions = [0, width, width * 2];
    const panelLabels = ['H', 'S', 'I'];

    panelLabels.forEach((label, index) => {
        const panelXoffset = panelPositions[index];
        const boxX = panelXoffset + margin;
        const boxY = margin;
        
        // Draw background for text for better visibility
        outCtx.fillStyle = 'rgba(0, 0, 0, 0.65)'; 
        outCtx.fillRect(boxX, boxY, labelBoxWidth, labelBoxHeight);
        
        // Draw text
        outCtx.fillStyle = 'white';
        outCtx.fillText(label, boxX + labelBoxWidth / 2, boxY + labelBoxHeight / 2);
    });

    return outputCanvas;
}

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 HSI Color Display Tool is designed to analyze an image and separate its color information into three distinct panels: Hue, Saturation, and Intensity (HSI). This tool can be particularly useful for artists, designers, and photographers who wish to understand the color composition of an image, enhance their color grading techniques, or simply visualize the color properties of their work. By providing a detailed view of each color component, users can make informed decisions about color corrections and adjustments in their projects.

Leave a Reply

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