I have 100,000 images which are not under my control. Some of these images are excellent in that the image stretches to the boundaries whilst some have excessive amounts of white space.
When there is excessive white space it makes the page look terrible and means images on the screen all look like they are different sizes.
You can see what I mean here:
http://www.fitness-saver.com/uk/shop/mountain-bikes/
What I have been hunting for is a jQuery method of cropping the images and removing the whitespace automatically.
1) The amount of whitespace is different in every image
2) The ratios of the images are different
3) I want to use javascript rather than pre-processing the images.
I hope you can help!
Edit: Here's an example image - http://images.productserve.com/preview/3395/128554505.jpg. Note the images come from various affiliate sites and are definitely from a different domain.
To analyse the blank spaces in an image, the only way I know is to load that image into a canvas:
var img = new Image(),
$canvas = $("<canvas>"), // create an offscreen canvas
canvas = $canvas[0],
context = canvas.getContext("2d");
img.onload = function () {
context.drawImage(this, 0, 0); // put the image in the canvas
$("body").append($canvas);
removeBlanks(this.width, this.height);
};
// test image
img.src = 'http://images.productserve.com/preview/1302/218680281.jpg';
Next, use the getImageData() method. This method returns an ImageData object that you can use to inspect each pixel data (color).
var removeBlanks = function (imgWidth, imgHeight) {
var imageData = context.getImageData(0, 0, canvas.width, canvas.height),
data = imageData.data,
getRBG = function(x, y) {
return {
red: data[(imgWidth*y + x) * 4],
green: data[(imgWidth*y + x) * 4 + 1],
blue: data[(imgWidth*y + x) * 4 + 2]
};
},
isWhite = function (rgb) {
return rgb.red == 255 && rgb.green == 255 && rgb.blue == 255;
},
scanY = function (fromTop) {
var offset = fromTop ? 1 : -1;
// loop through each row
for(var y = fromTop ? 0 : imgHeight - 1; fromTop ? (y < imgHeight) : (y > -1); y += offset) {
// loop through each column
for(var x = 0; x < imgWidth; x++) {
if (!isWhite(getRBG(x, y))) {
return y;
}
}
}
return null; // all image is white
},
scanX = function (fromLeft) {
var offset = fromLeft? 1 : -1;
// loop through each column
for(var x = fromLeft ? 0 : imgWidth - 1; fromLeft ? (x < imgWidth) : (x > -1); x += offset) {
// loop through each row
for(var y = 0; y < imgHeight; y++) {
if (!isWhite(getRBG(x, y))) {
return x;
}
}
}
return null; // all image is white
};
var cropTop = scanY(true),
cropBottom = scanY(false),
cropLeft = scanX(true),
cropRight = scanX(false);
// cropTop is the last topmost white row. Above this row all is white
// cropBottom is the last bottommost white row. Below this row all is white
// cropLeft is the last leftmost white column.
// cropRight is the last rightmost white column.
};
Frankly I was unable to test this code for a good reason: I came across the infamous "Unable to get image data from canvas because the canvas has been tainted by cross-origin data." security exception.
This is not a bug, it is an intended feature. From the specs:
The toDataURL(), toDataURLHD(), toBlob(), getImageData(), and
getImageDataHD() methods check the flag and will throw a SecurityError
exception rather than leak cross-origin data.
This happens when drawImage() loads files from external domains, which causes the canvas's origin-clean flag to be set to false, preventing further data manipulations.
I am afraid you will run into the same problem, but anyway, here is the code.
Even if this works on client side, I can imagine how miserable will be performance-wise. So, as Jan said, if you can download the images and pre-process them on the server side, that would be better.
Edit: I was curious to see if my code would really crop an image, and indeed it does.
You can check it out here
It only works for images from your domain, as stated before. You can choose your own image with white background and change the last line:
// define here an image from your domain
img.src = 'http://localhost/strawberry2.jpg';
Obviously, you will need to run the code from your domain, not from jsFiddle.
Edit2: If you want to crop and scale up to keep the same aspect ratio, then change this
var $croppedCanvas = $("<canvas>").attr({ width: cropWidth, height: cropHeight });
// finally crop the guy
$croppedCanvas[0].getContext("2d").drawImage(canvas,
cropLeft, cropTop, cropWidth, cropHeight,
0, 0, cropWidth, cropHeight);
to
var $croppedCanvas = $("<canvas>").attr({ width: imgWidth, height: imgHeight });
// finally crop the guy
$croppedCanvas[0].getContext("2d").drawImage(canvas,
cropLeft, cropTop, cropWidth, cropHeight,
0, 0, imgWidth, imgHeight);
Edit3: One fast way to crop images on the browser, is to parallelize the workload through the use of Web Workers, as this excellent article explains.
Based on the great answer that provided by Jose Rui Santos, I've changed his code to work with just image object without jQuery library to be loaded.
The return of this function is cropped image data URL to be used directly in image element.
/*
Source: http://jsfiddle.net/ruisoftware/ddZfV/7/
Updated by: Mohammad M. AlBanna
Website: MBanna.info
Facebook: FB.com/MBanna.info
*/
var myImage = new Image();
myImage.crossOrigin = "Anonymous";
myImage.onload = function(){
var imageData = removeImageBlanks(myImage); //Will return cropped image data
}
myImage.src = "IMAGE SOURCE";
//-----------------------------------------//
function removeImageBlanks(imageObject) {
imgWidth = imageObject.width;
imgHeight = imageObject.height;
var canvas = document.createElement('canvas');
canvas.setAttribute("width", imgWidth);
canvas.setAttribute("height", imgHeight);
var context = canvas.getContext('2d');
context.drawImage(imageObject, 0, 0);
var imageData = context.getImageData(0, 0, imgWidth, imgHeight),
data = imageData.data,
getRBG = function(x, y) {
var offset = imgWidth * y + x;
return {
red: data[offset * 4],
green: data[offset * 4 + 1],
blue: data[offset * 4 + 2],
opacity: data[offset * 4 + 3]
};
},
isWhite = function (rgb) {
// many images contain noise, as the white is not a pure #fff white
return rgb.red > 200 && rgb.green > 200 && rgb.blue > 200;
},
scanY = function (fromTop) {
var offset = fromTop ? 1 : -1;
// loop through each row
for(var y = fromTop ? 0 : imgHeight - 1; fromTop ? (y < imgHeight) : (y > -1); y += offset) {
// loop through each column
for(var x = 0; x < imgWidth; x++) {
var rgb = getRBG(x, y);
if (!isWhite(rgb)) {
if (fromTop) {
return y;
} else {
return Math.min(y + 1, imgHeight);
}
}
}
}
return null; // all image is white
},
scanX = function (fromLeft) {
var offset = fromLeft? 1 : -1;
// loop through each column
for(var x = fromLeft ? 0 : imgWidth - 1; fromLeft ? (x < imgWidth) : (x > -1); x += offset) {
// loop through each row
for(var y = 0; y < imgHeight; y++) {
var rgb = getRBG(x, y);
if (!isWhite(rgb)) {
if (fromLeft) {
return x;
} else {
return Math.min(x + 1, imgWidth);
}
}
}
}
return null; // all image is white
};
var cropTop = scanY(true),
cropBottom = scanY(false),
cropLeft = scanX(true),
cropRight = scanX(false),
cropWidth = cropRight - cropLeft,
cropHeight = cropBottom - cropTop;
canvas.setAttribute("width", cropWidth);
canvas.setAttribute("height", cropHeight);
// finally crop the guy
canvas.getContext("2d").drawImage(imageObject,
cropLeft, cropTop, cropWidth, cropHeight,
0, 0, cropWidth, cropHeight);
return canvas.toDataURL();
}
Related
Before you go off saying I'm crazy, believe me, I know. I'm not going for a website that renders fast, loads fast, or gets a high lighthouse score. I just want it to work.
I have some javascript that picks up all the pixel colors of an image. With this function, I create a div element that is 1px by 1px and set the background color to the pixel color of those same coordinates. Then the coordinates are used to set the top and left values. My code does what it's told.
Here's my problem, my image is 700px by 387px. If you do the math, that works out to 270,900 html elements. Chrome, simply isn't built for this madness. I want to see this work, I want to "manually" create an image with div elements, somehow. My cpu maxes out when I try to do so, and I'm sure I'd run out of ram eventually.
Everything works fine if I only try hundreds or a few thousand pixels, but any more, and chrome dies. I'm not sure if it's calculating in the browser that may be my problem, or if chrome cant display this many elements, or both. I suppose I could do the same math on my server with python, and append it to html, but then chrome probably couldn't display it.
Obviously, this isn't super important, just fun. I think the community will enjoy the challenge as well.
Here's calculating 100 pixels:
onload = e => {
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(r, g, b) {
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
function img(x, y) {
var img = document.getElementById('my-image');
var canvas = document.createElement('canvas');
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(x, y, 1, 1).data;
return rgbToHex(pixelData[0], pixelData[1], pixelData[2]);
}
//x = 700 y = 387
for (var x = 0; x < 10; x++) {
for (var y = 0; y < 10; y++) {
document.body.insertAdjacentHTML("beforeend", "<div style='top:" + y + "px; left:" + x + "px;background:" + img(x, y) + ";' />");
}
}
};
div {
position: absolute;
width: 1px;
height: 1px;
}
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/New_born_Frisian_red_white_calf.jpg/640px-New_born_Frisian_red_white_calf.jpg" id="my-image" crossorigin="anonymous">
here's calculating 2500px (still works, takes a while)
onload = e => {
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(r, g, b) {
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
function img(x, y) {
var img = document.getElementById('my-image');
var canvas = document.createElement('canvas');
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(x, y, 1, 1).data;
return rgbToHex(pixelData[0], pixelData[1], pixelData[2]);
}
//x = 700 y = 387
for (var x = 0; x < 50; x++) {
for (var y = 0; y < 50; y++) {
document.body.insertAdjacentHTML("beforeend", "<div style='top:" + y + "px; left:" + x + "px;background:" + img(x, y) + ";' />");
}
}
};
div {
position: absolute;
width: 1px;
height: 1px;
}
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/New_born_Frisian_red_white_calf.jpg/640px-New_born_Frisian_red_white_calf.jpg" id="my-image" crossorigin="anonymous">
Cheers, Isaac.
At the moment, you are doing the following for every pixel
Create canvas
get the context and draw to the image
get the context and get the pixel data for one pixel
create a DIV
add it to the DOM
Now, lets streamline this
The following can be done ONCE
create the canvas
get the context
use the context to draw the image
use the context to get the image data
create an empty string
Now, for each pixel
get the pixel data
add the html for the div to the string
And finally, just ONCE
Add the string with all the divs to the DOM
Something like:
const componentToHex = c => c.toString(16).padStart(2, '0');
const rgbToHex = (r, g, b) => `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`;
const img = document.getElementById('my-image');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const w = canvas.width = img.width;
const h = canvas.height = img.height;
context.drawImage(img, 0, 0, img.width, img.height);
const imageData = context.getImageData(0, 0, w, h).data;
const pixel = (x, y) => {
const index = (w * y + x) * 4;
return rgbToHex(imageData[index], imageData[index + 1], imageData[index + 2]);
}
const df = document.createDocumentFragment();
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const div = df.appendChild(document.createElement('div'));
div.style.top = y + "px";
div.style.left = x + "px";
div.style.backgroundColor = pixel(x, y);
}
}
document.body.appendChild(df);
Note: it may not be the case now, but such a loop may work faster inside a function - often loops in the global context are slower
So, you could wrap the whole code above in
(() => {
// the code from above
})();
And see significant improvement again - not sure, used to be the case in years gone by
changed to use document fragment for a further 25% speed improvement
Now takes 1.4 seconds in firefox for a 640x480 image, 2.3 seconds in chrome - which didn't really see a big difference between using insertAdjacentHTML vs a document fragment
another thing to note. In Firefox the page becomes sluggish, in chrome, for 640x480, no such issue
So I am fooling around with pixel manipulation in canvas. Right now I have code that allows you to draw to canvas. Then, when you have something drawn, there is a button you can press to manipulate the pixels, translating them either one tile to the right or one tile to the left, alternating every other row. The code looks something like this:
First, pushing the button will start a function that creates two empty arrays where the pixel data is going to go. Then it goes through the pixels, row by row, making each row it's own array. All the row arrays are added into one array of all the pixels data.
$('#shift').click(function() {
var pixels = [];
var rowArray = [];
// get a list of all pixels in a row and add them to pixels array
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
var src = ctx.getImageData(x, y, 1, 1)
var copy = ctx.createImageData(src.width, src.height);
copy.data.set(src.data);
pixels.push(copy);
};
rowArray.push(pixels);
pixels = [];
};
Continuing in the function, next it clears the canvas and shifts the arrays every other either going one to the right or one to the left.
// clear canvas and points list
clearCanvas(ctx);
// take copied pixel lists, shift them
for (i = 0; i < rowArray.length; i ++) {
if (i % 2 == 0) {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, 1));
} else {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, rowArray[i].length - 1));
};
};
Last part of the function now takes the shifted lists of pixel data and distributes them back onto the canvas.
// take the new shifted pixel lists and distribute
// them back onto the canvas
var listCounter = 0;
var listCounter2 = 0;
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
ctx.putImageData(rowArray[listCounter][listCounter2], x, y);
listCounter2 ++;
}
listCounter2 = 0;
listCounter ++;
}
});
As of right now, it works fine. No data is lost and pixels are shifted correctly. What I am wondering if possible, is there a way to do this that is more efficient? Right now, doing this pixel by pixel takes a long time so I have to go by 20x20 px tiles or higher to have realistic load times. This is my first attempt at pixel manipulation so there is probably quite a few things I'm unaware of. It could be my laptop is not powerful enough. Also, I've noticed that sometimes running this function multiple times in a row will significantly reduce load times. Any help or suggestions are much appreciated!
Full function :
$('#shift').click(function() {
var pixels = [];
var rowArray = [];
// get a list of all pixels in a row and add them to pixels array
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
var src = ctx.getImageData(x, y, 1, 1)
var copy = ctx.createImageData(src.width, src.height);
copy.data.set(src.data);
pixels.push(copy);
};
rowArray.push(pixel);
pixels = [];
};
// clear canvas and points list
clearCanvas(ctx);
// take copied pixel lists, shift them
for (i = 0; i < pixelsListList.length; i ++) {
if (i % 2 == 0) {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, 1));
} else {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, rowArray[i].length - 1));
};
};
// take the new shifted pixel lists and distribute
// them back onto the canvas
var listCounter = 0;
var listCounter2 = 0;
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
ctx.putImageData(rowArray[listCounter][listCounter2], x, y);
listCounter2 ++;
}
listCounter2 = 0;
listCounter ++;
}
});
Performance pixel manipulation.
The given answer is so bad that I have to post a better solution.
And with that a bit of advice when it comes to performance critical code. Functional programming has no place in code that requires the best performance possible.
The most basic pixel manipulation.
The example does the same as the other answer. It uses a callback to select the processing and provides a set of functions to create, filter, and set the pixel data.
Because images can be very large 2Megp plus the filter is timed to check performance. The number of pixels, time taken in µs (1/1,000,000th second), pixels per µs and pixels per second. For realtime processing of a HD 1920*1080 you need a rate of ~125,000,000 pixels per second (60fps).
NOTE babel has been turned off to ensure code is run as is. Sorry IE11 users time to upgrade don`t you think?
canvas.addEventListener('click', ()=>{
var time = performance.now();
ctx.putImageData(processPixels(randomPixels,invertPixels), 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
info.textContent = "Time to process " + pixelCount.toLocaleString() + " pixels : " + (time | 0).toLocaleString() + "µs, "+ (rate|0) + "pix per µs "+pps+" pixel per second";
});
const ctx = canvas.getContext("2d");
const pixelCount = innerWidth * innerHeight;
canvas.width = innerWidth;
canvas.height = innerHeight;
const randomPixels = putPixels(ctx,createImageData(canvas.width, canvas.height, randomRGB));
function createImageData(width, height, filter){
return processPixels(ctx.createImageData(width, height), filter);;
}
function processPixels(pixelData, filter = doNothing){
return filter(pixelData);
}
function putPixels(context,pixelData,x = 0,y = 0){
context.putImageData(pixelData,x,y);
return pixelData;
}
// Filters must return pixeldata
function doNothing(pd){ return pd }
function randomRGB(pixelData) {
var i = 0;
var dat32 = new Uint32Array(pixelData.data.buffer);
while (i < dat32.length) { dat32[i++] = 0xff000000 + Math.random() * 0xFFFFFF }
return pixelData;
}
function invertPixels(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
i ++; // skip alpha
}
return pixelData;
}
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
background : rgba(255,255,255,0.75);
}
.m {
top : 100px;
z-index : 10;
}
#info {
z-index : 10;
}
<div class="abs" id="info"></div>
<div class="abs m">Click to invert</div>
<canvas class="abs" id="canvas"></canvas>
Why functional programming is bad for pixel processing.
To compare below is a timed version of George Campbell Answer that uses functional programming paradigms. The rate will depend on the device and browser but is 2 orders of magnitude slower.
Also if you click, repeating the invert function many times you will notice the GC lags that make functional programming such a bad choice for performance code.
The standard method (first snippet) does not suffer from GC lag because it barely uses any memory apart from the original pixel buffer.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
//maybe put inside resize event listener
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const pixelCount = innerWidth * innerHeight;
//create some test pixels (random colours) - only once for entire width/height, not for each pixel
let randomPixels = createImageData(width, height, randomRGB);
//create image data and apply callback for each pixel, set this in the ImageData
function createImageData(width, height, cb){
let createdPixels = ctx.createImageData(width, height);
if(cb){
let pixelData = editImageData(createdPixels, cb);
createdPixels.data.set(pixelData);
}
return createdPixels;
}
//edit each pixel in ImageData using callback
//pixels ImageData, cb Function (for each pixel, returns r,g,b,a Boolean)
function editImageData(pixels, cb = (p)=>p){
return Array.from(pixels.data).map((pixel, i) => {
//red or green or blue or alpha
let newValue = cb({r: i%4 === 0, g:i%4 === 1, b:i%4 === 2, a:i%4 === 3, value: pixel});
if(typeof newValue === 'undefined' || newValue === null){
throw new Error("undefined/null pixel value "+typeof newValue+" "+newValue);
}
return newValue;
});
}
//callback to apply to each pixel (randomize)
function randomRGB({a}){
if(a){
return 255; //full opacity
}
return Math.floor(Math.random()*256);
};
//another callback to apply, this time invert
function invertRGB({a, value}){
if(a){
return 255; //full opacity
}
return 255-value;
};
ctx.putImageData(randomPixels, 0, 0);
//click to change invert image data (or any custom pixel manipulation)
canvas.addEventListener('click', ()=>{
var time = performance.now();
randomPixels.data.set(editImageData(randomPixels, invertRGB));
ctx.putImageData(randomPixels, 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
if(rate < 1){
rate = "less than 1";
}
info.textContent = "Time to process " + pixelCount.toLocaleString() + " pixels : " + (time|0).toLocaleString() + "µs, "+ rate + "pix per µs "+pps+" pixel per second";
});
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
background : rgba(255,255,255,0.75);
}
.m {
top : 100px;
z-index : 10;
}
#info {
z-index : 10;
}
<div class="abs" id="info"></div>
<div class="abs m">George Campbell Answer. Click to invert</div>
<canvas class="abs" id="canvas"></canvas>
Some more pixel processing
The next sample demonstrates some basic pixel manipulation.
Random. Totaly random pixels
Invert. Inverts the pixel colors
B/W. Converts to simple black and white (not perceptual B/W)
Noise. Adds strong noise to pixels. Will reduce total brightness.
2 Bit. Pixel channel data is reduced to 2 bits per RGB.
Blur. Most basic blur function requires a copy of the pixel data to work and is thus expensive in terms of memory and processing overheads. But as NONE of the canvas/SVG filters do the correct logarithmic filter this is the only way to get a good quality blur for the 2D canvas. Unfortunately it is rather slow.
Channel Shift. Moves channels blue to red, red to green, green to blue
Shuffle pixels. Randomly shuffles pixels with one of its neighbours.
For larger images. To prevent filters from blocking the page you would move the imageData to a worker and process the pixels there.
document.body.addEventListener('click', (e)=>{
if(e.target.type !== "button" || e.target.dataset.filter === "test"){
testPattern();
pixels = getImageData(ctx);
info.textContent = "Untimed content render."
return;
}
var time = performance.now();
ctx.putImageData(processPixels(pixels,pixelFilters[e.target.dataset.filter]), 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
info.textContent = "Filter "+e.target.value+ " " +(e.target.dataset.note ? e.target.dataset.note : "") + pixelCount.toLocaleString() + "px : " + (time | 0).toLocaleString() + "µs, "+ (rate|0) + "px per µs "+pps+" pps";
});
const ctx = canvas.getContext("2d");
const pixelCount = innerWidth * innerHeight;
canvas.width = innerWidth;
canvas.height = innerHeight;
var min = Math.min(innerWidth,innerHeight) * 0.45;
function testPattern(){
var grad = ctx.createLinearGradient(0,0,0,canvas.height);
grad.addColorStop(0,"#000");
grad.addColorStop(0.5,"#FFF");
grad.addColorStop(1,"#000");
ctx.fillStyle = grad;
ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height);
"000,AAA,FFF,F00,00F,A00,00A,FF0,0FF,AA0,0AA,0F0,F0F,0A0,A0A".split(",").forEach((col,i) => {
circle("#"+col, min * (1-i/16));
});
}
function circle(col,size){
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(canvas.width / 2, canvas.height / 2, size, 0 , Math.PI * 2);
ctx.fill();
}
testPattern();
var pixels = getImageData(ctx);
function getImageData(ctx, x = 0, y = 0,width = ctx.canvas.width, height = ctx.canvas.height){
return ctx.getImageData(x,y,width, height);
}
function createImageData(width, height, filter){
return processPixels(ctx.createImageData(width, height), filter);;
}
function processPixels(pixelData, filter = doNothing){
return filter(pixelData);
}
function putPixels(context,pixelData,x = 0,y = 0){
context.putImageData(pixelData,x,y);
return pixelData;
}
// Filters must return pixeldata
function doNothing(pd){ return pd }
function randomRGB(pixelData) {
var i = 0;
var dat32 = new Uint32Array(pixelData.data.buffer);
while (i < dat32.length) { dat32[i++] = 0xff000000 + Math.random() * 0xFFFFFF }
return pixelData;
}
function randomNoise(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = Math.random() * dat[i++];
dat[i] = Math.random() * dat[i++];
dat[i] = Math.random() * dat[i++];
i ++; // skip alpha
}
return pixelData;
}
function twoBit(pixelData) {
var i = 0;
var dat = pixelData.data;
var scale = 255 / 196;
while (i < dat.length) {
dat[i] = (dat[i++] & 196) * scale;
dat[i] = (dat[i++] & 196) * scale;
dat[i] = (dat[i++] & 196) * scale;
i ++; // skip alpha
}
return pixelData;
}
function invertPixels(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
i ++; // skip alpha
}
return pixelData;
}
function simpleBW(pixelData) {
var bw,i = 0;
var dat = pixelData.data;
while (i < dat.length) {
bw = (dat[i] + dat[i+1] + dat[i+2]) / 3;
dat[i++] = bw;
dat[i++] = bw;
dat[i++] = bw;
i ++; // skip alpha
}
return pixelData;
}
function simpleBlur(pixelData) {
var i = 0;
var dat = pixelData.data;
var buf = new Uint8Array(dat.length);
buf.set(dat);
var w = pixelData.width * 4;
i += w;
while (i < dat.length - w) {
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
i ++; // skip alpha
}
return pixelData;
}
function channelShift(pixelData) {
var r,g,i = 0;
var dat = pixelData.data;
while (i < dat.length) {
r = dat[i];
g = dat[i+1];
dat[i] = dat[i+2];
dat[i+1] = r;
dat[i+2] = g;
i += 4;
}
return pixelData;
}
function pixelShuffle(pixelData) {
var r,g,b,n,rr,gg,bb,i = 0;
var dat = pixelData.data;
var next = [-pixelData.width*4,pixelData.width*4,-4,4];
var len = dat.length;
while (i < dat.length) {
n = (i + next[Math.random() * 4 | 0]) % len;
r = dat[i];
g = dat[i+1];
b = dat[i+2];
dat[i] = dat[n];
dat[i+1] = dat[n + 1];
dat[i+2] = dat[n + 2];
dat[n] = r;
dat[n+1] = g;
dat[n+2] = b;
i += 4;
}
return pixelData;
}
const pixelFilters = {
randomRGB,
invertPixels,
simpleBW,
randomNoise,
twoBit,
simpleBlur,
channelShift,
pixelShuffle,
}
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
}
.m {
top : 30px;
z-index : 20;
}
#info {
z-index : 10;
background : rgba(255,255,255,0.75);
}
<canvas class="abs" id="canvas"></canvas>
<div class="abs" id="buttons">
<input type ="button" data-filter = "randomRGB" value ="Random"/>
<input type ="button" data-filter = "invertPixels" value ="Invert"/>
<input type ="button" data-filter = "simpleBW" value ="B/W"/>
<input type ="button" data-filter = "randomNoise" value ="Noise"/>
<input type ="button" data-filter = "twoBit" value ="2 Bit" title = "pixel channel data is reduced to 2 bits per RGB"/>
<input type ="button" data-note="High quality blur using logarithmic channel values. " data-filter = "simpleBlur" value ="Blur" title = "Blur requires a copy of pixel data"/>
<input type ="button" data-filter = "channelShift" value ="Ch Shift" title = "Moves channels blue to red, red to green, green to blue"/>
<input type ="button" data-filter = "pixelShuffle" value ="Shuffle" title = "randomly shuffles pixels with one of its neighbours"/>
<input type ="button" data-filter = "test" value ="Test Pattern"/>
</div>
<div class="abs m" id="info"></div>
It makes more sense to use something like ctx.getImageData or .createImageData only once per image, not for each pixel.
You can loop the ImageData.data "array-like" Uint8ClampedArray. Each 4 items in the array represent a single pixel, these being red, green, blue, and alpha parts of the pixel. Each can be an integer between 0 and 255, where [0,0,0,0,255,255,255,255,...] means the first pixel is transparent (and black?), and the second pixel is white and full opacity.
here is something I just made, not benchmarked but likely more efficient.
It creates image data, and you can edit image data by passing in a function to the edit image data function, the callback function is called for each pixel in an image data and returns an object containing value (between 0 and 255), and booleans for r, g, b.
For example for invert you can return 255-value.
this example starts with random pixels, clicking them will apply the invertRGB function to it.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
//maybe put inside resize event listener
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
//create some test pixels (random colours) - only once for entire width/height, not for each pixel
let randomPixels = createImageData(width, height, randomRGB);
//create image data and apply callback for each pixel, set this in the ImageData
function createImageData(width, height, cb){
let createdPixels = ctx.createImageData(width, height);
if(cb){
let pixelData = editImageData(createdPixels, cb);
createdPixels.data.set(pixelData);
}
return createdPixels;
}
//edit each pixel in ImageData using callback
//pixels ImageData, cb Function (for each pixel, returns r,g,b,a Boolean)
function editImageData(pixels, cb = (p)=>p){
let i = 0;
let len = pixels.data.length;
let outputPixels = [];
for(i=0;i<len;i++){
let pixel = pixels.data[i];
outputPixels.push( cb(i%4, pixel) );
}
return outputPixels;
}
//callback to apply to each pixel (randomize)
function randomRGB(colour){
if( colour === 3){
return 255; //full opacity
}
return Math.floor(Math.random()*256);
};
//another callback to apply, this time invert
function invertRGB(colour, value){
if(colour === 3){
return 255; //full opacity
}
return 255-value;
};
ctx.putImageData(randomPixels, 0, 0);
//click to change invert image data (or any custom pixel manipulation)
canvas.addEventListener('click', ()=>{
let t0 = performance.now();
randomPixels.data.set(editImageData(randomPixels, invertRGB));
ctx.putImageData(randomPixels, 0, 0);
let t1 = performance.now();
console.log(t1-t0+"ms");
});
#canvas {
position: absolute;
top: 0;
left: 0;
}
<canvas id="canvas"></canvas>
code gist: https://gist.github.com/GCDeveloper/c02ffff1d067d6f1b1b13341a72efe79
check out https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas which should help, including loading an actual image as ImageData for usage.
right now I am writing a small chrome extension. the extension grabs the Bing Image of the Day and sets it as the body background image, and then the extension also grabs the weather, time, date, etc. The problem is that sometimes the image of the day is for example, bright on one side and dark on the other. So, no matter what color the text is, some of it is illegible. How can I determine what color each text should be depending on the background image color behind a SPECIFIC text?
Thanks in advance.
You can use jQuery's attr() function. For example, if you img tag has an id attribute of 'my_image':
<img id="my_image" src="first.jpg"/>
Jquery
$("#my_image").attr("src","second.jpg");
I got something to work.... It is kind of makeshift and not foolproof, but I found a script that calculates overall brightness, which then allows the script to decide whether it should display black or white text. Seems to work well, as of now. Here it is:
function getImageBrightness(imageSrc) {
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;
}
brightness = Math.floor(colorSum / (this.width * this.height));
console.log(brightness);
if (brightness < 150) {
$(".contrast").css("color", "white");
} else {
$(".contrast").css("color", "black");
}
}
}
credit to: https://jsfiddle.net/s7Wx2/
I have a problem on my project.
I am developing a perspective mockup creating module for designers. Users upload images and i get them for placing in mockups with making some perspective calculations. Then users can download this image. I made all of this on clientside with js.
But there is a problem for images which are drawn on canvas with perspective calculations like this;
Sample img: http://oi62.tinypic.com/2h49dec.jpg
orginal image size: 6500 x 3592 and you can see spread edges on image...
I tried a few technics like ctx.imageSmoothingEnabled true etc.. But result was always same.
What can i do for solve this problem? What do you think about this?
edit
For more detail;
I get an image (Resolution free) from user then crop it for mockup ratio. For example in my sample image, user image was cropped for imac ratio 16:9 then making calculation with four dot of screen. By the way, my mockup image size is 6500 x 3592. so i made scale, transform etc this cropped image and put it in mockup on canvas. And then use blob to download this image to client...
Thanks.
Solved.
I use perspective.js for calculation on canvas. so I made some revisions on this js source.
If you wanna use or check source;
// Copyright 2010 futomi http://www.html5.jp/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// perspective.js v0.0.2
// 2010-08-28
/* -------------------------------------------------------------------
* define objects (name space) for this library.
* ----------------------------------------------------------------- */
if (typeof html5jp == 'undefined') {
html5jp = new Object();
}
(function() {
html5jp.perspective = function(ctxd, image) {
// check the arguments
if (!ctxd || !ctxd.strokeStyle) {
return;
}
if (!image || !image.width || !image.height) {
return;
}
// prepare a <canvas> for the image
var cvso = document.createElement('canvas');
cvso.width = parseInt(image.width) * 2;
cvso.height = parseInt(image.height) * 2;
var ctxo = cvso.getContext('2d');
ctxo.drawImage(image, 0, 0, cvso.width, cvso.height);
// prepare a <canvas> for the transformed image
var cvst = document.createElement('canvas');
cvst.width = ctxd.canvas.width;
cvst.height = ctxd.canvas.height;
var ctxt = cvst.getContext('2d');
ctxt.imageSmoothingEnabled = true;
ctxt.mozImageSmoothingEnabled = true;
ctxt.webkitImageSmoothingEnabled = true;
ctxt.msImageSmoothingEnabled = true;
// parameters
this.p = {
ctxd: ctxd,
cvso: cvso,
ctxo: ctxo,
ctxt: ctxt
}
};
var proto = html5jp.perspective.prototype;
proto.draw = function(points) {
var d0x = points[0][0];
var d0y = points[0][1];
var d1x = points[1][0];
var d1y = points[1][1];
var d2x = points[2][0];
var d2y = points[2][1];
var d3x = points[3][0];
var d3y = points[3][1];
// compute the dimension of each side
var dims = [
Math.sqrt(Math.pow(d0x - d1x, 2) + Math.pow(d0y - d1y, 2)), // top side
Math.sqrt(Math.pow(d1x - d2x, 2) + Math.pow(d1y - d2y, 2)), // right side
Math.sqrt(Math.pow(d2x - d3x, 2) + Math.pow(d2y - d3y, 2)), // bottom side
Math.sqrt(Math.pow(d3x - d0x, 2) + Math.pow(d3y - d0y, 2)) // left side
];
//
var ow = this.p.cvso.width;
var oh = this.p.cvso.height;
// specify the index of which dimension is longest
var base_index = 0;
var max_scale_rate = 0;
var zero_num = 0;
for (var i = 0; i < 4; i++) {
var rate = 0;
if (i % 2) {
rate = dims[i] / ow;
} else {
rate = dims[i] / oh;
}
if (rate > max_scale_rate) {
base_index = i;
max_scale_rate = rate;
}
if (dims[i] == 0) {
zero_num++;
}
}
if (zero_num > 1) {
return;
}
//
var step = 0.10;
var cover_step = step * 250;
//
var ctxo = this.p.ctxo;
var ctxt = this.p.ctxt;
//*** ctxt.clearRect(0, 0, ctxt.canvas.width, ctxt.canvas.height);
if (base_index % 2 == 0) { // top or bottom side
var ctxl = this.create_canvas_context(ow, cover_step);
var cvsl = ctxl.canvas;
for (var y = 0; y < oh; y += step) {
var r = y / oh;
var sx = d0x + (d3x - d0x) * r;
var sy = d0y + (d3y - d0y) * r;
var ex = d1x + (d2x - d1x) * r;
var ey = d1y + (d2y - d1y) * r;
var ag = Math.atan((ey - sy) / (ex - sx));
var sc = Math.sqrt(Math.pow(ex - sx, 2) + Math.pow(ey - sy, 2)) / ow;
ctxl.setTransform(1, 0, 0, 1, 0, -y);
ctxl.drawImage(ctxo.canvas, 0, 0);
//
ctxt.translate(sx, sy);
ctxt.rotate(ag);
ctxt.scale(sc, sc);
ctxt.drawImage(cvsl, 0, 0);
//
ctxt.setTransform(1, 0, 0, 1, 0, 0);
}
} else if (base_index % 2 == 1) { // right or left side
var ctxl = this.create_canvas_context(cover_step, oh);
var cvsl = ctxl.canvas;
for (var x = 0; x < ow; x += step) {
var r = x / ow;
var sx = d0x + (d1x - d0x) * r;
var sy = d0y + (d1y - d0y) * r;
var ex = d3x + (d2x - d3x) * r;
var ey = d3y + (d2y - d3y) * r;
var ag = Math.atan((sx - ex) / (ey - sy));
var sc = Math.sqrt(Math.pow(ex - sx, 2) + Math.pow(ey - sy, 2)) / oh;
ctxl.setTransform(1, 0, 0, 1, -x, 0);
ctxl.drawImage(ctxo.canvas, 0, 0);
//
ctxt.translate(sx, sy);
ctxt.rotate(ag);
ctxt.scale(sc, sc);
ctxt.drawImage(cvsl, 0, 0);
//
ctxt.setTransform(1, 0, 0, 1, 0, 0);
}
}
// set a clipping path and draw the transformed image on the destination canvas.
this.p.ctxd.save();
this.set_clipping_path(this.p.ctxd, [
[d0x, d0y],
[d1x, d1y],
[d2x, d2y],
[d3x, d3y]
]);
this.p.ctxd.drawImage(ctxt.canvas, 0, 0);
this.p.ctxd.restore();
}
proto.create_canvas_context = function(w, h) {
var canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.mozImageSmoothingEnabled = true;
ctx.webkitImageSmoothingEnabled = true;
ctx.msImageSmoothingEnabled = true;
return ctx;
};
proto.set_clipping_path = function(ctx, points) {
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (var i = 1; i < points.length; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
ctx.closePath();
ctx.clip();
};
})();
The problem is (most likely, but no code shows so..) that the image is actually too big.
The canvas typically uses bi-linear interpolation (2x2 samples) rather than bi-cubic (4x4 samples). That means if you scale it down a large percentage in one chunk the algorithm will skip some pixels that otherwise should have been sampled, resulting in a more pixelated look.
The solution do is to resize the image in steps, ie. 50% of itself repeatably until a suitable size is achieved. Then use perspective calculations on it. The exact destination size is something you need to find by trial and error, but a good starting point is to use the largest side of the resulting perspective image.
Here is one way to step-down rescale an image in steps.
I am having problems measure the height of a font which I have included with CSS using this code:
measureFontHeight3: function(font)
{
var left = 0;
var top = 0;
var height = 50;
var width = 50;
// Draw the text in the specified area
var canvas = ig.$new('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
ctx.font = font;
ctx.textBaseline = 'top';
ctx.fillText('gM', 0,0);
// Get the pixel data from the canvas
var data = ctx.getImageData(left, top, width, height).data,
first = false,
last = false,
r = height,
c = 0;
// Find the last line with a non-white pixel
while(!last && r)
{
r--;
for(c = 0; c < width; c++)
{
if(data[r * width * 4 + c * 4 + 3])
{
last = r;
break;
}
}
}
// Find the first line with a non-white pixel
while(r)
{
r--;
for(c = 0; c < width; c++)
{
if(data[r * width * 4 + c * 4 + 3]) {
first = r;
break;
}
}
// If we've got it then return the height
if(first != r)
{
var result = last - first;
console.log("3: " +result);
return result;
}
}
// We screwed something up... What do you expect from free code?
return 0;
},
When I measure a font which the system already has installed, the function is quite accurate, but when I try to measure a font which I have included in a CSS file, the measurement does not work, i.e. it measure wrongly.
Is it because of the new canvas not being able to "see" the new font or is something else wrong ?
Could it be because you want to measure the font before it's been fully loaded ?
In my example it seems to be working fine : Font example