reddy
reddy

Reputation: 1821

More efficient way to calculate the average of a RGB color block in Javascript

I have an array of object with each holding rgb values and I want to get the average of channel.

[{ r: 234, g: 250, b: 0 },  { r: 234, g: 250, b: 0 }, { r: 234, g: 250, b: 0 }]

The straight forward method is to map through the array, get the sum of each of the r, g and b values, then divide each by the length of the array.

const arrLength = colorBlock.length

let redArr = colorBlock.map(obj => obj.r)
let greenArr = colorBlock.map(obj => obj.g)
let blueArr = colorBlock.map(obj => obj.b)

const add = (total, num) => total + num;
const totalR = redArr.reduce(add, 0);
const totalG = greenArr.reduce(add, 0);
const totalB = blueArr.reduce(add, 0);

const averageR = parseInt(totalR / arrLength)
const averageG = parseInt(totalG / arrLength)
const averageB = parseInt(totalB / arrLength)

My problem with this is that it's very slow when I have a big color block, a 900 x 900 block took about 5 seconds. Is there a more efficient way to do this?

EDIT: Sorry guy I make a mistake, the causes for my slow down actually came from the function that create the color block, not the average calculation. The calculation only took a few hundred millisecond.

Upvotes: 0

Views: 382

Answers (4)

trincot
trincot

Reputation: 350770

Your approach has the optimal time complexity. You could gain a factor of speed by avoiding callback functions, and relying on the plain old for loop (not the in or of variant, but the plain one):

const arrLength = colorBlock.length;
let totalR = 0, totalG = 0, totalB = 0;
for (let i = 0; i < arrLength; i++) {
    let rgb = colorBlock[i];
    totalR += rgb.r;
    totalG += rgb.g;
    totalB += rgb.b;
}

const averageR = Math.floor(totalR / arrLength);
const averageG = Math.floor(totalG / arrLength);
const averageB = Math.floor(totalB / arrLength);

You may at most halve the time tp process a 900x900 input with this, but that's about it.

To really improve more, you will need to rely on some heuristic that says that most of the time neighboring pixels will have about the same color code. And so you would then skip pixels. That will give you an estimated average, but that might be good enough for your purposes.

Remark: don't use parseInt when the argument is numeric. This will unnecessarily convert the argument to string, only to convert it back to number. Instead use Math.floor.

Upvotes: 1

Trobol
Trobol

Reputation: 1250

Using a simple for loop instead of map and reduce can save quite a bit of time.

//Generate random colors
function genColor() {
  return Math.floor(Math.random() * 255);
}
const SIZE = 500000;
const colorBlock = new Array(SIZE);
for(let i = 0; i < SIZE; i++) {
  colorBlock[i] = {r:genColor(), g:genColor(), b:genColor()};
}


const arrLength = colorBlock.length;

var startTime0 = performance.now();

let redArr0 = colorBlock.map(obj => obj.r)
let greenArr0 = colorBlock.map(obj => obj.g)
let blueArr0 = colorBlock.map(obj => obj.b)

const add = (total, num) => total + num;
const totalR0 = redArr0.reduce(add, 0);
const totalG0 = greenArr0.reduce(add, 0);
const totalB0 = blueArr0.reduce(add, 0);

const averageR0 = Math.floor(totalR0 / arrLength)
const averageG0 = Math.floor(totalG0 / arrLength)
const averageB0 = Math.floor(totalB0 / arrLength)

var endTime0 = performance.now();


var totalR1 = 0;
var totalG1 = 0;
var totalB1 = 0;

for(let i = 0; i < SIZE; i++) {
  totalR1 += colorBlock[i].r;
  totalG1 += colorBlock[i].g;
  totalB1 += colorBlock[i].b;
}

const averageR1 = Math.floor(totalR1 / arrLength)
const averageG1 = Math.floor(totalG1 / arrLength)
const averageB1 = Math.floor(totalB1 / arrLength)


var endTime1 = performance.now();

console.log( averageR0,  averageG0,  averageB0)
console.log( averageR1,  averageG1,  averageB1)

console.log("Reduce", endTime0 - startTime0);
console.log("for loop", endTime1 - endTime0);

Upvotes: 0

symlink
symlink

Reputation: 12218

Use Array.reduce() on the entire object all at once, then Array.map() to calculate the averages:

const obj = [{ r: 200, g: 161, b: 1 },  { r: 50, g: 0, b: 3 }, { r: 50, g: 0, b: 5 }]

const res = obj.reduce((acc,cur) => {
    acc[0] += cur.r
    acc[1] += cur.g
    acc[2] += cur.b
    return acc
},[0,0,0]).map(cur => Math.round(cur / obj.length))

console.log(res)

Upvotes: 0

tevemadar
tevemadar

Reputation: 13225

Drop the separation of channels. Traversing one existing array once is likely more efficient than creating and filling 3 new arrays (which itself means traversing the original array 3 times), and then traversing all of them again.

let totals=colorBlock.reduce(
  (totals,current)=>{
    totals[0]+=current.r;
    totals[1]+=current.g;
    totals[2]+=current.b;
    return totals;
  },[0,0,0]);
let averageR=totals[0]/totals.length;
let averageG=totals[1]/totals.length;
let averageB=totals[2]/totals.length;

Upvotes: 1

Related Questions