Draw a line profile for an image using canvas html5

Viewed 146

I am trying to draw intensity profile for an image with x axis as the length of the line on the image and the y-axis with intensity values along the length of the line. How can i do this on html 5 canvas? I tried the below code but I am not getting the right intensity values. Not sure where i am going wrong.

private getLineIntensityVals = function (lineObj, img) {
 const slope = this.calculateSlopeOfLine(lineObj.upPos, lineObj.downPos);
 const intercept = this.calculateIntercept(lineObj.downPos, slope);
 const ctx = img.getContext('2d');
 const coordinates = [];
 const intensities = [];
 for (let x = lineObj.downPos.x; x <= lineObj.upPos.x; x++) {
  const y = slope * x + intercept;
  const pixelData = ctx.getImageData(x, y, 1, 1).data;
  pixelData[0] = 255 - pixelData[0];
  pixelData[1] = 255 - pixelData[1];
  pixelData[2] = 255 - pixelData[2];
  const intensity = ((0.299 * pixelData[0]) + (0.587 * pixelData[1]) + (0.114 * pixelData[2]));
  intensities.push(intensity);
 }
 return intensities;
};

private calculateSlopeOfLine = function (upPos, downPos) {
 if (upPos.x === downPos.x || upPos.y === downPos.y) {
  return null;
 }
 return (downPos.y - upPos.y) / (downPos.x - upPos.x);
};

private calculateIntercept = function (startPoint, slope) {
  if (slope === null) {
   return startPoint.x;
  }
  return startPoint.y - slope * startPoint.x;
};

private calculateLineLength(line) {
  const dim = {width: Math.abs(line.downPos.x -line.upPos.x),height:Math.abs(line.downPos.y- line.upPos.y)};
  length = Math.sqrt(Math.pow(dim.width, 2) + Math.pow(dim.height, 2));
  return length;
};
1 Answers

Image data

Don't get the image data one pixel at a time. Gaining access to pixel data is expensive (CPU cycles), and memory is cheap. Get all the pixels once and reuse that data.

Sampling the data

Most lines will not fit into pixels evenly. To solve divide the line into the number of samples you want (You can use the line length)

Then step to each sample in turn getting the 4 neighboring pixels values and interpolating the color at the sample point.

As we are interpolating we need to ensure that we do not use the wrong color model. In this case we use sRGB.

We thus get the function

// imgData is the pixel date
// x1,y1 and x2,y2 are the line end points
// sampleRate is number of samples per pixel
// Return array 3 values for each sample.
function getProfile(imgData, x1, y1, x2, y2, sampleRate) {

    // convert line to vector
    const dx = x2 - x1;
    const dy = y2 - y1;

    // get length and calculate number of samples for sample rate
    const samples = (dx * dx + dy * dy) ** 0.5 * Math.abs(sampleRate) + 1 | 0;

    // Divide line vector by samples to get x, and y step per sample
    const nx = dx / samples;
    const ny = dy / samples;

    const w = imgData.width;
    const h = imgData.height;
    const pixels = imgData.data;
    const values = [];

    // Offset line to center of pixel
    var x = x1 + 0.5;
    var y = y1 + 0.5;
    var i = samples;

    while (i--) { // for each sample

        // make sure we are in the image
        if (x >= 0 && x < w - 1 && y >= 0 && y < h - 1) {

            // get 4 closest pixel indexes
            const idxA = ((x | 0) + (y | 0) * w) * 4;
            const idxB = ((x + 1 | 0) + (y | 0) * w) * 4;
            const idxC = ((x + 1 | 0) + (y + 1 | 0) * w) * 4;
            const idxD = ((x | 0) + (y + 1 | 0) * w) * 4;

            // Get channel data using sRGB approximation
            const r1 = pixels[idxA] ** 2.2;
            const r2 = pixels[idxB] ** 2.2;
            const r3 = pixels[idxC] ** 2.2;
            const r4 = pixels[idxD] ** 2.2;

            const g1 = pixels[idxA + 1] ** 2.2;
            const g2 = pixels[idxB + 1] ** 2.2;
            const g3 = pixels[idxC + 1] ** 2.2;
            const g4 = pixels[idxD + 1] ** 2.2;

            const b1 = pixels[idxA + 2] ** 2.2;
            const b2 = pixels[idxB + 2] ** 2.2;
            const b3 = pixels[idxC + 2] ** 2.2;
            const b4 = pixels[idxD + 2] ** 2.2;

            // find value at location via linear interpolation
            const xf = x % 1;
            const yf = y % 1;

            const rr = (r2 - r1) * xf + r1;
            const gg = (g2 - g1) * xf + g1;       
            const bb = (b2 - b1) * xf + b1;       

            /// store channels as uncompressed sRGB
            values.push((((r3 - r4) * xf + r4) - rr) * yf + rr);
            values.push((((g3 - g4) * xf + g4) - gg) * yf + gg);
            values.push((((b3 - b4) * xf + b4) - bb) * yf + bb);  
        } else {
            // outside image
            values.push(0,0,0);
        }
        // step to next sample
        x += nx;
        y += ny;
    }
    return values;
}

Conversion to values

The array hold raw sample data. There are a variety of ways to convert to a value. That is why we separate the sampling from the conversion to values.

The next function takes the raw sample array and converts it to values. It returns an array of values. While it is doing the conversion it also get the max value so that the data can be plotted to fit a graph.

function convertToMean(values) {
    var i = 0, v;
    const results = [];
    results._max = 0;
    while (i < values.length) {
        results.push(v = (values[i++] * 0.299 + values[i++] * 0.587 + values[i++] * 0.114) ** (1/2.2));
        results._max = Math.max(v, results._max);
    } 
    return results;
}

Now you can plot the data how you like.

Example

Click drag line on image (when loaded)

Results are plotted real time.

Move mouse over plot to see values.

Use full page to see all.

const ctx = canvas.getContext("2d");
const ctx1 = canvas1.getContext("2d");
const SCALE_IMAGE = 0.5;
const PLOT_WIDTH = 500;
const PLOT_HEIGHT = 150;
canvas1.width = PLOT_WIDTH;
canvas1.height = PLOT_HEIGHT;
const line = {x1: 0, y1: 0, x2: 0, y2:0, canUse: false, haveData: false, data: undefined};
var bounds, bounds1, imgData;
// ix iy image coords, px, py plot coords
const mouse  = {ix: 0, iy: 0, overImage: false, px: 0, py:0, overPlot: false, button : false, dragging: 0};
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const img = new Image;
img.crossOrigin = "Anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Black_and_yellow_garden_spider%2C_Washington_DC.jpg/800px-Black_and_yellow_garden_spider%2C_Washington_DC.jpg";
img.addEventListener("load",() => {
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img,0,0);
    imgData = ctx.getImageData(0,0,ctx.canvas.width, ctx.canvas.height);
    canvas.width = img.width * SCALE_IMAGE;
    canvas.height = img.height * SCALE_IMAGE;
    bounds = canvas.getBoundingClientRect();
    bounds1 = canvas1.getBoundingClientRect();
    requestAnimationFrame(update);
},{once: true});

function getProfile(imgData, x1, y1, x2, y2, sampleRate) {
    x1 *= 1 / SCALE_IMAGE;
    y1 *= 1 / SCALE_IMAGE;
    x2 *= 1 / SCALE_IMAGE;
    y2 *= 1 / SCALE_IMAGE;
    const dx = x2 - x1;
    const dy = y2 - y1;
    const samples = (dx * dx + dy * dy) ** 0.5 * Math.abs(sampleRate) + 1 | 0;
    const nx = dx / samples;
    const ny = dy / samples;
    const w = imgData.width;
    const h = imgData.height;
    const pixels = imgData.data;
    const values = [];
    var x = x1 + 0.5;
    var y = y1 + 0.5;
    var i = samples;
    while (i--) {
        if (x >= 0 && x < w - 1 && y >= 0 && y < h - 1) {
            // get 4 closest pixel indexs
            const idxA = ((x | 0) + (y | 0) * w) * 4;
            const idxB = ((x + 1 | 0) + (y | 0) * w) * 4;
            const idxC = ((x + 1 | 0) + (y + 1 | 0) * w) * 4;
            const idxD = ((x | 0) + (y + 1 | 0) * w) * 4;

            // Get channel data using sRGB approximation
            const r1 = pixels[idxA] ** 2.2;
            const r2 = pixels[idxB] ** 2.2;
            const r3 = pixels[idxC] ** 2.2;
            const r4 = pixels[idxD] ** 2.2;

            const g1 = pixels[idxA + 1] ** 2.2;
            const g2 = pixels[idxB + 1] ** 2.2;
            const g3 = pixels[idxC + 1] ** 2.2;
            const g4 = pixels[idxD + 1] ** 2.2;

            const b1 = pixels[idxA + 2] ** 2.2;
            const b2 = pixels[idxB + 2] ** 2.2;
            const b3 = pixels[idxC + 2] ** 2.2;
            const b4 = pixels[idxD + 2] ** 2.2;

            // find value at location via linear interpolation
            const xf = x % 1;
            const yf = y % 1;

            const rr = (r2 - r1) * xf + r1;
            const gg = (g2 - g1) * xf + g1;       
            const bb = (b2 - b1) * xf + b1;       

            /// store channels as uncompressed sRGB
            values.push((((r3 - r4) * xf + r4) - rr) * yf + rr);
            values.push((((g3 - g4) * xf + g4) - gg) * yf + gg);
            values.push((((b3 - b4) * xf + b4) - bb) * yf + bb);  
        } else {
            // outside image
            values.push(0,0,0);
        }
        x += nx;
        y += ny;
    }
    values._nx = nx;
    values._ny = ny;
    values._x = x1;
    values._y = y1;
    return values;
}
function convertToMean(values) {
    var i = 0, max = 0, v;
    const results = [];
    while (i < values.length) {
        results.push(v = (values[i++] * 0.299 + values[i++] * 0.587 + values[i++] * 0.114) ** (1/2.2));
        max = Math.max(v, max);
    }
    results._max = max;
    results._nx = values._nx;
    results._ny = values._ny;
    results._x = values._x;
    results._y = values._y;    
    return results;
}
function plotValues(ctx, values) {
    const count = values.length;
    const scaleX = ctx.canvas.width / count;
    // not using max in example
    // const scaleY = (ctx.canvas.height-3) / values._max;
    const scaleY = (ctx.canvas.height-3) / 255;
    ctx1.clearRect(0,0, ctx.canvas.width, ctx.canvas.height);
    var i = 0;
    ctx.beginPath();
    ctx.strokeStyle = "#000";
    ctx.lineWidth = 2;
    while (i < count) {
        const y = ctx.canvas.height - values[i] * scaleY + 1;
        ctx.lineTo(i++ * scaleX, y);
    }
    ctx.stroke();
    if (!mouse.button && mouse.overPlot) {
        ctx.fillStyle = "#f008";
        ctx.fillRect(mouse.px, 0, 1, ctx.canvas.height);
        const val = values[mouse.px / scaleX | 0];
        info.textContent = "Value: " + (val !== undefined ? val.toFixed(2) : "");
    }
}


function update() {
    ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
    ctx.drawImage(img, 0, 0, img.width * SCALE_IMAGE, img.height * SCALE_IMAGE);
    var atSample = 0;
    if (!mouse.button) {
        if (line.canUse) {
            if (line.haveData && mouse.overPlot) {
                const count = line.data.length;
                const scaleX = ctx1.canvas.width / count            
                atSample = mouse.px / scaleX;
            }
        }
    }
    if (mouse.button) {
       if (mouse.dragging === 1) { // dragging line
          line.x2 = mouse.ix;
          line.y2 = mouse.iy;
          line.canUse = true;
          line.haveData = false;
       } else if(mouse.overImage) {
           mouse.dragging = 1;
           line.x1 = mouse.ix;
           line.y1 = mouse.iy;
           line.canUse = false;
           line.haveData = false;
           canvas.style.cursor = "none";
       }
    } else {
       mouse.dragging = 0;
       canvas.style.cursor = "crosshair";
    }
    if (line.canUse) {
        ctx.strokeStyle = "#F00";
        ctx.strokeWidth = 2;
        ctx.beginPath();
        ctx.lineTo(line.x1, line.y1);
        ctx.lineTo(line.x2, line.y2);
        ctx.stroke();
        if (atSample) {
            ctx.fillStyle = "#FF0";
            ctx.beginPath();
            ctx.arc(
                (line.data._x + line.data._nx * atSample) * SCALE_IMAGE,
                (line.data._y + line.data._ny * atSample) * SCALE_IMAGE,
                line.data[atSample | 0] / 32,
                0, Math.PI * 2
            );
            ctx.fill();
        
        }
        if (!line.haveData) {
            const vals = getProfile(imgData, line.x1, line.y1, line.x2, line.y2, 1);
            line.data = convertToMean(vals);
            line.haveData = true;
            plotValues(ctx1, line.data);
        } else {
            plotValues(ctx1, line.data);
        }
    }
    requestAnimationFrame(update);
}

function mouseEvents(e){
    if (bounds) {
        mouse.ix = e.pageX - bounds.left;
        mouse.iy = e.pageY - bounds.top;
        mouse.overImage = mouse.ix >= 0 && mouse.ix < bounds.width && mouse.iy >= 0 && mouse.iy < bounds.height;
        mouse.px = e.pageX - bounds1.left;
        mouse.py = e.pageY - bounds1.top;
        mouse.overPlot = mouse.px >= 0 && mouse.px < bounds1.width && mouse.py >= 0 && mouse.py < bounds1.height; 
    }
    mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
canvas {
    border: 2px solid black;
}    
<canvas id="canvas"></canvas>
<div id="info">Click drag line over image</div>
<canvas id="canvas1"></canvas>

Image source: https://commons.wikimedia.org/w/index.php?curid=93680693 By BethGuay - Own work, CC BY-SA 4.0,

Related