I have an array colors:
const colors = [
[0, 10, 56],
[40, 233, 247],
[50, 199, 70],
[255, 0, 0],
...
];
and a function reduceColor:
const reduceColor = pix => {
let lowdist = Number.MAX_SAFE_INTEGER;
let closestColor;
for (let color of colors) {
const dist = Math.abs(pix[0] - color[0]) +
Math.abs(pix[1] - color[1]) +
Math.abs(pix[2] - color[2]) ;
if (dist<lowdist) {
lowdist = dist;
closestColor = color;
}
}
return closestColor;
}
console.log(reduceColor([240, 10, 30]));
// [255, 0, 0]
Which works fine, but is slow in the context of a whole image.
Is there a way to, when supplied an array, check another array (made up of subarrays) for the closest subarray, without having to iterate and check over every subarray?
I don't think there is a better solution, you have to check that every row, but you could use Typed Arrays that has usually faster implementations, if you make a good use of their built-in methods. Usually browsers use these typed arrays in audio processing or canvas rendering.
Surely avoid JS Arrays because they are not contiguos in the memory, huge operations on them can be very slow. Typed arrays are contiguos instead.
Also avoid to use Math methods because they are implemented with the abstraction of JS interpreter objects, so Math.abs could be quite slow (it should handle big number, floating points, string, null, NaN, etc.). Numerical operations can be faster in these cases.
For the solution, you could use a Uint32Array (MDN), by reinterpreting the colors as a 32-bit number. In hexadecimal we represent a byte with 2 hex digits, so 32-bit is 8 digits. We will store them in the array as:
-- RR GG BB
0x 00 00 00 00
function makeColor(r, g, b) {
// the 32-bit color will be "0x00RRGGBB" in hex
return (r << 16) + (g << 8) + b;
}
function parseColor(color) {
// isolating the corrispondent bit of the colors
return [ (color & 0xff0000) >> 16, (color & 0xff00) >> 8, color & 0xff ];
}
function makeColorArray(colors) {
// creating the Uint32Array from the colors array
return new Uint32Array(colors.map(c => makeColor(...c)));
}
function distanceRGB(p1, p2) {
let r1 = (p1 & 0xff0000) >> 16, // extract red p1
g1 = (p1 & 0xff00) >> 8, // extract green p1
b1 = p1 & 0xff, // extract blue p1
r2 = (p2 & 0xff0000) >> 16, // extract red p2
g2 = (p2 & 0xff00) >> 8, // extract green p2
b2 = p2 & 0xff, // extract blue p2
rd = r1 > r2 ? (r1 - r2) : (r2 - r1), // compute dist r
gd = g1 > g2 ? (g1 - g2) : (g2 - g1), // compute dist g
bd = b1 > b2 ? (b1 - b2) : (b2 - b1); // compute dist b
return rd + gd + bd; // sum them up
}
// rising the threshold can speed up the process
function findNearest(colors, distance, threshold) {
return function(pixel) {
let bestDist = 765, best = 0, curr; // 765 is the max distance
for (c of colors) {
curr = distance(pixel, c);
if (curr <= threshold) { return c; }
else if (curr < bestDist) {
best = c;
bestDist = curr;
}
}
return best;
};
}
const colors = makeColorArray([
[0, 10, 56],
[40, 233, 247],
[50, 199, 70],
[255, 0, 0],
//... other colors
]);
const image = makeColorArray([
[240, 10, 30]
//... other pixels
])
const nearest = Array.from(image.map(findNearest(colors, distanceRGB, 0))).map(parseColor)
// ===== TEST =====
function randomColor() {
return [ Math.floor(Math.random() * 255), Math.floor(Math.random() * 255), Math.floor(Math.random() * 255) ];
}
testImages = [];
for (let i = 0; i < 1000000; ++i) { testImages.push(randomColor()); }
testImages = makeColorArray(testImages)
testColors = [];
for (let i = 0; i < 1000; ++i) { testColors.push(randomColor()); }
testColors = makeColorArray(testColors);
// START
let testNow = Date.now();
Array.from(testImages.map(findNearest(testColors, distanceRGB, 0))).map(parseColor)
console.log(Date.now() - testNow)
I've tested it over a million random pixels (1000x1000) with 4 colors and takes less then a second (~250-450ms), with a thousand of test colors takes 12-15 seconds. The same experiment with a normal Array took 4-6 seconds just with 4 colors, I did not try the thousand colors test (all on my PC obviously).
Consider to pass heavy work to a Worker (MDN) to avoid UI freezing.
I don't know if it is enough to be runned on a bigger image. I'm sure it can be further optimized (maybe through Gray code hamming distance), but it is a good start point by the way.
You can also be interested to get a way to extract pixels values from images, just check ImageData (MDN) and how to retrive it (Stack Overflow) from a <img> or <canvas> element.
Related
I have single channel pixel data in plain DataView format. So the size of the DataView is width*height. For rendering a bitmap from that data, I need to get 4-channel data if I'm right. My current test/naive implementation is like this.
const data = new Uint8ClampedArray(data);
const expanded = new Uint8ClampedArray(width * height * 4);
data.forEach((v, i) => {
expanded[i * 4] = v;
expanded[i * 4 + 1] = v;
expanded[i * 4 + 2] = v;
expanded[i * 4 + 3] = 255;
});
But that's obviously not performant. On 12 megapixel data, this takes something like 300ms. Is there a more efficient way to do a merge like this?
Or even better as a side question: Can I draw single channel bitmap on img tag or canvas?
You can boost the performance by filling your target array with 32 bit instead of 8 bit unsigned integers. In this case the 32 bit value holds the r, g, b and alpha value which you can send using a single instruction.
To better understand let's look at an example. Say we have the color #4080c4 with full alpha ff. On a little-endian processor architecture this corresponds to:
0xffc48040 == 4291067968
Alpha==0xff==255
Blue==0xc4==196
Green=0x80==128
Red==0x40==64
So the 8 bit unsigned integer values are 255, 64, 128 and 196 respectively. To make a 32 bit unsigned integer out of this 4 individual values we need to use bit-shifting.
If we look back at the hexadecimal number - 0xffc48040 - in a naive way, we can see that the ff alpha value is at the left. That means there are 24 bits before which in turn means that ff has to be shifted 24 bits to the left. If we apply the same logic to the remaining three values we end up with this:
let UI32LE = (Alpha << 24) | (Blue << 16) | (Green << 8) | Red;
The << is JavaScript's bitshift operator.
Please note that the order of the bits is important! As I mentioned, the above applies to little-endian. If it's a big-endian architecture the order is different:
let UI32BE = (Red << 24) | (Green << 16) | (Blue << 8) | Alpha;
Now if we take this technique and use it on something close to your use case we end up with this:
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
let width = canvas.width;
let height = canvas.height;
let dummyData = [];
for (let a = 0; a < width * height; a++) {
dummyData.push(parseInt(Math.random() * 255));
}
const data = new Uint8ClampedArray(dummyData);
let buffer = new ArrayBuffer(width * height * 4);
let dataView = new Uint32Array(buffer);
let expanded = new Uint8ClampedArray(buffer);
data.forEach((v, i) => {
dataView[i] = (255 << 24) | (v << 16) | (v << 8) | v;
});
let imageData = context.getImageData(0, 0, width, height);
imageData.data.set(expanded);
context.putImageData(imageData, 0, 0);
<canvas id="canvas" width="200" height="100"></canvas>
So I have a 2D array created like this:
//Fill screen as blank
for(var x = 0; x<500; x++ ){
screen[x] = [];
for(var y = 0; y<500; y++ ){
screen[x][y] = '#ffffff';
}
}
And was wondering if there's an easy way to convert it to an ImageData object so I can display it on a canvas?
Flattening arrays
The first thing you'll have to learn is how to flatten a 2d array. You can use a nested loop and push to a new 1d array, but I prefer to use reduce and concat:
const concat = (xs, ys) => xs.concat(ys);
console.log(
[[1,2,3],[4,5,6]].reduce(concat)
)
Now you'll notice quickly enough that your matrix will be flipped. ImageData concatenates row by row, but your matrix is grouped by column (i.e. [x][y] instead of [y][x]). My advice is to flip your nested loop around :)
From "#ffffff" to [255, 255, 255, 255]
You now have the tool to create a 1d-array of hex codes (screen.reduce(concat)), but ImageData takes an Uint8ClampedArray of 0-255 values! Let's fix this:
const hexToRGBA = hexStr => [
parseInt(hexStr.substr(1, 2), 16),
parseInt(hexStr.substr(3, 2), 16),
parseInt(hexStr.substr(5, 2), 16),
255
];
console.log(
hexToRGBA("#ffffff")
);
Notice that I skip the first "#" char and hard-code the alpha value to 255.
Converting from hex to RGBA
We'll use map to convert the newly created 1d array at once:
screen.reduce(concat).map(hexToRGBA);
2d again?!
Back to square one... We're again stuck with an array of arrays:
[ [255, 255, 255, 255], [255, 255, 255, 255], /* ... */ ]
But wait... we already know how to fix this:
const flattenedRGBAValues = screen
.reduce(concat) // 1d list of hex codes
.map(hexToRGBA) // 1d list of [R, G, B, A] byte arrays
.reduce(concat); // 1d list of bytes
Putting the data to the canva
This is the part that was linked to in the comments, but I'll include it so you can have a working example!
const hexPixels = [
["#ffffff", "#000000"],
["#000000", "#ffffff"]
];
const concat = (xs, ys) => xs.concat(ys);
const hexToRGBA = hexStr => [
parseInt(hexStr.substr(1, 2), 16),
parseInt(hexStr.substr(3, 2), 16),
parseInt(hexStr.substr(5, 2), 16),
255
];
const flattenedRGBAValues = hexPixels
.reduce(concat) // 1d list of hex codes
.map(hexToRGBA) // 1d list of [R, G, B, A] byte arrays
.reduce(concat); // 1d list of bytes
// Render on screen for demo
const cvs = document.createElement("canvas");
cvs.width = cvs.height = 2;
const ctx = cvs.getContext("2d");
const imgData = new ImageData(Uint8ClampedArray.from(flattenedRGBAValues), 2, 2);
ctx.putImageData(imgData, 0, 0);
document.body.appendChild(cvs);
canvas { width: 128px; height: 128px; image-rendering: pixelated; }
I suspect your example code is just that, an example, but just in case it isn't there are easier way to fill an area with a single color:
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, 500, 500);
But back to flattening the array. If performance is a factor you can do it in for example the following way:
(side note: if possible - store the color information directly in the same type and byte-order you want to use as converting from string to number can be relatively costly when you deal with tens of thousands of pixels - binary/numeric storage is also cheaper).
Simply unwind/flatten the 2D array directly to a typed array:
var width = 500, height = 500;
var data32 = new Uint32Array(width * height); // create Uint32 view + underlying ArrayBuffer
for(var x, y = 0, p = 0; y < height; y++) { // outer loop represents rows
for(x = 0; x < width; x++) { // inner loop represents columns
data32[p++] = str2uint32(array[x][y]); // p = position in the 1D typed array
}
}
We also need to convert the string notation of the color to a number in little-endian order (format used by most consumer CPUs these days). Shift, AND and OR operations are multiple times faster than working on string parts, but if you can avoid strings at all that would be the ideal approach:
// str must be "#RRGGBB" with no alpha.
function str2uint32(str) {
var n = ("0x" + str.substr(1))|0; // to integer number
return 0xff000000 | // alpha (first byte)
(n << 16) | // blue (from last to second byte)
(n & 0xff00) | // green (keep position but mask)
(n >>> 16) // red (from first to last byte)
}
Here we first convert the string to a number - we shift it right away to a Uint32 value to optimize for the compiler now knowing we intend to use the number in the following conversion as a integer number.
Since we're most likely on a little endian plaform we have to shift, mask and OR around bytes to get the resulting number in the correct byte order (i.e. 0xAABBGGRR) and OR in a alpha channel as opaque (on a big-endian platform you would simply left-shift the entire value over 8 bits and OR in an alpha channel at the end).
Then finally create an ImageData object using the underlying ArrayBuffer we just filled and give it a Uint8ClampedArray view which ImageData require (this has almost no overhead since the underlying ArrayBuffer is shared):
var idata = new ImageData(new Uint8ClampedArray(data32.buffer), width, height);
From here you can use context.putImageData(idata, x, y).
Example
Here filling with a orange color to make the conversion visible (if you get a different color than orange then you're on a big-endian platform :) ):
var width = 500, height = 500;
var data32 = new Uint32Array(width * height);
var screen = [];
// Fill with orange color
for(var x = 0; x < width; x++ ){
screen[x] = [];
for(var y = 0; y < height; y++ ){
screen[x][y] = "#ff7700"; // orange to check final byte-order
}
}
// Flatten array
for(var x, y = 0, p = 0; y < height; y++){
for(x = 0; x < width; x++) {
data32[p++] = str2uint32(screen[x][y]);
}
}
function str2uint32(str) {
var n = ("0x" + str.substr(1))|0;
return 0xff000000 | (n << 16) | (n & 0xff00) | (n >>> 16)
}
var idata = new ImageData(new Uint8ClampedArray(data32.buffer), width, height);
c.getContext("2d").putImageData(idata, 0, 0);
<canvas id=c width=500 height=500></canvas>
I wrote js script which performs various operations (eg. summing up photos with a constant, square root, moving, applying filters) on the pictures in the canvas. But for large images (eg. 2000x200 pixels), the script frozen/crashes the browser (tested on Firefox), in addition, everything takes a long time.
function get_pixel (x, y, canvas)
{
var ctx = canvas.getContext("2d");
var imgData = ctx.getImageData(x, y, 1, 1);
return imgData.data;
}
function set_pixel (x, y, canvas, red, green, blue, alpha)
{
var ctx = canvas.getContext('2d');
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height),
pxData = imgData.data,
length = pxData.length;
var i = (x + y * canvas.width) * 4;
pxData[i] = red;
pxData[i + 1] = green;
pxData[i + 2] = blue;
pxData[i + 3] = alpha;
ctx.putImageData (imgData, 0, 0);
}
function sum (number, canvas1, canvas2)
{
show_button_normalization (false);
asyncLoop(
{
length : 5,
functionToLoop : function(loop, i){
setTimeout(function(){
asyncLoop(
{
length : 5,
functionToLoop : function(loop, i){
setTimeout(function(){
var pixel1 = get_pixel (i, j, canvas1);
var pixel2;
if (canvas2 != null)
{
pixel2 = get_pixel (i, j, canvas2);
}
else
{
pixel2 = new Array(4);
pixel2[0] = number;
pixel2[1] = number;
pixel2[2] = number;
pixel2[3] = number;
}
var pixel = new Array(4);
pixel[0] = parseInt (parseInt (pixel1[0]*0.5) + parseInt (pixel2[0]*0.5));
pixel[1] = parseInt (parseInt (pixel1[1]*0.5) + parseInt (pixel2[1]*0.5));
pixel[2] = parseInt (parseInt (pixel1[2]*0.5) + parseInt (pixel2[2]*0.5));
pixel[3] = parseInt (parseInt (pixel1[3]*0.5) + parseInt (pixel2[3]*0.5));
set_pixel (i, j, image1_a, pixel[0], pixel[1], pixel[2], pixel[3]);
loop();
},1000);
},
});
loop();
},1000);
},
});
/*for (var i=0; i<canvas1.width; i++)
{
for (var j=0; j<canvas1.height; j++)
{
var pixel1 = get_pixel (i, j, canvas1);
var pixel2;
if (canvas2 != null)
{
pixel2 = get_pixel (i, j, canvas2);
}
else
{
pixel2 = new Array(4);
pixel2[0] = number;
pixel2[1] = number;
pixel2[2] = number;
pixel2[3] = number;
}
var pixel = new Array(4);
pixel[0] = parseInt (parseInt (pixel1[0]*0.5) + parseInt (pixel2[0]*0.5));
pixel[1] = parseInt (parseInt (pixel1[1]*0.5) + parseInt (pixel2[1]*0.5));
pixel[2] = parseInt (parseInt (pixel1[2]*0.5) + parseInt (pixel2[2]*0.5));
pixel[3] = parseInt (parseInt (pixel1[3]*0.5) + parseInt (pixel2[3]*0.5));
set_pixel (i, j, image1_a, pixel[0], pixel[1], pixel[2], pixel[3]);
}
}*/
}
Is it possible to fix it?
Process pixels together!!
Looking at the code I would say that Firefox crashing and/or taking a long time is not a surprise at all. An image that is 2000 by 2000 pixels has 4 million pixels. I don't know what asyncLoop does but to me it looks like you are using timers to set groups of 5 pixels at a time. This is horrifically inefficient.
Problems with your code
Even looking at the commented code (which I assume is an alternative approch) you are processing the pixels with way to much overhead.
The array pixel you get from the function getPixel which returns the pixel array that is part of the object getImageData returns. If you look at the details of getImageData and te return object imageData you will see that the array is a typed array of type Uint8ClampedArray
That means most of the code you use to mix the pixels is redundant as that is done by javascript automatically when it assigns a number to any typed array.
pixel[0] = parseInt (parseInt (pixel1[0]*0.5) + parseInt (pixel2[0]*0.5));
Will be much quicker if you use
pixel[0] = (pixel1[0] + pixel2[0]) * 0.5; // a * n + b * n is the same as ( a+ b) *n
// with one less multiplication.
Standard simple image processing
But even then using a function call for each pixel adds a massive overhead to the basic operation you are performing. You should fetch all the pixels in one go and process them as two flat arrays.
Your sum function should look more like
function sum (number, canvas1, canvas2){
var i, data, ctx, imgData, imgData1, data1;
ctx = canvas1.getContext("2d");
imgData = ctx.getImageData(0, 0, canvas1.width, canvas1.height);
data = imgData.data; // get the array of pixels
if(canvas2 === null){
i = data.length;
number *= 0.5; // pre calculate number
while(i-- > 0){
data[i] = data[i] * 0.5 + number;
}
}else{
if(canvas1.width !== canvas2.width || canvas1.height !== canvas2.height){
throw new RangeError("Canvas size miss-match, can not process data as requested.");
}
data1 = canvas2.getContext("2d").getImageData(0,0,canvas2.width, canvas2.height).data
i = data.length;
while(i-- > 0){
data[i] = (data[i] + data1[i]) * 0.5;
}
}
ctx.setImageData(imgData,0,0); // put the new pixels back to the canvas
}
Bit math is quicker
You can improve on that if you use a bit of bit manipulation. Using a 32 bit typed array you can divide then add four 8 bit values in parallel (4* approx quicker for pixel calculations).
Note that this method will round down by one value a little more often than it should. ie Math.floor(199 * 233) === 216 is true while the method below will return 215. This can be corrected for by using the bottom bit of both inputs to add to the result. This completely eliminates the rounding error but the processing cost in my view is not worth the improvement. I have included the fix as commented code.
Note this method will only work for a / n + b / m where n and m are equal to 2^p and p is an integer > 0 and < 7 (in other words only if n and m are 2,4,8,16,32,64,127) and you must mask out the bottom p bits for a and b
Example performs C = C * 0.5 + C1 * 0.5 when C and C1 represent each R,G,B,A channel for canvas1 and canvas2
function sum (number, canvas1, canvas2){
var i, data, ctx, imgData, data32, data32A;
// this number is used to remove the bottom bit of each color channel
// The bottom bit is redundant as divide by 2 removes it
const botBitMask = 0b11111110111111101111111011111110;
// mask for rounding error (not used in this example)
// const botBitMaskA = 0b00000001000000010000000100000001;
ctx = canvas1.getContext("2d");
imgData = ctx.getImageData(0, 0, canvas1.width, canvas1.height);
data32 = new Uint32Array(imgData.data.buffer);
i = data32.length; // get the length that is 1/4 the size
if(canvas2 === null){
number >>= 1; // divide by 2
// fill to the 4 channels RGBA
number = (number << 24) + (number << 16) + (number << 8) + number;
// get reference to the 32bit version of the pixel data
while(i-- > 0){
// Remove bottom bit of each channel and then divide each channel by 2 using zero fill right shift (>>>) then add to number
data32[i] = ((data32[i] & botBitMask) >>> 1) + number;
}
}else{
if(canvas1.width !== canvas2.width || canvas1.height !== canvas2.height){
throw new RangeError("Canvas size miss-match, can not process data as requested.");
}
data32A = new Uint32Array(canvas2.getContext("2d").getImageData(0,0,canvas2.width, canvas2.height).data.buffer);
i = data32.length;
while(i-- > 0){
// for fixing rounding error include the following line removing the second one. Do the same for the above loop but optimise for number
// data32[i] = (((data32[i] & botBitMask) >>> 1) + ((data32A[i] & botBitMask) >>> 1)) | ((data32[i] & botBitMaskA) | (data32A[i] & botBitMaskA))
data32[i] = ((data32[i] & botBitMask) >>> 1) + ((data32A[i] & botBitMask) >>> 1);
}
}
ctx.setImageData(imgData,0,0); // put the new pixels back to the canvas
}
With all that you should not have any major problems. Though you will still have the page blocked while the image is being processed (depending on the machine and the image size it may take up to a second or 2)
Other solutions.
If you want to stop the image processing from blocking the page you can use a web worker and just send the data to them to process synchronously. You can find out how to do that but just searching stackOverflow.
Or use WebGL to process the images.
And you have one final option. The canvas api uses the GPU to do all its rendering and if you understand the way blending and compositing works you can do a surprising amount of maths using the canvas.
For example you can multiply all pixels RGBA channels with a value 0-1 using the following.
// multiplies all channels in source canvas by val and returns the resulting canvas
// returns the can2 the result of each pixel
// R *= val;
// G *= val;
// B *= val;
// A *= val;
function multiplyPixels(val, source)
var sctx = source.getContext("2d");
// need two working canvas. I create them here but if you are doing this
// many times you should create them once and reuse them
var can1 = document.createElement("canvas");
var can2 = document.createElement("canvas");
can1.width = can2.width = source.width;
can1.height= can2.height = source.height;
var ctx1 = can1.getContext("2d");
var ctx2 = can2.getContext("2d");
var chanMult = Math.round(255 * val);
// clamp it to 0-255 inclusive
chanMult = chanMult < 0 ? 0 : chanMult > 255 ? 255 : chanMult;
ctx1.drawImage(source,0,0); // copy the source
// multiply all RGB pixels by val
ctx1.fillStyle = "rgba(" + chanMult + "," + chanMult + "," + chanMult + ",1)";
ctx1.globalCompositeOperation = "multiply";
ctx1.fillRect(0, 0, source.width, source.height);
// now multiply the alpha channel by val. Clamp it to 0-1
ctx2.globalAlpha = val < 0 ? 0 : val > 1 ? 1 : val;
ctx2.drawImage(can1,0,0);
return can2;
}
There are quite a few composite operation that you can use in combination to do multiplication, addition, subtraction and division. Note though the accuracy is a little less than 8bits as addition and subtraction requires weighted values to compensate for the blending's (automatic) multiplication. Also the alpha channel must be handled separately from the RGB channels using globalAlpha and the compositing operations.
Realtime
The processing you are doing is very simple and a 2000 by 2000 pixel image can easily be processed in realtime. WebGl filter is an example of using webGL to do image processing. Though the filter system is not modular and the code is very old school it is a good backbone for webGL filters and offers much higher quality results because it uses floating point RGBA values.
Pretty straight-forward, take yellow and white:
back_color = {r:255,g:255,b:255}; //white
text_color = {r:255,g:255,b:0}; //yellow
What law of physics on God's Earth of universal constants, makes the fact that yellow text can't be read on white backgrounds but blue text can?
For the sake of my customizable widget I tried all possible color models that I found conversions functions for; neither can say that green can be on white and yellow can't, based on just numerical comparisons.
I looked at Adsense (which is created by the Budda of all Internet) and guess what they did, they made presets and color cells distance calculations. I can't to do that. My users have the right to pick even the most retina-inflammatory, unaesthetic combinations, as long as the text can still be read.
According to Wikipedia, when converting to grayscale representation of luminance, "one must obtain the values of its red, green, and blue" and mix them in next proportion: R:30% G:59% B:11%
Therefore white will have 100% luminance and yellow will have 89%. At the same time, green has as small as 59%. 11% is almost four times lower than 41% difference!
And even lime (#00ff00) is not good for reading large amounts of texts.
IMHO for good contrast colors' brightness should differ at least for 50%. And this brightness should be measured as converted to grayscale.
upd: Recently found a comprehensive tool for that on the web
which in order uses formula from w3 document
Threshold values could be taken from #1.4
Here is an implementation for this more advanced thing.
function luminance(r, g, b) {
var a = [r, g, b].map(function(v) {
v /= 255;
return v <= 0.03928 ?
v / 12.92 :
Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
function contrast(rgb1, rgb2) {
var lum1 = luminance(rgb1[0], rgb1[1], rgb1[2]);
var lum2 = luminance(rgb2[0], rgb2[1], rgb2[2]);
var brightest = Math.max(lum1, lum2);
var darkest = Math.min(lum1, lum2);
return (brightest + 0.05) /
(darkest + 0.05);
}
console.log(contrast([255, 255, 255], [255, 255, 0])); // 1.074 for yellow
console.log(contrast([255, 255, 255], [0, 0, 255])); // 8.592 for blue
// minimal recommended contrast ratio is 4.5, or 3 for larger font-sizes
For more information, check the WCAG 2.0 documentation on how to compute this value.
There are various ways for calculating contrast, but a common way is this formula:
brightness = (299*R + 587*G + 114*B) / 1000
You do this for both colors, and then you take the difference. This obviously gives a much greater contrast for blue on white than yellow on white.
const getLuminanace = (values) => {
const rgb = values.map((v) => {
const val = v / 255;
return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
});
return Number((0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]).toFixed(3));
};
const getContrastRatio = (colorA, colorB) => {
const lumA = getLuminanace(colorA);
const lumB = getLuminanace(colorB);
return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05);
};
// usage:
const back_color = [255,255,255]; //white
const text_color = [255,255,0]; //yellow
getContrastRatio(back_color, text_color); // 1.0736196319018405
Based on the kirilloid answer:
Angular Service that will calculate contrast and luminescence by passing in the hex value:
.service('ColorContrast', [function() {
var self = this;
/**
* Return iluminance value (base for getting the contrast)
*/
self.calculateIlluminance = function(hexColor) {
return calculateIluminance(hexColor);
};
/**
* Calculate contrast value to white
*/
self.contrastToWhite = function(hexColor){
var whiteIlluminance = 1;
var illuminance = calculateIlluminance(hexColor);
return whiteIlluminance / illuminance;
};
/**
* Bool if there is enough contrast to white
*/
self.isContrastOkToWhite = function(hexColor){
return self.contrastToWhite(hexColor) > 4.5;
};
/**
* Convert HEX color to RGB
*/
var hex2Rgb = function(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
/**
* Calculate iluminance
*/
var calculateIlluminance = function(hexColor) {
var rgbColor = hex2Rgb(hexColor);
var r = rgbColor.r, g = rgbColor.g, b = rgbColor.b;
var a = [r, g, b].map(function(v) {
v /= 255;
return (v <= 0.03928) ?
v / 12.92 :
Math.pow(((v + 0.055) / 1.055), 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
};
}]);
Recently I came across the answer on this page, and I used the code make a script for Adobe Illustrator to calculate the contrast ratio's.
Here you can see the result: http://screencast.com/t/utT481Ut
Some of the shorthand notations of the script above where confusing for me and where not working in Adobe extend script. Therefore I thought would be nice to share my improvement/interpretation of the code that kirilloid shared.
function luminance(r, g, b) {
var colorArray = [r, g, b];
var colorFactor;
var i;
for (i = 0; i < colorArray.length; i++) {
colorFactor = colorArray[i] / 255;
if (colorFactor <= 0.03928) {
colorFactor = colorFactor / 12.92;
} else {
colorFactor = Math.pow(((colorFactor + 0.055) / 1.055), 2.4);
}
colorArray[i] = colorFactor;
}
return (colorArray[0] * 0.2126 + colorArray[1] * 0.7152 + colorArray[2] * 0.0722) + 0.05;
}
And of course you need to call this function
within a for loop I get all the colors from my illustrator object
//just a snippet here to demonstrate the notation
var selection = app.activeDocument.selection;
for (i = 0; i < selection.length; i++) {
red[i] = selection[i].fillColor.red;
//I left out the rest,because it would become to long
}
//this can then be used to calculate the contrast ratio.
var foreGround = luminance(red[0], green[0], blue[0]);
var background = luminance(red[1], green[1], blue[1]);
luminanceValue = foreGround / background;
luminanceValue = round(luminanceValue, 2);
//for rounding the numbers I use this function:
function round(number, decimals) {
return +(Math.round(number + "e+" + decimals) + "e-" + decimals);
}
More information about contrast ratio: http://webaim.org/resources/contrastchecker/
module.exports = function colorcontrast (hex) {
var color = {};
color.contrast = function(rgb) {
// check if we are receiving an element or element background-color
if (rgb instanceof jQuery) {
// get element background-color
rgb = rgb.css('background-color');
} else if (typeof rgb !== 'string') {
return;
}
// Strip everything except the integers eg. "rgb(" and ")" and " "
rgb = rgb.split(/\(([^)]+)\)/)[1].replace(/ /g, '');
// map RGB values to variables
var r = parseInt(rgb.split(',')[0], 10),
g = parseInt(rgb.split(',')[1], 10),
b = parseInt(rgb.split(',')[2], 10),
a;
// if RGBA, map alpha to variable (not currently in use)
if (rgb.split(',')[3] !== null) {
a = parseInt(rgb.split(',')[3], 10);
}
// calculate contrast of color (standard grayscale algorithmic formula)
var contrast = (Math.round(r * 299) + Math.round(g * 587) + Math.round(b * 114)) / 1000;
return (contrast >= 128) ? 'black' : 'white';
};
// Return public interface
return color;
};
For calculate the contrast programmatically, you can use the NPM's color package; if you just want a tool for compare colors then you can use a Contrast Ratio Calculator
I need to compute the difference between two hex color values so the output is a percentage value. The first thing I discarted was converting the hex value into decimal, as the first one will have much higher weight than the last.
The second option is to compute the difference between each of the RGB values and then add them all. However, the difference between 0, 0, 0 and 30, 30, 30 is much lower than the one between 0, 0, 0 and 90, 0, 0.
This question recommends using YUV, but I can't figure out how to use it to establish the difference.
Also, this other question has a nice formula to compute the difference and output a RGB value, but it's not quite there.
For those just looking for a quick copy/paste, here's the code from this repo by antimatter15 (with some tweaks for ease of use):
function deltaE(rgbA, rgbB) {
let labA = rgb2lab(rgbA);
let labB = rgb2lab(rgbB);
let deltaL = labA[0] - labB[0];
let deltaA = labA[1] - labB[1];
let deltaB = labA[2] - labB[2];
let c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]);
let c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
let deltaC = c1 - c2;
let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
let sc = 1.0 + 0.045 * c1;
let sh = 1.0 + 0.015 * c1;
let deltaLKlsl = deltaL / (1.0);
let deltaCkcsc = deltaC / (sc);
let deltaHkhsh = deltaH / (sh);
let i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
return i < 0 ? 0 : Math.sqrt(i);
}
function rgb2lab(rgb){
let r = rgb[0] / 255, g = rgb[1] / 255, b = rgb[2] / 255, x, y, z;
r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
x = (x > 0.008856) ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;
y = (y > 0.008856) ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;
z = (z > 0.008856) ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;
return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)]
}
To use it, just pass in two rgb arrays:
deltaE([128, 0, 255], [128, 0, 255]); // 0
deltaE([128, 0, 255], [128, 0, 230]); // 3.175
deltaE([128, 0, 255], [128, 0, 230]); // 21.434
deltaE([0, 0, 255], [255, 0, 0]); // 61.24
The above table is from here. The above code is based on the 1994 version of DeltaE.
the issue is that you want something like a distance on a 3 dimensionnal world,
but that rgb representation is not intuitive at all : 'near' colors can be
much different that 'far' color.
Take for instance two shades of grey c1 : (120,120,120) and c2 : (150,150,150) and a now take c3 : (160,140,140) it is closer to c2 than c1, yet it is purple, and for the eye the darker grey is much closer to grey than a purple.
I would suggest you to use hsv : a color is defined by a 'base' color (hue), the saturation, and the intensity. colors having close hue are indeed very close. colors having very different hue do not relate one to another (expl : yellow and green ) but might seem closer with a (very) low saturation and (very) low intensity.
( At night all colors are alike. )
Since the hue is divided into 6 blocks, cyl = Math.floor( hue / 6 ) gives you the first step of your similarity evalution : if same part of the cylinder -> quite close.
If they don't belong to same cylinder, they might still be (quite) close if (h2-h1) is small, compare it to (1/6). If (h2-h1) > 1/6 this might just be too different colors.
Then you can be more precise with the (s,v). Colors they are nearer if both low/very low saturation and/or low intensity.
Play around with a color picker supporting both rgb and hsv until you know what you would like to have as a difference value. But be aware that you cannot have a 'true' similarity measure.
you have a rgb --> hsv javascript convertor here : http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
Just compute an Euclidean distance:
var c1 = [0, 0, 0],
c2 = [30, 30, 30],
c3 = [90, 0, 0],
distance = function(v1, v2){
var i,
d = 0;
for (i = 0; i < v1.length; i++) {
d += (v1[i] - v2[i])*(v1[i] - v2[i]);
}
return Math.sqrt(d);
};
console.log( distance(c1, c2), distance(c1, c3), distance(c2, c3) );
//will give you 51.96152422706632 90 73.48469228349535
I released an npm/Bower package for calculating the three CIE algorithms: de76, de94, and de00.
It's public domain and on Github:
http://zschuessler.github.io/DeltaE/
Here's a quickstart guide:
Install via npm
npm install delta-e
Usage
// Include library
var DeltaE = require('delta-e');
// Create two test LAB color objects to compare!
var color1 = {L: 36, A: 60, B: 41};
var color2 = {L: 100, A: 40, B: 90};
// 1976 formula
console.log(DeltaE.getDeltaE76(color1, color2));
// 1994 formula
console.log(DeltaE.getDeltaE94(color1, color2));
// 2000 formula
console.log(DeltaE.getDeltaE00(color1, color2));
You will need to convert to LAB color to use this library. d3.js has an excellent API for doing that - and I'm sure you can find something adhoc as well.
The 3rd rule for color comparisons on ColorWiki is "Never attempt to convert between color differences calculated by different equations through the use of averaging factors". This is because colors that are mathematically close to each other aren't always visually similar to us humans.
What you're looking for is probably delta-e, which is a single number that represents the 'distance' between two colors.
The most popular algorithms are listed below, with CIE76 (aka CIE 1976 or dE76) being the most popular.
CIE76
CMC l:c
dE94
dE2000
Each one goes about things in a different way, but for the most part they all require you to convert to a better (for comparison) color model than RGB.
Wikipedia has all the formulae: http://en.wikipedia.org/wiki/Color_difference
You can check your work with online color calculators:
CIE76
CMC l:c
Finally, it's not javascript but there's an open-source c# library I started will do some of these conversions and calculations: https://github.com/THEjoezack/ColorMine
Using Color.js:
let Color = await import("https://cdn.jsdelivr.net/npm/colorjs.io#0.0.5/dist/color.esm.js").then(m => m.default);
let color1 = new Color(`rgb(10,230,95)`);
let color2 = new Color(`rgb(100,20,130)`);
let colorDistance = color1.deltaE2000(color2);
A distance of 0 means the colors are identical, and a value of 100 means they're opposite.
deltaE2000 is the current industry standard (it's better than the 1994 version mentioned in top answer), but Color.js also has other algorithms like deltaE76, deltaECMC and deltaEITP.