1

I am making an image/video to ASCII converter. For this, I need to get the average darkness for each character I will use. I modified the answer to this question, which gets the average brightness of an image. But it keeps saying that the brightness is 0. What did I do wrong?

//this is the character I will get the darkness of
const char = "#";

const canvas = document.querySelector("canvas"),
  ctx = canvas.getContext("2d")

const charWidth = canvas.width,
  charHeight = canvas.height;

ctx.font = "30px Arial";

//for centering text
ctx.textAlign = "center";
ctx.textBaseline = "middle";

//draws text to canvas
ctx.fillText(char, charWidth / 2, charHeight / 2);

let colorSum = 0;

const imageData = ctx.getImageData(0, 0, charWidth, charHeight);
const data = imageData.data;
let r, g, b, avg;

//loops through image data
for (let x = 0, len = data.length; x < len; x += 4) {
  //r, g, and b are always 0
  r = data[x];
  g = data[x + 1];
  b = data[x + 2];
  
  avg = Math.floor((r + g + b) / 3);
  colorSum += avg;
}

const brightness = Math.floor(colorSum / (charWidth * charHeight));
console.log(brightness);
canvas {
  width: 200px;
  height: 200px;
  outline: 1px solid #000000;
}
<canvas width="30" height="30"></canvas>
luek baja
  • 1,475
  • 8
  • 20

2 Answers2

1

For starters, please don't set CSS properties on canvas nodes -- this stretches and warps the image. Only use the HTML element attributes canvas.width and canvas.height to get and set the width and height of the canvas.

As for the main issue, all of your rgb values are 0 (black) on your canvas by default. If you print data[x+3] you'll see the alpha channel varies from 0-255 but this is never taken into consideration.

Depending on what you'd like to do, simply average the alpha in this channel, or fill the canvas with white (a non-transparent background color) before drawing your text and then use rgb as you're doing.

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

const char = "#";
ctx.font = "30px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(char, canvas.width / 2, canvas.height / 2);

const {data} = ctx.getImageData(
  0, 0, canvas.width, canvas.height
);
let colorSum = 0;

for (let i = 0; i < data.length; i += 4) {
  colorSum += data[i+3];
}

const brightness = Math.floor(
  255 - (colorSum / (canvas.width * canvas.height))
);
console.log(brightness);
canvas {
  outline: 1px solid #000000;
}
<canvas width="30" height="30"></canvas>

You can try this with ctx.fillRect(0, 0, canvas.width / 2, canvas.height); instead of the character and you'll get 127 as the average, which is expected because you've filled half the canvas with black.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
0

Correct brightness

The answer by ggorlen is incorrect

  1. it assumes the background color of white
  2. it uses the incorrect calculation to get the brightness

Unknown background color

It will be difficult to match the canvas background color at each pixel so the best solution is avoid transparent pixels. This can be done by filling the canvas with a non transparent background color before adding the text.

sRGB

To calculate the brightness (lum) is complicated.

  • First convert the sRGB values to a perceptual corrected linear value.
  • Then sum the calculated value for all pixels.
  • Get the mean linear value by dividing by number of pixels.
  • Convert the resulting value back to sRGB

Details can be found on the wiki page

Example

Example show correct brightness calculation for a set of characters.

const BG_COLOR = "white";
const CHAR_COLOR = "black";
const char = "#";
const width = 30;
const height = 32;

const tag = (tag, props = {}) => Object.assign(document.createElement(tag), props);
const append = (el, ...sibs) => sibs.reduce((el, sib)=>(el.appendChild(sib), el), el);

const sC2Lin = (val) => {
    val /= 255;
    return val <= 0.04045 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
}
const RGB2Gama = (r, g, b) => {
    return sC2Lin(r) * 0.212655 +
           sC2Lin(g) * 0.715158 +
           sC2Lin(b) * 0.072187;
}
const brightToHex = b => {
    b = Math.round(b);
    const hex = b.toString(16).padStart(2,"0");
    return "#" + hex + hex + hex;
}

function charBrightness(ctx, char, color = CHAR_COLOR, bgColor = BG_COLOR) {
    ctx.font = "38px Arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    
    ctx.fillStyle = bgColor;
    ctx.fillRect(0, 0, width, height);
    ctx.fillStyle = color;
    ctx.fillText(char, width / 2, height * (1.9 / 3));
    const d = ctx.getImageData(0, 0, width, height).data;
    
    var bSum = 0, i = 0;
    while (i < d.length) {
        bSum += RGB2Gama(d[i++], d[i++], d[i++]);
        i++;
    }
    const meanGam = bSum / (d.length / 4);
      return (meanGam <= 0.0031308 ? 
          meanGam * 12.92 : 
          1.055 * meanGam ** (1 / 2.4) - 0.055) * 255;
}
function testChar(char) {
    const can1 = tag("canvas", {width, height});
    const can2 = tag("canvas", {width, height});
    const ctx1 = can1.getContext("2d");
    const ctx2 = can2.getContext("2d");

    const bright = Math.round(charBrightness(ctx1, char));
    console.log("Brightness: '" + char + "' " + Math.round(bright));
    ctx2.fillStyle = brightToHex(bright);
    ctx2.fillRect(0, 0, width, height);
    append(document.body, can1, can2)
}
testChar("#")
testChar("$")
testChar("H")
testChar("J")
testChar("L")
testChar("I")
testChar("i")
testChar("-")
testChar(".")
canvas {
  border: 1px solid #000000;
  margin-right: 2px;
}

More

There is also an additional problem that will be very hard to solve.

The perceptual brightness of a character is not just the mean of the pixels, there is a geometric component. Our perception reduces the brightness of light lines between dark lines and small light areas in dark areas

Note example the characters pairs #,$ and L,J have the same calculated brightness

Blindman67
  • 51,134
  • 11
  • 73
  • 136