Image brightness detection in client side script
Asked Answered
S

6

33

Does anyone know if there is a script available to detect darkness/lightness in an image (HTML included) using a client sided script?

I basically want to be able to detect the brightness of image (dark/light) used in the background and have CSS/HTML/jQuery/JS adapt the page based on a variable that is either dark or light (true or false).

I know there are server-side scripts available but cannot use that for this particular development project.

Sinuate answered 7/12, 2012 at 12:11 Comment(2)
The only way to do this is by using a html <canvas>.Bezique
Not a real answer to this question, but still for those whom wanna put some text above an image: just add a black outline to your text!Liggins
C
68

This function will convert each color to gray scale and return average of all pixels, so final value will be between 0 (darkest) and 255 (brightest)

function getImageLightness(imageSrc,callback) {
    var img = document.createElement("img");
    img.src = imageSrc;
    img.style.display = "none";
    document.body.appendChild(img);

    var colorSum = 0;

    img.onload = function() {
        // create canvas
        var canvas = document.createElement("canvas");
        canvas.width = this.width;
        canvas.height = this.height;

        var ctx = canvas.getContext("2d");
        ctx.drawImage(this,0,0);

        var imageData = ctx.getImageData(0,0,canvas.width,canvas.height);
        var data = imageData.data;
        var r,g,b,avg;

        for(var x = 0, len = data.length; x < len; x+=4) {
            r = data[x];
            g = data[x+1];
            b = data[x+2];

            avg = Math.floor((r+g+b)/3);
            colorSum += avg;
        }

        var brightness = Math.floor(colorSum / (this.width*this.height));
        callback(brightness);
    }
}

Usage:

getImageLightness("image.jpg",function(brightness){
    console.log(brightness);
});

JSFiddle:

http://jsfiddle.net/s7Wx2/

Cordwain answered 7/12, 2012 at 12:25 Comment(6)
I can't get this to work with remote images. Any ideas?Balance
@Balance you need to make your code for CORS applicable. Also the remote image should have appropriate header for CORS. Please check this:jsfiddle.net/ray986/rLe0zLr0 I used remote image which has CORS header, and also I have set crossOrigin="anonymous" attribute for the img element which keeps remote image.Fondly
Does not work in Firefox 58 and also not on current Chromium on linux. Nothing happens on lick of the images.Bacardi
Not sure where this is coming from but JSfiddle fails to load mootools even with adblocker disabled on both browsers. Copied it over to a codepen and it works.Bacardi
To fix CORS issues, you have to add the line img.crossOrigin = "anonymous"; before document.body.appendChild(img); (src: developer.mozilla.org/en-US/docs/Web/HTML/Attributes/…)Receiptor
I suggest decoupling the brightness algorithm from fetching the image. These are separate concerns. Ideally, the brightness function would accept an arbitrary, already-loaded canvas or image object as its argument.Giacinta
L
39

My answer reuses most of the code in @lostsource's answer but it uses a different method to attempt to distinguish between dark and light images.

First we need to (briefly) analyze what is the result of the average value of the sum of the RGB channels. For humans, it is meaningless. Is pink brighter than green? I.e., why would you want (0, 255, 0) to give a lower brightness value than (255, 0, 255)? Also, is a mid gray (128, 128, 128) bright just like a mid green (128, 255, 0)? To take this into consideration, I only deal with the color brightness of the channel as is done in the HSV color space. This is simply the maximum value of a given RGB triplet.

The rest is heuristics. Let max_rgb = max(RGB_i) for some point i. If max_rgb is lower than 128 (assuming a 8bpp image), then we found a new point i that is dark, otherwise it is light. Doing this for every point i, we get A points that are light and B points that are dark. If (A - B)/(A + B) >= 0 then we say the image is light. Note that if every point is dark, then you get a value of -1, conversely if every point is light you get +1. The previous formula can be tweaked so you can accept images barely dark. In the code I named the variable as fuzzy, but it does no justice to the fuzzy field in Image Processing. So, we say the image is light if (A - B)/(A + B) + fuzzy >= 0.

The code is at http://jsfiddle.net/s7Wx2/328/, it is very straightforward, don't let my notations scare you.

Lanell answered 7/12, 2012 at 15:56 Comment(4)
This is a real solution that actually thinks about how to replicate human perception - not just a naïve RGB average. +1Utica
Brilliant. Fiddle is a little broken but the function works great.Jarboe
I fixed the fiddle and edited the link, it should work again. jsfiddle.net/s7Wx2/328Shaven
I'd suggest using the alpha to adjust the calculation to handle transparent images.Paeon
M
9

A script called Background Check can detect the darkness/lightness in an image. It uses JavaScript to do so.

Here is a link to it:

http://www.kennethcachia.com/background-check/

I hope that helps anyone wanting to make a slider with this type of detection within it.

Mccowyn answered 1/10, 2013 at 12:15 Comment(1)
Well, this plugin seems not working very well, also make the page slow somehow. I have coded myself using @Cordwain 's idea, and it works very well.Fondly
P
2

MarvinJ provides the method averageColor(image) to get the average color of a given image. Having the average color, you can create rules to define the color of the label over the image.

Loading an image:

var image = new MarvinImage();
image.load("https://i.imgur.com/oOZmCas.jpg", imageLoaded);

Getting the average color:

var averageColor = Marvin.averageColor(image2); // [R,G,B]

The output of the snippet of this post:

enter image description here

var canvas = document.getElementById("canvas");
var image1 = new MarvinImage();
image1.load("https://i.imgur.com/oOZmCas.jpg", imageLoaded);
var image2 = new MarvinImage();
image2.load("https://i.imgur.com/1bZlwv9.jpg", imageLoaded);
var loaded=0;

function imageLoaded(){
  if(++loaded == 2){
    var averageColor;
    averageColor = Marvin.averageColor(image1);
    setText("LION", averageColor, "text1");
    averageColor = Marvin.averageColor(image2);
    setText("LION", averageColor, "text2");
  }
}

function setText(text, averageColor, id){
  if(averageColor[0] <= 80 && averageColor[1] <= 80 && averageColor[2] <= 80){
     document.getElementById(id).innerHTML = "<font color='#ffffff'>"+text+"</font>";
  }
  else if(averageColor[0] >= 150 && averageColor[1] >= 150 && averageColor[2] >= 150){
     document.getElementById(id).innerHTML = "<font color='#000000'>"+text+"</font>";
  }
  
}
.divImage{
  width:400px; 
  height:268px;
  display:grid;
}

.divText{
  font-family:Verdana;
  font-size:56px;
  font-weight:bold;
  margin:auto;
  display:table-cell;
}
<script src="https://www.marvinj.org/releases/marvinj-0.8.js"></script>
<div id="result"></div>
<div style="background-image:url(https://i.imgur.com/oOZmCas.jpg);" class="divImage">
   <div id="text1", class="divText"></div>
</div>
<div style="background-image:url(https://i.imgur.com/1bZlwv9.jpg);" class="divImage">
   <div id="text2", class="divText"></div>
</div>
Propagandism answered 29/3, 2018 at 12:27 Comment(1)
I hope it will work cross-origin, but it does not with CORS enabled. Any way great library.Favour
R
2

In my setup I wanted to check whether an image was transparent, and if it was, whether it was primarily dark or light in the non-transparent pixels. I also wanted to use remote images (URL's) not base64 encoded data images, so I've tweaked @lostsource's excellent answer to include a few extra features:

  • Returns transparency and nonTransparentBrightness (as well as brightness)
  • CORS support with crossOrigin="anonymous" (src)
  • Using 0 - 100 so results are %'s (rather than 0 - 255)
  • Makes sure to use the full image size not the rendered image size (with img.naturalWidth and img.naturalHeight), as the smaller the image the less reliable the results are (potentially due to pixelisation)

In my setup I'm displaying the images on a dark BG, so if nonTransparentBrightness <= 60 && transparency > 0 I add a small amount of padding and a white BG.

This hasn't been thoroughly tested yet so might have some quirks, but seems directionally correct and worked for my use cases so far.

const getImageBrightness = (img) => {
  if (!img) return null;

  let alphaSum = 0;
  let colorSum = 0;

  var canvas = document.createElement("canvas");
  
  // make the canvas use the full size of the image, not the rendered size
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;

  var ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);

  var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  var data = imageData.data;
  var r, g, b, a, avg;

  for (var x = 0, len = data.length; x < len; x += 4) {
    r = data[x];
    g = data[x + 1];
    b = data[x + 2];
    a = data[x + 3];

    avg = Math.floor((r + g + b) / 3);
    colorSum += avg;
    alphaSum += a;
  }

  const transparency = 100 - Math.floor((alphaSum / (img.naturalWidth * img.naturalHeight) / 255) * 100);
  const brightness = Math.floor((colorSum / (img.naturalWidth * img.naturalHeight) / 255) * 100);
  const nonTransparentBrightness = Math.floor((brightness / (100 - transparency)) * 100);

  return { brightness, transparency, nonTransparentBrightness };
};

var imgs = document.body.getElementsByTagName('img');

const handleImage = (imageSrc) => {
  const img = document.createElement("img");
  img.src = imageSrc;
  img.style.display = "none";
  img.crossOrigin = "anonymous";
  document.body.appendChild(img);

  img.onload = () => {
    const { brightness, nonTransparentBrightness, transparency } =
      getImageBrightness(img);

    document.getElementsByTagName('pre')[0].innerHTML = `brightness: ${brightness} | nonTransparentBrightness: ${nonTransparentBrightness} | transparency: ${transparency}`;
  };
}

for(var x = 0; x < imgs.length; x++) {
  imgs[x].onclick = function() {
    const imgEl = this;
    handleImage(imgEl.src);
  }
}
body {
  background-color: #999;
}

img {
  width: 100px;
}
<img src="https://static.files.bbci.co.uk/orbit/8161b75793cc3c38d814e1a4a19a2f6a/img/blq-orbit-blocks_grey.svg" />
<img src="https://static.aviva.io/assets/logo/aviva-logo.svg" />
<img src="https://www.moneyrates.com/wp-content/uploads/imagesrv_wp/2516/barclays_bank_logo_thumbnail.png" />

<pre>Click on image to get values</pre>
Receiptor answered 17/1, 2022 at 16:19 Comment(0)
F
1

In my scenario, I'm working with images with transparent backgrounds, so I need to treat those pixels differently.

See this fiddle for the code. There are two images with transparent backgrounds. The one with a light foreground was previously considered a dark image with the code from @mmgp, but now it's correctly interpreted as light.

My answer is based on the jsfiddle of this answer from mmgp. The difference is pixels that are completely transparent are not counted as light or dark. Also, the dark to light ratio only considers pixels that are one or the other, and not transparent.

// Ignore transparent pixels
if (data[x+3] === 0) {
  continue;
}
// Calculate the dark to light ratio
var dl_diff = ((light - dark) / (light + dark));

Hopefully someone that knows more about the alpha channel can improve it. I feel it's wrong to only consider pixels that are fully transparent (0?) as transparent and to not handle the other values (1 to 255). There's probably some math to be done, like dividing the alpha value by 255 and then...?

Fetation answered 8/12, 2020 at 20:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.