HTML canvas element creating undesired white border - javascript

There's probably a real simple explanation for this, but I'm struggling really hard to find it. I'm trying to implement a crop picture feature and I'm getting an undesired border on the right and bottom sides of the picture, as seen here: http://imgur.com/Xjtk39K. I think I've narrowed it down to something having to do with the first image translation that happens when I crop the picture. Here's the applicable code (the doubling of the cropped image size in submitPhotoUpload is due to some unknown halving of the picture height in my code. Advice for that would be welcome too):
cropPhoto: function(evt){
var formType = App.UploadPhotoView.whichForm;
var canvas = document.getElementById("crop-canvas-preview-"+formType);
var context = canvas.getContext("2d");
if(this.canvasHidden){
canvas.style.display = 'inline';
App.UploadPhotoView.canvasHidden = false;
}
var x1 = App.UploadPhotoView.x1;
var y1 = App.UploadPhotoView.y1;
var width = App.UploadPhotoView.width;
var previewSize = $("#upload-div-"+formType).height() * 0.25;
var resizeX = App.UploadPhotoView.scalingFactorX;
var resizeY = App.UploadPhotoView.scalingFactorY;
var img = document.getElementById("preview-photo-"+formType);
context.clearRect(0, 0, previewSize, previewSize);
context.drawImage(img, x1*resizeX, y1*resizeY, width*resizeX,
width*resizeY, 0, 0, previewSize*2, previewSize);
},
submitPhotoUpload: function(){
var formType = "pic";
if ($('#preview-photo-'+formType).attr('src') != '#') {
var uploadImage = new Image();
uploadImage.src = document.getElementById('crop-canvas-preview-'+formType).toDataURL();
var canvas = document.createElement('canvas');
canvas.width = uploadImage.width * 3;
canvas.height = uploadImage.height * 6;
canvas.getContext('2d').drawImage(uploadImage, 0, 0, canvas.width, canvas.height);
var newPhoto = App.Photo.createRecord({
filename: canvas.toDataURL(),
location: this.get('location'),
description: this.get('description')
});
this.get('store').commit();
resetPhotoUpload(formType);
}else{
this.set('submissionFailed', true);
}
},

The reason is that you are using fractional values with the clearing and clipping.
The clearRect call will clear on sub-pixel basis which means it can leave "half" pixel clipped.
The clip however cannot clip the image with sub-pixels as images are pixel based and will truncate the value to match an integer value, hence you get a small gap in the final result.
Luckily this is easy to get around by simply forcing the values to integer values:
ONLINE DEMO HERE
context.clearRect(0, 0, previewSize|0, previewSize|0);
context.drawImage(img, (x1*resizeX)|0, (y1*resizeY)|0, (width*resizeX)|0,
(width*resizeY)|0, 0, 0, previewSize|0, previewSize|0);
Of course, you might consider doing this outside the function call. If you need a more accurate rounding just add 0.5 to the values before doing as here, shifting 0 bits (which force a float number to integer) or use Math.round() instead.
Here is the result:
left: cleared and clipped using frational positions, right: using integer values

Related

Why is canvas messing with my image's colors?

I'm developing an app that has a painting feature. The user can paint on an image that is initially made of only pure black and pure white pixels. Later, after the user has finished painting, I need to do some processing on that image based on the colors of each pixel.
However, I realized that by the time I processed the image, the pixels weren't purely black/white anymore, but there were lots of greys in between, even if the user didn't paint anything. I wrote some code to check it and found out there were over 250 different colors on the image, while I was expecting only two (black and white). I suspect canvas is messing with my colors somehow, but I can't figure out why.
I hosted a demo on GitHub, showcasing the problem.
The image
This is the image. It is visibly made of only black and white pixels, but if you want to check by yourself you can use this website. It's source code is available on GitHub and I used it as a reference for my own color counting implementation.
My code
Here is the code where I load the image and count the unique colors. You can get the full source here.
class AppComponent {
/* ... */
// Rendering the image
ngAfterViewInit() {
this.context = this.canvas.nativeElement.getContext('2d');
const image = new Image();
image.src = 'assets/image.png';
image.onload = () => {
if (!this.context) return;
this.context.globalCompositeOperation = 'source-over';
this.context.drawImage(image, 0, 0, this.width, this.height);
};
}
// Counting unique colors
calculate() {
const imageData = this.context?.getImageData(0, 0, this.width, this.height);
const data = imageData?.data || [];
const uniqueColors = new Set();
for (let i = 0; i < data?.length; i += 4) {
const [red, green, blue, alpha] = data.slice(i, i + 4);
const color = `rgba(${red}, ${green}, ${blue}, ${alpha})`;
uniqueColors.add(color);
}
this.uniqueColors = String(uniqueColors.size);
}
This is the implementation from the other site:
function countPixels(data) {
const colorCounts = {};
for(let index = 0; index < data.length; index += 4) {
const rgba = `rgba(${data[index]}, ${data[index + 1]}, ${data[index + 2]}, ${(data[index + 3] / 255)})`;
if (rgba in colorCounts) {
colorCounts[rgba] += 1;
} else {
colorCounts[rgba] = 1;
}
}
return colorCounts;
}
As you can see, besides the implementations being similar, they output very different results - my site says I have 256 unique colors, while the other says there's only two. I also tried to just copy and paste the implementation but I got the same 256. That's why I imagine the problem is in my canvas, but I can't figure out what's going on.
You are scaling your image, and since you didn't tell which interpolation algorithm to use, a default smoothing one is being used.
This will make all the pixels that were on fixed boundaries and should now span on multiple pixels to be "mixed" with their white neighbors and produce shades of gray.
There is an imageSmoothingEnabled property that tells the browser to use a closest-neighbor algorithm, which will improve the situation, but even then you may not have a perfect result:
const canvas = document.querySelector("canvas");
const width = canvas.width = innerWidth;
const height = canvas.height = innerHeight;
const ctx = canvas.getContext("2d");
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "https://raw.githubusercontent.com/ajsaraujo/unique-color-count-mre/master/src/assets/image.png";
img.decode().then(() => {
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, 0, 0, width, height);
const data = ctx.getImageData(0, 0, width, height).data;
const pixels = new Set(new Uint32Array(data.buffer));
console.log(pixels.size);
});
<canvas></canvas>
So the best would be to not scale your image, or to do so in a computer friendly fashion (using a factor that is a multiple of 2).

Trying to draw image from pixel array on canvas

I have a pixel-array of 256 values that looks something like this:
[-7.440151952041672e-45, -3.549412689007182e-44, -1.6725845544560632e-43, -7.785321621854041e-43, -3.579496378208359e-42, -1.6256392941914181e-41, ...]
What I am trying to achieve is, to draw this pixel array image onto the canvas. The reason why I need this, is because I will need recalculate the pixel array based on a convolution value to redraw the image with a specific filter over it.
I am currently trying to draw it using this:
function generate_image() {
// create an offscreen canvas
var canvas = document.createElement('canvas');
canvas.setAttribute("id", "canvas_id");
canvas.style.cssText = 'width: 400px; height: 400px; z-index: 1;';
document.body.appendChild(canvas);
var ctx=canvas.getContext("2d");
var images = generate_image_array_data();
// This is the pixel array of the image
var image = images[2];
var palette = ctx.getImageData(0, 0, 160, 120);
palette.data.set(new Uint8ClampedArray(image));
ctx.putImageData(palette, 0, 0);
}
Using the above code nothing happens on the canvas. I am fairly certain I am doing something wrong, but I am not exactly sure what. If anyone would be able to point me in the right direction, that would be greatly appreciated.
The log you shown can not be the one of an Uint8ClampedArray.
Clamped Uint8 values can't be negative, nor floats.
These are thus either 32 bits float values, or 64 bits float ones.
Once you determine which it is, you can convert it to clamped Uint8 values by doing
const data = new Uint8ClampedArray( original_data.buffer );
if original_data is already a TypedArray.
In case it is not, but rather a normal Array, you'd have to first rebuild a TypedArray from the values in that Array and then do the same conversion.
Assuming 64 bits float values:
const data = new Uint8ClampedArray( new Float64Array( images[ 2 ] ).buffer );
const palette = new ImageData( data, 160, 120 );
generate_image();
function generate_image() {
// create an offscreen canvas
var canvas = document.createElement('canvas');
canvas.setAttribute("id", "canvas_id");
canvas.style.cssText = 'width: 400px; height: 400px; z-index: 1;';
document.body.appendChild(canvas);
var ctx=canvas.getContext("2d");
var images = generate_image_array_data();
// This is the pixel array of the image
var image = images[2];
var data = new Uint8ClampedArray( new Float64Array( image ).buffer );
var palette = new ImageData(data, 3,4);
ctx.putImageData(palette, 0, 0);
ctx.imageSmootingEnabled = false;
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, 3, 4, 0, 0, canvas.width, canvas.height);
}
function generate_image_array_data() {
return [, , [-7.440151952041672e-45, -3.549412689007182e-44, -1.6725845544560632e-43, -7.785321621854041e-43, -3.579496378208359e-42, -1.6256392941914181e-41 ] ];
}
PS: Don't call getImageData if you don't need the pixels on the canvas, prefer ctx.createImageData(width, height) or even new ImageData(width, height), which will both avoid your canvas bitmap buffer be (slowly) moved from the GPU to the CPU for nothing

Explorer - canvas image editing issue

I'm currently building a site that comes with a set of icons. They are white .png images with a transparent area. Each image is placed over a div whose background colour changes on mouseover/mouseout events.
I wrote a small JavaScript function that I can use to set the white portion of each .png to whatever the header background colour is so that the images blend in with the header and all you see is the transparent cut-away area change on mouse events - it's quite a nice effect.
So the white portion (not the transparent part) of the image should be changed to whatever the header background colour is, and the function works really great on Firefox, but for some reason doesn't work so well on Explorer (11).
Is there something I'm not seeing here? Here's the code that does the image editing - all I need to do is pass the id of the image and the hexadecimal colour string:
function setImageBg(
imageID,
imageColor
) {
var img = document.getElementById(imageID);
var canvas = document.createElement('canvas');
// Get the red, green and blue values from the hex color
// string.
var redMix = parseInt(imageColor.substr(1, 2), 16);
var greenMix = parseInt(imageColor.substr(2, 2), 16);
var blueMix = parseInt(imageColor.substr(5, 2), 16);
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
// Get the image data...
var pixelData = canvas.getContext('2d').getImageData(0, 0, img.width, img.height);
var imgData = pixelData.data;
var ctx = canvas.getContext('2d');
var imgDataURL;
// Edit the image data.
for (imagePos = 0; imagePos < ((img.width * img.height) * 4); imagePos += 4) {
if (pixelData.data[(imagePos + 3)] <= 99)
continue;
imgData[imagePos] = redMix;
imgData[(imagePos + 1)] = greenMix;
imgData[(imagePos + 2)] = blueMix;
}
ctx.putImageData(pixelData, 0, 0);
imgDataURL = canvas.toDataURL();
document.getElementById(imageID).src = imgDataURL;
}
Like I say, it's great on Firefox - on Explorer all I see is the div, which still dutifully changes colour on mouse events. It's as though the entire image has been made transparent.
Any help would be greatly appreciate, much obliged.
UPDATE:
Ok, so I thought I'd cracked this...here's what happened.
The problem was that the image was loading and being edited in both IE and FF, when I noticed the image had loaded and rendered in IE I thought I'd found and fixed the bug...but after a refresh the image again disappeared.
I scratched my head over it for a bit then decided I'd wasted enough time on it and brushed it aside. Now everything else is in place and working, so I'm back to this and it's a proper head scratcher.
So - IE DOES actually load and edit/render the image, when you initially open the page that is. If you refresh the page, the image disappears...or does it?
I made some adjustments to the code, using alert()'s to try and diagnose the problem, I decided what I do was add some code to check the source file - instead of diving in and changing the pixel colours - I decided to check the image before it is being edited.
Seems that, on the second run of my function - the image loaded is completely black and completely transparent. All pixels have a value of 0!
What does this mean? There's definitely something there, but not what should be.
Is htis a cacheing issue? I messed about with it for a bit, it's odd that IE will load and display the image initially but will neglect to do so after a refresh, but Ff is fine. I tried a few tricks to disable cacheing but nothing works...so now I'm thinking, perhaps it's something else.
Any ideas would be greatly appreciated, I mean it's not a deal-breaker. Everything is fine without this single feature but it's a shame if I can't get it to work.
For anyone interested, here's the updated function after various changes/edits:
function setImageBg(
imageSource,
imageSrc,
imageDst,
imageColor
) {
var img = document.getElementById(imageSrc);
var canvas = document.createElement('canvas');
var redMix = parseInt(imageColor.substr(1, 2), 16);
var greenMix = parseInt(imageColor.substr(3, 2), 16);
var blueMix = parseInt(imageColor.substr(5, 2), 16);
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
var pixelData = canvas.getContext('2d').getImageData(0, 0, img.width, img.height);
var imgData = pixelData.data;
var ctx = canvas.getContext('2d');
var imgDataURL;
// Here's a change I made - on the first call of this function
// when the page loads, all is good - on the second call after a
// refresh this always returns on IE - it's as though the loaded
// image (from file) is completely black and transparent
// (everything is 0)...problem is the image file is never actually
// altered - the loaded image is altered but the actual file never
// changed - very odd...
//
if (pixelData.data[0] == redMix) {
if (pixelData.data[1] == greenMix) {
if (pixelData.data[2] == blueMix) {
document.getElementById(imageDst).src = document.getElementById(imageSrc).src;
return;
}
}
}
for (imagePos = 0; imagePos < ((img.width * img.height) * 4); imagePos += 4) {
if (pixelData.data[(imagePos + 3)] <= 99)
continue;
pixelData.data[imagePos] = redMix;
pixelData.data[(imagePos + 1)] = greenMix;
pixelData.data[(imagePos + 2)] = blueMix;
}
ctx.putImageData(pixelData, 0, 0);
document.getElementById(imageDst).src = canvas.toDataURL();
return true;
}

HTML5 Canvas transparency weirdness with pngs

I'm trying to apply transparency to some ImageData retrieved from a canvas. Simple, right? Just set an appropriate value to every fourth datapoint... only, it only seems to work with pixels that don't have pre-existing alpa values.
//create 2 canvases
var canvas = document.createElement("canvas");
document.body.appendChild(canvas);
var ctx = canvas.getContext("2d");
canvas.id = "tempCanvas";
var canvas2 = document.createElement("canvas");
document.body.appendChild(canvas2);
var ctx2 = canvas2.getContext("2d");
canvas2.id = "tempCanvas2";
//draw the png in the first canvas
var testimg = document.getElementById("testimg");
ctx.drawImage(testimg, 0, 0, 200, 200);
//helper function to opacity to the pixels
opacity = function(pixels, value){
var d = pixels.data;
for(var i=0;i<d.length;i+=4){
d[i+3] = value*255; //scale by 255
if(d[i+3] > 255) d[i+3] = 255; //clamp
}
return pixels;
}
//make the first canvas' image data opaque
var data = ctx.getImageData(0,0,200,200);
data = opacity(data, 0.5);
//draw to second canvas
ctx2.fillStyle="#900";
ctx2.fillRect(0,0,200,200);
//should get a jsfiddle logo overlayed on red background
//instead, get yucky grey background
ctx2.putImageData(data, 0, 0);
(It's a little hard to reproduce because of same-origin policy, but here's a fiddle that uses the png jsfiddle logo: http://jsfiddle.net/97c0eetr/ )
Why does this opacity algorithm not work for the jsfiddle logo (and my other transparent pngs that I'm testing on http://localhost)?
Edit: FF42 Ubuntu, if it makes a difference.
You've hit the big endian / little endian issue : On most of today's computers/devices, the endianness is little endian, meaning that for a 32 bit value, byte order is 3-2-1-0.
So the opacity, which is the 4th byte (index 3) will be found at pixelIndex * 4 + 0 ... == pixelIndex * 4.
Just change, in your code, your opacity function to :
opacity = function(pixels, value){
var d = pixels.data;
for(var i=0;i<d.length;i+=4){
d[i] = value*255; //scale by 255
if(d[i] > 255) d[i] = 255; //clamp
}
return pixels;
}
updated fiddle here :
http://jsfiddle.net/97c0eetr/5/
The best 100% solution of course is to have two opacity function that you choose depending on endinannes of the browser. But by betting on little endian, you cover like 99% of the devices.
If you want to know endianness, i once made a small gist to get it, find it here :
https://gist.github.com/TooTallNate/4750953
Here's the code :
function endianness () {
var b = new ArrayBuffer(4);
var a = new Uint32Array(b);
var c = new Uint8Array(b);
a[0] = 0xdeadbeef;
if (c[0] == 0xef) return 'LE';
if (c[0] == 0xde) return 'BE';
throw new Error('unknown endianness');
}
And by the way, your function could simplify to
opacity = function(pixels, value){
value = Math.floor(value * 255);
if (value>255) value = 255;
var d = pixels.data;
for(var i=0;i<d.length;i+=4){
d[i] = value;
}
return pixels;
}
Edit : So why the background isn't red ? -->> When using putImageData, you are using a raw 1 pixel for 1 pixel copy method. So everything gets replaced, whatever, for instance, the globalCompositeOperation or any setting. So the red background is just overwritten by the new data.
Solution is to putImageData on a temporary canvas -it will be written with the right opacity-, then to drawImage it on your target canvas, so that the opacity is taken into account for the draw.
Since you already had two canvases, change was easy, see updated fiddle here :
http://jsfiddle.net/97c0eetr/4/

Get average color of image via Javascript

Not sure this is possible, but looking to write a script that would return the average hex or rgb value for an image. I know it can be done in AS but looking to do it in JavaScript.
AFAIK, the only way to do this is with <canvas/>...
DEMO V2: http://jsfiddle.net/xLF38/818/
Note, this will only work with images on the same domain and in browsers that support HTML5 canvas:
function getAverageRGB(imgEl) {
var blockSize = 5, // only visit every 5 pixels
defaultRGB = {r:0,g:0,b:0}, // for non-supporting envs
canvas = document.createElement('canvas'),
context = canvas.getContext && canvas.getContext('2d'),
data, width, height,
i = -4,
length,
rgb = {r:0,g:0,b:0},
count = 0;
if (!context) {
return defaultRGB;
}
height = canvas.height = imgEl.naturalHeight || imgEl.offsetHeight || imgEl.height;
width = canvas.width = imgEl.naturalWidth || imgEl.offsetWidth || imgEl.width;
context.drawImage(imgEl, 0, 0);
try {
data = context.getImageData(0, 0, width, height);
} catch(e) {
/* security error, img on diff domain */
return defaultRGB;
}
length = data.data.length;
while ( (i += blockSize * 4) < length ) {
++count;
rgb.r += data.data[i];
rgb.g += data.data[i+1];
rgb.b += data.data[i+2];
}
// ~~ used to floor values
rgb.r = ~~(rgb.r/count);
rgb.g = ~~(rgb.g/count);
rgb.b = ~~(rgb.b/count);
return rgb;
}
For IE, check out excanvas.
Figured I'd post a project I recently came across to get dominant color:
Color Thief
A script for grabbing the dominant color or a representative color palette from an image. Uses javascript and canvas.
The other solutions mentioning and suggesting dominant color never really answer the question in proper context ("in javascript"). Hopefully this project will help those who want to do just that.
"Dominant Color" is tricky. What you want to do is compare the distance between each pixel and every other pixel in color space (Euclidean Distance), and then find the pixel whose color is closest to every other color. That pixel is the dominant color. The average color will usually be mud.
I wish I had MathML in here to show you Euclidean Distance. Google it.
I have accomplished the above execution in RGB color space using PHP/GD here: https://gist.github.com/cf23f8bddb307ad4abd8
This however is very computationally expensive. It will crash your system on large images, and will definitely crash your browser if you try it in the client. I have been working on refactoring my execution to:
- store results in a lookup table for future use in the iteration over each pixel.
- to divide large images into grids of 20px 20px for localized dominance.
- to use the euclidean distance between x1y1 and x1y2 to figure out the distance between x1y1 and x1y3.
Please let me know if you make progress on this front. I would be happy to see it. I will do the same.
Canvas is definitely the best way to do this in the client. SVG is not, SVG is vector based. After I get the execution down, the next thing I want to do is get this running in the canvas (maybe with a webworker for each pixel's overall distance calculation).
Another thing to think about is that RGB is not a good color space for doing this in, because the euclidean distance between colors in RGB space is not very close to the visual distance. A better color space for doing this might be LUV, but I have not found a good library for this, or any algorythims for converting RGB to LUV.
An entirely different approach would be to sort your colors in a rainbow, and build a histogram with tolerance to account for varying shades of a color. I have not tried this, because sorting colors in a rainbow is hard, and so are color histograms. I might try this next. Again, let me know if you make any progress here.
First: it can be done without HTML5 Canvas or SVG.
Actually, someone just managed to generate client-side PNG files using JavaScript, without canvas or SVG, using the data URI scheme.
Edit: You can simply create a PNG with document.getElementById("canvas").toDataURL("image/png", 1.0)
Second: you might actually not need Canvas, SVG or any of the above at all.
If you only need to process images on the client side, without modifying them, all this is not needed.
You can get the source address from the img tag on the page, make an XHR request for it - it will most probably come from the browser cache - and process it as a byte stream from Javascript.
You will need a good understanding of the image format. (The above generator is partially based on libpng sources and might provide a good starting point.)
function get_average_rgb(img) {
var context = document.createElement('canvas').getContext('2d');
if (typeof img == 'string') {
var src = img;
img = new Image;
img.setAttribute('crossOrigin', '');
img.src = src;
}
context.imageSmoothingEnabled = true;
context.drawImage(img, 0, 0, 1, 1);
return context.getImageData(0, 0, 1, 1).data.slice(0,3);
}
console.log(get_average_rgb(document.querySelector('#img1')));
console.log(get_average_rgb(document.querySelector('#img2')));
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAEBgIApD5fRAAAAABJRU5ErkJggg==" width="32" height="32" id="img1">
<img src="https://lh3.googleusercontent.com/a/AEdFTp4Wi1oebZlBCwFID8OZZuG0HLsL-xIxO5m2TNw=k-s32" id="img2" crossOrigin="anonymous">
Less accurate but fastest way to get average color of the image with datauri support:
function get_average_rgb(img) {
var context = document.createElement('canvas').getContext('2d');
if (typeof img == 'string') {
var src = img;
img = new Image;
img.setAttribute('crossOrigin', '');
img.src = src;
}
context.imageSmoothingEnabled = true;
context.drawImage(img, 0, 0, 1, 1);
return context.getImageData(0, 0, 1, 1).data.slice(0,3);
}
I would say via the HTML canvas tag.
You can find here a post by #Georg talking about a small code by the Opera dev :
// Get the CanvasPixelArray from the given coordinates and dimensions.
var imgd = context.getImageData(x, y, width, height);
var pix = imgd.data;
// Loop over each pixel and invert the color.
for (var i = 0, n = pix.length; i < n; i += 4) {
pix[i ] = 255 - pix[i ]; // red
pix[i+1] = 255 - pix[i+1]; // green
pix[i+2] = 255 - pix[i+2]; // blue
// i+3 is alpha (the fourth element)
}
// Draw the ImageData at the given (x,y) coordinates.
context.putImageData(imgd, x, y);
This invert the image by using the R, G and B value of each pixel. You could easily store the RGB values, then round up the Red, Green and Blue arrays, and finally converting them back into an HEX code.
This is #350D's answer but async (as some images may take time to load) and in typescript
async function get_average_rgb(src: string): Promise<Uint8ClampedArray> {
/* https://stackoverflow.com/questions/2541481/get-average-color-of-image-via-javascript */
return new Promise(resolve => {
let context = document.createElement('canvas').getContext('2d');
context!.imageSmoothingEnabled = true;
let img = new Image;
img.src = src;
img.crossOrigin = "";
img.onload = () => {
context!.drawImage(img, 0, 0, 1, 1);
resolve(context!.getImageData(0, 0, 1, 1).data.slice(0,3));
};
});
}
I recently came across a jQuery plugin which does what I originally wanted https://github.com/briangonzalez/jquery.adaptive-backgrounds.js in regards to getting a dominiate color from an image.
EDIT: Only after posting this, did I realize that #350D's answer does the exact same thing.
Surprisingly, this can be done in just 4 lines of code:
const canvas = document.getElementById("canvas"),
preview = document.getElementById("preview"),
ctx = canvas.getContext("2d");
canvas.width = 1;
canvas.height = 1;
preview.width = 400;
preview.height = 400;
function getDominantColor(imageObject) {
//draw the image to one pixel and let the browser find the dominant color
ctx.drawImage(imageObject, 0, 0, 1, 1);
//get pixel color
const i = ctx.getImageData(0, 0, 1, 1).data;
console.log(`rgba(${i[0]},${i[1]},${i[2]},${i[3]})`);
console.log("#" + ((1 << 24) + (i[0] << 16) + (i[1] << 8) + i[2]).toString(16).slice(1));
}
// vvv all of this is to just get the uploaded image vvv
const input = document.getElementById("input");
input.type = "file";
input.accept = "image/*";
input.onchange = event => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = readerEvent => {
const image = new Image();
image.onload = function() {
//shows preview of uploaded image
preview.getContext("2d").drawImage(
image,
0,
0,
preview.width,
preview.height,
);
getDominantColor(image);
};
image.src = readerEvent.target.result;
};
reader.readAsDataURL(file, "UTF-8");
};
canvas {
width: 200px;
height: 200px;
outline: 1px solid #000000;
}
<canvas id="preview"></canvas>
<canvas id="canvas"></canvas>
<input id="input" type="file" />
How it works:
Create the canvas context
const context = document.createElement("canvas").getContext("2d");
This will draw the image to only one canvas pixel, making the browser find the dominant color for you.
context.drawImage(imageObject, 0, 0, 1, 1);
After that, just get the image data for the pixel:
const i = context.getImageData(0, 0, 1, 1).data;
Finally, convert to rgba or HEX:
const rgba = `rgba(${i[0]},${i[1]},${i[2]},${i[3]})`;
const HEX = "#" + ((1 << 24) + (i[0] << 16) + (i[1] << 8) + i[2]).toString(16).slice(1);
There is one problem with this method though, and that is that getImageData will sometimes throw errors Unable to get image data from canvas because the canvas has been tainted by cross-origin data., which is the reason you need to upload images in the demo instead of inputting a URL for example.
This method can also be used for pixelating images by increasing the width and height to draw the image.
This works on chrome but may not on other browsers.
Javascript does not have access to an image's individual pixel color data. At least, not maybe until html5 ... at which point it stands to reason that you'll be able to draw an image to a canvas, and then inspect the canvas (maybe, I've never done it myself).
All-In-One Solution
I would personally combine Color Thief along with this modified version of Name that Color to obtain a more-than-sufficient array of dominant color results for images.
Example:
Consider the following image:
You can use the following code to extract image data relating to the dominant color:
let color_thief = new ColorThief();
let sample_image = new Image();
sample_image.onload = () => {
let result = ntc.name('#' + color_thief.getColor(sample_image).map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join(''));
console.log(result[0]); // #f0c420 : Dominant HEX/RGB value of closest match
console.log(result[1]); // Moon Yellow : Dominant specific color name of closest match
console.log(result[2]); // #ffff00 : Dominant HEX/RGB value of shade of closest match
console.log(result[3]); // Yellow : Dominant color name of shade of closest match
console.log(result[4]); // false : True if exact color match
};
sample_image.crossOrigin = 'anonymous';
sample_image.src = document.getElementById('sample-image').src;
This is about "Color Quantization" that has several approachs like MMCQ (Modified Median Cut Quantization) or OQ (Octree Quantization). Different approach use K-Means to obtain clusters of colors.
I have putted all together here, since I was finding a solution for tvOS where there is a subset of XHTML, that has no <canvas/> element:
Generate the Dominant Colors for an RGB image with XMLHttpRequest
As pointed out in other answers, often what you really want the dominant color as opposed to the average color which tends to be brown. I wrote a script that gets the most common color and posted it on this gist
To get the average (not the dominant) color of an image, create a tiny canvas of the scaled-down original image (of max size i.e: 10 px). Then loop the canvas area imageData to construct the average RGB:
const getAverageColor = (img) => {
const max = 10; // Max size (Higher num = better precision but slower)
const {naturalWidth: iw, naturalHeight: ih} = img;
const ctx = document.createElement`canvas`.getContext`2d`;
const sr = Math.min(max / iw, max / ih); // Scale ratio
const w = Math.ceil(iw * sr); // Width
const h = Math.ceil(ih * sr); // Height
const a = w * h; // Area
img.crossOrigin = 1;
ctx.canvas.width = w;
ctx.canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
const data = ctx.getImageData(0, 0, w, h).data;
let r = g = b = 0;
for (let i=0; i<data.length; i+=4) {
r += data[i];
g += data[i+1];
b += data[i+2];
}
r = ~~(r/a);
g = ~~(g/a);
b = ~~(b/a);
return {r, g, b};
};
const setBgFromAverage = (img) => {
img.addEventListener("load", () => {
const {r,g,b} = getAverageColor(img);
img.style.backgroundColor = `rgb(${r},${g},${b})`;
});
};
document.querySelectorAll('.thumb').forEach(setBgFromAverage);
.thumbs { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.thumb { height: 100px; padding: 20px; }
<div class="thumbs">
<img class="thumb" alt="image" src="https://i.imgur.com/22BrBjx.jpeg">
<img class="thumb" alt="image" src="https://i.imgur.com/MR0dUpw.png">
<img class="thumb" alt="image" src="https://i.imgur.com/o7lpiDR.png">
<img class="thumb" alt="image" src="https://i.imgur.com/egYvHp6.jpeg">
<img class="thumb" alt="image" src="https://i.imgur.com/62EAzOY.jpeg">
<img class="thumb" alt="image" src="https://i.imgur.com/3VxBMeF.jpeg">
</div>
There is a online tool pickimagecolor.com that helps you to find the average or the dominant color of image.You just have to upload a image from your computer and then click on the image. It gives the average color in HEX , RGB and HSV. It also find the color shades matching that color to choose from. I have used it multiple times.

Categories