I have 2D canvas with things drawn to it and I want to know if all the pixels in a region (rect - x,y,w,h) are all empty/fully transparent? I know this can be done with getImageData but is there a faster way? I am writing a simple java script image packer and I wish to exclude the empty images from the final sheet.
The only way to read pixels is to use getImageData(), but you can speed this sort of checks up by using a different view than the default Uint8ClampedArray, for example Uint32Array which allows you to read a single pixel per iteration:
function isEmpty(ctx, x, y, w, h) {
var idata = ctx.getImageData(x, y, w, h), // needed as usual ...
u32 = new Uint32Array(idata.data.buffer), // reads 1x uint32 instead of 4x uint8
i = 0, len = u32.length;
while(i < len) if (u32[i++]) return false; // if !== 0 return false, not empty
return true // all empty, all OK
}
However, this cannot be used to check for transparency though. Even if a pixel is fully transparent there may be color data present in the other channels. For example, this would produce an invisible pixel: rgba(255,128,0,0) and isEmpty() would report the area to be non-empty even if the pixel isn't visible.
To check those cases you'll have to check the alpha channel only, and you could simply modify the above to use an AND mask to filter out color data, or, shift the alpha channel bits over, pushing the other bits out - in either case we are after non-0 values.
As this is in little-endian (LSB) format (as on most main stream computers nowadays), the components are in the order ABGR (0xAABBGGRR) so we can do either:
u32[i] & 0xff000000
or use shift (sign does not matter so much in this case, but personally I prefer to use unsigned shift (>>> rather than >>) when I deal with unsigned numbers to begin with):
u32[i]>>>24
Performance wise there is very little difference, I would guess ANDing is slightly faster if anything:
ANDing
function isTransparent(ctx, x, y, w, h) {
var idata = ctx.getImageData(x, y, w, h), // needed as usual ...
u32 = new Uint32Array(idata.data.buffer), // reads 1x uint32 instead of 4x bytes
i = 0, len = u32.length;
while(i < len) if (u32[i++] & 0xff000000) return false; // not transparent?
return true // all transparent, all OK
}
Bit-shifting
function isTransparent(ctx, x, y, w, h) {
var idata = ctx.getImageData(x, y, w, h), // needed as usual ...
u32 = new Uint32Array(idata.data.buffer), // reads 1x uint32 instead of 4x bytes
i = 0, len = u32.length;
while(i < len) if (u32[i++]>>>24) return false; // not transparent?
return true // all transparent, all OK
}
Update:
Speed up tricks
If you know that the data you're checking is of at least some size, lets say 2x2 pixels, you can also improve the speed by skipping every other pixel, even every other line:
while(i < len) if (u32[(i += 2)]>>>24) return false; // skips every 2. pixel
For lines, you need two iterators:
while(i < len) {
var endLine = i + width, p = i; // p in case you deal with odd widths
while(p < endLine) if (u32[(p += 2)]>>>24) return false; // skip every 2. pixel
i += width * 2; // skip a line
}
Related
I am using a recursive flood fill algorithm in javascript and I am not sure how to avoid exceeding the max call stack size. This is a little project that runs in the browser.
I got the idea from here: https://guide.freecodecamp.org/algorithms/flood-fill/
I chose this algorithm because it's easy to understand and so far I like it because it's pretty quick.
x and y are the 2-d coordinates from the top-left, targetColor and newColor are each a Uint8ClampedArray, and id = ctx.createImageData(1,1); that gets its info from newColor.
function floodFill2(x, y, targetColor, newColor, id) {
let c = ctx.getImageData(x, y, 1, 1).data;
// if the pixel doesnt match the target color, end function
if (c[0] !== targetColor[0] || c[1] !== targetColor[1] || c[2] !== targetColor[2]) {
return;
}
// if the pixel is already the newColor, exit function
if (c[0] === newColor[0] && c[1] === newColor[1] && c[2] === newColor[2]) {
// this 'probably' means we've already been here, so we should ignore the pixel
return;
}
// if the fn is still alive, then change the color of the pixel
ctx.putImageData(id, x, y);
// check neighbors
floodFill2(x-1, y, targetColor, newColor, id);
floodFill2(x+1, y, targetColor, newColor, id);
floodFill2(x, y-1, targetColor, newColor, id);
floodFill2(x, y+1, targetColor, newColor, id);
return;
}
If the section is small, this code works fine. If the section is big, only a portion gets filled in and then I get the max call stack size error.
Questions
Is there something that doesn't make sense in the above code? (ie. maybe an issue for code review?)
If the code looks ok, is it the possible that I am simply using an algorithm that is inappropriate to flood fill a large section?
I would like to say that my hope for this question is to have a simple function similar to the one above which will work even for a very large, oddly shaped region but that I suppose is contingent on the generality of the algorithm. Like, am I trying to drive a nail with a screwdriver kind of thing?
Use a stack or Why recursion in JavaScript sucks.
Recursion is just a lazy mans stack. Not only is it lazy, it uses more memory and is far slower than traditional stacks
To top it off (as you have discovered) In JavaScript recursion is risky as the call stack is very small and you can never know how much of the call stack has been used when your function is called.
Some bottle necks while here
Getting image data via getImageData is an intensive task for many devices. It can take just as long to get 1 pixel as getting 65000 pixels. Calling getImageData for every pixel is a very bad idea. Get all pixels once and get access to pixels directly from RAM
Use an Uint32Array so you can process a pixel in one step rather than having to check each channel in turn.
Example
Using a simple array as a stack, each item pushed to the stack is the index of a new pixel to fill. Thus rather than have to create a new execution context, a new local scope and associated variables, closure, and more. A single 64bit number takes the place of a callStack entry.
See demo for an alternative flood fill pixel search method
function floodFill(x, y, targetColor, newColor) {
const w = ctx.canvas.width, h = ctx.canvas.height;
const imgData = ctx.getImageData(0, 0, w, h);
const p32 = new Uint32Array(imgData.data.buffer);
const channelMask = 0xFFFFFF; // Masks out Alpha NOTE order of channels is ABGR
const cInvMask = 0xFF000000; // Mask out BGR
const canFill = idx => (p32[idx] & channelMask) === targetColor;
const setPixel = (idx, newColor) => p32[idx] = (p32[idx] & cInvMask) | newColor;
const stack = [x + y * w]; // add starting pos to stack
while (stack.length) {
let idx = stack.pop();
setPixel(idx, newColor);
// for each direction check if that pixel can be filled and if so add it to the stack
canFill(idx + 1) && stack.push(idx + 1); // check right
canFill(idx - 1) && stack.push(idx - 1); // check left
canFill(idx - w) && stack.push(idx - w); // check Up
canFill(idx + w) && stack.push(idx + w); // check down
}
// all done when stack is empty so put pixels back to canvas and return
ctx.putImageData(imgData,0, 0);
}
Usage
To use the function is slightly different. id is not used and the colors targetColor and newColor need to be 32bit words with the red, green, blue, alpha reversed.
For example if targetColor was yellow = [255, 255, 0] and newColor was blue =[0, 0, 255] then revers RGB for each and call fill with
const yellow = 0xFFFF;
const blue = 0xFF0000;
floodFill(x, y, yellow, blue);
Note that I am matching your function and completely ignoring alpha
Inefficient algorithm
Note that this style of fill (mark up to 4 neighbors) is very inefficient as many of the pixels will be marked to fill and by the time they are popped from the stack it will already have been filled by another neighbor.
The following GIF best illustrates the problem. Filling the 4 by 3 area with green.
First set the pixel green,
Then push to stack if not green right, left, up, down [illustration red, orange, cyan, purple boxes]
Pop bottom and set to green
Repeat
When a location that already is on the stack is added it is inset (just for illustration purpose)
Note that when all pixels are green there are still 6 items on the stack that still need to be popped. I estimate on average you will be processing 1.6 times the number of pixels needed. For a large image 2000sq thats 2million (alot of) pixels
Using an array stack rather than call stack means
No more call stack overflows
Inherently faster code.
Allows for many optimizations
Demo
The demo is a slightly different version as your logic has some problems. It still uses a stack, but limits the number of entries pushed to the stack to be equal to the number of unique columns in the fill area.
Includes alpha in the pixel fill test and pixel write color. Simplifying the pixel read and write code.
Checks against the edges of the canvas rather than filling outside the canvas width (looping back AKA asteroids style)
Reads target color from the canvas at the first x,y pixel
Fills columns from the top most pixel in each column and only branching left or right if the previous left or right pixel was not the target color. This reduces the number of pixels to push the stack by orders of magnitude.
Click to flood fill
function floodFill(x, y, newColor) {
var left, right, leftEdge, rightEdge;
const w = ctx.canvas.width, h = ctx.canvas.height, pixels = w * h;
const imgData = ctx.getImageData(0, 0, w, h);
const p32 = new Uint32Array(imgData.data.buffer);
const stack = [x + y * w]; // add starting pos to stack
const targetColor = p32[stack[0]];
if (targetColor === newColor || targetColor === undefined) { return } // avoid endless loop
while (stack.length) {
let idx = stack.pop();
while(idx >= w && p32[idx - w] === targetColor) { idx -= w }; // move to top edge
right = left = false;
leftEdge = (idx % w) === 0;
rightEdge = ((idx +1) % w) === 0;
while (p32[idx] === targetColor) {
p32[idx] = newColor;
if(!leftEdge) {
if (p32[idx - 1] === targetColor) { // check left
if (!left) {
stack.push(idx - 1); // found new column to left
left = true; //
}
} else if (left) { left = false }
}
if(!rightEdge) {
if (p32[idx + 1] === targetColor) {
if (!right) {
stack.push(idx + 1); // new column to right
right = true;
}
} else if (right) { right = false }
}
idx += w;
}
}
ctx.putImageData(imgData,0, 0);
return;
}
var w = canvas.width;
var h = canvas.height;
const ctx = canvas.getContext("2d");
var i = 400;
const fillCol = 0xFF0000FF
const randI = v => Math.random() * v | 0;
ctx.fillStyle = "#FFF";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "#000";
while(i--) {
ctx.fillRect(randI(w), randI(h), 20, 20);
ctx.fillRect(randI(w), randI(h), 50, 20);
ctx.fillRect(randI(w), randI(h), 10, 60);
ctx.fillRect(randI(w), randI(h), 180, 2);
ctx.fillRect(randI(w), randI(h), 2, 182);
ctx.fillRect(randI(w), randI(h), 80, 6);
ctx.fillRect(randI(w), randI(h), 6, 82);
ctx.fillRect(randI(w), randI(h), randI(40), randI(40));
}
i = 400;
ctx.fillStyle = "#888";
while(i--) {
ctx.fillRect(randI(w), randI(h), randI(40), randI(40));
ctx.fillRect(randI(w), randI(h), randI(4), randI(140));
}
var fillIdx = 0;
const fillColors = [0xFFFF0000,0xFFFFFF00,0xFF00FF00,0xFF00FFFF,0xFF0000FF,0xFFFF00FF];
canvas.addEventListener("click",(e) => {
floodFill(e.pageX | 0, e.pageY | 0, fillColors[(fillIdx++) % fillColors.length]);
});
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas" width="2048" height="2048">
Flood fill is a problematic process with respect to stack size requirements (be it the system stack or one managed on the heap): in the worst case you will need a recursion depth on the order of the image size. Such cases can occur when you binarize random noise, they are not so improbable.
There is a version of flood filling that is based on filling whole horizontal runs in a single go (https://en.wikipedia.org/wiki/Flood_fill#Scanline_fill). It is advisable in general because it roughly divides the recursion depth by the average length of the runs and is faster in the "normal" cases. Anyway, it doesn't solve the worst-case issue.
There is also an interesting truly stackless algorithm as described here: https://en.wikipedia.org/wiki/Flood_fill#Fixed-memory_method_(right-hand_fill_method). But the implementation looks cumbersome.
I'm working on a simple web app which simplifies the colours of an uploaded image to a colour palette selected by the user. The script works, but it takes a really long time to loop through the whole image (for large images it's over a few minutes), changing the pixels.
Initially, I was writing to the canvas itself, but I changed the code so that changes are made to an ImageData object and the canvas is only updated at the end of the script. However, this didn't really make much difference.
// User selects colours:
colours = [[255,45,0], [37,36,32], [110,110,105], [18,96,4]];
function colourDiff(colour1, colour2) {
difference = 0
difference += Math.abs(colour1[0] - colour2[0]);
difference += Math.abs(colour1[1] - colour2[1]);
difference += Math.abs(colour1[2] - colour2[2]);
return(difference);
}
function getPixel(imgData, index) {
return(imgData.data.slice(index*4, index*4+4));
}
function setPixel(imgData, index, pixelData) {
imgData.data.set(pixelData, index*4);
}
data = ctx.getImageData(0,0,canvas.width,canvas.height);
for(i=0; i<(canvas.width*canvas.height); i++) {
pixel = getPixel(data, i);
lowestDiff = 1024;
lowestColour = [0,0,0];
for(colour in colours) {
colour = colours[colour];
difference = colourDiff(colour, pixel);
if(lowestDiff < difference) {
continue;
}
lowestDiff = difference;
lowestColour = colour;
}
console.log(i);
setPixel(data, i, lowestColour);
}
ctx.putImageData(data, 0, 0);
During the entire process, the website is completely frozen, so I can't even display a progress bar. Is there any way to optimise this so that it takes less time?
There is no need to slice the array each iteration. (As niklas has already stated).
I would loop over the data array instead of looping over the canvas dimensions and directly edit the array.
for(let i = 0; i < data.length; i+=4) { // i+=4 to step over each r,g,b,a pixel
let pixel = getPixel(data, i);
...
setPixel(data, i, lowestColour);
}
function setPixel(data, i, colour) {
data[i] = colour[0];
data[i+1] = colour[1];
data[i+2] = colour[2];
}
function getPixel(data, i) {
return [data[i], data[i+1], data[i+2]];
}
Also, console.log can bring a browser to it's knees if you've got the console open. If your image is 1920 x 1080 then you will be logging to the console 2,073,600 times.
You can also pass all of the processing off to a Web Worker for ultimate threaded performance. Eg. https://jsfiddle.net/pnmz75xa/
One problem or option for improvement is clearly your slice function, which will create a new array every time it is called, you do not need this. I would change the for loop like so:
for y in canvas.height {
for x in canvas.width {
//directly alter the canvas' pixels
}
}
Finding difference in color
I am adding an answer because you have use a very poor color match algorithm.
Finding how closely a color matches another is best done if you imagine each unique possible colour as a point in 3D space. The red, green, and blue values represent the x,y,z coordinate.
You can then use some basic geometry to locate the distance from one colour to the another.
// the two colours as bytes 0-255
const colorDist = (r1, g1, b1, r2, g2, b2) => Math.hypot(r1 - r2, g1 - g2, b1 - b2);
It is also important to note that the channel value 0-255 is a compressed value, the actual intensity is close to that value squared (channelValue ** 2.2). That means that red = 255 is 65025 times more intense than red = 1
The following function is a close approximation of the colour difference between two colors. Avoiding the Math.hypot function as it is very slow.
const pallet = [[1,2,3],[2,10,30]]; // Array of arrays representing rgb byte
// of the colors you are matching
function findClosest(r,g,b) {
var closest;
var dist = Infinity;
r *= r;
g *= g;
b *= b;
for (const col of pallet) {
const d = ((r - col[0] * col[0]) + (g - col[1] * col[1]) + (b - col[2] * col[2])) ** 0.5;
if (d < dist) {
if (d === 0) { // if same then return result
return col;
}
closest = col;
dist = d;
}
}
return closest;
}
As for performance, your best bet is either via a web worker, or use webGL to do the conversion in realtime.
If you want to keep it simple to prevent the code from blocking the page cut the job into smaller slices using a timer to allow the page breathing room.
The example uses setTimeout and performance.now() to do 10ms slices letting other page events and rendering to do there thing. It returns a promise that resolves when all pixels are processed
function convertBitmap(canvas, maxTime) { // maxTime in ms (1/1000 second)
return new Promise(allDone => {
const ctx = canvas.getContext("2d");
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = pixels.data;
var idx = data.length / 4;
processPixels(); // start processing
function processPixels() {
const time = performance.now();
while (idx-- > 0) {
if (idx % 1024) { // check time every 1024 pixels
if (performance.now() - time > maxTime) {
setTimeout(processPixels, 0);
idx++;
return;
}
}
let i = idx * 4;
const col = findClosest(data[i], data[i + 1], data[i + 2]);
data[i++] = col[0];
data[i++] = col[1];
data[i] = col[2];
}
ctx.putImageData(pixels, 0, 0);
allDone("Pixels processed");
}
});
}
// process pixels in 10ms slices.
convertBitmap(myCanvas, 10).then(mess => console.log(mess));
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.
The problem is currently solved. In case some one wants to see the colored fractal, the code is here.
Here is the previous problem:
Nonetheless the algorithm is straight forward, I seems to have a small error (some fractals are drawing correctly and some are not). You can quickly check it in jsFiddle that c = -1, 1/4 the fractal is drawing correctly but if I will take c = i; the image is totally wrong.
Here is implementation.
HTML
<canvas id="a" width="400" height="400"></canvas>
JS
function point(pos, canvas){
canvas.fillRect(pos[0], pos[1], 1, 1); // there is no drawpoint in JS, so I simulate it
}
function conversion(x, y, width, R){ // transformation from canvas coordinates to XY plane
var m = R / width;
var x1 = m * (2 * x - width);
var y2 = m * (width - 2 * y);
return [x1, y2];
}
function f(z, c){ // calculate the value of the function with complex arguments.
return [z[0]*z[0] - z[1] * z[1] + c[0], 2 * z[0] * z[1] + c[1]];
}
function abs(z){ // absolute value of a complex number
return Math.sqrt(z[0]*z[0] + z[1]*z[1]);
}
function init(){
var length = 400,
width = 400,
c = [-1, 0], // all complex number are in the form of [x, y] which means x + i*y
maxIterate = 100,
R = (1 + Math.sqrt(1+4*abs(c))) / 2,
z;
var canvas = document.getElementById('a').getContext("2d");
var flag;
for (var x = 0; x < width; x++){
for (var y = 0; y < length; y++){ // for every point in the canvas plane
flag = true;
z = conversion(x, y, width, R); // convert it to XY plane
for (var i = 0; i < maxIterate; i++){ // I know I can change it to while and remove this flag.
z = f(z, c);
if (abs(z) > R){ // if during every one of the iterations we have value bigger then R, do not draw this point.
flag = false;
break;
}
}
// if the
if (flag) point([x, y], canvas);
}
}
}
Also it took me few minutes to write it, I spent much more time trying to find why does not it work for all the cases. Any idea where I screwed up?
Good news! (or bad news)
You're implementation is completely. correct. Unfortunately, with c = [0, 1], the Julia set has very few points. I believe it is measure zero (unlike say, the Mandelbrot set). So the probability of a random point being in that Julia set is 0.
If you reduce your iterations to 15 (JSFiddle), you can see the fractal. One hundred iterations is more "accurate", but as the number of iterations increase, the chance that a point on your 400 x 400 grid will be included in your fractal approximation decreases to zero.
Often, you will see the Julia fractal will multiple colors, where the color indicates how quickly it diverges (or does not diverge at all), like in this Flash demonstration. This allows the Julia fractal to be somewhat visible even in cases like c = i.
Your choices are
(1) Reduce your # of iterations, possibly depending on c.
(2) Increase the size of your sampling (and your canvas), possibly depending on c.
(3) Color the points of your canvas according to the iteration # at which R was exceeded.
The last option will give you the most robust result.